00001 /** @file Psycho/GaborSnake.C sequences of contour-aligned gabor elements */ 00002 00003 // //////////////////////////////////////////////////////////////////// // 00004 // The iLab Neuromorphic Vision C++ Toolkit - Copyright (C) 2000-2005 // 00005 // by the 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: Rob Peters <rjpeters at usc dot edu> 00034 // $HeadURL: svn://isvn.usc.edu/software/invt/trunk/saliency/src/Psycho/GaborSnake.C $ 00035 // $Id: GaborSnake.C 9078 2007-12-11 21:11:45Z rjpeters $ 00036 // 00037 00038 // Code herein is derived from GroovX, also licensed under the GPL 00039 // Copyright (c) 2002-2004 California Institute of Technology 00040 // Copyright (c) 2004-2007 University of Southern California 00041 // [http://ilab.usc.edu/rjpeters/groovx/] 00042 00043 #ifndef PSYCHO_GABORSNAKE_C_DEFINED 00044 #define PSYCHO_GABORSNAKE_C_DEFINED 00045 00046 #include "Psycho/GaborSnake.H" 00047 00048 #include "Image/geom.h" 00049 #include "Image/vec2.h" 00050 #include "Util/Assert.H" 00051 #include "Util/log.H" 00052 #include "rutz/rand.h" 00053 00054 #include <algorithm> 00055 #include <cmath> 00056 00057 using geom::vec2d; 00058 00059 namespace 00060 { 00061 const double MAX_DELTA = M_PI/4.0; // == 45 degrees 00062 const double TEMPERATURE = 0.05; 00063 const double INITIAL_INCREMENT = 0.1; 00064 00065 struct Tuple4 00066 { 00067 Tuple4() {} 00068 00069 Tuple4(double a0, double a1, double a2, double a3) 00070 { arr[0] = a0; arr[1] = a1; arr[2] = a2; arr[3] = a3; } 00071 00072 double operator[](int i) const { return arr[i]; } 00073 double& operator[](int i) { return arr[i]; } 00074 00075 double arr[4]; 00076 }; 00077 00078 void pickRandom4(int length, int i[], rutz::urand& urand) 00079 { 00080 i[0] = i[1] = i[2] = i[3] = urand.idraw(length); 00081 00082 while (i[1] == i[0]) 00083 { 00084 i[1] = urand.idraw(length); 00085 } 00086 00087 while (i[2] == i[0] || i[2] == i[1]) 00088 { 00089 i[2] = urand.idraw(length); 00090 } 00091 00092 while (i[3] == i[0] || i[3] == i[1] || i[3] == i[2]) 00093 { 00094 i[3] = urand.idraw(length); 00095 } 00096 00097 std::sort(i, i+4); 00098 } 00099 00100 Tuple4 getEdgeLengths(const vec2d no[4]) 00101 { 00102 return Tuple4(no[0].distance_to(no[1]), 00103 no[1].distance_to(no[2]), 00104 no[2].distance_to(no[3]), 00105 no[3].distance_to(no[0])); 00106 } 00107 00108 Tuple4 getThetas(const vec2d no[4]) 00109 { 00110 return Tuple4(no[0].angle_to(no[1]), 00111 no[1].angle_to(no[2]), 00112 no[2].angle_to(no[3]), 00113 no[3].angle_to(no[0])); 00114 } 00115 00116 Tuple4 getAlphas(const Tuple4& theta) 00117 { 00118 return Tuple4(geom::rad_0_2pi(M_PI - theta[0] + theta[3]), 00119 geom::rad_0_2pi(M_PI - theta[1] + theta[0]), 00120 geom::rad_0_2pi(M_PI - theta[2] + theta[1]), 00121 geom::rad_0_2pi(M_PI - theta[3] + theta[2])); 00122 } 00123 00124 // Must return "true" in order to proceed with new nodes in jiggle(). 00125 bool squashQuadrangle(const vec2d& old_0, const vec2d& old_1, 00126 const vec2d& old_2, const vec2d& old_3, 00127 vec2d* new_0, vec2d* new_1, 00128 vec2d* new_2, vec2d* new_3, 00129 double new_theta) 00130 { 00131 { 00132 // Here we adjust the position of node-1 by jiggling its angle with 00133 // respect to node-0: 00134 00135 const double d = old_0.distance_to(old_1); 00136 00137 new_1->x() = old_0.x() + d * cos(new_theta); 00138 new_1->y() = old_0.y() + d * sin(new_theta); 00139 } 00140 00141 /* */ 00142 /* . (2) */ 00143 /* ./| */ 00144 /* b ./ `. */ 00145 /* ./ | */ 00146 /* ./ `. */ 00147 /* ./ | */ 00148 /* ./ `. c */ 00149 /* (NEW 1) ./_________ | */ 00150 /* `--. ang `. */ 00151 /* `--. | */ 00152 /* `--. `. */ 00153 /* a `--. | */ 00154 /* `--. (3) */ 00155 /* */ 00156 00157 00158 /* 00159 00160 OK, we've picked a new node-1... now we want to hold node-3 fixed and 00161 try to find a node-2 that will keep the other distances constant. In 00162 trigonometric terms, we're trying to solve for the missing vertex of a 00163 triangle, given (1) the positions of the other two vertices, and (2) 00164 the lengths of all three sides. 00165 00166 We do this in two steps: 00167 00168 (1) assume node-1 is at (0,0), the origin 00169 and node-3 is at (a,0) 00170 00171 then find node-2 in this unrotated coord system 00172 00173 (2) now take the rotation angle (ang) required to put node-3 back in 00174 its proper position, and rotate node-2 by that angle as well 00175 00176 From http://mathworld.wolfram.com/Triangle.html: 00177 00178 A triangle with sides a, b, and c can be constructed by selecting 00179 vertices (0,0), (a,0), and (x,y), then solving: 00180 00181 x^2 + y^2 = b^2 00182 (x-a)^2 + y^2 = c^2 00183 00184 to obtain: 00185 00186 a^2 + b^2 - c^2 00187 x = --------------- 00188 2a 00189 00190 ,------------------------------ 00191 / (a^2 + b^2 - c^2)^2 00192 y = +/- _ / b^2 - --------------------- 00193 \/ 4a^2 00194 00195 ,----------------------------------------------- 00196 -\/ 2(a^2b^2 + b^2c^2 + a^2c^2) - a^4 - b^4 - c^4 00197 = +/- ----------------------------------------------------- 00198 2a 00199 00200 (note that the website has an incorrect formula for the solved y!) 00201 00202 */ 00203 00204 const double a = old_3.distance_to(*new_1); 00205 const double b = old_1.distance_to(old_2); 00206 const double c = old_2.distance_to(old_3); 00207 00208 const double cos_ang = (old_3.x() - new_1->x()) / a; // adjac / hypot 00209 const double sin_ang = (old_3.y() - new_1->y()) / a; // oppos / hypot 00210 00211 const double a_sq = a*a; 00212 const double b_sq = b*b; 00213 const double c_sq = c*c; 00214 00215 const double det = 00216 2*(a_sq*b_sq + b_sq*c_sq + a_sq*c_sq) 00217 - a_sq*a_sq - b_sq*b_sq - c_sq*c_sq; 00218 00219 if (det < 0) 00220 return false; 00221 00222 // OK, here's the unrotated coords of node-2: 00223 00224 const double xp = (a*a + b*b - c*c) / (2.0*a); 00225 const double yp = sqrt(det) / (2.0*a); 00226 00227 // Here's the two candidate re-rotated coords: 00228 00229 const vec2d new_2a 00230 (new_1->x() + xp*cos_ang - yp*sin_ang, 00231 new_1->y() + xp*sin_ang + yp*cos_ang); 00232 00233 const vec2d new_2b 00234 (new_1->x() + xp*cos_ang + yp*sin_ang, 00235 new_1->y() + xp*sin_ang - yp*cos_ang); 00236 00237 const double d_2_2a = new_2a.distance_to(old_2); 00238 const double d_2_2b = new_2b.distance_to(old_2); 00239 00240 *new_0 = old_0; 00241 *new_2 = (d_2_2a < d_2_2b) ? new_2a : new_2b; 00242 *new_3 = old_3; 00243 00244 return true; 00245 } 00246 00247 bool deltaTooBig(const Tuple4& delta) 00248 { 00249 return (fabs(delta[0]) > MAX_DELTA 00250 || fabs(delta[1]) > MAX_DELTA 00251 || fabs(delta[2]) > MAX_DELTA 00252 || fabs(delta[3]) > MAX_DELTA); 00253 } 00254 00255 bool acceptNewDelta(const Tuple4& newdelta, const Tuple4& olddelta, 00256 rutz::urand& urand) 00257 { 00258 const double energy_diff = 00259 newdelta[0]*newdelta[0] - olddelta[0]*olddelta[0] 00260 + newdelta[1]*newdelta[1] - olddelta[1]*olddelta[1] 00261 + newdelta[2]*newdelta[2] - olddelta[2]*olddelta[2] 00262 + newdelta[3]*newdelta[3] - olddelta[3]*olddelta[3]; 00263 00264 // Note, if energy_diff<0, then probability>1. 00265 const double probability = exp(-energy_diff/TEMPERATURE); 00266 00267 return urand.fdraw() <= probability; 00268 } 00269 } 00270 00271 GaborSnake::GaborSnake(int l, double spacing, int seed, int xcenter, int ycenter, 00272 GaborPatchItemFactory& factory) : 00273 itsLength(l), 00274 itsElem(itsLength) 00275 { 00276 rutz::urand urand(seed); 00277 00278 const double radius = (itsLength * spacing) / (2*M_PI); 00279 00280 const double alpha_start = 2 * M_PI * urand.fdraw(); 00281 00282 for (int n = 0; n < itsLength; ++n) 00283 { 00284 const double alpha = alpha_start + 2 * M_PI * n / itsLength; 00285 00286 itsElem[n].set_polar_rad(radius, -alpha); 00287 } 00288 00289 const int ITERS = 400; 00290 00291 for (int count = 0; count < ITERS; ++count) 00292 { 00293 const bool convergeOk = this->jiggle(urand); 00294 if ( !convergeOk ) 00295 { 00296 LERROR("failed to converge"); 00297 break; 00298 } 00299 } 00300 00301 // Now reset so that the center of the ring is at the origin 00302 vec2d c; 00303 00304 for (int n = 0; n < itsLength; ++n) 00305 c += itsElem[n]; 00306 00307 c /= itsLength; 00308 00309 for (int n = 0; n < itsLength; ++n) 00310 itsElem[n] -= c; 00311 00312 for (int n = 0; n < itsLength; ++n) 00313 { 00314 itsElem[n].x() += xcenter; 00315 itsElem[n].y() += ycenter; 00316 } 00317 00318 for (int n = 0; n < itsLength; ++n) 00319 { 00320 const geom::vec2d pos(0.5 * (elem(n).x() + elem(n+1).x()), 00321 0.5 * (elem(n).y() + elem(n+1).y())); 00322 const double theta = geom::rad_0_2pi(-elem(n).angle_to(elem(n+1))); 00323 00324 itsPatches.push_back(factory.makeForeground(pos, theta)); 00325 } 00326 } 00327 00328 GaborSnake::~GaborSnake() 00329 {} 00330 00331 rutz::shared_ptr<SearchItem> GaborSnake::getElement(int n) const 00332 { 00333 ASSERT(size_t(n) < itsPatches.size()); 00334 00335 return itsPatches[n]; 00336 } 00337 00338 /* (pos.x[n],ypos[n]) position n */ 00339 /* (xpos[n+1],ypos[n+1]) position n+1 */ 00340 /* (0.5*(xpos[n]+xpos[n+1]), 0.5*(ypos[n]+ypos[n+1])) */ 00341 /* location of patch n */ 00342 /* theta angle between pos n and pos n+1 */ 00343 /* and orientation of patch n */ 00344 /* delta angle difference between patch n and n+1 */ 00345 /* */ 00346 /* theta[0] */ 00347 /* no[0]__________ */ 00348 /* ,-~ `-. */ 00349 /* ,-~ `-. */ 00350 /* ,-~ alpha[0] `-. */ 00351 /* a[0] ,-~ `-. a[3] */ 00352 /* ,-~ `-. */ 00353 /* ,-~ `-. */ 00354 /* ,-~ `-. */ 00355 /* ,-~ `-. */ 00356 /*no[1]~ `-. theta[3] */ 00357 /* \ alpha[1] `-. */ 00358 /* \ alpha[3] no[3]------ */ 00359 /* \ _,-~ */ 00360 /* \ _,-~ */ 00361 /* \ _,-~ */ 00362 /* \ _,-~ */ 00363 /* a[1]\ _,-~ */ 00364 /* \ _,-~ */ 00365 /* \ _,-~ a[2] */ 00366 /* \ alpha[2] _,-~ */ 00367 /* \ _,-~ */ 00368 /* \ _,-~ theta[2] */ 00369 /* no[2]~_________ */ 00370 00371 bool GaborSnake::jiggle(rutz::urand& urand) 00372 { 00373 int i[4]; 00374 pickRandom4(itsLength, i, urand); 00375 00376 const vec2d old_pos[4] = 00377 { 00378 itsElem[i[0]], itsElem[i[1]], itsElem[i[2]], itsElem[i[3]] 00379 }; 00380 00381 const Tuple4 old_theta = getThetas(old_pos); 00382 const Tuple4 old_alpha = getAlphas(old_theta); 00383 00384 const Tuple4 old_delta 00385 (geom::rad_npi_pi(elem(i[0]).angle_to(elem(i[0]+1)) - elem(i[0]-1).angle_to(elem(i[0]))), 00386 geom::rad_npi_pi(elem(i[1]).angle_to(elem(i[1]+1)) - elem(i[1]-1).angle_to(elem(i[1]))), 00387 geom::rad_npi_pi(elem(i[2]).angle_to(elem(i[2]+1)) - elem(i[2]-1).angle_to(elem(i[2]))), 00388 geom::rad_npi_pi(elem(i[3]).angle_to(elem(i[3]+1)) - elem(i[3]-1).angle_to(elem(i[3])))); 00389 00390 vec2d new_pos[4]; 00391 00392 double increment = INITIAL_INCREMENT; 00393 00394 const int MAX_ITERS = 10000; 00395 00396 int k = 0; 00397 00398 for (; k < MAX_ITERS; ++k) 00399 { 00400 const double incr = urand.booldraw() ? -increment : increment; 00401 00402 const int r = urand.idraw(4); 00403 00404 const double incr_theta = old_theta[r] - incr; 00405 00406 const bool ok = 00407 squashQuadrangle(old_pos[(r+0)%4], old_pos[(r+1)%4], 00408 old_pos[(r+2)%4], old_pos[(r+3)%4], 00409 &new_pos[(r+0)%4], &new_pos[(r+1)%4], 00410 &new_pos[(r+2)%4], &new_pos[(r+3)%4], 00411 incr_theta); 00412 00413 if (!ok) 00414 { 00415 increment *= 0.5; 00416 continue; 00417 } 00418 00419 const Tuple4 new_theta = getThetas(new_pos); 00420 const Tuple4 new_alpha = getAlphas(new_theta); 00421 const Tuple4 new_delta 00422 (old_delta[0] + (old_alpha[0]-new_alpha[0]), 00423 old_delta[1] + (old_alpha[1]-new_alpha[1]), 00424 old_delta[2] + (old_alpha[2]-new_alpha[2]), 00425 old_delta[3] + (old_alpha[3]-new_alpha[3])); 00426 00427 if (deltaTooBig(new_delta)) 00428 { 00429 increment *= 0.8; 00430 continue; 00431 } 00432 00433 if (acceptNewDelta(new_delta, old_delta, urand)) 00434 break; 00435 } 00436 00437 bool didConverge = (k < MAX_ITERS); 00438 00439 for (int n = 0; n < 4; ++n) 00440 this->transformPath(i[n], new_pos[n], 00441 i[(n+1)%4], new_pos[(n+1)%4]); 00442 00443 for (int n = 0; n < 4; ++n) 00444 { 00445 itsElem[i[n]] = new_pos[n]; 00446 } 00447 00448 return didConverge; 00449 } 00450 00451 void GaborSnake::transformPath(int i1, const vec2d& new1, 00452 int i2, const vec2d& new2) 00453 { 00454 const vec2d old1 = itsElem[i1]; 00455 const vec2d old2 = itsElem[i2]; 00456 00457 // OK, our jiggling algorithm has determined new locations for node-1 and 00458 // node-2. Now, we want to adjust all the intermediate points 00459 // accordingly. This amounts to (1) a translation (move node-1 from its 00460 // old to new location), (2) a rotation (rotate node-2 around node-1 to 00461 // its new location), and (3) a scaling (contract or expand to put node-2 00462 // in the right spot). We can then apply these same transformations to 00463 // the intermediate points. 00464 00465 /* */ 00466 /* scale by rotate by (a'-a) */ 00467 /* distance degrees */ 00468 /* ratio | */ 00469 /* | | */ 00470 /* | v */ 00471 /* v */ 00472 /* a11 a12 cos a'-a -sin a'-a */ 00473 /* ( ) = d'/d * ( ) */ 00474 /* a21 a12 sin a'-a cos a'-a */ 00475 /* */ 00476 00477 const double d = old1.distance_to(old2); 00478 const double a = old1.angle_to(old2); 00479 00480 const double dp = new1.distance_to(new2); 00481 const double ap = new1.angle_to(new2); 00482 00483 const double diff_a = ap - a; 00484 00485 const double ratio_d = (dp/d); 00486 00487 const double cos_diff_a = cos(diff_a); 00488 const double sin_diff_a = sin(diff_a); 00489 00490 const double a11 = ratio_d * cos_diff_a; 00491 const double a12 = - ratio_d * sin_diff_a; 00492 const double a21 = ratio_d * sin_diff_a; 00493 const double a22 = ratio_d * cos_diff_a; 00494 00495 // Loop over all the nodes in between the nodes given by i1 and i2 00496 for (int n = (i1+1)%itsLength; n != i2; n = (n+1)%itsLength) 00497 { 00498 /* */ 00499 /* x' c1 a11 a12 x - b1 */ 00500 /* ( ) = ( ) + ( ) ( ) */ 00501 /* y' c2 a21 a12 y - b2 */ 00502 /* */ 00503 00504 const double diffx = itsElem[n].x() - old1.x(); 00505 const double diffy = itsElem[n].y() - old1.y(); 00506 00507 itsElem[n].x() = new1.x() + a11*diffx + a12*diffy; 00508 itsElem[n].y() = new1.y() + a21*diffx + a22*diffy; 00509 } 00510 } 00511 00512 // ###################################################################### 00513 /* So things look consistent in everyone's emacs... */ 00514 /* Local Variables: */ 00515 /* mode: c++ */ 00516 /* indent-tabs-mode: nil */ 00517 /* End: */ 00518 00519 #endif // PSYCHO_GABORSNAKE_C_DEFINED