Tekkotsu Homepage
Demos
Overview
Downloads
Dev. Resources
Reference
Credits

TagDetector.cc

Go to the documentation of this file.
00001 #include <algorithm>
00002 #include <cmath>
00003 #include <climits>
00004 #include <map>
00005 #include <vector>
00006 
00007 #include "Vision/AprilTags/Edge.h"
00008 #include "Vision/AprilTags/FloatImage.h"
00009 #include "Vision/AprilTags/Gaussian.h"
00010 #include "Vision/AprilTags/GrayModel.h"
00011 #include "Vision/AprilTags/GLine2D.h"
00012 #include "Vision/AprilTags/GLineSegment2D.h"
00013 #include "Vision/AprilTags/Gridder.h"
00014 #include "Vision/AprilTags/Homography33.h"
00015 #include "Vision/AprilTags/MathUtil.h"
00016 #include "Vision/AprilTags/Quad.h"
00017 #include "Vision/AprilTags/Segment.h"
00018 #include "Vision/AprilTags/TagFamily.h"
00019 #include "Vision/AprilTags/UnionFindSimple.h"
00020 #include "Vision/AprilTags/XYWeight.h"
00021 
00022 #include "Vision/AprilTags/TagDetector.h"
00023 
00024 #include "DualCoding/Sketch.h"
00025 
00026 using namespace std;
00027 using namespace DualCoding;
00028 
00029 namespace AprilTags {
00030 
00031 std::vector<TagDetection> TagDetector::extractTags(const DualCoding::Sketch<DualCoding::uchar> &rawY) {
00032   return extractTags(FloatImage(rawY));  // convert sketch to FloatImage
00033 }
00034 
00035 std::vector<TagDetection> TagDetector::extractTags(const FloatImage& fimOrig) {
00036 
00037   //================================================================
00038   // Step one: preprocess image (convert to grayscale) and low pass if necessary
00039 
00040   FloatImage fim = fimOrig;
00041   
00042   //! Gaussian smoothing kernel applied to image (0 == no filter).
00043   /*! Used when sampling bits. Filtering is a good idea in cases
00044    * where A) a cheap camera is introducing artifical sharpening, B)
00045    * the bayer pattern is creating artifcats, C) the sensor is very
00046    * noisy and/or has hot/cold pixels. However, filtering makes it
00047    * harder to decode very small tags. Reasonable values are 0, or
00048    * [0.8, 1.5].
00049    */
00050   float sigma = 0;
00051 
00052   //! Gaussian smoothing kernel applied to image (0 == no filter).
00053   /*! Used when detecting the outline of the box. It is almost always
00054    * useful to have some filtering, since the loss of small details
00055    * won't hurt. Recommended value = 0.8. The case where sigma ==
00056    * segsigma has been optimized to avoid a redundant filter
00057    * operation.
00058    */
00059   float segSigma = 0.8f;
00060 
00061   if (sigma > 0) {
00062     int filtsz = ((int) max(3.0f, 3*sigma)) | 1;
00063     std::vector<float> filt = Gaussian::makeGaussianFilter(sigma, filtsz);
00064     fim.filterFactoredCentered(filt, filt);
00065   }
00066 
00067   //================================================================
00068   // Step two: Compute the local gradient. We store the direction and magnitude.
00069   // This step is quite sensitve to noise, since a few bad theta estimates will
00070   // break up segments, causing us to miss Quads. It is useful to do a Gaussian
00071   // low pass on this step even if we don't want it for encoding.
00072 
00073   FloatImage fimSeg;
00074   if (segSigma > 0) {
00075     if (segSigma == sigma) {
00076       fimSeg = fim;
00077     } else {
00078       // blur anew
00079       int filtsz = ((int) max(3.0f, 3*segSigma)) | 1;
00080       std::vector<float> filt = Gaussian::makeGaussianFilter(segSigma, filtsz);
00081       fimSeg = fimOrig;
00082       fimSeg.filterFactoredCentered(filt, filt);
00083     }
00084   } else {
00085     fimSeg = fimOrig;
00086   }
00087 
00088   FloatImage fimTheta(fimSeg.getWidth(), fimSeg.getHeight());
00089   FloatImage fimMag(fimSeg.getWidth(), fimSeg.getHeight());
00090   
00091   for (int y = 1; y+1 < fimSeg.getHeight(); y++) {
00092     for (int x = 1; x+1 < fimSeg.getWidth(); x++) {
00093       float Ix = fimSeg.get(x+1, y) - fimSeg.get(x-1, y);
00094       float Iy = fimSeg.get(x, y+1) - fimSeg.get(x, y-1);
00095             
00096       float mag = Ix*Ix + Iy*Iy;
00097       float theta = atan2(Iy, Ix);
00098       
00099       fimTheta.set(x, y, theta);
00100       fimMag.set(x, y, mag);
00101     }
00102   }
00103 
00104   // Debugging code
00105   /*
00106   NEW_SKETCH(im1, uchar, rawY);
00107   NEW_SKETCH(skFim, uchar, visops::zeros(im1));
00108   skFim->setColorMap(grayMap); 
00109   NEW_SKETCH(skRawTheta, float, visops::zeros(im1));
00110   skRawTheta->setColorMap(jetMapScaled);
00111   NEW_SKETCH(skDisplayTheta, uchar, visops::zeros(im1));
00112   skDisplayTheta->setColorMap(grayMap);
00113   NEW_SKETCH(skRawMag, float, visops::zeros(im1));
00114   skRawMag->setColorMap(jetMapScaled);
00115   NEW_SKETCH(skMagNorm, float, visops::zeros(skRawMag));
00116   skMagNorm->setColorMap(jetMap);
00117   NEW_SKETCH(skDisplayMag, uchar, visops::zeros(im1));
00118   skDisplayMag->setColorMap(grayMap); 
00119 
00120   fimSeg.copyToSketch(skFim);
00121   fimTheta.copyToSketch(skRawTheta);
00122   skDisplayTheta = ((skRawTheta+4.0f) * 30.0f); // float to uchar
00123   FloatImage fimMagNorm = fimMag;
00124   fimMagNorm.normalize();
00125   fimMag.copyToSketch(skRawMag);
00126   fimMagNorm.copyToSketch(skMagNorm);
00127   skMagNorm *= 255.0f;
00128   skDisplayMag = (Sketch<uchar>)skMagNorm;
00129   */
00130 
00131   //================================================================
00132   // Step three. Extract edges by grouping pixels with similar
00133   // thetas together. This is a greedy algorithm: we start with
00134   // the most similar pixels.  We use 4-connectivity.
00135   UnionFindSimple uf(fimSeg.getWidth()*fimSeg.getHeight());
00136   
00137   int width = fimSeg.getWidth();
00138   int height = fimSeg.getHeight();
00139 
00140   vector<Edge> edges(width*height*4);
00141   size_t nEdges = 0;
00142 
00143   // Bounds on the thetas assigned to this group. Note that because
00144   // theta is periodic, these are defined such that the average
00145   // value is contained *within* the interval.
00146     { // limit scope of storage
00147       /* Previously all this was on the stack, but this is 1.2MB for 320x240 images
00148        * That's already a problem for OS X (default 512KB thread stack size),
00149        * could be a problem elsewhere for bigger images... so store on heap */
00150       vector<float> storage(width*height*4);  // do all the memory in one big block, exception safe
00151       float * tmin = &storage[width*height*0];
00152       float * tmax = &storage[width*height*1];
00153       float * mmin = &storage[width*height*2];
00154       float * mmax = &storage[width*height*3];
00155       
00156       for (int y = 0; y+1 < height; y++) {
00157         for (int x = 0; x+1 < width; x++) {
00158           
00159           float mag0 = fimMag.get(x,y);
00160           if (mag0 < Edge::minMag)
00161             continue;
00162           mmax[y*width+x] = mag0;
00163           mmin[y*width+x] = mag0;
00164           
00165           float theta0 = fimTheta.get(x,y);
00166           tmin[y*width+x] = theta0;
00167           tmax[y*width+x] = theta0;
00168           
00169           // Calculates then adds edges to 'vector<Edge> edges'
00170           Edge::calcEdges(theta0, x, y, fimTheta, fimMag, edges, nEdges);
00171           
00172           // XXX Would 8 connectivity help for rotated tags?
00173           // Probably not much, so long as input filtering hasn't been disabled.
00174         }
00175       }
00176       
00177       edges.resize(nEdges);
00178       std::stable_sort(edges.begin(), edges.end());
00179       Edge::mergeEdges(edges,uf,tmin,tmax,mmin,mmax);
00180     }
00181     
00182   //================================================================
00183   // Step four: Loop over the pixels again, collecting statistics for each cluster.
00184   // We will soon fit lines (segments) to these points.
00185 
00186   map<int, vector<XYWeight> > clusters;
00187   for (int y = 0; y+1 < fimSeg.getHeight(); y++) {
00188     for (int x = 0; x+1 < fimSeg.getWidth(); x++) {
00189       if (uf.getSetSize(y*fimSeg.getWidth()+x) < Segment::minimumSegmentSize)
00190   continue;
00191 
00192       int rep = (int) uf.getRepresentative(y*fimSeg.getWidth()+x);
00193      
00194       map<int, vector<XYWeight> >::iterator it = clusters.find(rep);
00195       if ( it == clusters.end() ) {
00196   clusters[rep] = vector<XYWeight>();
00197   it = clusters.find(rep);
00198       }
00199       vector<XYWeight> &points = it->second;
00200       points.push_back(XYWeight(x,y,fimMag.get(x,y)));
00201     }
00202   }
00203 
00204   //================================================================
00205   // Step five: Loop over the clusters, fitting lines (which we call Segments).
00206   std::vector<Segment> segments; //used in Step six
00207   std::map<int, std::vector<XYWeight> >::const_iterator clustersItr;
00208   for (clustersItr = clusters.begin(); clustersItr != clusters.end(); clustersItr++) {
00209     std::vector<XYWeight> points = clustersItr->second;
00210     GLineSegment2D gseg = GLineSegment2D::lsqFitXYW(points);
00211 
00212     // filter short lines
00213     float length = MathUtil::distance2D(gseg.getP0(), gseg.getP1());
00214     if (length < Segment::minimumLineLength)
00215       continue;
00216 
00217     Segment seg;
00218     float dy = gseg.getP1().second - gseg.getP0().second;
00219     float dx = gseg.getP1().first - gseg.getP0().first;
00220 
00221     float tmpTheta = std::atan2(dy,dx);
00222 
00223     seg.setTheta(tmpTheta);
00224     seg.setLength(length);
00225 
00226     // We add an extra semantic to segments: the vector
00227     // p1->p2 will have dark on the left, white on the right.
00228     // To do this, we'll look at every gradient and each one
00229     // will vote for which way they think the gradient should
00230     // go. This is way more retentive than necessary: we
00231     // could probably sample just one point!
00232 
00233     float flip = 0, noflip = 0;
00234     for (unsigned int i = 0; i < points.size(); i++) {
00235       XYWeight xyw = points[i];
00236       
00237       float theta = fimTheta.get((int) xyw.x, (int) xyw.y);
00238       float mag = fimMag.get((int) xyw.x, (int) xyw.y);
00239 
00240       // err *should* be +M_PI/2 for the correct winding, but if we
00241       // got the wrong winding, it'll be around -M_PI/2.
00242       float err = MathUtil::mod2pi(theta - seg.getTheta());
00243 
00244       if (err < 0)
00245   noflip += mag;
00246       else
00247   flip += mag;
00248     }
00249 
00250     if (flip > noflip) {
00251       float temp = seg.getTheta() + (float)M_PI;
00252       seg.setTheta(temp);
00253     }
00254 
00255     float dot = dx*std::cos(seg.getTheta()) + dy*std::sin(seg.getTheta());
00256     if (dot > 0) {
00257       seg.setX0(gseg.getP1().first); seg.setY0(gseg.getP1().second);
00258       seg.setX1(gseg.getP0().first); seg.setY1(gseg.getP0().second);
00259     }
00260     else {
00261       seg.setX0(gseg.getP0().first); seg.setY0(gseg.getP0().second);
00262       seg.setX1(gseg.getP1().first); seg.setY1(gseg.getP1().second);
00263     }
00264 
00265     segments.push_back(seg);
00266   }
00267 
00268   // Display segments for debugging
00269   /*
00270   for (unsigned int i=0; i < segments.size(); i++) {
00271     if (segments[i].getLength() >= 25) {
00272     NEW_SHAPE(myLine, LineData, new LineData(VRmixin::camShS, 
00273                Point(segments[i].getX0(),segments[i].getY0()),
00274                Point(segments[i].getX1(),segments[i].getY1())));
00275     char buff[30];
00276     sprintf(buff, "segment%d", segments[i].getId());
00277     myLine->setName(std::string(buff));
00278     myLine->setColor("green");
00279     }
00280   }
00281   */
00282 
00283   // Step six: For each segment, find segments that begin where this segment ends.
00284   // (We will chain segments together next...) The gridder accelerates the search by
00285   // building (essentially) a 2D hash table.
00286   Gridder<Segment> gridder(0,0,width,height,10);
00287   
00288   // add every segment to the hash table according to the position of the segment's
00289   // first point. Remember that the first point has a specific meaning due to our
00290   // left-hand rule above.
00291   for (unsigned int i = 0; i < segments.size(); i++) {
00292     gridder.add(segments[i].getX0(), segments[i].getY0(), &segments[i]);
00293   }
00294   
00295   // Now, find child segments that begin where each parent segment ends.
00296   for (unsigned i = 0; i < segments.size(); i++) {
00297     Segment &parentseg = segments[i];
00298       
00299     //compute length of the line segment
00300     GLine2D parentLine(std::pair<float,float>(parentseg.getX0(), parentseg.getY0()),
00301            std::pair<float,float>(parentseg.getX1(), parentseg.getY1()));
00302 
00303     Gridder<Segment>::iterator iter = gridder.find(parentseg.getX1(), parentseg.getY1(), 0.5f*parentseg.getLength());
00304     while(iter.hasNext()) {
00305       Segment &child = iter.next();
00306       if (MathUtil::mod2pi(child.getTheta() - parentseg.getTheta()) > 0) {
00307   continue;
00308       }
00309 
00310       // compute intersection of points
00311       GLine2D childLine(std::pair<float,float>(child.getX0(), child.getY0()),
00312       std::pair<float,float>(child.getX1(), child.getY1()));
00313 
00314       std::pair<float,float> p = parentLine.intersectionWith(childLine);
00315       if (p.first == -1) {
00316   continue;
00317       }
00318 
00319       float parentDist = MathUtil::distance2D(p, std::pair<float,float>(parentseg.getX1(),parentseg.getY1()));
00320       float childDist = MathUtil::distance2D(p, std::pair<float,float>(child.getX0(),child.getY0()));
00321 
00322       if (max(parentDist,childDist) > parentseg.getLength()) {
00323   // cout << "intersection too far" << endl;
00324   continue;
00325       }
00326 
00327       // everything's OK, this child is a reasonable successor.
00328       parentseg.children.push_back(&child);
00329     }
00330   }
00331 
00332   // Debug: print children of each segment
00333   /*
00334   for (unsigned int i = 0; i < segments.size(); i++) {
00335     for (unsigned int j = 0; j < segments[i].children.size(); j++) {
00336       if (segments[i].children.size() == 0){
00337   cout << "segment_id:" << segments[i].getId() << " has no children" << endl;
00338   continue;
00339       }
00340       cout << "segment_id:" << segments[i].children[j]->getId() << " is a child of segment_id:" << segments[i].getId() << endl;
00341     }
00342   }
00343   */
00344 
00345   //================================================================
00346   // Step seven: Search all connected segments to see if any form a loop of length 4.
00347   // Add those to the quads list.
00348   vector<Quad> quads;
00349   
00350   vector<Segment*> tmp(5);
00351   for (unsigned int i = 0; i < segments.size(); i++) {
00352     tmp[0] = &segments[i];
00353     Quad::search(fimOrig, tmp, segments[i], 0, quads);
00354   }
00355 
00356 
00357   // Display quads for debugging
00358   /*
00359   for (unsigned int i = 0; i < (unsigned int)min(160,(int)quads.size()); i++) {
00360     // first line
00361     NEW_SHAPE(Quad1, LineData, 
00362         new LineData(VRmixin::camShS,
00363          Point(quads[i].quadPoints[0].first, quads[i].quadPoints[0].second),
00364          Point(quads[i].quadPoints[1].first, quads[i].quadPoints[1].second)));
00365     Quad1->setColor("pink");
00366 
00367     // second line
00368     NEW_SHAPE(Quad2, LineData, 
00369         new LineData(VRmixin::camShS, 
00370          Point(quads[i].quadPoints[1].first, quads[i].quadPoints[1].second),
00371          Point(quads[i].quadPoints[2].first, quads[i].quadPoints[2].second)));
00372     Quad2->setColor("green");
00373     
00374     // third line
00375     NEW_SHAPE(Quad3, LineData, 
00376         new LineData(VRmixin::camShS,
00377          Point(quads[i].quadPoints[2].first, quads[i].quadPoints[2].second),
00378          Point(quads[i].quadPoints[3].first, quads[i].quadPoints[3].second)));
00379     Quad3->setColor("blue");
00380 
00381     // fourth line
00382     NEW_SHAPE(Quad4, LineData,
00383         new LineData(VRmixin::camShS,
00384          Point(quads[i].quadPoints[3].first, quads[i].quadPoints[3].second),
00385          Point(quads[i].quadPoints[0].first, quads[i].quadPoints[0].second)));
00386     Quad4->setColor("blue");
00387   }
00388   */
00389 
00390   //================================================================
00391   // Step eight. Decode the quads. For each quad, we first estimate a
00392   // threshold color to decide between 0 and 1. Then, we read off the
00393   // bits and see if they make sense.
00394 
00395   std::vector<TagDetection> detections;
00396 
00397   for (unsigned int qi = 0; qi < quads.size(); qi++ ) {
00398     Quad &quad = quads[qi];
00399 
00400     // Find a threshold
00401     GrayModel blackModel, whiteModel;
00402     const int dd = 2 * thisTagFamily.blackBorder + thisTagFamily.dimension;
00403 
00404     for (int iy = -1; iy <= dd; iy++) {
00405       float y = (iy + 0.5f) / dd;
00406       for (int ix = -1; ix <= dd; ix++) {
00407   float x = (ix + 0.5f) / dd;
00408   std::pair<float,float> pxy = quad.interpolate01(x, y);
00409   int irx = (int) (pxy.first + 0.5);
00410   int iry = (int) (pxy.second + 0.5);
00411   if (irx < 0 || irx >= width || iry < 0 || iry >= height)
00412     continue;
00413   float v = fim.get(irx, iry);
00414   if (iy == -1 || iy == dd || ix == -1 || ix == dd)
00415     whiteModel.addObservation(x, y, v);
00416   else if (iy == 0 || iy == (dd-1) || ix == 0 || ix == (dd-1))
00417     blackModel.addObservation(x, y, v);
00418       }
00419     }
00420 
00421     bool bad = false;
00422     unsigned long long tagCode = 0;
00423     for ( int iy = thisTagFamily.dimension-1; iy >= 0; iy-- ) {
00424       float y = (thisTagFamily.blackBorder + iy + 0.5f) / dd;
00425       for (int ix = 0; ix < thisTagFamily.dimension; ix++ ) {
00426   float x = (thisTagFamily.blackBorder + ix + 0.5f) / dd;
00427   std::pair<float,float> pxy = quad.interpolate01(x, y);
00428   int irx = (int) (pxy.first + 0.5);
00429   int iry = (int) (pxy.second + 0.5);
00430   if (irx < 0 || irx >= width || iry < 0 || iry >= height) {
00431     cout << "*** bad:  irx=" << irx << "  iry=" << iry << endl;
00432     bad = true;
00433     continue;
00434   }
00435   float threshold = (blackModel.interpolate(x,y) + whiteModel.interpolate(x,y)) * 0.5f;
00436   float v = fim.get(irx, iry);
00437   /*
00438   float diff = threshold-v;
00439   if ( fabs(diff) < 0.25) { 
00440     std::cout << "  ix/iy = " << ix << " " << iy
00441         << "  x/y = " << x << " " << y
00442         << "   pxy = " << irx << " " << iry;
00443     std::cout << " " << threshold << "/" << v << "/" << diff << std::endl;
00444   }
00445   */
00446   tagCode = tagCode << 1;
00447   if ( v > threshold)
00448     tagCode |= 1;
00449       }
00450     }
00451 
00452     if ( !bad ) {
00453       TagDetection thisTagDetection;
00454       thisTagFamily.decode(thisTagDetection, tagCode);
00455       // cout << std::endl << "bad=" << bad << "  good=" << thisTagDetection.good
00456       //      << " tagCode=" << (void*)tagCode << std::endl;
00457 
00458       // compute the homography (and rotate it appropriately)
00459       thisTagDetection.homography = quad.homography.getH();
00460       thisTagDetection.hxy = quad.homography.getCXY();
00461 
00462       float c = std::cos(thisTagDetection.rotation*(float)M_PI/2);
00463       float s = std::sin(thisTagDetection.rotation*(float)M_PI/2);
00464       fmat::Matrix<3,3> R;
00465       R(0,0) = R(1,1) = c;
00466       R(0,1) = -s;
00467       R(1,0) = s;
00468       R(2,2) = 1;
00469       thisTagDetection.homography *= R;
00470 
00471       // Rotate points in detection according to decoded
00472       // orientation.  Thus the order of the points in the
00473       // detection object can be used to determine the
00474       // orientation of the target.
00475       std::pair<float,float> bottomLeft = thisTagDetection.interpolate(-1,-1);
00476       int bestRot = -1;
00477       float bestDist = FLT_MAX;
00478       for ( int i=0; i<4; i++ ) {
00479   float const dist = AprilTags::MathUtil::distance2D(bottomLeft, quad.quadPoints[i]);
00480   if ( dist < bestDist ) {
00481     bestDist = dist;
00482     bestRot = i;
00483   }
00484       }
00485       for (int i=0; i< 4; i++)
00486   thisTagDetection.p[i] = quad.quadPoints[(i+bestRot) % 4];
00487 
00488       if (thisTagDetection.good) {
00489   thisTagDetection.cxy = quad.interpolate01(0.5f, 0.5f);
00490   thisTagDetection.observedPerimeter = quad.observedPerimeter;
00491   detections.push_back(thisTagDetection);
00492       }
00493     }
00494   }
00495 
00496   //================================================================
00497   //Step nine: Some quads may be detected more than once, due to
00498   //partial occlusion and our aggressive attempts to recover from
00499   //broken lines. When two quads (with the same id) overlap, we will
00500   //keep the one with the lowest error, and if the error is the same,
00501   //the one with the greatest observed perimeter.
00502 
00503   std::vector<TagDetection> goodDetections;
00504 
00505   // NOTE: allow multiple non-overlapping detections of the same target.
00506 
00507   for ( vector<TagDetection>::const_iterator it = detections.begin();
00508   it != detections.end(); it++ ) {
00509     const TagDetection &thisTagDetection = *it;
00510 
00511     bool newFeature = true;
00512 
00513     for ( unsigned int odidx = 0; odidx < goodDetections.size(); odidx++) {
00514       TagDetection &otherTagDetection = goodDetections[odidx];
00515 
00516       if ( thisTagDetection.id != otherTagDetection.id ||
00517      ! thisTagDetection.overlapsTooMuch(otherTagDetection) )
00518   continue;
00519 
00520       // There's a conflict.  We must pick one to keep.
00521       newFeature = false;
00522 
00523       // This detection is worse than the previous one... just don't use it.
00524       if ( thisTagDetection.hammingDistance > otherTagDetection.hammingDistance )
00525   continue;
00526 
00527       // Otherwise, keep the new one if it either has strictly *lower* error, or greater perimeter.
00528       if ( thisTagDetection.hammingDistance < otherTagDetection.hammingDistance ||
00529      thisTagDetection.observedPerimeter > otherTagDetection.observedPerimeter )
00530   goodDetections[odidx] = thisTagDetection;
00531     }
00532 
00533      if ( newFeature )
00534        goodDetections.push_back(thisTagDetection);
00535 
00536     /*
00537      if ( newFeature ) {
00538      cout << "thisTagDetection: cxy=" << thisTagDetection.cxy << endl;
00539       cout << "   p[0]=" << thisTagDetection.p[0] << endl;
00540       cout << "   p[1]=" << thisTagDetection.p[1] << endl;
00541       cout << "   p[2]=" << thisTagDetection.p[2] << endl;
00542       cout << "   p[3]=" << thisTagDetection.p[3] << endl;
00543       cout << "   hxy = " << thisTagDetection.hxy << endl;
00544       cout << "   homography = " << thisTagDetection.homography << endl;
00545       cout << "   code = " << thisTagDetection.code << endl;
00546     }
00547     */
00548 
00549   }
00550 
00551   cout << "AprilTags: edges=" << nEdges
00552        << " clusters=" << clusters.size()
00553        << " segments=" << segments.size()
00554        << " quads=" << quads.size()
00555        << " detections=" << detections.size()
00556        << " unique tags=" << goodDetections.size() << endl;
00557 
00558   return goodDetections;
00559 }
00560 
00561 } // namespace

Tekkotsu v5.1CVS
Generated Sat May 4 06:33:03 2013 by Doxygen 1.6.3