00001 /*!@file HMAX/Hmax.C Riesenhuber & Poggio's HMAX model for object recognition */ 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: Laurent Itti <itti@usc.edu> 00034 // $HeadURL: svn://isvn.usc.edu/software/invt/trunk/saliency/src/HMAX/Hmax.C $ 00035 // $Id: Hmax.C 14376 2011-01-11 02:44:34Z pez $ 00036 // 00037 00038 #include "HMAX/Hmax.H" 00039 00040 #include "Image/FilterOps.H" // for convolve() etc. 00041 #include "Image/Image.H" 00042 #include "Image/Kernels.H" // for dogFilter() 00043 #include "Image/MathOps.H" 00044 #include "Util/MathFunctions.H" 00045 #include "Util/Types.H" 00046 #include "Util/log.H" 00047 #include "Util/safecopy.H" 00048 00049 #include <cmath> 00050 #include <fstream> 00051 #include <iostream> 00052 #include <iomanip> 00053 #include <cstdio> 00054 #include <cstdlib> 00055 #include <limits> 00056 00057 // ###################################################################### 00058 Hmax::Hmax() 00059 { initialized = false; } 00060 00061 // ###################################################################### 00062 Hmax::Hmax(const int nori, const std::vector<int>& spacess, 00063 const std::vector<int>& scaless, const int c1spaceol, 00064 const bool angleflag, const float s2t, const float s2s, 00065 const float stdmin, const float stdstep, 00066 const int fsmin, const int fsstep) 00067 { 00068 initialized = false; 00069 init(nori, spacess, scaless, c1spaceol, angleflag, s2t, s2s); 00070 initFilters(stdmin,stdstep,fsmin,fsstep); 00071 initialized = true; 00072 } 00073 00074 // ###################################################################### 00075 void Hmax::init(const int nori, const std::vector<int>& spacess, 00076 const std::vector<int>& scaless, const int c1spaceol, 00077 const bool angleflag, const float s2t, const float s2s) 00078 { 00079 freeMem(); ns = scaless[scaless.size() - 1]; no = nori; 00080 c1SpaceOL = c1spaceol; angleFlag = angleflag; s2Target = s2t; s2Sigma = s2s; 00081 spaceSS.resize(spacess.size()); scaleSS.resize(scaless.size()); 00082 00083 // detrmine number of scale bands from length of vector scaleSS: 00084 nsb = scaleSS.size() - 1; 00085 00086 for (unsigned int i = 0; i < spacess.size(); i ++) spaceSS[i] = spacess[i]; 00087 for (unsigned int i = 0; i < scaless.size(); i ++) scaleSS[i] = scaless[i]; 00088 00089 } 00090 00091 void Hmax::initFilters(const float stdmin, const float stdstep, const int fsmin, const int fsstep) 00092 { 00093 // create the filters: 00094 typedef Image<float>* FloatImagePtr; 00095 filter = new FloatImagePtr[ns]; 00096 for(int s = 0; s < ns; s++) 00097 { 00098 filter[s] = new Image<float>[no]; 00099 for(int o = 0; o < no; o ++) 00100 { 00101 // create DoG filter: 00102 filter[s][o] = dogFilter<float>(stdmin + stdstep * s, 00103 (float)o * 180.0F / (float)no, 00104 fsmin + fsstep * s); 00105 // normalize to zero mean: 00106 filter[s][o] -= mean(filter[s][o]); 00107 00108 // normalize to unit of sum-of-squares: 00109 filter[s][o] /= sum(squared(filter[s][o])); 00110 } 00111 } 00112 } 00113 00114 int Hmax::getNumOrientations() 00115 { 00116 return no; 00117 } 00118 00119 // ###################################################################### 00120 void Hmax::freeMem() 00121 { 00122 if (initialized) 00123 { 00124 initialized = false; 00125 for(int s = 0; s < ns; s++) delete [] filter[s]; 00126 delete [] filter; 00127 } 00128 } 00129 00130 // ###################################################################### 00131 Hmax::~Hmax() 00132 { freeMem(); } 00133 00134 // ###################################################################### 00135 std::vector<std::string> Hmax::readDir(std::string inName) 00136 { 00137 DIR *dp = opendir(inName.c_str()); 00138 if(dp == NULL) 00139 { 00140 LFATAL("Directory does not exist %s",inName.c_str()); 00141 } 00142 dirent *dirp; 00143 std::vector<std::string> fList; 00144 while ((dirp = readdir(dp)) != NULL ) { 00145 if (dirp->d_name[0] != '.') 00146 fList.push_back(inName + '/' + std::string(dirp->d_name)); 00147 } 00148 LINFO("%"ZU" files in the directory\n", fList.size()); 00149 LINFO("file list : \n"); 00150 for (unsigned int i=0; i<fList.size(); i++) 00151 LINFO("\t%s", fList[i].c_str()); 00152 std::sort(fList.begin(),fList.end()); 00153 return fList; 00154 } 00155 00156 // ###################################################################### 00157 std::vector<std::string> Hmax::readList(std::string inName) 00158 { 00159 std::ifstream inFile; 00160 inFile.open(inName.c_str(),std::ios::in); 00161 if(!inFile){ 00162 LFATAL("Unable to open image path list file: %s",inName.c_str()); 00163 } 00164 std::string sLine; 00165 std::vector<std::string> fList; 00166 while (std::getline(inFile, sLine)) { 00167 std::cout << sLine << std::endl; 00168 fList.push_back(sLine); 00169 } 00170 LINFO("%"ZU" paths in the file\n", fList.size()); 00171 LINFO("file list : \n"); 00172 for (unsigned int i=0; i<fList.size(); i++) 00173 LINFO("\t%s", fList[i].c_str()); 00174 inFile.close(); 00175 return fList; 00176 } 00177 00178 00179 // ###################################################################### 00180 // This is the code of Max Riesenhuber almost straight out of the box. 00181 // Minor modifications have been made, to: 00182 // - use float rather than double throughout 00183 // - interface with our Image class rather than Matlab matrices 00184 // - limit lines of code at 80 chars 00185 // - allocate memory through malloc rather than Matlab functions 00186 // - use Image<float> rather than Matlab matrices for result 00187 // - changed fSiz, spaceSS, scaleSS from float to int (avoid compiler warnings) 00188 // - made all pointers to input parameters pointers to const data 00189 // - switched scaleSS to zero-based rather than Matlab's one-based 00190 // - switched to using our built-in array of filters 00191 Image<float> Hmax::origGetC2(const Image<float>& input) 00192 { 00193 int offTab[8]={0,0,0,-1,-1,0,-1,-1}; 00194 int imgy,imgx; 00195 int i,j,k,x,y,bufX,bufY; 00196 float *buf,*bStart,*currB; 00197 int fx,fy; 00198 float res; 00199 float imgLen; 00200 float *retPtr,*rPtr2; 00201 Image<float> retC2; 00202 float *ret,*c2Ptr; 00203 float *sBuf,*sBufPtr,*sPtr2; 00204 float *c1Act,s2Resp; 00205 int maxFS,sSS,scaleInd,numScaleBands,numSimpleFilters,numPos; 00206 int currScale,yS,xS,sFInd; 00207 int f1,f2,f3,f4,c2Ind,maxXY; 00208 int poolRange; 00209 00210 Image<float>::const_iterator image = input.begin(); 00211 imgy = input.getHeight(); 00212 imgx = input.getWidth(); 00213 00214 maxFS = filter[ns - 1][0].getWidth(); // LI: use our built-in filters 00215 //for(maxFS=0,i=fSizY*fSizX-1;i>=0;i--) /* get maximum filter size */ 00216 // maxFS=fSiz[i]>maxFS?fSiz[i]:maxFS; 00217 00218 /* for each interval in scaleSS, filter type, go through spaceSS, 00219 allocate enough mem to calc all these response fields in S1 & 00220 then do max over the required range */ 00221 numScaleBands=scaleSS.size()-1; /* convention: last element in 00222 c1ScaleSS is max index + 1 */ 00223 00224 // numScales=scaleSS[numScaleBands]; 00225 /* last index in scaleSS contains scale index where next band would 00226 start, i.e., 1 after highest scale!! */ 00227 00228 numSimpleFilters = no; // LI: use our built-in filters 00229 // numSimpleFilters=fSizY*fSizX/numScales; 00230 00231 /* calculate number of positions for each C-cell */ 00232 /* base number of positions on smallest pooling range */ 00233 numPos=(int)(ceil((double)(imgy/spaceSS[0]))*ceil((double)(imgx/spaceSS[0])))* 00234 c1SpaceOL*c1SpaceOL; 00235 00236 /* be a little wasteful: in each filter band, allocate enough for 00237 smallest scale */ 00238 retC2.resize(numSimpleFilters*numSimpleFilters, 00239 numSimpleFilters*numSimpleFilters); 00240 c2Ptr=retC2.getArrayPtr(); 00241 /* allocate memory for C1 activations */ 00242 ret=(float*)malloc(numPos*numScaleBands*numSimpleFilters*sizeof(float)); 00243 00244 /* s1 activations before pooling over space (sBuf already pools over 00245 *scale*) */ 00246 sBuf=(float*)malloc(numSimpleFilters*imgy*imgx*sizeof(float)); 00247 00248 /* buf is buffer to perform convolutions in (with zero padding) */ 00249 bufX=imgx+maxFS; 00250 bufY=imgy+maxFS; 00251 buf=(float*)malloc(bufX*bufY*sizeof(float)); 00252 00253 /* copy image and pad with zeros to half max filter size */ 00254 memset(buf,0,bufX*bufY*sizeof(float)); 00255 Image<float>::const_iterator currI = image; 00256 for(currB=buf+(maxFS>>1)*bufX+(maxFS>>1),i=0;i<imgy;i++, 00257 currI+=imgx,currB+=bufX) 00258 safecopy(currB,currI,imgx); 00259 00260 for(scaleInd=0;scaleInd<numScaleBands;scaleInd++){ 00261 memset(sBuf,0,numSimpleFilters*imgy*imgx*sizeof(float)); 00262 for(currScale=scaleSS[scaleInd];currScale<scaleSS[scaleInd+1];currScale++){ 00263 for(sBufPtr=sBuf,sFInd=0;sFInd<numSimpleFilters;sFInd++){ 00264 fx = filter[currScale][sFInd].getWidth(); 00265 fy = filter[currScale][sFInd].getHeight(); 00266 for(y=0,bStart=buf+(maxFS>>1)*bufX+(maxFS>>1);y<imgy;y++,bStart+= 00267 bufX-imgx){ 00268 for(x=0;x<imgx;x++,bStart++,sBufPtr++){ 00269 /* center filter on current image point */ 00270 Image<float>::const_iterator currF = 00271 filter[currScale][sFInd].begin(); 00272 for(res=0,imgLen=0,currB=bStart-fy/2*bufX-fx/2, 00273 j=0;j<fy;j++,currB+=bufX-fx) 00274 for(k=0;k<fx;k++){ 00275 imgLen+=*currB**currB; 00276 res += *currB++**currF++; 00277 } 00278 if(angleFlag && (imgLen>0)) res/=sqrt(imgLen); 00279 res=fabs(res); 00280 *sBufPtr = *sBufPtr>res?*sBufPtr:res; /*already do max over scale*/ 00281 } 00282 } 00283 } 00284 } 00285 00286 /* now pool over space, take overlap into account */ 00287 /* take ceiling here otherwise might get more than c1SpaceOL times */ 00288 for(retPtr=ret+numPos*numSimpleFilters*scaleInd,sSS=(int) 00289 ceil((float)spaceSS[scaleInd]/c1SpaceOL), 00290 poolRange=spaceSS[scaleInd],sFInd=0;sFInd<numSimpleFilters;sFInd++) 00291 for(rPtr2=retPtr+numPos*sFInd,yS=0;yS<imgy;yS+=sSS) 00292 for(xS=0;xS<imgx;xS+=sSS,rPtr2++) 00293 /* eee still have same pooling range!! 00294 division by c1SpaceOL only in stepping of start pos! */ 00295 for(*rPtr2=0.0,sBufPtr=sBuf+imgx*(imgy*sFInd+yS)+xS,y=yS; 00296 (y-yS<poolRange)&&(y<imgy);y++) 00297 for(sPtr2=sBufPtr+(y-yS)*imgx,x=xS;(x-xS<poolRange)&&(x<imgx); 00298 x++,sPtr2++) 00299 *rPtr2=*rPtr2>*sPtr2?*rPtr2:*sPtr2; 00300 } 00301 00302 /* now: do S2 calculation by doing all combinations of features */ 00303 /* to make things a little more efficient, the outer loop runs over 00304 the 4 filters that a S2 cell combines, the inner loop does the calculation 00305 for all pos & filter bands & takes the max (ret contains C2 then w/out 00306 exp) */ 00307 for(c1Act=ret,c2Ind=0,f1=0;f1<numSimpleFilters;f1++) 00308 for(f2=0;f2<numSimpleFilters;f2++) 00309 for(f3=0;f3<numSimpleFilters;f3++) 00310 for(f4=0;f4<numSimpleFilters;f4++,c2Ind++){ 00311 for(c2Ptr[c2Ind]=res=-1e10,scaleInd=0;scaleInd<numScaleBands; 00312 scaleInd++){ 00313 /* eee assume square image */ 00314 for(maxXY=(int)ceil(imgy/ceil((float) 00315 spaceSS[scaleInd]/c1SpaceOL)), 00316 y=c1SpaceOL;y<maxXY;y++) 00317 for(x=c1SpaceOL;x<maxXY;x++){ 00318 /* use the fact that exp is monotonous in abs(arg): 00319 just pass back max of neg. dist (arg of exp) */ 00320 s2Resp=squareOf<float>(c1Act[numPos*(scaleInd*numSimpleFilters+f1)+ 00321 y+maxXY*x]-s2Target)+ 00322 squareOf<float>(c1Act[numPos*(scaleInd*numSimpleFilters+f2)+y+ 00323 c1SpaceOL*offTab[2]+ 00324 maxXY*(x+c1SpaceOL*offTab[3])]-s2Target)+ 00325 squareOf<float>(c1Act[numPos*(scaleInd*numSimpleFilters+f3)+y+ 00326 c1SpaceOL*offTab[4]+ 00327 maxXY*(x+c1SpaceOL*offTab[5])]-s2Target)+ 00328 squareOf<float>(c1Act[numPos*(scaleInd*numSimpleFilters+f4)+y+ 00329 c1SpaceOL*offTab[6]+ 00330 maxXY*(x+c1SpaceOL*offTab[7])]-s2Target); 00331 res=s2Resp>res?s2Resp:res; 00332 } 00333 c2Ptr[c2Ind]=c2Ptr[c2Ind]>res?c2Ptr[c2Ind]:res; /*max over scale*/ 00334 } 00335 } 00336 free(sBuf); 00337 free(buf); 00338 free(ret); 00339 return hmaxActivation(retC2, s2Sigma); 00340 } 00341 00342 00343 // ###################################################################### 00344 void Hmax::getC1(const Image<float>& input, Image<float>** &c1Res) 00345 { 00346 Image<float> *c1Buff = new Image<float>[no]; 00347 Image<float> s1Res; 00348 // loop over scale bands: 00349 00350 00351 for(int sb = 0; sb < nsb; sb ++) { 00352 00353 // clear our buffers: 00354 for(int i = 0; i < no; i ++) 00355 c1Buff[i].resize(input.getWidth(), input.getHeight(), true);; 00356 00357 // loop over scales within current scale band: 00358 for(int s = scaleSS[sb]; s < scaleSS[sb + 1]; s++) { 00359 // loop over orientations: 00360 for(int o = 0; o < no; o ++) { 00361 // convolve image by filter at current orient & scale: 00362 if (angleFlag) { 00363 s1Res = convolveHmax(input, filter[s][o]); // normalize by image energy 00364 //printCorners("s1Res",s1Res,s==scaleSS[0]&&sb==0&&o==0); 00365 } 00366 else { 00367 s1Res = convolve(input, filter[s][o], CONV_BOUNDARY_CLEAN); // no normalization 00368 // take absolute value of the convolution result: 00369 s1Res = abs(s1Res); 00370 } 00371 00372 // take max between this convolution and previous ones for 00373 // that orientation but other scales within current scale band: 00374 c1Buff[o] = takeMax<float>(c1Buff[o], s1Res); 00375 } 00376 } 00377 00378 // compute RF spacing (c1R) and pool range (c1PR): 00379 int c1R = (int)ceil((float)spaceSS[sb] / (float)c1SpaceOL); 00380 int c1PR = spaceSS[sb]; 00381 00382 // pool over space for each orientation (and scale band): 00383 for(int o = 0; o < no; o ++){ 00384 c1Res[sb][o] = spatialPoolMax(c1Buff[o], c1R, c1R, c1PR, c1PR); 00385 //printCorners("c1Res",c1Res[sb][o],sb==0&&o==0); 00386 } 00387 00388 } 00389 00390 delete [] c1Buff; 00391 } 00392 00393 void Hmax::initC1(Image<float> **&c1Res) 00394 { 00395 c1Res = new Image<float>*[nsb]; 00396 for (int sb = 0; sb < nsb; sb ++) c1Res[sb] = new Image<float>[no]; 00397 } 00398 00399 void Hmax::clearC1(Image<float> **&c1Res) 00400 { 00401 for (int sb = 0; sb < nsb; sb ++) delete [] c1Res[sb]; 00402 delete [] c1Res; 00403 } 00404 00405 void Hmax::printCorners(const char name[], const Image<float>& im, bool cond) 00406 { 00407 if(cond) { 00408 printf("%s\n",name); 00409 int w = im.getWidth(); 00410 int h = im.getHeight(); 00411 /* 00412 std::string s; 00413 if(w>2 && h>2) { 00414 printf("%f\t%f\t%f\t%f\t%f\n",im.getVal(0,0),im.getVal(1,0),im.getVal(2,0),im.getVal(w-2,0),im.getVal(w-1,0)); 00415 printf("%f\t%f\t\t\t%f\t%f\n\n", im.getVal(0,1),im.getVal(1,1),im.getVal(w-2,1),im.getVal(w-1,1)); 00416 printf("%f\t%f\t\t\t%f\t%f\n", im.getVal(0,h-2),im.getVal(1,h-2),im.getVal(w-2,h-2),im.getVal(w-1,h-2)); 00417 printf("%f\t%f\t%f\t%f\t%f\n",im.getVal(0,h-1),im.getVal(1,h-1),im.getVal(2,h-1),im.getVal(w-2,h-1),im.getVal(w-1,h-1)); 00418 } 00419 else if(w>1 && h>1) { 00420 printf("%f\t%f\n",im.getVal(0,0),im.getVal(1,0)); 00421 printf("%f\t%f\n", im.getVal(0,1),im.getVal(1,1)); 00422 } 00423 else if(w>0 && h>0){ 00424 printf("%f\n",im.getVal(0,0)); 00425 } 00426 */ 00427 std::cout << "Mean of " << name << " " << mean(im) << std::endl; 00428 std::cout << "Var of " << name << " " << (stdev(im)*stdev(im)) << std::endl; 00429 std::cout << "Width of " << w << " and height of " << h << std::endl; 00430 //float mi,ma; getMinMax(input,mi,ma); 00431 //writeOutImage(inputf,name); 00432 //std::cout << "Min " << mi << " Max " << ma << std::endl; 00433 } 00434 } 00435 00436 void Hmax::writeOutImage(const Image<float>& im,std::string & fName) 00437 { 00438 std::ofstream oFile; 00439 Image<float> d; 00440 oFile.open(fName.c_str(),std::ios::out); 00441 d = im; 00442 int w,h; 00443 w = d.getWidth(); 00444 h = d.getHeight(); 00445 for(int i=0;i<w;i++){ 00446 for(int j=0;j<h;j++){ 00447 oFile << d.getVal(i,j) <<" "; 00448 } 00449 if(i!=w-1) 00450 oFile << std::endl; 00451 } 00452 oFile.close(); 00453 00454 } 00455 00456 00457 // ###################################################################### 00458 Image<float> Hmax::getC2(const Image<float>& input) 00459 { 00460 // detrmine number of scale bands from length of vector scaleSS: 00461 int nsb = scaleSS.size() - 1; 00462 00463 // allocate buffers for intermediary results: 00464 Image<float> **c1Res; 00465 initC1(c1Res); 00466 00467 // ****************************** 00468 // *** Compute S1/C1 output: 00469 // ****************************** 00470 getC1(input, c1Res); 00471 00472 // ****************************** 00473 // *** Compute S2/C2 output: 00474 // ****************************** 00475 Image<float> c2Res(no * no, no * no, NO_INIT); 00476 c2Res.clear(-1.0E10F); 00477 int idx = 0; 00478 00479 // loop over four filters giving inputs to an S2 cell: 00480 for (int f1 = 0; f1 < no; f1++) 00481 for (int f2 = 0; f2 < no; f2++) 00482 for (int f3 = 0; f3 < no; f3++) 00483 for (int f4 = 0; f4 < no; f4++) { 00484 00485 float c2r = -1.0E10; 00486 // loop over scale band: 00487 for (int sb = 0; sb < nsb; sb ++) { 00488 00489 float s2r = featurePoolHmax(c1Res[sb][f1], c1Res[sb][f2], 00490 c1Res[sb][f3], c1Res[sb][f4], 00491 c1SpaceOL, c1SpaceOL, s2Target); 00492 if (s2r > c2r) c2r = s2r; 00493 } 00494 c2Res.setVal(idx, c2r); idx ++; 00495 } 00496 00497 // free allocated temporary images: 00498 clearC1(c1Res); 00499 00500 return hmaxActivation(c2Res, s2Sigma); 00501 } 00502 00503 void Hmax::sumFilter(const Image<float>& image, const float radius, Image<float>& newImage) 00504 { 00505 Rectangle sumSupport = Rectangle::tlbrI(0,0,int(radius*2.0F),int(radius*2.0F)); 00506 sumFilter(image,sumSupport,newImage); 00507 } 00508 00509 void Hmax::sumFilter(const Image<float>& image, const Rectangle& support, Image<float>& newImage) 00510 { 00511 00512 Dims d(support.top()+support.bottomI()+1,support.left()+support.rightI()+1); 00513 Image<float> a(d,NO_INIT); 00514 a.clear(1.0F); 00515 00516 //convolve the image with a matrix of 1's that is as big as the rectangle 00517 // This two step process is doing effectively the same thing by taking the center part of the convolution 00518 //I2 = conv2(ones(1,radius(2)+radius(4)+1), ones(radius(1)+radius(3)+1,1), I); 00519 //I3 = I2((radius(4)+1:radius(4)+size(I,1)), (radius(3)+1:radius(3)+size(I,2))); 00520 //Image<float> i; 00521 //i = convolution(image,a,MATLAB_STYLE_CONVOLUTION); 00522 //Rectangle r = Rectangle::tlbrI(support.bottomI()+1,support.rightI()+1,support.bottomI()+image.getHeight(),support.rightI()+image.getWidth()); 00523 //newImage = crop(i,r); 00524 // Can be done in one step 00525 newImage = convolve(image,a,CONV_BOUNDARY_ZERO); 00526 } 00527 00528 00529 // ###################################################################### 00530 /* So things look consistent in everyone's emacs... */ 00531 /* Local Variables: */ 00532 /* indent-tabs-mode: nil */ 00533 /* End: */