00001
00002 #include "ptam/OpenGL.h"
00003 #include "ptam/CalibImage.h"
00004 #include <stdlib.h>
00005
00006 #include <cvd/utility.h>
00007 #include <cvd/convolution.h>
00008 #include <cvd/fast_corner.h>
00009 #include <cvd/vector_image_ref.h>
00010 #include <cvd/image_interpolate.h>
00011
00012 #include <TooN/se3.h>
00013 #include <TooN/SVD.h>
00014 #include <TooN/wls.h>
00015
00016 #include <ptam/Params.h>
00017
00018 using namespace std;
00019 using namespace CVD;
00020
00021
00022 inline bool IsCorner(Image<byte> &im, ImageRef ir, int nGate)
00023 {
00024
00025
00026
00027
00028
00029 int nSum = 0;
00030 static byte abPixels[16];
00031 for(int i=0; i<16; i++)
00032 {
00033 abPixels[i] = im[ir + fast_pixel_ring[i]];
00034 nSum += abPixels[i];
00035 };
00036 int nMean = nSum / 16;
00037 int nHiThresh = nMean + nGate;
00038 int nLoThresh = nMean - nGate;
00039
00040
00041 int nCenter = im[ir];
00042 if(nCenter <= nLoThresh || nCenter >= nHiThresh)
00043 return false;
00044
00045
00046 bool bState = (abPixels[15] > nMean);
00047 int nSwaps = 0;
00048 for(int i=0; i<16; i++)
00049 {
00050 byte bValNow = abPixels[i];
00051 if(bState)
00052 {
00053 if(bValNow < nLoThresh)
00054 {
00055 bState = false;
00056 nSwaps++;
00057 }
00058 }
00059 else
00060 if(bValNow > nHiThresh)
00061 {
00062 bState = true;
00063 nSwaps++;
00064 };
00065 }
00066 return (nSwaps == 4);
00067 };
00068
00069 Vector<2> GuessInitialAngles(Image<byte> &im, ImageRef irCenter)
00070 {
00071
00072
00073
00074
00075
00076
00077
00078
00079 image_interpolate<Interpolate::Bilinear, byte> imInterp(im);
00080 double dBestAngle = 0;
00081 double dBestGradMag = 0;
00082 double dGradAtBest = 0;
00083 for(double dAngle = 0.0; dAngle < M_PI; dAngle += 0.1)
00084 {
00085 Vector<2> v2Dirn;
00086 v2Dirn[0] = cos(dAngle); v2Dirn[1] = sin(dAngle);
00087 Vector<2> v2Perp;
00088 v2Perp[1] = -v2Dirn[0]; v2Perp[0] = v2Dirn[1];
00089
00090 double dG = imInterp[vec(irCenter) + v2Dirn * 3.0 + v2Perp * 0.1] -
00091 imInterp[vec(irCenter) + v2Dirn * 3.0 - v2Perp * 0.1]
00092 + imInterp[vec(irCenter) - v2Dirn * 3.0 - v2Perp * 0.1] -
00093 imInterp[vec(irCenter) - v2Dirn * 3.0 + v2Perp * 0.1];
00094 if(fabs(dG) > dBestGradMag)
00095 {
00096 dBestGradMag = fabs(dG);
00097 dGradAtBest = dG;
00098 dBestAngle = dAngle;
00099 };
00100 }
00101
00102 Vector<2> v2Ret;
00103 if(dGradAtBest < 0)
00104 { v2Ret[0] = dBestAngle; v2Ret[1] = dBestAngle + M_PI / 2.0; }
00105 else
00106 { v2Ret[1] = dBestAngle; v2Ret[0] = dBestAngle - M_PI / 2.0; }
00107 return v2Ret;
00108 }
00109
00110 bool CalibImage::MakeFromImage(Image<byte> &im)
00111 {
00112
00113
00114 const FixParams& pPars = PtamParameters::fixparams();
00115 static int gvnCornerPatchSize = pPars.CameraCalibrator_CornerPatchSize;
00116
00117
00118 mvCorners.clear();
00119 mvGridCorners.clear();
00120
00121 mim = im;
00122 mim.make_unique();
00123
00124
00125
00126
00127 {
00128 Image<byte> imBlurred = mim;
00129 imBlurred.make_unique();
00130
00131 double blursigma = pPars.Calibrator_BlurSigma;
00132
00133 convolveGaussian(imBlurred, blursigma);
00134 ImageRef irTopLeft(5,5);
00135 ImageRef irBotRight = mim.size() - irTopLeft;
00136 ImageRef ir = irTopLeft;
00137 glPointSize(1);
00138 glColor3f(1,0,1);
00139 glBegin(GL_POINTS);
00140 int nGate = pPars.Calibrator_MeanGate;
00141 do
00142 if(IsCorner(imBlurred, ir, nGate))
00143 {
00144 mvCorners.push_back(ir);
00145 glVertex(ir);
00146 }
00147 while(ir.next(irTopLeft, irBotRight));
00148 glEnd();
00149 }
00150
00151
00152
00153 int MinCornersForGrabbedImage=pPars.Calibrator_MinCornersForGrabbedImage;
00154 if((int) mvCorners.size() < MinCornersForGrabbedImage)
00155 return false;
00156
00157
00158 ImageRef irCenterOfImage = mim.size() / 2;
00159 ImageRef irBestCenterPos;
00160 unsigned int nBestDistSquared = 99999999;
00161 for(unsigned int i=0; i<mvCorners.size(); i++)
00162 {
00163 unsigned int nDist = (mvCorners[i] - irCenterOfImage).mag_squared();
00164 if(nDist < nBestDistSquared)
00165 {
00166 nBestDistSquared = nDist;
00167 irBestCenterPos = mvCorners[i];
00168 }
00169 }
00170
00171
00172 CalibCornerPatch Patch(gvnCornerPatchSize);
00173 CalibCornerPatch::Params Params;
00174 Params.v2Pos = vec(irBestCenterPos);
00175 Params.v2Angles = GuessInitialAngles(mim, irBestCenterPos);
00176 Params.dGain = 80.0;
00177 Params.dMean = 120.0;
00178
00179 if(!Patch.IterateOnImageWithDrawing(Params, mim))
00180 return false;
00181
00182
00183 CalibGridCorner cFirst;
00184 cFirst.Params = Params;
00185 mvGridCorners.push_back(cFirst);
00186 cFirst.Draw();
00187
00188
00189
00190 if(!(ExpandByAngle(0,0) || ExpandByAngle(0,2)))
00191 return false;
00192 if(!(ExpandByAngle(0,1) || ExpandByAngle(0,3)))
00193 return false;
00194
00195 mvGridCorners[1].mInheritedSteps = mvGridCorners[2].mInheritedSteps = mvGridCorners[0].GetSteps(mvGridCorners);
00196
00197
00198 int nNext;
00199 int nSanityCounter = 0;
00200 const int nSanityCounterLimit = 500;
00201 while((nNext = NextToExpand()) >= 0 && nSanityCounter < nSanityCounterLimit )
00202 {
00203 ExpandByStep(nNext);
00204 nSanityCounter++;
00205 }
00206 if(nSanityCounter == nSanityCounterLimit)
00207 return false;
00208
00209 DrawImageGrid();
00210 return true;
00211 }
00212
00213 bool CalibImage::ExpandByAngle(int nSrc, int nDirn)
00214 {
00215
00216
00217 const FixParams& pPars = PtamParameters::fixparams();
00218 static int gvnCornerPatchSize = pPars.CameraCalibrator_CornerPatchSize;
00219
00220
00221 CalibGridCorner &gSrc = mvGridCorners[nSrc];
00222
00223 ImageRef irBest;
00224 double dBestDist = 99999;
00225 Vector<2> v2TargetDirn = gSrc.Params.m2Warp().T()[nDirn%2];
00226 if(nDirn >= 2)
00227 v2TargetDirn *= -1;
00228 for(unsigned int i=0; i<mvCorners.size(); i++)
00229 {
00230 Vector<2> v2Diff = vec(mvCorners[i]) - gSrc.Params.v2Pos;
00231 if(v2Diff * v2Diff < 100)
00232 continue;
00233 if(v2Diff * v2Diff > dBestDist * dBestDist)
00234 continue;
00235 Vector<2> v2Dirn = v2Diff;
00236 normalize(v2Dirn);
00237 if(v2Dirn * v2TargetDirn < cos(M_PI / 18.0))
00238 continue;
00239 dBestDist = sqrt(v2Diff * v2Diff);
00240 irBest = mvCorners[i];
00241 }
00242
00243 CalibGridCorner gTarget;
00244 gTarget.Params = gSrc.Params;
00245 gTarget.Params.v2Pos = vec(irBest);
00246 gTarget.Params.dGain *= -1;
00247
00248 CalibCornerPatch Patch(gvnCornerPatchSize);
00249 if(!Patch.IterateOnImageWithDrawing(gTarget.Params, mim))
00250 {
00251 gSrc.aNeighborStates[nDirn].val = N_FAILED;
00252 return false;
00253 }
00254
00255 gTarget.irGridPos = gSrc.irGridPos;
00256 if(nDirn < 2)
00257 gTarget.irGridPos[nDirn]++;
00258 else gTarget.irGridPos[nDirn%2]--;
00259
00260 mvGridCorners.push_back(gTarget);
00261 mvGridCorners.back().aNeighborStates[(nDirn + 2) % 4].val = nSrc;
00262 mvGridCorners[nSrc].aNeighborStates[nDirn].val = mvGridCorners.size() - 1;
00263
00264 mvGridCorners.back().Draw();
00265 return true;
00266 }
00267
00268
00269 void CalibGridCorner::Draw()
00270 {
00271 glColor3f(0,1,0);
00272 glEnable(GL_LINE_SMOOTH);
00273 glEnable(GL_BLEND);
00274 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
00275 glBegin(GL_LINES);
00276 glVertex(Params.v2Pos + Params.m2Warp() * vec(ImageRef( 10,0)));
00277 glVertex(Params.v2Pos + Params.m2Warp() * vec(ImageRef(-10,0)));
00278 glVertex(Params.v2Pos + Params.m2Warp() * vec(ImageRef( 0, 10)));
00279 glVertex(Params.v2Pos + Params.m2Warp() * vec(ImageRef( 0,-10)));
00280 glEnd();
00281 }
00282
00283
00284 double CalibGridCorner::ExpansionPotential()
00285 {
00286
00287
00288
00289
00290 int nMissing = 0;
00291 for(int i=0; i<4; i++)
00292 if(aNeighborStates[i].val == N_NOT_TRIED)
00293 nMissing++;
00294
00295 if(nMissing == 0)
00296 return 0.0;
00297
00298 if(nMissing == 1)
00299 return 100.0;
00300
00301 if(nMissing == 3)
00302 return 1.0;
00303
00304 if(nMissing == 2)
00305 {
00306 int nFirst = 0;
00307 while(aNeighborStates[nFirst].val != N_NOT_TRIED)
00308 nFirst++;
00309 if(aNeighborStates[(nFirst + 2) % 4].val == N_NOT_TRIED)
00310 return 10.0;
00311 else
00312 return 20.0;
00313 }
00314 assert(0);
00315 return 0.0;
00316 };
00317
00318
00319 Matrix<2> CalibGridCorner::GetSteps(vector<CalibGridCorner> &vgc)
00320 {
00321 Matrix<2> m2Steps;
00322 for(int dirn=0; dirn<2; dirn++)
00323 {
00324 Vector<2> v2Dirn;
00325 int nFound = 0;
00326 v2Dirn = Zeros;
00327 if(aNeighborStates[dirn].val >=0)
00328 {
00329 v2Dirn += vgc[aNeighborStates[dirn].val].Params.v2Pos - Params.v2Pos;
00330 nFound++;
00331 }
00332 if(aNeighborStates[dirn+2].val >=0)
00333 {
00334 v2Dirn -= vgc[aNeighborStates[dirn+2].val].Params.v2Pos - Params.v2Pos;
00335 nFound++;
00336 }
00337 if(nFound == 0)
00338 m2Steps[dirn] = mInheritedSteps[dirn];
00339 else
00340 m2Steps[dirn] = v2Dirn / nFound;
00341 }
00342 return m2Steps;
00343 };
00344
00345 int CalibImage::NextToExpand()
00346 {
00347 int nBest = -1;
00348 double dBest = 0.0;
00349
00350 for(unsigned int i=0; i<mvGridCorners.size(); i++)
00351 {
00352 double d = mvGridCorners[i].ExpansionPotential();
00353 if(d > dBest)
00354 {
00355 nBest = i;
00356 dBest = d;
00357 }
00358 }
00359 return nBest;
00360 }
00361
00362 void CalibImage::ExpandByStep(int n)
00363 {
00364
00365
00366
00367 const FixParams& pPars = PtamParameters::fixparams();
00368 static double gvdMaxStepDistFraction = pPars.CameraCalibrator_MaxStepDistFraction;
00369 static int gvnCornerPatchSize = pPars.CameraCalibrator_CornerPatchSize;
00370
00371
00372
00373
00374 CalibGridCorner &gSrc = mvGridCorners[n];
00375
00376
00377
00378 int nDirn = -10;
00379 for(int i=0; nDirn == -10 && i<4; i++)
00380 {
00381 if(gSrc.aNeighborStates[i].val == N_NOT_TRIED &&
00382 gSrc.aNeighborStates[(i+2) % 4].val >= 0)
00383 nDirn = i;
00384 }
00385 if(nDirn == -10)
00386 for(int i=0; nDirn == -10 && i<4; i++)
00387 {
00388 if(gSrc.aNeighborStates[i].val == N_NOT_TRIED)
00389 nDirn = i;
00390 }
00391 assert(nDirn != -10);
00392
00393 Vector<2> v2Step;
00394 ImageRef irGridStep = IR_from_dirn(nDirn);
00395
00396 v2Step = gSrc.GetSteps(mvGridCorners).T() * vec(irGridStep);
00397
00398 Vector<2> v2SearchPos = gSrc.Params.v2Pos + v2Step;
00399
00400
00401 gSrc.aNeighborStates[nDirn].val = N_FAILED;
00402
00403 ImageRef irBest;
00404 double dBestDist = 99999;
00405 for(unsigned int i=0; i<mvCorners.size(); i++)
00406 {
00407 Vector<2> v2Diff = vec(mvCorners[i]) - v2SearchPos;
00408 if(v2Diff * v2Diff > dBestDist * dBestDist)
00409 continue;
00410 dBestDist = sqrt(v2Diff * v2Diff);
00411 irBest = mvCorners[i];
00412 }
00413
00414 double dStepDist= sqrt(v2Step * v2Step);
00415 if(dBestDist > gvdMaxStepDistFraction * dStepDist)
00416 return;
00417
00418 CalibGridCorner gTarget;
00419 gTarget.Params = gSrc.Params;
00420 gTarget.Params.v2Pos = vec(irBest);
00421 gTarget.Params.dGain *= -1;
00422 gTarget.irGridPos = gSrc.irGridPos + irGridStep;
00423 gTarget.mInheritedSteps = gSrc.GetSteps(mvGridCorners);
00424 CalibCornerPatch Patch(gvnCornerPatchSize);
00425 if(!Patch.IterateOnImageWithDrawing(gTarget.Params, mim))
00426 return;
00427
00428
00429 int nTargetNum = mvGridCorners.size();
00430 for(int dirn = 0; dirn<4; dirn++)
00431 {
00432 ImageRef irSearch = gTarget.irGridPos + IR_from_dirn(dirn);
00433 for(unsigned int i=0; i<mvGridCorners.size(); i++)
00434 if(mvGridCorners[i].irGridPos == irSearch)
00435 {
00436 gTarget.aNeighborStates[dirn].val = i;
00437 mvGridCorners[i].aNeighborStates[(dirn + 2) % 4].val = nTargetNum;
00438 }
00439 }
00440 mvGridCorners.push_back(gTarget);
00441 mvGridCorners.back().Draw();
00442 }
00443
00444 void CalibImage::DrawImageGrid()
00445 {
00446 glLineWidth(2);
00447 glColor3f(0,0,1);
00448 glEnable(GL_LINE_SMOOTH);
00449 glEnable(GL_BLEND);
00450 glBegin(GL_LINES);
00451
00452 for(int i=0; i< (int) mvGridCorners.size(); i++)
00453 {
00454 for(int dirn=0; dirn<4; dirn++)
00455 if(mvGridCorners[i].aNeighborStates[dirn].val > i)
00456 {
00457 glVertex(mvGridCorners[i].Params.v2Pos);
00458 glVertex(mvGridCorners[mvGridCorners[i].aNeighborStates[dirn].val].Params.v2Pos);
00459 }
00460 }
00461 glEnd();
00462
00463 glPointSize(5);
00464 glEnable(GL_POINT_SMOOTH);
00465 glColor3f(1,1,0);
00466 glBegin(GL_POINTS);
00467 for(unsigned int i=0; i<mvGridCorners.size(); i++)
00468 glVertex(mvGridCorners[i].Params.v2Pos);
00469 glEnd();
00470 };
00471
00472 void CalibImage::Draw3DGrid(ATANCamera &Camera, bool bDrawErrors)
00473 {
00474 glLineWidth(2);
00475 glColor3f(0,0,1);
00476 glEnable(GL_LINE_SMOOTH);
00477 glEnable(GL_BLEND);
00478 glBegin(GL_LINES);
00479
00480 for(int i=0; i< (int) mvGridCorners.size(); i++)
00481 {
00482 for(int dirn=0; dirn<4; dirn++)
00483 if(mvGridCorners[i].aNeighborStates[dirn].val > i)
00484 {
00485 Vector<3> v3; v3[2] = 0.0;
00486 v3.slice<0,2>() = vec(mvGridCorners[i].irGridPos);
00487 glVertex(Camera.Project(project(mse3CamFromWorld * v3)));
00488 v3.slice<0,2>() = vec(mvGridCorners[mvGridCorners[i].aNeighborStates[dirn].val].irGridPos);
00489 glVertex(Camera.Project(project(mse3CamFromWorld * v3)));
00490 }
00491 }
00492 glEnd();
00493
00494 if(bDrawErrors)
00495 {
00496 glColor3f(1,0,0);
00497 glLineWidth(1);
00498 glBegin(GL_LINES);
00499 for(int i=0; i< (int) mvGridCorners.size(); i++)
00500 {
00501 Vector<3> v3; v3[2] = 0.0;
00502 v3.slice<0,2>() = vec(mvGridCorners[i].irGridPos);
00503 Vector<2> v2Pixels_Projected = Camera.Project(project(mse3CamFromWorld * v3));
00504 Vector<2> v2Error = mvGridCorners[i].Params.v2Pos - v2Pixels_Projected;
00505 glVertex(v2Pixels_Projected);
00506 glVertex(v2Pixels_Projected + 10.0 * v2Error);
00507 }
00508 glEnd();
00509 }
00510 };
00511
00512 ImageRef CalibImage::IR_from_dirn(int nDirn)
00513 {
00514 ImageRef ir;
00515 ir[nDirn%2] = (nDirn < 2) ? 1: -1;
00516 return ir;
00517 }
00518
00519
00520 void CalibImage::GuessInitialPose(ATANCamera &Camera)
00521 {
00522
00523
00524
00525
00526 int nPoints = mvGridCorners.size();
00527 Matrix<> m2Nx9(2*nPoints, 9);
00528 for(int n=0; n<nPoints; n++)
00529 {
00530
00531 Vector<2> v2UnProj = Camera.UnProject(mvGridCorners[n].Params.v2Pos);
00532 double u = v2UnProj[0];
00533 double v = v2UnProj[1];
00534
00535 double x = mvGridCorners[n].irGridPos.x;
00536 double y = mvGridCorners[n].irGridPos.y;
00537
00538 m2Nx9[n*2+0][0] = x;
00539 m2Nx9[n*2+0][1] = y;
00540 m2Nx9[n*2+0][2] = 1;
00541 m2Nx9[n*2+0][3] = 0;
00542 m2Nx9[n*2+0][4] = 0;
00543 m2Nx9[n*2+0][5] = 0;
00544 m2Nx9[n*2+0][6] = -x*u;
00545 m2Nx9[n*2+0][7] = -y*u;
00546 m2Nx9[n*2+0][8] = -u;
00547
00548 m2Nx9[n*2+1][0] = 0;
00549 m2Nx9[n*2+1][1] = 0;
00550 m2Nx9[n*2+1][2] = 0;
00551 m2Nx9[n*2+1][3] = x;
00552 m2Nx9[n*2+1][4] = y;
00553 m2Nx9[n*2+1][5] = 1;
00554 m2Nx9[n*2+1][6] = -x*v;
00555 m2Nx9[n*2+1][7] = -y*v;
00556 m2Nx9[n*2+1][8] = -v;
00557 }
00558
00559
00560 SVD<> svdHomography(m2Nx9);
00561 Vector<9> vH = svdHomography.get_VT()[8];
00562 Matrix<3> m3Homography;
00563 m3Homography[0] = vH.slice<0,3>();
00564 m3Homography[1] = vH.slice<3,3>();
00565 m3Homography[2] = vH.slice<6,3>();
00566
00567
00568
00569 {
00570 SVD<2> svdTopLeftBit(m3Homography.slice<0,0,2,2>());
00571 Vector<2> v2Diagonal = svdTopLeftBit.get_diagonal();
00572 m3Homography = m3Homography / v2Diagonal[0];
00573 v2Diagonal = v2Diagonal / v2Diagonal[0];
00574 double dLambda2 = v2Diagonal[1];
00575
00576 Vector<2> v2b;
00577 v2b[0] = 0.0;
00578 v2b[1] = sqrt( 1.0 - (dLambda2 * dLambda2));
00579
00580 Vector<2> v2aprime = v2b * svdTopLeftBit.get_VT();
00581
00582 Vector<2> v2a = m3Homography[2].slice<0,2>();
00583 double dDotProd = v2a * v2aprime;
00584
00585 if(dDotProd>0)
00586 m3Homography[2].slice<0,2>() = v2aprime;
00587 else
00588 m3Homography[2].slice<0,2>() = -v2aprime;
00589 }
00590
00591
00592
00593
00594
00595 Matrix<3> mRotation;
00596 Vector<3> vTranslation;
00597 double dMag1 = sqrt(m3Homography.T()[0] * m3Homography.T()[0]);
00598 m3Homography = m3Homography / dMag1;
00599
00600 mRotation.T()[0] = m3Homography.T()[0];
00601
00602
00603
00604 mRotation.T()[1] = m3Homography.T()[1] - m3Homography.T()[0]*(m3Homography.T()[0]*m3Homography.T()[1]);
00605 mRotation.T()[1] /= sqrt(mRotation.T()[1] * mRotation.T()[1]);
00606 mRotation.T()[2] = mRotation.T()[0]^mRotation.T()[1];
00607 vTranslation = m3Homography.T()[2];
00608
00609
00610 mse3CamFromWorld.get_rotation()=mRotation;
00611 mse3CamFromWorld.get_translation() = vTranslation;
00612 };
00613
00614 vector<CalibImage::ErrorAndJacobians> CalibImage::Project(ATANCamera &Camera)
00615 {
00616 vector<ErrorAndJacobians> vResult;
00617 for(unsigned int n=0; n<mvGridCorners.size(); n++)
00618 {
00619 ErrorAndJacobians EAJ;
00620
00621
00622 Vector<3> v3World;
00623 v3World[2] = 0.0;
00624 v3World.slice<0,2>() = vec(mvGridCorners[n].irGridPos);
00625
00626 Vector<3> v3Cam = mse3CamFromWorld * v3World;
00627 if(v3Cam[2] <= 0.001)
00628 continue;
00629
00630 Vector<2> v2Image = Camera.Project(project(v3Cam));
00631 if(Camera.Invalid())
00632 continue;
00633
00634 EAJ.v2Error = mvGridCorners[n].Params.v2Pos - v2Image;
00635
00636
00637 double dOneOverCameraZ = 1.0 / v3Cam[2];
00638 Matrix<2> m2CamDerivs = Camera.GetProjectionDerivs();
00639
00640 for(int dof=0; dof<6; dof++)
00641 {
00642 const Vector<4> v4Motion = SE3<>::generator_field(dof, unproject(v3Cam));
00643 Vector<2> v2CamFrameMotion;
00644 v2CamFrameMotion[0] = (v4Motion[0] - v3Cam[0] * v4Motion[2] * dOneOverCameraZ) * dOneOverCameraZ;
00645 v2CamFrameMotion[1] = (v4Motion[1] - v3Cam[1] * v4Motion[2] * dOneOverCameraZ) * dOneOverCameraZ;
00646 EAJ.m26PoseJac.T()[dof] = m2CamDerivs * v2CamFrameMotion;
00647 };
00648
00649
00650 EAJ.m2NCameraJac = Camera.GetCameraParameterDerivs();
00651 vResult.push_back(EAJ);
00652 }
00653 return vResult;
00654 };
00655
00656
00657
00658
00659
00660
00661