Main Page | Namespace List | Class Hierarchy | Alphabetical List | Class List | File List | Namespace Members | Class Members | File Members

RGBImage.cc

Go to the documentation of this file.
00001 // -*- C++ -*- 00002 00003 // PLearn (A C++ Machine Learning Library) 00004 // Copyright (C) 1998 Pascal Vincent 00005 // Copyright (C) 1999-2002 Pascal Vincent, Yoshua Bengio and University of Montreal 00006 // 00007 00008 // Redistribution and use in source and binary forms, with or without 00009 // modification, are permitted provided that the following conditions are met: 00010 // 00011 // 1. Redistributions of source code must retain the above copyright 00012 // notice, this list of conditions and the following disclaimer. 00013 // 00014 // 2. Redistributions in binary form must reproduce the above copyright 00015 // notice, this list of conditions and the following disclaimer in the 00016 // documentation and/or other materials provided with the distribution. 00017 // 00018 // 3. The name of the authors may not be used to endorse or promote 00019 // products derived from this software without specific prior written 00020 // permission. 00021 // 00022 // THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 00023 // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 00024 // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 00025 // NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 00026 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 00027 // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 00028 // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 00029 // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 00030 // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 00031 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 00032 // 00033 // This file is part of the PLearn library. For more information on the PLearn 00034 // library, go to the PLearn Web site at www.plearn.org 00035 00036 00037 00038 00039 /* ******************************************************* 00040 * $Id: RGBImage.cc,v 1.5 2004/07/21 16:30:51 chrish42 Exp $ 00041 * AUTHORS: Pascal Vincent & Yoshua Bengio 00042 * This file is part of the PLearn library. 00043 ******************************************************* */ 00044 00045 #include <fstream> 00046 #include "RGBImage.h" 00047 #include <plearn/math/TMat_maths.h> 00048 00049 namespace PLearn { 00050 using namespace std; 00051 00052 const RGB RGB::BLACK(0,0,0); 00053 const RGB RGB::WHITE(255,255,255); 00054 const RGB RGB::RED(255,0,0); 00055 const RGB RGB::GREEN(0,255,0); 00056 const RGB RGB::BLUE(0,0,255); 00057 const RGB RGB::DISCOCOLORS[] = {RGB::RED, RGB::GREEN, RGB::BLUE}; 00058 00059 00060 /************/ 00061 /* RGBImage */ 00062 /************/ 00063 00064 RGBImage::RGBImage(int the_height, int the_width) 00065 :height_(the_height), width_(the_width) 00066 { 00067 tmpnam(tmpfilename); 00068 if(height_>0 && width_>0) 00069 { 00070 storage = new Storage<RGB>(height_*width_); 00071 clear(); 00072 } 00073 } 00074 00075 RGBImage::RGBImage(Mat r, Mat g, Mat b) 00076 :height_(r.length()), width_(r.width()) 00077 { 00078 tmpnam(tmpfilename); 00079 storage = new Storage<RGB>(height_*width_); 00080 for(int i=0; i<height_; i++) 00081 for(int j=0; j<width_; j++) 00082 setPixel(i,j,RGB((unsigned char)r(i,j), (unsigned char)g(i,j), (unsigned char)b(i,j))); 00083 } 00084 00085 00086 RGBImage::~RGBImage() 00087 { 00088 unlink(tmpfilename); 00089 } 00090 00091 void 00092 RGBImage::resize(int newheight, int newwidth) 00093 { 00094 if(storage) 00095 storage->resize(newheight*newwidth); 00096 else 00097 storage = new Storage<RGB>(newheight*newwidth); 00098 height_ = newheight; 00099 width_ = newwidth; 00100 } 00101 00102 void 00103 RGBImage::removeBorders(int top_border,int bottom_border, 00104 int left_border,int right_border) 00105 { 00106 if(!storage) 00107 PLERROR("Empty RGBImage"); 00108 int newwidth = width_ - (left_border + right_border); 00109 int newheight = height_ - (top_border + bottom_border); 00110 if (newheight < 1) 00111 PLERROR("RGBImage::removeBorders(%d,%d,%d,%d): can't remove more than height_(%d)", 00112 top_border,bottom_border,left_border,right_border,height_); 00113 if (newwidth < 1) 00114 PLERROR("RGBImage::removeBorders(%d,%d,%d,%d): can't remove more than width_(%d)", 00115 top_border,bottom_border,left_border,right_border,width_); 00116 PP< Storage<RGB> > newstorage = new Storage<RGB>(newheight*newwidth); 00117 RGB* d= newstorage->data; 00118 RGB* data = storage->data; 00119 for (int r=0;r<newheight;r++) 00120 for (int c=0;c<newwidth;c++,d++) 00121 *d = data[(top_border+r)*width_+(left_border+c)]; 00122 height_=newheight; 00123 width_=newwidth; 00124 storage = newstorage; 00125 } 00126 00127 void 00128 RGBImage::fill(RGB color) 00129 { 00130 if(!storage) 00131 PLERROR("Empty RGBImage"); 00132 RGB* data = storage->data; 00133 for(int pos=0; pos<height_*width_; pos++) 00134 data[pos] = color; 00135 } 00136 00137 void 00138 RGBImage::loadPPM(const char* filename) 00139 { 00140 int ncolors; 00141 ifstream in(filename); 00142 if(!in) 00143 PLERROR("In RGBImage::loadPPM could not open file %s for reading",filename); 00144 00145 char imagetype[20]; 00146 int newheight, newwidth; 00147 in >> imagetype >> newwidth >> newheight >> ncolors; 00148 char tmp[100]; 00149 in.getline(tmp,99); // skip until end of line 00150 if(strcmp(imagetype,"P6")!=0) 00151 PLERROR("In RGBImage::loadPPM unsupported format (not a P6 PPM)"); 00152 resize(newheight, newwidth); 00153 RGB* data = storage->data; 00154 PLWARNING("In RGBImage::loadPPM - Code has been changed to compile, but hasn't been tested (remove this warning if it works)"); 00155 // The following line: 00156 // in.read((unsigned char*)data,height_*width_*3); 00157 // has been replaced by: 00158 in.read((char*)data,height_*width_*3); 00159 } 00160 00161 void 00162 RGBImage::savePPM(const char* filename) const 00163 { 00164 if(!storage) 00165 PLERROR("Empty RGBImage"); 00166 RGB* data = storage->data; 00167 ofstream out(filename); 00168 if(!out) 00169 PLERROR("In RGBImage::savePPM could not open file for writing"); 00170 out << "P6\n" << width_ << ' ' << height_ << ' ' << 255 << endl; 00171 PLWARNING("In RGBImage::loadPPM - Code has been changed to compile, but hasn't been tested (remove this warning if it works)"); 00172 // The following line: 00173 // out.write((unsigned char*)data, height_*width_*3); 00174 // has been replaced by: 00175 out.write((char*)data, height_*width_*3); 00176 out.flush(); 00177 } 00178 00179 void 00180 RGBImage::display() const 00181 { 00182 savePPM(tmpfilename); 00183 char command[1000]; 00184 sprintf(command,"xv %s &",tmpfilename); 00185 system(command); 00186 } 00187 00188 void 00189 RGBImage::loadJPEG(const char* filename, int scale) 00190 { 00191 char command[1000]; 00192 sprintf(command,"djpeg -pnm -scale 1/%d %s > %s", scale, filename, tmpfilename); 00193 system(command); 00194 cout << command << endl; 00195 loadPPM(tmpfilename); 00196 unlink(tmpfilename); 00197 } 00198 00199 void 00200 RGBImage::displayAndWait() const 00201 { 00202 savePPM(tmpfilename); 00203 char command[1000]; 00204 sprintf(command,"xv %s",tmpfilename); 00205 system(command); 00206 unlink(tmpfilename); 00207 } 00208 00209 void 00210 RGBImage::shrinkToIntersection(int& i, int& j, int& h, int& w) const 00211 { 00212 int istart = MAX(0,i); 00213 int iend = MIN(i+h,height_); 00214 int jstart = MAX(0,j); 00215 int jend = MIN(j+w,width_); 00216 00217 i = istart; 00218 j = jstart; 00219 h = MAX(iend-istart,0); 00220 w = MAX(jend-jstart,0); 00221 } 00222 00223 void 00224 RGBImage::blit(int desti, int destj, const RGBImage srcim) 00225 { 00226 int blit_i = desti; 00227 int blit_j = destj; 00228 int blit_h = srcim.height_; 00229 int blit_w = srcim.width_; 00230 00231 shrinkToIntersection(blit_i,blit_j,blit_h,blit_w); 00232 if(blit_h==0 || blit_w==0) 00233 return; 00234 00235 int src_i = blit_i-desti; 00236 int src_j = blit_j-destj; 00237 00238 for(int i=0; i<blit_h; i++) 00239 for(int j=0; j<blit_w; j++) 00240 setPixel(blit_i+i, blit_j+j, srcim.getPixel(src_i+i,src_j+j)); 00241 } 00242 00243 RGB 00244 RGBImage::computeAverage() const 00245 { 00246 if(!storage) 00247 PLERROR("Empty RGBImage"); 00248 RGB* data = storage->data; 00249 int r = 0; 00250 int g = 0; 00251 int b = 0; 00252 int npixels = height_*width_; 00253 for(int pos=0; pos<npixels; pos++) 00254 { 00255 RGB& pixl = data[pos]; 00256 r += pixl.r; 00257 g += pixl.g; 00258 b += pixl.b; 00259 } 00260 return RGB(r/npixels, g/npixels, b/npixels); 00261 } 00262 00263 Vec 00264 RGBImage::computeHistogram(int r_bins, int g_bins, int b_bins, bool do_normalize) 00265 { 00266 Vec histo(r_bins*g_bins*b_bins); 00267 for(int i=0; i<height_; i++) 00268 for(int j=0; j<width_; j++) 00269 { 00270 RGB pix = getPixel(i,j); 00271 int r_bin = pix.r*r_bins/256; 00272 int g_bin = pix.g*g_bins/256; 00273 int b_bin = pix.b*b_bins/256; 00274 int pos = r_bin*g_bins*b_bins + g_bin*b_bins + b_bin; 00275 histo[pos]++; 00276 } 00277 if(do_normalize) 00278 histo /= height_*width_; 00279 return histo; 00280 } 00281 00282 /******************/ 00283 /*** RGBImageDB ***/ 00284 /******************/ 00285 00286 RGBImageDB::RGBImageDB(int the_subsample_factor, int the_remove_border, int the_max_n_images_in_memory) 00287 : subsample_factor(the_subsample_factor), remove_border(the_remove_border), 00288 max_n_images_in_memory(the_max_n_images_in_memory), n_images_in_memory(0) 00289 { 00290 } 00291 00292 RGBImageDB::RGBImageDB(char* dbfilename, int the_subsample_factor,int the_remove_border, int the_max_n_images_in_memory) 00293 : subsample_factor(the_subsample_factor), remove_border(the_remove_border), 00294 max_n_images_in_memory(the_max_n_images_in_memory), n_images_in_memory(0) 00295 { 00296 load(dbfilename); 00297 } 00298 00299 void 00300 RGBImageDB::load(char* dbfile) 00301 { 00302 ifstream in(dbfile); 00303 if(!in) 00304 PLERROR("In RGBImageDB::load could not open dbfile %s for reading",dbfile); 00305 00306 char filename[300]; 00307 int classnum; 00308 00309 int n = 0; 00310 in >> filename >> classnum; 00311 while(in) 00312 { 00313 int imageid = append(filename); 00314 n++; 00315 imageIdAndClass.resize(n,2); 00316 imageIdAndClass(n-1,0) = imageid; 00317 imageIdAndClass(n-1,1) = classnum; 00318 in >> filename >> classnum; 00319 } 00320 } 00321 00322 int 00323 RGBImageDB::append(char* filename) 00324 { 00325 filenames.append(strdup(filename)); 00326 images.append(0); 00327 return filenames.size()-1; 00328 } 00329 00330 RGBImage 00331 RGBImageDB::getImage(int imageid) 00332 { 00333 if(!images[imageid]) 00334 { 00335 RGBImage* newimage = new RGBImage; 00336 newimage->loadJPEG(filenames[imageid],subsample_factor); 00337 if (remove_border>0) 00338 newimage->removeBorders(remove_border); 00339 n_images_in_memory++; 00340 if (n_images_in_memory>max_n_images_in_memory) 00341 { 00342 for(int i=0; i<images.size(); i++) 00343 if(images[i]!=0) 00344 { 00345 delete images[i]; 00346 images[i] = 0; 00347 } 00348 } 00349 images[imageid] = newimage; 00350 } 00351 return *images[imageid]; 00352 } 00353 00354 00355 Mat 00356 RGBImageDB::computeHistogramRepresentation(int r_bins, int g_bins, int b_bins, bool do_normalize) 00357 { 00358 Mat dataset(imageIdAndClass.length(), r_bins*g_bins*b_bins+1); 00359 Mat histograms = dataset.subMatColumns(0,r_bins*g_bins*b_bins); 00360 Mat classnums = dataset.lastColumn(); 00361 00362 for(int i=0; i<imageIdAndClass.length(); i++) 00363 { 00364 RGBImage im = getImage((int)imageIdAndClass(i,0)); 00365 histograms(i) << im.computeHistogram(r_bins,g_bins,b_bins,do_normalize); 00366 classnums(i,0) = imageIdAndClass(i,0); 00367 } 00368 return dataset; 00369 } 00370 00371 00372 RGBImageDB::~RGBImageDB() 00373 { 00374 for(int i=0; i<filenames.size(); i++) 00375 { 00376 free(filenames[i]); 00377 if(images[i]) 00378 delete images[i]; 00379 } 00380 } 00381 00382 00383 00384 00385 /*****************************/ 00386 /*** RGBImageVMatrix ***/ 00387 /*****************************/ 00388 00389 RGBImageVMatrix:: 00390 RGBImageVMatrix(RGBImage the_image, 00391 const Vec& drow, const Vec& dcol, 00392 real the_scale, 00393 real the_offset) 00394 :image(the_image), 00395 delta_row(drow), delta_column(dcol), 00396 scale(the_scale), offset_(the_offset) 00397 { 00398 if (delta_row.length()!=delta_column.length()) 00399 PLERROR("RGBImageVMatrix: delta_row(%d) and delta_column(%d) have" 00400 " differing sizes",delta_row.length(),delta_column.length()); 00401 width_ = 3 * (delta_row.length() + 1); 00402 if (delta_row.length()>0) 00403 { 00404 max_delta_row = (int)max(delta_row); 00405 int min_delta_row = (int)min(delta_row); 00406 max_delta_column = (int)max(delta_column); 00407 int min_delta_column = (int)min(delta_column); 00408 first_row = MAX(0,-min_delta_row); 00409 first_column = MAX(0,-min_delta_column); 00410 bottom_border_row = MIN(image.height(),image.height()-max_delta_row); 00411 right_border_col = MIN(image.width(),image.width()-max_delta_column); 00412 } 00413 else 00414 { 00415 first_row = 0; 00416 first_column = 0; 00417 bottom_border_row = image.height(); 00418 right_border_col = image.width(); 00419 } 00420 n_cols = right_border_col - first_column; 00421 reset(); 00422 } 00423 00424 void RGBImageVMatrix::setImage(RGBImage new_image) 00425 { 00426 image = new_image; 00427 if (delta_row.length()>0) 00428 { 00429 bottom_border_row = 00430 MIN(image.height(),image.height()-max_delta_row); 00431 right_border_col = 00432 MIN(image.width(),image.width()-max_delta_column); 00433 } 00434 else 00435 { 00436 bottom_border_row = image.height(); 00437 right_border_col = image.width(); 00438 } 00439 n_cols = right_border_col - first_column; 00440 reset(); 00441 } 00442 00443 int RGBImageVMatrix::length() 00444 { return (bottom_border_row-first_row)*n_cols; } 00445 00446 int RGBImageVMatrix::width() 00447 { return width_; } 00448 00449 void RGBImageVMatrix::reset() 00450 { 00451 current_i=first_row; 00452 current_j=first_column; 00453 } 00454 00455 void RGBImageVMatrix::seek(int position) 00456 { 00457 if (position>=length()) 00458 PLERROR("RGBImageVMatrix::seek(%d > size=%d)",position,length()); 00459 int i = position / n_cols; 00460 current_i = first_row + i; 00461 current_j = first_column + position - i*n_cols; 00462 } 00463 00464 int RGBImageVMatrix::position() 00465 { 00466 return (current_j-first_column) + (current_i-first_row)*n_cols; 00467 } 00468 00469 void RGBImageVMatrix::sample(Vec& samplevec) 00470 { 00471 samplevec.resize(width_); 00472 real *sample = samplevec.data(); 00473 real *dr = delta_row.data(); 00474 real *dc = delta_column.data(); 00475 int np = delta_row.length(); 00476 // copy the value of its neighbors 00477 for (int p=0; p<np; p++) { 00478 int offset_i=(int)dr[p]; 00479 int offset_j=(int)dc[p]; 00480 RGB parent_rgb = image.getPixel(current_i+offset_i,current_j+offset_j); 00481 *(sample++) = (parent_rgb.r+offset_)*scale; 00482 *(sample++) = (parent_rgb.g+offset_)*scale; 00483 *(sample++) = (parent_rgb.b+offset_)*scale; 00484 } 00485 00486 // copy the value of the pixel 00487 RGB target_rgb = image.getPixel(current_i,current_j); 00488 *(sample++) = (target_rgb.r+offset_)*scale; 00489 *(sample++) = (target_rgb.g+offset_)*scale; 00490 *(sample++) = (target_rgb.b+offset_)*scale; 00491 00492 // update current_i and current_j, traversing in 00493 // the usual left-to-right / top-to-bottom order: 00494 current_j++; 00495 if (current_j==right_border_col) 00496 { 00497 current_j = first_column; 00498 current_i++; 00499 if (current_i==bottom_border_row) 00500 current_i = first_row; 00501 } 00502 } 00503 00504 /*****************************/ 00505 /*** RGBImagesVMatrix ***/ 00506 /*****************************/ 00507 00508 RGBImagesVMatrix:: 00509 RGBImagesVMatrix(RGBImageDB& imagesdb, 00510 const Vec& delta_row, 00511 const Vec& delta_col, 00512 bool appendclass, 00513 real scale, 00514 real offset_) 00515 :image_distr(imagesdb.getImage((int)imagesdb.imageIdAndClass(0,0)),delta_row,delta_col,scale,offset_), 00516 images_db(imagesdb), current_image(0), append_class(appendclass), 00517 length_(0), image_start(imagesdb.imageIdAndClass.length()) 00518 { 00519 width_ = 3*(delta_row.length()+1); 00520 if(append_class) 00521 width_++; 00522 00523 for (int i=0;i<images_db.imageIdAndClass.length();i++) 00524 { 00525 image_start[i] = length_; 00526 image_distr.setImage(images_db.getImage((int)imagesdb.imageIdAndClass(i,0))); 00527 length_ += image_distr.length(); 00528 } 00529 if (append_class) 00530 { 00531 int n = image_distr.width(); 00532 pixelsAndClass.resize(n+1); 00533 pixels = pixelsAndClass.subVec(0,n); 00534 } 00535 } 00536 00537 int RGBImagesVMatrix::length() 00538 { return length_; } 00539 00540 int RGBImagesVMatrix::width() 00541 { return width_; } 00542 00543 void RGBImagesVMatrix::reset() 00544 { 00545 current_image = 0; 00546 image_distr.setImage(images_db.getImage((int)images_db.imageIdAndClass(0,0))); 00547 image_distr.reset(); 00548 } 00549 00550 void RGBImagesVMatrix::sample(Vec& samplevec) 00551 { 00552 samplevec.resize(width_); 00553 if (append_class) 00554 { 00555 Vec tmpvec = samplevec.subVec(0,width_-1); 00556 image_distr.sample(tmpvec); 00557 samplevec[width_-1] = images_db.imageIdAndClass(current_image,1); 00558 } 00559 else 00560 image_distr.sample(samplevec); 00561 00562 if (image_distr.first()) 00563 { 00564 current_image++; 00565 if (current_image == images_db.imageIdAndClass.length()) 00566 current_image = 0; 00567 image_distr.setImage(images_db.getImage((int)images_db.imageIdAndClass(current_image,0))); 00568 } 00569 } 00570 00571 00572 void RGBImagesVMatrix::seek(int position) 00573 { 00574 int n=images_db.imageIdAndClass.length(); 00575 int i; 00576 for (i=0;i<n && position>=image_start[i];i++); 00577 current_image = i-1; 00578 image_distr.setImage(images_db.getImage((int)images_db.imageIdAndClass(current_image,0))); 00579 image_distr.seek(int(position-image_start[current_image])); 00580 } 00581 00582 00583 int RGBImagesVMatrix::position() 00584 { 00585 return (int)image_start[current_image] + image_distr.position(); 00586 } 00587 00588 bool RGBImagesVMatrix::firstSampleOfObject() 00589 { 00590 return image_distr.first(); 00591 } 00592 00593 int RGBImagesVMatrix::nSamplesOfObject() 00594 { 00595 return image_distr.length(); 00596 } 00597 00598 00599 int RGBImagesVMatrix::nObjects() 00600 { 00601 return images_db.imageIdAndClass.length(); 00602 } 00603 00604 00605 /*** ImageVMatrix ***/ 00606 00607 /* 00608 00609 ImageVMatrix::ImageVMatrix(const Mat& the_data, 00610 int the_ncol_image, const Mat& the_parents, 00611 int the_border_size=1, real the_scale=1.0, 00612 real the_offset=0.0) 00613 :data(the_data), n_values_per_pixel(the_data.width()), ncol_image(the_ncol_image), 00614 parents(the_parents), border_size(the_border_size), 00615 scale(the_scale), offset(the_offset) 00616 { 00617 width_ = n_values_per_pixel * (the_parents.length() + 1); 00618 reset(); 00619 } 00620 00621 00622 void ImageVMatrix::nextPixel() 00623 { 00624 // skip the first row and the first column in the image 00625 // this will be changed 00626 currentpos++; 00627 while ((currentpos % ncol_image < border_size) || 00628 (currentpos % ncol_image >= ncol_image - border_size)) 00629 currentpos++; 00630 if(currentpos>=data.length()-ncol_image*border_size) // cycling 00631 reset(); 00632 } 00633 00634 void ImageVMatrix::reset() 00635 { 00636 currentpos = border_size*ncol_image; 00637 nextPixel(); 00638 } 00639 00640 void ImageVMatrix::sample(Vec& samplevec) 00641 { 00642 samplevec.resize(width_); 00643 real *sample = samplevec.data(); 00644 real *pixel_value; 00645 00646 // copy the value of its parents 00647 for (int p=0; p<parents.length(); p++) 00648 { 00649 int offset_x=(int)parents(p,0); 00650 int offset_y=(int)parents(p,1); 00651 pixel_value = data[currentpos+offset_x+ncol_image*offset_y]; 00652 for (int i=0; i<n_values_per_pixel; i++) 00653 sample[i] = (pixel_value[i]+offset_) * scale; 00654 } 00655 00656 // copy the value of the pixel 00657 pixel_value=data[currentpos]; 00658 for (int i=0; i<n_values_per_pixel; i++) 00659 sample[i + n_values_per_pixel * parents.length()] 00660 = (pixel_value[i]+offset_) * scale; 00661 00662 nextPixel(); 00663 } 00664 00665 int ImageVMatrix::length() 00666 { return data.length() - 2*border_size*ncol_image - 00667 2*border_size*(data.length()/ncol_image-2*border_size); } 00668 00669 int ImageVMatrix::width() 00670 { return width_; } 00671 00672 */ 00673 00674 } // end of namespace PLearn

Generated on Tue Aug 17 16:04:12 2004 for PLearn by doxygen 1.3.7