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         bool debugging = false; // (thisTagDetection.id == 7) | (thisTagDetection.id == 8);
00479         if ( debugging )
00480           std::cout << "  tag id=" << thisTagDetection.id << " rot=" << thisTagDetection.rotation;
00481         for ( int i=0; i<4; i++ ) {
00482           float const dist = AprilTags::MathUtil::distance2D(bottomLeft, quad.quadPoints[i]);
00483           if ( debugging )
00484             std::cout << std::setw(12) << dist;
00485           if ( dist < bestDist ) {
00486             bestDist = dist;
00487             bestRot = i;
00488           }
00489         }
00490         for (int i=0; i < 4; i++)
00491           thisTagDetection.p[i] = quad.quadPoints[(i+bestRot) % 4];
00492         if ( debugging ) {
00493           const TagDetection &d = thisTagDetection;
00494           std::cout << "  best=" << bestRot << endl;
00495           std::cout << "   " << d.p[0].first << " " << d.p[1].first
00496                     << " " << d.p[2].first << " " << d.p[3].first << std::endl;
00497           std::cout << R.fmt("%8.5f") << std::endl << thisTagDetection.homography.fmt("%8.5f") << std::endl;
00498         }
00499 
00500         if (thisTagDetection.good) {
00501           thisTagDetection.cxy = quad.interpolate01(0.5f, 0.5f);
00502           thisTagDetection.observedPerimeter = quad.observedPerimeter;
00503           detections.push_back(thisTagDetection);
00504         }
00505       }
00506     }
00507 
00508     //================================================================
00509     //Step nine: Some quads may be detected more than once, due to
00510     //partial occlusion and our aggressive attempts to recover from
00511     //broken lines. When two quads (with the same id) overlap, we will
00512     //keep the one with the lowest error, and if the error is the same,
00513     //the one with the greatest observed perimeter.
00514 
00515     std::vector<TagDetection> goodDetections;
00516 
00517     // NOTE: allow multiple non-overlapping detections of the same target.
00518 
00519     for ( vector<TagDetection>::const_iterator it = detections.begin();
00520           it != detections.end(); it++ ) {
00521       const TagDetection &thisTagDetection = *it;
00522 
00523       bool newFeature = true;
00524 
00525       for ( unsigned int odidx = 0; odidx < goodDetections.size(); odidx++) {
00526         TagDetection &otherTagDetection = goodDetections[odidx];
00527 
00528         if ( thisTagDetection.id != otherTagDetection.id ||
00529              ! thisTagDetection.overlapsTooMuch(otherTagDetection) )
00530           continue;
00531 
00532         // There's a conflict.  We must pick one to keep.
00533         newFeature = false;
00534 
00535         // This detection is worse than the previous one... just don't use it.
00536         if ( thisTagDetection.hammingDistance > otherTagDetection.hammingDistance )
00537           continue;
00538 
00539         // Otherwise, keep the new one if it either has strictly *lower* error, or greater perimeter.
00540         if ( thisTagDetection.hammingDistance < otherTagDetection.hammingDistance ||
00541              thisTagDetection.observedPerimeter > otherTagDetection.observedPerimeter )
00542           goodDetections[odidx] = thisTagDetection;
00543       }
00544 
00545       if ( newFeature )
00546         goodDetections.push_back(thisTagDetection);
00547 
00548       /*
00549         if ( newFeature ) {
00550         cout << "thisTagDetection: cxy=" << thisTagDetection.cxy << endl;
00551         cout << "   p[0]=" << thisTagDetection.p[0] << endl;
00552         cout << "   p[1]=" << thisTagDetection.p[1] << endl;
00553         cout << "   p[2]=" << thisTagDetection.p[2] << endl;
00554         cout << "   p[3]=" << thisTagDetection.p[3] << endl;
00555         cout << "   hxy = " << thisTagDetection.hxy << endl;
00556         cout << "   homography = " << thisTagDetection.homography << endl;
00557         cout << "   code = " << thisTagDetection.code << endl;
00558         }
00559       */
00560 
00561     }
00562 
00563     /*
00564       cout << "AprilTags: edges=" << nEdges
00565       << " clusters=" << clusters.size()
00566       << " segments=" << segments.size()
00567       << " quads=" << quads.size()
00568       << " detections=" << detections.size()
00569       << " unique tags=" << goodDetections.size() << endl;
00570     */
00571 
00572     return goodDetections;
00573   }
00574 
00575 } // namespace

Tekkotsu v5.1CVS
Generated Mon May 9 04:58:51 2016 by Doxygen 1.6.3