00001 /*!@file SIFT/VisualObjectMatch.C Visual Object matches */ 00002 00003 // //////////////////////////////////////////////////////////////////// // 00004 // The iLab Neuromorphic Vision C++ Toolkit - Copyright (C) 2001 by the // 00005 // University of Southern California (USC) and the iLab at USC. // 00006 // See http://iLab.usc.edu for information about this project. // 00007 // //////////////////////////////////////////////////////////////////// // 00008 // Major portions of the iLab Neuromorphic Vision Toolkit are protected // 00009 // under the U.S. patent ``Computation of Intrinsic Perceptual Saliency // 00010 // in Visual Environments, and Applications'' by Christof Koch and // 00011 // Laurent Itti, California Institute of Technology, 2001 (patent // 00012 // pending; application number 09/912,225 filed July 23, 2001; see // 00013 // http://pair.uspto.gov/cgi-bin/final/home.pl for current status). // 00014 // //////////////////////////////////////////////////////////////////// // 00015 // This file is part of the iLab Neuromorphic Vision C++ Toolkit. // 00016 // // 00017 // The iLab Neuromorphic Vision C++ Toolkit is free software; you can // 00018 // redistribute it and/or modify it under the terms of the GNU General // 00019 // Public License as published by the Free Software Foundation; either // 00020 // version 2 of the License, or (at your option) any later version. // 00021 // // 00022 // The iLab Neuromorphic Vision C++ Toolkit is distributed in the hope // 00023 // that it will be useful, but WITHOUT ANY WARRANTY; without even the // 00024 // implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // 00025 // PURPOSE. See the GNU General Public License for more details. // 00026 // // 00027 // You should have received a copy of the GNU General Public License // 00028 // along with the iLab Neuromorphic Vision C++ Toolkit; if not, write // 00029 // to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, // 00030 // Boston, MA 02111-1307 USA. // 00031 // //////////////////////////////////////////////////////////////////// // 00032 // 00033 // Primary maintainer for this file: Philip Williams <plw@usc.edu> 00034 // $HeadURL: svn://isvn.usc.edu/software/invt/trunk/saliency/src/SIFT/VisualObjectMatch.C $ 00035 // $Id: VisualObjectMatch.C 13084 2010-03-30 02:42:00Z kai $ 00036 // 00037 00038 #include "SIFT/VisualObjectMatch.H" 00039 #include "SIFT/VisualObject.H" 00040 #include "SIFT/KDTree.H" 00041 #include "SIFT/SIFThough.H" 00042 #include "Image/MatrixOps.H" 00043 #include "Image/CutPaste.H" 00044 #include "Image/DrawOps.H" 00045 00046 // ###################################################################### 00047 VisualObjectMatch::VisualObjectMatch(const rutz::shared_ptr<VisualObject>& voref, 00048 const rutz::shared_ptr<VisualObject>& votest, 00049 const VisualObjectMatchAlgo algo, 00050 const uint thresh) : 00051 itsVoRef(voref), itsVoTest(votest), itsMatches(), itsKDTree(), 00052 itsHasAff(false), itsAff(), 00053 itsHasKpAvgDist(false), itsHasAfAvgDist(false) 00054 { 00055 uint nm = 0U; 00056 00057 switch (algo) 00058 { 00059 case VOMA_SIMPLE: nm = matchSimple(thresh); break; 00060 case VOMA_KDTREE: nm = matchKDTree(thresh, 0); break; 00061 case VOMA_KDTREEBBF: nm = matchKDTree(thresh, 40); break; 00062 } 00063 LDEBUG("Got %u KP matches (th=%d) btw %s and %s", 00064 nm, thresh, voref->getName().c_str(), votest->getName().c_str()); 00065 } 00066 00067 // ###################################################################### 00068 VisualObjectMatch::VisualObjectMatch(const rutz::shared_ptr<KDTree>& kdref, 00069 const rutz::shared_ptr<VisualObject>& votest, 00070 const VisualObjectMatchAlgo algo, 00071 const uint thresh) : 00072 itsVoRef(new VisualObject("KDref")), itsVoTest(votest), itsMatches(), 00073 itsKDTree(kdref), itsHasAff(false), itsAff(), 00074 itsHasKpAvgDist(false), itsHasAfAvgDist(false) 00075 { 00076 uint nm = 0U; 00077 00078 switch (algo) 00079 { 00080 case VOMA_SIMPLE: 00081 LFATAL("Can't use Simple match when constructing from a KDTree"); break; 00082 case VOMA_KDTREE: nm = matchKDTree(thresh, 0); break; 00083 case VOMA_KDTREEBBF: nm = matchKDTree(thresh, 40); break; 00084 } 00085 LDEBUG("Got %u KP matches (th=%d) btw %s and %s", 00086 nm, thresh, itsVoRef->getName().c_str(), votest->getName().c_str()); 00087 } 00088 00089 // ###################################################################### 00090 VisualObjectMatch::VisualObjectMatch(const rutz::shared_ptr<VisualObject>& voref, 00091 const rutz::shared_ptr<VisualObject>& votest, 00092 const std::vector<KeypointMatch>& kpm) : 00093 itsVoRef(voref), itsVoTest(votest), itsMatches(kpm), 00094 itsKDTree(), itsHasAff(false), itsAff(), 00095 itsHasKpAvgDist(false), itsHasAfAvgDist(false) 00096 { 00097 LDEBUG("Got %"ZU" KP matches btw %s and %s", 00098 itsMatches.size(), itsVoRef->getName().c_str(), 00099 itsVoTest->getName().c_str()); 00100 } 00101 00102 // ###################################################################### 00103 VisualObjectMatch::~VisualObjectMatch() 00104 { } 00105 00106 // ###################################################################### 00107 uint VisualObjectMatch::prune(const uint maxn, const uint minn) 00108 { 00109 // do not go below a min number of matches: 00110 if (itsMatches.size() <= minn) return 0U; 00111 uint ndel = 0U; 00112 00113 // if we have lots of matches, start by cutting some of them easily 00114 // by enforcing a better best-to-second-best keypoint match 00115 // ratio. But don't go too far in this direction, as sometimes 00116 // poorer matches may be the correct ones: 00117 const uint targetn1 = maxn * 2U; uint distthresh = 9U; 00118 while (itsMatches.size() > targetn1 && distthresh > 5) 00119 ndel += pruneByDist(distthresh--, targetn1); 00120 LDEBUG("After pruning by distance: total %d outliers pruned.", ndel); 00121 00122 // let's now do a Hough-based pruning: 00123 const uint targetn2 = (maxn + minn) / 2; uint iter = 2U; 00124 while (itsMatches.size() > targetn2 && iter > 0) 00125 { ndel += pruneByHough(0.6F, targetn2); --iter; } 00126 LDEBUG("After pruning by Hough: total %d outliers pruned.", ndel); 00127 00128 // finally a few passes of pruning by inconsistency with the full 00129 // affine transform. Hopefully by now gross outliers have been 00130 // eliminated (especially by the Hough transform) and this will work: 00131 const uint targetn3 = minn; float dist = 5.0F; iter = 3U; 00132 while (itsMatches.size() > targetn3 && iter > 0) 00133 { ndel += pruneByAff(dist, targetn3); dist *= 0.75F; --iter; } 00134 LDEBUG("After pruning by affine: total %d outliers pruned.", ndel); 00135 00136 return ndel; 00137 } 00138 00139 // ###################################################################### 00140 uint VisualObjectMatch::pruneByDist(const uint thresh, const uint minn) 00141 { 00142 // do not go below a min number of matches: 00143 if (itsMatches.size() <= minn) return 0U; 00144 std::vector<KeypointMatch>::iterator itr = itsMatches.begin(); 00145 const uint t2 = thresh * thresh; uint ndel = 0U; 00146 00147 while (itr < itsMatches.end()) 00148 { 00149 if (100U * itr->distSq >= t2 * itr->distSq2) 00150 { 00151 itr = itsMatches.erase(itr); ++ ndel; 00152 if (itsMatches.size() <= minn) return ndel; 00153 } 00154 else ++ itr; 00155 // note: the behavior of vector::erase() guarantees this code works... 00156 } 00157 return ndel; 00158 } 00159 00160 // ###################################################################### 00161 uint VisualObjectMatch::pruneByHough(const float rangefac, const uint minn) 00162 { 00163 // do not go below a min number of matches: 00164 if (itsMatches.size() <= minn) return 0U; 00165 std::vector<KeypointMatch>::iterator 00166 itr = itsMatches.begin(), stop = itsMatches.end(); 00167 00168 // do a first pass over the matches to determine the range: 00169 float dxmi = 1.0e30F, dxma = -1.0e30F; // difference in X position 00170 float dymi = 1.0e30F, dyma = -1.0e30F; // difference in Y position 00171 float domi = 1.0e30F, doma = -1.0e30F; // difference in orientation 00172 float dsmi = 1.0e30F, dsma = -1.0e30F; // difference in scale 00173 while (itr < stop) 00174 { 00175 // get the keypoint differences: dx is the difference between X 00176 // of the test keypoint and X of the ref keypoint, etc: 00177 float dx, dy, doo, ds; 00178 getKdiff(*itr, dx, dy, doo, ds); 00179 00180 if (dx < dxmi) dxmi = dx; else if (dx > dxma) dxma = dx; 00181 if (dy < dymi) dymi = dy; else if (dy > dyma) dyma = dy; 00182 if (ds < dsmi) dsmi = ds; else if (ds > dsma) dsma = ds; 00183 if (doo < domi) domi = doo; else if (doo > doma) doma = doo; 00184 00185 ++ itr; 00186 } 00187 //LINFO("dx = [%f .. %f]", dxmi, dxma); 00188 //LINFO("dy = [%f .. %f]", dymi, dyma); 00189 //LINFO("do = [%f .. %f]", domi, doma); 00190 //LINFO("ds = [%f .. %f]", dsmi, dsma); 00191 00192 // make sure our ranges are not empty: 00193 if (dxma - dxmi < 1.0F) dxma = dxmi + 1.0F; 00194 if (dyma - dymi < 1.0F) dyma = dymi + 1.0F; 00195 if (doma - domi < 1.0e-3F) doma = domi + 1.0e-3F; 00196 if (dsma - dsmi < 1.0e-3F) dsma = dsmi + 1.0e-3F; 00197 00198 // we are going to divide each range into eight bins: 00199 const float facx = 8.0F / (dxma - dxmi); 00200 const float facy = 8.0F / (dyma - dymi); 00201 const float faco = 8.0F / (doma - domi); 00202 const float facs = 8.0F / (dsma - dsmi); 00203 00204 // let's populate a SIFThough: 00205 SIFThough h; 00206 itr = itsMatches.begin(); 00207 while (itr < stop) 00208 { 00209 // get the keypoint differences: 00210 float dx, dy, doo, ds; 00211 getKdiff(*itr, dx, dy, doo, ds); 00212 00213 // add to our Hough accumulator: 00214 h.addValue((dx - dxmi) * facx, (dy - dymi) * facy, 00215 (doo - domi) * faco, (ds - dsmi) * facs, 1.0F); 00216 00217 ++ itr; 00218 } 00219 00220 // all right, let's get the peak out: 00221 float peakx, peaky, peako, peaks; 00222 h.getPeak(peakx, peaky, peako, peaks); 00223 00224 // convert back from bin to real coordinates: 00225 peakx = peakx / facx + dxmi; 00226 peaky = peaky / facy + dymi; 00227 peako = peako / faco + domi; 00228 peaks = peaks / facs + dsmi; 00229 //LINFO("Peak at dx=%f, dy=%f, do=%f, ds=%f", peakx, peaky, peako, peaks); 00230 00231 // compute the acceptable range: 00232 const float rxmi = peakx - rangefac * (dxma - dxmi); 00233 const float rxma = peakx + rangefac * (dxma - dxmi); 00234 const float rymi = peaky - rangefac * (dyma - dymi); 00235 const float ryma = peaky + rangefac * (dyma - dymi); 00236 const float romi = peako - rangefac * (doma - domi); 00237 const float roma = peako + rangefac * (doma - domi); 00238 const float rsmi = peaks - rangefac * (dsma - dsmi); 00239 const float rsma = peaks + rangefac * (dsma - dsmi); 00240 00241 // let's prune away matches that are more than some fraction of the 00242 // range from the peak: 00243 uint ndel = 0U; itr = itsMatches.begin(); 00244 while (itr < itsMatches.end()) // do not use 'stop' as size will shrink 00245 { 00246 // get the keypoint differences: 00247 float dx, dy, doo, ds; 00248 getKdiff(*itr, dx, dy, doo, ds); 00249 00250 // prune that outlier? 00251 if (dx < rxmi || dx > rxma || 00252 dy < rymi || dy > ryma || 00253 doo < romi || doo > roma || 00254 ds < rsmi || ds > rsma) 00255 { 00256 itr = itsMatches.erase(itr); ++ ndel; 00257 if (itsMatches.size() <= minn) return ndel; 00258 } 00259 else 00260 ++ itr; 00261 } 00262 return ndel; 00263 } 00264 00265 // ###################################################################### 00266 uint VisualObjectMatch::pruneByAff(const float dist, const uint minn) 00267 { 00268 // do not go below a min number of matches: 00269 if (itsMatches.size() <= minn) return 0U; 00270 uint ndel = 0U; const float dist2 = dist * dist; 00271 00272 // get our affine transform given our current matches: 00273 computeAffine(); 00274 00275 // loop over our matches and find the outliers: 00276 std::vector<KeypointMatch>::iterator itr = itsMatches.begin(); 00277 while (itr < itsMatches.end()) 00278 { 00279 // get residual distance between affine-transformed ref 00280 // keypoint and test keypoint: 00281 const float d = itsAff.getResidualDistSq(*itr); 00282 00283 if (d > dist2) 00284 { 00285 // delete that outlier match: 00286 itr = itsMatches.erase(itr); ++ndel; 00287 00288 // do not go below a min number of remaining matches: 00289 if (itsMatches.size() <= minn) return ndel; 00290 } 00291 else 00292 ++ itr; 00293 } 00294 return ndel; 00295 } 00296 00297 // ###################################################################### 00298 void VisualObjectMatch::computeAffine() 00299 { 00300 const uint nmatches = itsMatches.size(); 00301 00302 // we require at least 3 matches for this to work: 00303 if (nmatches < 3) 00304 { 00305 LDEBUG("Too few matches (%u) -- RETURNING IDENTITY", nmatches); 00306 itsAff = SIFTaffine(); // default constructor is identity 00307 return; 00308 } 00309 00310 // we are going to solve the linear system Ax=b in the least-squares sense 00311 Image<float> A(3, nmatches, NO_INIT); 00312 Image<float> b(2, nmatches, NO_INIT); 00313 00314 for (uint i = 0; i < nmatches; i ++) 00315 { 00316 rutz::shared_ptr<Keypoint> refkp = itsMatches[i].refkp; 00317 rutz::shared_ptr<Keypoint> tstkp = itsMatches[i].tstkp; 00318 00319 A.setVal(0, i, refkp->getX()); 00320 A.setVal(1, i, refkp->getY()); 00321 A.setVal(2, i, 1.0f); 00322 00323 b.setVal(0, i, tstkp->getX()); 00324 b.setVal(1, i, tstkp->getY()); 00325 } 00326 00327 try 00328 { 00329 // the solution to Ax=b is x = [A^t A]^-1 A^t b: 00330 Image<float> At = transpose(A); 00331 00332 Image<float> x = 00333 matrixMult(matrixMult(matrixInv(matrixMult(At, A)), At), b); 00334 00335 // store into our SIFTaffine: 00336 itsAff.m1 = x.getVal(0, 0); itsAff.m3 = x.getVal(1, 0); 00337 itsAff.m2 = x.getVal(0, 1); itsAff.m4 = x.getVal(1, 1); 00338 itsAff.tx = x.getVal(0, 2); itsAff.ty = x.getVal(1, 2); 00339 00340 // ok, we have it: 00341 itsHasAff = true; 00342 } 00343 catch (SingularMatrixException& e) 00344 { 00345 LDEBUG("Couldn't invert matrix -- RETURNING IDENTITY"); 00346 itsAff = SIFTaffine(); // default constructor is identity 00347 itsHasAff = false; 00348 } 00349 } 00350 00351 // ###################################################################### 00352 bool VisualObjectMatch::checkSIFTaffine(const float maxrot, 00353 const float maxscale, 00354 const float maxshear) 00355 { 00356 if (itsHasAff == false) computeAffine(); 00357 if (itsAff.isInversible() == false) return false; 00358 00359 float theta, sx, sy, str; 00360 itsAff.decompose(theta, sx, sy, str); 00361 00362 LDEBUG("theta=%fdeg sx=%f sy=%f shx=%f shy=%f", 00363 theta * 180.0F / M_PI, sx, sy, str/sx, str/sy); 00364 00365 // check the rotation: 00366 if (fabsf(theta) > maxrot) return false; 00367 00368 // check the scaling: 00369 if (fabsf(sx) > maxscale || fabsf(sx) < 1.0F / maxscale) return false; 00370 if (fabsf(sy) > maxscale || fabsf(sy) < 1.0F / maxscale) return false; 00371 00372 // check the shearing. Note: from the previous check, we are 00373 // guaranteed that sx and sy are non-zero: 00374 if (fabsf(str/sx) > maxshear) return false; 00375 if (fabsf(str/sy) > maxshear) return false; 00376 00377 // if we get here, the affine is not weird: 00378 return true; 00379 } 00380 00381 // ###################################################################### 00382 float VisualObjectMatch::getScore(const float kcoeff, 00383 const float acoeff) 00384 { 00385 if(!itsHasKpAvgDist) getKeypointAvgDist(); 00386 if(!itsHasAfAvgDist) getAffineAvgDist(); 00387 00388 // keypoint average distance 00389 // FIX: figure out a good ratio with the affine 00390 // cap out at .05 for now 00391 float kp = itsKpAvgDist; if(kp < .05) kp = .05; 00392 00393 // affine average distance: capped out at .05 00394 // object that close in affine transform is probably a good match 00395 // no need to insert in a lower value 00396 // otherwise it will saturates the other factor 00397 float af = itsAfAvgDist; if(af < .05) af = .05; 00398 00399 // number of keypoint matches score 00400 // cap at 20 matches: .05 * 20 = 1.0 00401 // any bigger should not add more weight; they're all good matches 00402 float nm = 0.05F * float(itsMatches.size()); if(nm > 1.0) nm = 1.0F; 00403 00404 // matching score: 00405 // max value: 21.0: .5/.05 + .5/.05 + 1.0 = 10.0 + 10.0 + 1.0 00406 float score = kcoeff/kp + acoeff/af + nm; 00407 00408 return score; 00409 } 00410 00411 // ###################################################################### 00412 float VisualObjectMatch::getSalScore(const float wcoeff, 00413 const float hcoeff ) 00414 { 00415 // score = feature similarity * distance 00416 float sscore = getSalDiff(); 00417 if(sscore == -1.0F) return sscore; 00418 float sdist = getSalDist(); 00419 if(sdist == -1.0F) return sscore; 00420 00421 float maxDist = 0.0; 00422 rutz::shared_ptr<VisualObject> obj1 = getVoRef(); 00423 rutz::shared_ptr<VisualObject> obj2 = getVoTest(); 00424 if(wcoeff == 0.0F && hcoeff == 0.0F) 00425 { 00426 uint w1 = obj1->getImage().getWidth(); 00427 uint h1 = obj1->getImage().getHeight(); 00428 float dist1 = sqrt(w1*w1 + h1*h1); 00429 uint w2 = obj2->getImage().getWidth(); 00430 uint h2 = obj2->getImage().getHeight(); 00431 float dist2 = sqrt(w2*w2 + h2*h2); 00432 maxDist = dist1 + dist2; 00433 } 00434 else 00435 { 00436 maxDist = sqrt(wcoeff * wcoeff + hcoeff * hcoeff); 00437 } 00438 float dscore = 1.0 - sdist/maxDist; 00439 LINFO("dist score : 1.0 - %f/%f = %f", sdist, maxDist, dscore); 00440 00441 float score = dscore * sscore; 00442 LINFO("dist * sim: %f * %f = %f", dscore, sscore, score); 00443 00444 return score; 00445 } 00446 00447 // ###################################################################### 00448 float VisualObjectMatch::getSalDiff() 00449 { 00450 // check if both has salient points and feature vectors 00451 rutz::shared_ptr<VisualObject> obj1 = getVoRef(); 00452 rutz::shared_ptr<VisualObject> obj2 = getVoTest(); 00453 const std::vector<float>& feat1 = obj1->getFeatures(); 00454 const std::vector<float>& feat2 = obj2->getFeatures(); 00455 bool compfeat = ((feat1.size() > 0) && (feat1.size() == feat2.size())); 00456 if (!compfeat) return -1.0F; 00457 00458 // feature similarity [ 0.0 ... 1.0 ]: 00459 float cval = 0.0; 00460 for(uint i = 0; i < feat1.size(); i++) 00461 { 00462 const float val = feat1[i] - feat2[i]; 00463 cval += val * val; 00464 } 00465 cval = sqrtf(cval / feat1.size()); 00466 float sscore = 1.0F - cval; 00467 LDEBUG("cval: %f: score: %f", cval, sscore); 00468 return sscore; 00469 } 00470 00471 // ###################################################################### 00472 float VisualObjectMatch::getSalDist() 00473 { 00474 // check if both has salient points and feature vectors 00475 rutz::shared_ptr<VisualObject> obj1 = getVoRef(); 00476 rutz::shared_ptr<VisualObject> obj2 = getVoTest(); 00477 Point2D<int> salpt1 = obj1->getSalPoint(); 00478 Point2D<int> salpt2 = obj2->getSalPoint(); 00479 bool compfeat = ((salpt1.i != -1) && (salpt2.i != -1)); 00480 if(!compfeat) return -1.0F; 00481 00482 // forward affine transform [A * ref -> tst ] 00483 SIFTaffine aff = getSIFTaffine(); 00484 float u, v; aff.transform(salpt1.i, salpt1.j, u, v); 00485 float dist = salpt2.distance(Point2D<int>(int(u+0.5F),int(v+0.5F))); 00486 LDEBUG("pos1: (%d,%d) -> (%f,%f) & pos2: (%d,%d): dist: %f", 00487 salpt1.i, salpt1.j, u, v, salpt2.i, salpt2.j, dist); 00488 return dist; 00489 } 00490 00491 // ###################################################################### 00492 float VisualObjectMatch::getKeypointAvgDist() 00493 { 00494 if(itsHasKpAvgDist) return itsKpAvgDist; 00495 if (itsMatches.size() == 0U) return 1.0e30F; 00496 00497 float d = 0.0F; 00498 std::vector<KeypointMatch>::const_iterator 00499 itr = itsMatches.begin(), stop = itsMatches.end(); 00500 float fac = 1.0F; if (itr != stop) fac /= float(itr->refkp->getFVlength()); 00501 00502 while(itr != stop) 00503 { 00504 d += sqrtf(float(itr->refkp->distSquared(itr->tstkp)) * fac); ++itr; 00505 } 00506 00507 itsKpAvgDist = 0.1F * d / float(itsMatches.size()); 00508 itsHasKpAvgDist = true; 00509 00510 return itsKpAvgDist; 00511 } 00512 00513 // ###################################################################### 00514 float VisualObjectMatch::getAffineAvgDist() 00515 { 00516 if(itsHasAfAvgDist) return itsAfAvgDist; 00517 if (itsMatches.size() < 3U) return 1.0e30F; 00518 if (itsHasAff == false) computeAffine(); 00519 00520 float d = 0.0F; 00521 std::vector<KeypointMatch>::const_iterator 00522 itr = itsMatches.begin(), stop = itsMatches.end(); 00523 00524 while(itr != stop) 00525 { d += sqrtf(itsAff.getResidualDistSq(*itr)); ++itr; } 00526 00527 itsAfAvgDist = d / float(itsMatches.size()); 00528 itsHasAfAvgDist = true; 00529 00530 return itsAfAvgDist; 00531 } 00532 00533 // ###################################################################### 00534 uint VisualObjectMatch::matchSimple(const uint thresh) 00535 { 00536 const uint refnkp = itsVoRef->numKeypoints(); 00537 const uint tstnkp = itsVoTest->numKeypoints(); 00538 if (refnkp == 0 || tstnkp == 0) return 0U; 00539 00540 const int maxdsq = itsVoRef->getKeypoint(0)->maxDistSquared(); 00541 uint nmatches = 0; uint thresh2 = thresh * thresh; 00542 00543 // loop over all of the test object's keypoints: 00544 for (uint i = 0; i < tstnkp; i++) 00545 { 00546 int distsq1 = maxdsq, distsq2 = maxdsq; 00547 rutz::shared_ptr<Keypoint> tstkey = itsVoTest->getKeypoint(i); 00548 rutz::shared_ptr<Keypoint> refkey; 00549 00550 // loop over all of the ref object's keypoints: 00551 for (uint j = 0; j < refnkp; j ++) 00552 { 00553 rutz::shared_ptr<Keypoint> rkey = itsVoRef->getKeypoint(j); 00554 const int distsq = rkey->distSquared(tstkey); 00555 00556 // is this better than our best one? 00557 if (distsq < distsq1) 00558 { 00559 distsq2 = distsq1; // old best becomes second best 00560 distsq1 = distsq; // we got a new best 00561 refkey = rkey; // remember the best keypoint 00562 } 00563 else if (distsq < distsq2) // maybe between best and second best? 00564 distsq2 = distsq; 00565 } 00566 00567 // Check that best distance less than thresh of second best distance: 00568 if (100U * distsq1 < thresh2 * distsq2) 00569 { 00570 KeypointMatch m; 00571 m.refkp = refkey; m.tstkp = tstkey; 00572 m.distSq = distsq1; m.distSq2 = distsq2; 00573 itsMatches.push_back(m); 00574 ++ nmatches; 00575 } 00576 } 00577 00578 // return number of matches: 00579 return nmatches; 00580 } 00581 00582 // ###################################################################### 00583 uint VisualObjectMatch::matchKDTree(const uint thresh, const int bbf) 00584 { 00585 const uint refnkp = itsVoRef->numKeypoints(); 00586 const uint tstnkp = itsVoTest->numKeypoints(); 00587 if (refnkp == 0 || tstnkp == 0) return 0U; 00588 const int maxdsq = itsVoRef->getKeypoint(0)->maxDistSquared(); 00589 uint nmatches = 0; uint thresh2 = thresh * thresh; 00590 00591 // do we already have a valid KDTree for our ref object? Otherwise 00592 // let's build one: 00593 if (itsKDTree.is_invalid()) 00594 { 00595 LINFO("Building KDTree for VisualObject '%s'...", 00596 itsVoRef->getName().c_str()); 00597 itsKDTree.reset(new KDTree(itsVoRef->getKeypoints())); 00598 LINFO("KDTree for VisualObject '%s' complete.", 00599 itsVoRef->getName().c_str()); 00600 } 00601 00602 // loop over all of our test object's keypoints: 00603 for (uint i = 0; i < tstnkp; i++) 00604 { 00605 int distsq1 = maxdsq, distsq2 = maxdsq; 00606 rutz::shared_ptr<Keypoint> tstkey = itsVoTest->getKeypoint(i); 00607 00608 // find nearest neighbor in our KDTree: 00609 uint matchIndex = bbf > 0 ? 00610 itsKDTree->nearestNeighborBBF(tstkey, bbf, distsq1, distsq2) : 00611 itsKDTree->nearestNeighbor(tstkey, distsq1, distsq2); 00612 00613 // Check that best distance less than 0.6 of second best distance: 00614 if (100U * distsq1 < thresh2 * distsq2) 00615 { 00616 KeypointMatch m; 00617 m.refkp = itsVoRef->getKeypoint(matchIndex); m.tstkp = tstkey; 00618 m.distSq = distsq1; m.distSq2 = distsq2; 00619 itsMatches.push_back(m); 00620 ++ nmatches; 00621 } 00622 } 00623 00624 // return number of matches: 00625 return nmatches; 00626 } 00627 00628 // ###################################################################### 00629 Image< PixRGB<byte> > 00630 VisualObjectMatch::getMatchImage(const float scale) const 00631 { 00632 // get a keypoint image (without vectors) for both objects: 00633 Image< PixRGB<byte> > refimg = itsVoRef->getKeypointImage(scale, 0.0F); 00634 Image< PixRGB<byte> > tstimg = itsVoTest->getKeypointImage(scale, 0.0F); 00635 LDEBUG("r[%d %d] t[%d %d]", 00636 refimg.getWidth(), refimg.getHeight(), 00637 tstimg.getWidth(), tstimg.getHeight()); 00638 00639 // put ref image on top of the other: 00640 int refdx, tstdx; 00641 const int w = std::max(refimg.getWidth(), tstimg.getWidth()); 00642 const int dy = refimg.getHeight(); 00643 const PixRGB<byte> greycol(128), linkcol(255, 200, 100); 00644 Image< PixRGB<byte> > combo(w, dy + tstimg.getHeight(), ZEROS); 00645 00646 if (refimg.getWidth() > tstimg.getWidth()) 00647 { refdx = 0; tstdx = (w - tstimg.getWidth()) / 2; } 00648 else 00649 { refdx = (w - refimg.getWidth()) / 2; tstdx = 0; } 00650 00651 if (refimg.coordsOk(itsVoRef->getSalPoint())) 00652 drawDisk(refimg, itsVoRef->getSalPoint(), 3, PixRGB<byte>(255,255,0)); 00653 00654 if (tstimg.coordsOk(itsVoTest->getSalPoint())) 00655 drawDisk(tstimg, itsVoTest->getSalPoint(), 3, PixRGB<byte>(255,255,0)); 00656 00657 inplacePaste(combo, refimg, Point2D<int>(refdx, 0)); 00658 inplacePaste(combo, tstimg, Point2D<int>(tstdx, dy)); 00659 00660 drawLine(combo, Point2D<int>(0, dy-1), Point2D<int>(w-1, dy-1), greycol); 00661 drawLine(combo, Point2D<int>(0, dy), Point2D<int>(w-1, dy), greycol); 00662 00663 // let's link the matched keypoints: 00664 const uint nm = itsMatches.size(); 00665 for (uint i = 0; i < nm; i ++) 00666 { 00667 rutz::shared_ptr<Keypoint> refk = itsMatches[i].refkp; 00668 rutz::shared_ptr<Keypoint> tstk = itsMatches[i].tstkp; 00669 00670 drawLine(combo, 00671 Point2D<int>(int(refk->getX() * scale + 0.5F) + refdx, 00672 int(refk->getY() * scale + 0.5F)), 00673 Point2D<int>(int(tstk->getX() * scale + 0.5F) + tstdx, 00674 int(tstk->getY() * scale + 0.5F) + dy), 00675 linkcol); 00676 } 00677 00678 return combo; 00679 } 00680 00681 // ###################################################################### 00682 Image< PixRGB<byte> > 00683 VisualObjectMatch::getMatchImage(Dims frameSize, 00684 Point2D<int> refOffset, Point2D<int> testOffset, 00685 const float scale) const 00686 { 00687 int w = frameSize.w(); 00688 int h = frameSize.h(); 00689 00690 // get a keypoint image (without vectors) for both objects: 00691 Image< PixRGB<byte> > refimg = 00692 getVoRef()->getKeypointImage(scale, 0.0F); 00693 Image< PixRGB<byte> > tstimg = 00694 getVoTest()->getKeypointImage(scale, 0.0F); 00695 00696 // draw the salient point locations 00697 drawDisk(refimg, getVoRef()->getSalPoint(), 3, PixRGB<byte>(255,255,0)); 00698 drawDisk(tstimg, getVoTest()->getSalPoint(), 3, PixRGB<byte>(255,255,0)); 00699 00700 const PixRGB<byte> greycol(128), linkcol(255, 200, 100); 00701 Image< PixRGB<byte> > combo(w, 2*h, ZEROS); 00702 00703 // put ref image on top of the other: 00704 inplacePaste(combo, refimg, Point2D<int>(0, 0)+ refOffset); 00705 inplacePaste(combo, tstimg, Point2D<int>(0, h)+ testOffset); 00706 00707 drawLine(combo, Point2D<int>(0, h-1), Point2D<int>(w-1, h-1), greycol); 00708 00709 // let's link the matched keypoints: 00710 std::vector<KeypointMatch> matches = getKeypointMatches(); 00711 00712 const uint nm = matches.size(); 00713 for (uint i = 0; i < nm; i ++) 00714 { 00715 rutz::shared_ptr<Keypoint> refk = matches[i].refkp; 00716 rutz::shared_ptr<Keypoint> tstk = matches[i].tstkp; 00717 00718 drawLine 00719 (combo, 00720 Point2D<int>(int(refk->getX() * scale + 0.5F), 00721 int(refk->getY() * scale + 0.5F)) 00722 + refOffset, 00723 Point2D<int>(int(tstk->getX() * scale + 0.5F), 00724 int(tstk->getY() * scale + 0.5F)) 00725 + testOffset + Point2D<int>(0,h), 00726 linkcol); 00727 } 00728 00729 return combo; 00730 } 00731 00732 // ###################################################################### 00733 Image< PixRGB<byte> > VisualObjectMatch:: 00734 getTransfTestImage(const Image< PixRGB<byte> >& im) 00735 { 00736 SIFTaffine aff = getSIFTaffine(); 00737 00738 // we loop over all pixel locations in the ref image, transform the 00739 // coordinates using the forward affine transform, get the pixel 00740 // value in the test image, and plot it: 00741 Image< PixRGB<byte> > result(im); 00742 if (result.initialized() == false) 00743 result.resize(itsVoRef->getImage().getDims(), true); 00744 Image< PixRGB<byte> > tsti = itsVoTest->getImage(); 00745 00746 uint w = result.getWidth(), h = result.getHeight(); 00747 Image< PixRGB<byte> >::iterator dptr = result.beginw(); 00748 00749 for (uint j = 0; j < h; j ++) 00750 for (uint i = 0; i < w; i ++) 00751 { 00752 float u, v; 00753 aff.transform(float(i), float(j), u, v); 00754 00755 if (tsti.coordsOk(u, v)) 00756 *dptr++ = tsti.getValInterp(u, v); 00757 else 00758 ++dptr; 00759 } 00760 return result; 00761 } 00762 00763 // ###################################################################### 00764 void VisualObjectMatch:: 00765 getTransfTestOutline(Point2D<int>& tl, Point2D<int>& tr, Point2D<int>& br, Point2D<int>& bl) 00766 { 00767 SIFTaffine a = getSIFTaffine(); 00768 SIFTaffine aff = a.inverse(); 00769 00770 // transform the four corners of the test image using the inverse affine: 00771 00772 const Dims objSize = itsVoTest->getObjectSize(); 00773 const uint w = objSize.w(); 00774 const uint h = objSize.h(); 00775 float u, v; 00776 00777 aff.transform(0.0F, 0.0F, u, v); 00778 tl.i = int(u + 0.5F); tl.j = int(v + 0.5F); 00779 00780 aff.transform(float(w-1), 0.0F, u, v); 00781 tr.i = int(u + 0.5F); tr.j = int(v + 0.5F); 00782 00783 aff.transform(float(w-1), float(h-1), u, v); 00784 br.i = int(u + 0.5F); br.j = int(v + 0.5F); 00785 00786 aff.transform(0.0F, float(h-1), u, v); 00787 bl.i = int(u + 0.5F); bl.j = int(v + 0.5F); 00788 } 00789 00790 // ###################################################################### 00791 Image< PixRGB<byte> > VisualObjectMatch::getFusedImage(const float mix) 00792 { 00793 SIFTaffine aff = getSIFTaffine(); 00794 00795 // we loop over all pixel locations in the ref image, transform the 00796 // coordinates using the forward affine transform, get the pixel 00797 // value in the test image, and mix: 00798 Image< PixRGB<byte> > refi = itsVoRef->getImage(); 00799 Image< PixRGB<byte> > tsti = itsVoTest->getImage(); 00800 00801 uint w = refi.getWidth(), h = refi.getHeight(); 00802 Image< PixRGB<byte> > result(w, h, NO_INIT); 00803 Image< PixRGB<byte> >::const_iterator rptr = refi.begin(); 00804 Image< PixRGB<byte> >::iterator dptr = result.beginw(); 00805 00806 for (uint j = 0; j < h; j ++) 00807 for (uint i = 0; i < w; i ++) 00808 { 00809 float u, v; 00810 aff.transform(float(i), float(j), u, v); 00811 PixRGB<byte> rval = *rptr++; 00812 00813 if (tsti.coordsOk(u, v)) 00814 { 00815 PixRGB<byte> tval = tsti.getValInterp(u, v); 00816 PixRGB<byte> mval = PixRGB<byte>(rval * mix + tval * (1.0F - mix)); 00817 *dptr++ = mval; 00818 } 00819 else 00820 *dptr++ = PixRGB<byte>(rval * mix); 00821 } 00822 return result; 00823 } 00824 00825 // ###################################################################### 00826 Point2D<int> VisualObjectMatch::getSpatialDist 00827 ( Point2D<int> offset1, Point2D<int> offset2) 00828 { 00829 // get the matches 00830 // translation is: t = Ar + b 00831 // [testX] [ a.m1 a.m2 ] [refX] [ a.tx ] 00832 // [testY] = [ a.m3 a.m4 ] [refY] + [ a.ty ] 00833 SIFTaffine a = getSIFTaffine(); 00834 00835 // get the needed matrix 00836 Image<double> A(2,2, ZEROS); 00837 A.setVal(0, 0, a.m1); A.setVal(1, 0, a.m2); 00838 A.setVal(0, 1, a.m3); A.setVal(1, 1, a.m4); 00839 00840 Image<double> b(1,2, ZEROS); 00841 b.setVal(0, 0, a.tx); 00842 b.setVal(0, 1, a.ty); 00843 00844 Image<double> r(1,2,ZEROS); 00845 r.setVal(0,0, -offset1.i); 00846 r.setVal(0,1, -offset1.j); 00847 00848 LINFO("[ %7.3f %7.3f ][ %7.3f ] [ %7.3f ]", 00849 A.getVal(0,0), A.getVal(1,0), r.getVal(0,0), b.getVal(0,0)); 00850 LINFO("[ %7.3f %7.3f ][ %7.3f ] + [ %7.3f ]", 00851 A.getVal(0,1), A.getVal(1,1), r.getVal(0,1), b.getVal(0,1)); 00852 00853 Image<double> diff = matrixMult(A,r) + b; 00854 Point2D<int> diffPt = Point2D<int>(int(diff.getVal(0,0)), 00855 int(diff.getVal(0,1))); 00856 Point2D<int> res = diffPt + offset2; 00857 00858 LINFO("diff:[%d %d] + offset:[%d %d] = [%d %d]", 00859 diffPt.i, diffPt.j, offset2.i, offset2.j, res.i, res.j); 00860 return res; 00861 } 00862 00863 // ###################################################################### 00864 Rectangle VisualObjectMatch::getOverlapRect() 00865 { 00866 // NOTE: overlap assume only translational transformation 00867 00868 SIFTaffine aff = getSIFTaffine(); 00869 00870 // we loop over all pixel locations in the ref image, transform the 00871 // coordinates using the forward affine transform [A * ref -> tst ] 00872 // get the pixel value in the ref image and estimate 00873 00874 Image< PixRGB<byte> > refi = getVoRef()->getImage(); 00875 Image< PixRGB<byte> > tsti = getVoTest()->getImage(); 00876 00877 uint wr = refi.getWidth(), hr = refi.getHeight(); 00878 uint wt = tsti.getWidth(), ht = tsti.getHeight(); 00879 LDEBUG("r[%d,%d] t[%d,%d]", wr, hr, wt, ht); 00880 00881 // get the affine transforms of the test image corners 00882 // top-left corner of refI 00883 float u, v; 00884 aff.transform(float(0), float(0), u, v); 00885 float t = v, l = u, b = v, r = u; 00886 LDEBUG("t-l: [%f,%f]: [%f,%f,%f,%f]", u, v, t, l, b, r); 00887 00888 // top-right corner of refI 00889 aff.transform(float(wr-1), float(0), u, v); 00890 if(u < l) l = u; if(u > r) r = u; 00891 if(v < t) t = v; if(v > b) b = v; 00892 LDEBUG("t-r: [%f,%f]: [%f,%f,%f,%f]", u, v, t, l, b, r); 00893 00894 // bottom-left corner of refI 00895 aff.transform(float(0), float(hr-1), u, v); 00896 if(u < l) l = u; if(u > r) r = u; 00897 if(v < t) t = v; if(v > b) b = v; 00898 LDEBUG("b-l: [%f,%f]: [%f,%f,%f,%f]", u, v, t, l, b, r); 00899 00900 // bottom-right corner of refI 00901 aff.transform(float(wr-1), float(hr-1), u, v); 00902 if(u < l) l = u; if(u > r) r = u; 00903 if(v < t) t = v; if(v > b) b = v; 00904 LDEBUG("b-r: [%f,%f]: [%f,%f,%f,%f]", u, v, t, l, b, r); 00905 00906 const Rectangle refr = Rectangle::tlbrI(int(t+0.5F), int(l+0.5F), 00907 int(b+0.5F), int(r+0.5F)); 00908 const Rectangle tstr = tsti.getBounds(); 00909 const Rectangle ovl = refr.getOverlap(tstr); 00910 LINFO("overlap at: [%d, %d, %d, %d], %d, %d", 00911 ovl.left(), ovl.top(), ovl.rightI(), ovl.bottomI(), 00912 ovl.width(),ovl.height()); 00913 00914 return ovl; 00915 } 00916 00917 // ###################################################################### 00918 bool VisualObjectMatch::isOverlapping() 00919 { 00920 Image< PixRGB<byte> > refi = getVoRef()->getImage(); 00921 Image< PixRGB<byte> > tsti = getVoTest()->getImage(); 00922 00923 const Rectangle a = refi.getBounds(); 00924 const Rectangle b = tsti.getBounds(); 00925 00926 // print the rectangles 00927 const Rectangle ovl = getOverlapRect(); 00928 00929 LINFO("o:[%d, %d, %d, %d], [%d,%d] dbo:[%d, %d, %d, %d], [%d,%d]", 00930 a.left(), a.top(), a.rightI(), a.bottomI(), a.width(), a.height(), 00931 b.left(), b.top(), b.rightI(), b.bottomI(), b.width(), b.height() ); 00932 LINFO("overlap at: [%d, %d, %d, %d], %d, %d", 00933 ovl.left(), ovl.top(), ovl.rightI(), ovl.bottomI(), 00934 ovl.width(),ovl.height()); 00935 00936 // check percentage of overlap 50% 00937 if(!ovl.isValid()) { LINFO("do not overlap"); return false; } 00938 float ovlA = (ovl.width() * ovl.height())/(a.width() * a.height()+0.0); 00939 float ovlB = (ovl.width() * ovl.height())/(b.width() * b.height()+0.0); 00940 LINFO("ovl: a= %f, b= %f", ovlA, ovlB); 00941 if((ovlA > .5 && ovlB > .5) || (ovlA > .8) || (ovlB > .8)) 00942 { LINFO("the objects overlap"); return true; } 00943 else 00944 { LINFO("objects do not significantly overlap"); return false; } 00945 // maybe later check which one is the smaller one 00946 } 00947 00948 // ###################################################################### 00949 bool VisualObjectMatch::isOverlapping2() 00950 { 00951 rutz::shared_ptr<VisualObject> obj1 = getVoRef(); 00952 rutz::shared_ptr<VisualObject> obj2 = getVoTest(); 00953 LINFO("obj1 size %d, obj2 size: %d match size: %d", 00954 obj1->numKeypoints(), obj2->numKeypoints(), size()); 00955 00956 // get object 1 borders - from keypoints 00957 float t1 = -1.0f, b1 = -1.0f, l1 = -1.0f, r1 = -1.0f; 00958 if(obj1->numKeypoints() > 0) 00959 { 00960 float x = obj1->getKeypoint(0)->getX(); 00961 float y = obj1->getKeypoint(0)->getY(); 00962 t1 = y, b1 = y, l1 = x, r1 = x; 00963 //LINFO("[%f %f]",x,y); 00964 } 00965 for(uint i = 1; i < obj1->numKeypoints(); i++) 00966 { 00967 float x = obj1->getKeypoint(i)->getX(); 00968 float y = obj1->getKeypoint(i)->getY(); 00969 00970 if(t1 > y) t1 = y; else if(b1 < y) b1 = y; 00971 if(l1 > x) l1 = x; else if(r1 < x) r1 = x; 00972 //LINFO("[%f %f]",x,y); 00973 } 00974 float area1 = (b1 - t1)*(r1 - l1); 00975 LINFO("obj1[%d]: t1: %f, b1: %f, l1: %f, r1: %f; A: %f", 00976 obj1->numKeypoints(), t1, b1, l1, r1, area1); 00977 00978 // get object 1 borders - from keypoints 00979 float t2 = -1.0f, b2 = -1.0f, l2 = -1.0f, r2 = -1.0f; 00980 if(obj2->numKeypoints() > 0) 00981 { 00982 float x = obj2->getKeypoint(0)->getX(); 00983 float y = obj2->getKeypoint(0)->getY(); 00984 t2 = y, b2 = y, l2 = x, r2 = x; 00985 //LINFO("[%f %f]",x,y); 00986 } 00987 for(uint i = 1; i < obj2->numKeypoints(); i++) 00988 { 00989 float x = obj2->getKeypoint(i)->getX(); 00990 float y = obj2->getKeypoint(i)->getY(); 00991 00992 if(t2 > y) t2 = y; else if(b2 < y) b2 = y; 00993 if(l2 > x) l2 = x; else if(r2 < x) r2 = x; 00994 //LINFO("[%f %f]",x,y); 00995 } 00996 float area2 = (b2 - t2)*(r2 - l2); 00997 LINFO("obj2[%d]: t2: %f, b2: %f, l2: %f, r2: %f; A: %f", 00998 obj2->numKeypoints(), t2, b2, l2, r2, area2); 00999 01000 // go through all the matches from the ref side 01001 float tm1 = -1.0f, bm1 = -1.0f, lm1 = -1.0f, rm1 = -1.0f; 01002 if(size() > 0) 01003 { 01004 float x = itsMatches[0].refkp->getX(); 01005 float y = itsMatches[0].refkp->getY(); 01006 tm1 = y, bm1 = y, lm1 = x, rm1 = x; 01007 //LINFO("[%f %f]",x,y); 01008 } 01009 for(uint i = 1; i < size(); i++) 01010 { 01011 float x = itsMatches[i].refkp->getX(); 01012 float y = itsMatches[i].refkp->getY(); 01013 01014 if(tm1 > y) tm1 = y; else if(bm1 < y) bm1 = y; 01015 if(lm1 > x) lm1 = x; else if(rm1 < x) rm1 = x; 01016 //LINFO("[%f %f]",x,y); 01017 } 01018 float aream1 = (bm1 - tm1)*(rm1 - lm1); 01019 LINFO("m1[%d]: tm1: %f, bm1: %f, lm1: %f, rm1: %f; Am: %f", 01020 size(), tm1, bm1, lm1, rm1, aream1); 01021 01022 // go through all the matches from the tst side 01023 float tm2 = -1.0f, bm2 = -1.0f, lm2 = -1.0f, rm2 = -1.0f; 01024 if(size() > 0) 01025 { 01026 float x = itsMatches[0].tstkp->getX(); 01027 float y = itsMatches[0].tstkp->getY(); 01028 tm2 = y, bm2 = y, lm2 = x, rm2 = x; 01029 //LINFO("[%f %f]",x,y); 01030 } 01031 for(uint i = 1; i < size(); i++) 01032 { 01033 float x = itsMatches[i].tstkp->getX(); 01034 float y = itsMatches[i].tstkp->getY(); 01035 01036 if(tm2 > y) tm2 = y; else if(bm2 < y) bm2 = y; 01037 if(lm2 > x) lm2 = x; else if(rm2 < x) rm2 = x; 01038 //LINFO("[%f %f]",x,y); 01039 } 01040 float aream2 = (bm2 - tm2)*(rm2 - lm2); 01041 LINFO("m2[%d]: tm2: %f, bm2: %f, lm2: %f, rm2: %f; Am: %f", 01042 size(), tm2, bm2, lm2, rm2, aream2); 01043 01044 // if the incoming object overlaps by less than 20% 01045 // of its middle quarter 01046 float lmid = l2 + (r2-l2)/4; float rmid = r2 - (r2-l2)/4; 01047 float lo = lm2; if(lo < lmid) lo = lmid; 01048 float ro = rm2; if(ro > rmid) ro = rmid; 01049 float wo = 0.0; if(ro > lo) wo = ro - lo; 01050 01051 float tmid = t2 + (b2-t2)/4; float bmid = b2 - (b2-t2)/4; 01052 float to = tm2; if(to < tmid) to = tmid; 01053 float bo = bm2; if(bo > bmid) bo = bmid; 01054 float ho = 0.0; if(bo > to) ho = bo - to; 01055 float ao = ho * wo; 01056 01057 bool ret = true; if(ao/(area2/4.0) < .3) ret = false; 01058 LINFO("wo: %f ho: %f ao/(a2/4) = %f/%f = %f < .4 -> ret = %d", 01059 wo,ho, ao, area2/4.0, ao/(area2/4.0), ret); 01060 01061 bool ret2 = true; if(ao/aream2 < .4) ret2 = false; 01062 LINFO("wo: %f ho: %f ao/(am2) = %f/%f = %f < .5 -> ret = %d", 01063 wo,ho, ao, aream2, ao/aream2, ret2); 01064 return ret || ret2; 01065 } 01066 01067 // ###################################################################### 01068 /* So things look consistent in everyone's emacs... */ 01069 /* Local Variables: */ 01070 /* indent-tabs-mode: nil */ 01071 /* End: */