// classicdesc.cpp: model for classic ASM descriptors // // By "classic descriptor" we mean the Cootes' style one dimensional // profile along the whisker orthogonal to the shape boundary. // // Copyright (C) 2005-2013, Stephen Milborrow #include "stasm.h" namespace stasm { static void NormalizeMat( MAT& mat) // io: normalized so L2 length is 1 { double norm = cv::norm(mat); // L2 norm if (!IsZero(norm)) mat /= norm; } static const VEC Bisector( // return normalized bisector of three ordered points const VEC& prev, // in: x,y coords of previous point (1x2 matrix) const VEC& point, // in const VEC& next) // in { VEC u(1, 2); // u is point - prev, rotated by 90 degrees u(IX) = point(IY) - prev(IY); u(IY) = prev(IX) - point(IX); NormalizeMat(u); VEC v(1, 2); // v is next - point, rotated by 90 degrees v(IX) = next(IY) - point(IY); v(IY) = point(IX) - next(IX); NormalizeMat(v); VEC w(u + v); NormalizeMat(w); // are prev and next in the same line? if so, avoid numerical issues if (IsZero(w(IX)) && IsZero(w(IY))) { w = point - prev; NormalizeMat(w); } return w; // w is the direction of the bisector } // get x and y distances to take a single pixel step along the whisker static void WhiskerStep( double& xstep, // out: x dist to move one pix along whisker double& ystep, // out: y dist to move one pix along whisker const Shape& shape, // in int ipoint) // in: index of the current point { int prev, next; PrevAndNextLandmarks(prev, next, ipoint, shape); if ((Equal(shape(prev, IX), shape(ipoint, IX)) && Equal(shape(prev, IY), shape(ipoint, IY))) || (Equal(shape(next, IX), shape(ipoint, IX)) && Equal(shape(next, IY), shape(ipoint, IY)))) { // The prev or next point is on top of the current point. // Arbitrarily point the whisker in a horizontal direction. // TODO Revisit, this is common at low resolution pyramid levels. xstep = 1; ystep = 0; } else { const VEC whisker_direction(Bisector(shape.row(prev), shape.row(ipoint), shape.row(next))); xstep = -whisker_direction(IX); ystep = -whisker_direction(IY); // normalize so either xstep or ystep will be +-1, // and the other will be smaller than +-1 const double abs_xstep = ABS(xstep); const double abs_ystep = ABS(ystep); if (abs_xstep >= abs_ystep) { xstep /= abs_xstep; ystep /= abs_xstep; } else { xstep /= abs_ystep; ystep /= abs_ystep; } } } static inline int Step( // return x coord at the given offset along whisker double x, // in: x coord of center of whisker double xstep, // in: x dist to move one pixel along whisker int offset) // in: offset along whisker in pixels { return cvRound(x + (offset * xstep)); } static inline int Pix( // get pixel at ix and iy, forcing ix and iy in range const Image& img, // in int ix, // in int iy) // in { return img(Clamp(iy, 0, img.rows-1), Clamp(ix, 0, img.cols-1)); } // fullprof is the 1D profile along the whisker, including extra elements // to allow searching away from the current position of the landmark. // // shape[ipoint] is the current position of the landmark, // and the center point of the whisker. // We also use shape for figuring out the direction of the whisker. static void FullProf( VEC& fullprof, // out const Image& img, // in const MAT& shape, // in int ipoint) // in: index of the current point { double xstep; // x axis dist corresponding to one pixel along whisker double ystep; WhiskerStep(xstep, ystep, shape, ipoint); const double x = shape(ipoint, IX); // center point of the whisker const double y = shape(ipoint, IY); // number of pixs to sample in each direction along the whisker const int n = (NSIZE(fullprof) - 1) / 2; int prevpix = Pix(img, Step(x, xstep, -n-1), Step(y, ystep, -n-1)); for (int i = -n; i <= n; i++) { const int pix = Pix(img, Step(x, xstep, i), Step(y, ystep, i)); fullprof(i + n) = double(pix - prevpix); // signed gradient prevpix = pix; } } double SumAbsElems( // return the sum of the abs values of the elems of mat const MAT& mat) // in { CV_Assert(mat.isContinuous()); const double* const data = Buf(mat); double sum = 0; int i = NSIZE(mat); // number of elements while (i--) sum += ABS(data[i]); return sum; } // This returns a double equal to x.t() * mat * x. // // x is a vector (row or column, it doesn't matter). // // mat is assumed to be a symmetric matrix // (but only the upper right triangle of mat is actually used). // // mat and x are not modified. // // This function is equivalent to x.t() * mat * x(), but is optimized // for speed and is faster. It's faster because we use the fact that // mat is symmetric to roughly halve the number of operations. static double xAx( const VEC& x, // in const MAT& mat) //in: must be symmetric { const int n = NSIZE(x); CV_Assert(mat.rows == n && mat.cols == n && x.isContinuous()); const double* px = Buf(x); double diagsum = 0, sum = 0; int i = n; while (i--) { const double xi = px[i]; const double* const rowbuf = mat.ptr(i); diagsum += rowbuf[i] * SQ(xi); // sum diag elements for (int j = i+1; j < n; j++) // sum upper right triangle elements sum += rowbuf[j] * xi * px[j]; } return diagsum + 2 * sum; // "2 *" to include lower left triangle elements } // Get the profile distance. That is, get the image profile at the given // offset along the whisker, and return the Mahalanobis distance between // it and the model mean profile. Low distance means good fit. static double ProfDist( int offset, // in: offset along whisker in pixels int proflen, // in const VEC& fullprof, // in const VEC& meanprof, // in: mean of the training profiles for this point const MAT& covi) // in: inverse of the covar of the training profiles { VEC prof(1, proflen); // the profile at the given offset along whisker // copy the relevant part of fullprof into prof memcpy(Buf(prof), Buf(fullprof) + offset + NSIZE(fullprof)/2 - NSIZE(prof)/2, NSIZE(prof) * sizeof(prof(0))); // normalize prof double sum = SumAbsElems(prof); if (!IsZero(sum)) prof *= NSIZE(prof) / sum; // The following code is equivalent to // return (prof - meanprof).t() * covi * (prof - meanprof) // but is optimized for speed. prof -= meanprof; // for efficiency, use "-=" not "=" with "-" return xAx(prof, covi); } // If OpenMP is enabled, multiple instances of this function will be called // concurrently (each call will have a different value of x and y). Thus this // function and its callees do not modify any data that is not on the stack. void ClassicDescSearch( // search along whisker for best profile match double& x, // io: (in: old posn of landmark, out: new posn) double& y, // io: const Image& img, // in: the image scaled to this pyramid level const Shape& inshape, // in: current posn of landmarks (for whisker directions) int ipoint, // in: index of the current landmark const MAT& meanprof, // in: mean of the training profiles for this point const MAT& covi) // in: inverse of the covar of the training profiles { const int proflen = NSIZE(meanprof); CV_Assert(proflen % 2 == 1); // proflen must be odd in this implementation // fullprof is the 1D profile along the whisker including the extra // elements to allow search +-CLASSIC_MAX_OFFSET pixels away from // the current position of the landmark. // We precalculate the fullprof for efficiency in the for loop below. VEC fullprof(1, proflen + 2 * CLASSIC_MAX_OFFSET); CV_Assert(NSIZE(fullprof) % 2 == 1); // fullprof length must be odd FullProf(fullprof, img, inshape, ipoint); // move along the whisker looking for the best match int bestoffset = 0; double mindist = FLT_MAX; for (int offset = -CLASSIC_MAX_OFFSET; offset <= CLASSIC_MAX_OFFSET; offset += CLASSIC_SEARCH_RESOL) { const double dist = ProfDist(offset, proflen, fullprof, meanprof, covi); if (dist < mindist) { mindist = dist; bestoffset = offset; } } // change x,y to the best position along the whisker double xstep, ystep; WhiskerStep(xstep, ystep, inshape, ipoint); x = inshape(ipoint, IX) + (bestoffset * xstep); y = inshape(ipoint, IY) + (bestoffset * ystep); } } // namespace stasm