Skip to content

Commit

Permalink
Merge pull request #256 from valillon/master
Browse files Browse the repository at this point in the history
Wrapped polygon methods, faster copy and new skeletonization algorithm
  • Loading branch information
kylemcdonald committed Sep 13, 2019
2 parents 8459668 + 77203e0 commit fd79707
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 29 deletions.
14 changes: 9 additions & 5 deletions libs/ofxCv/include/ofxCv/ContourFinder.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
// to implement in ContourFinder:
// holes/no holes
// CV_THRESH_OTSU?
// cv::pointPolygonTest - inside, edge, outside
// cv::matchShapes - similarity between two contours
// cv::estimateRigidTransform? subdivision-based estimation for outline-flow?

Expand Down Expand Up @@ -53,10 +52,10 @@ namespace ofxCv {
ofPolyline& getPolyline(unsigned int i);

cv::Rect getBoundingRect(unsigned int i) const;
cv::Point2f getCenter(unsigned int i) const; // center of bounding box (most stable)
cv::Point2f getCentroid(unsigned int i) const; // center of mass (less stable)
cv::Point2f getAverage(unsigned int i) const; // average of contour vertices (least stable)
cv::Vec2f getBalance(unsigned int i) const; // difference between centroid and center
cv::Point2f getCenter(unsigned int i) const; // center of bounding box (most stable)
cv::Point2f getCentroid(unsigned int i) const; // center of mass (less stable)
cv::Point2f getAverage(unsigned int i) const; // average of contour vertices (least stable)
cv::Vec2f getBalance(unsigned int i) const; // difference between centroid and center
double getContourArea(unsigned int i) const;
double getArcLength(unsigned int i) const;
std::vector<cv::Point> getConvexHull(unsigned int i) const;
Expand All @@ -70,6 +69,11 @@ namespace ofxCv {

RectTracker& getTracker();
unsigned int getLabel(unsigned int i) const;

// Performs a point-in-contour test.
// The function determines whether the point is inside a contour, outside, or lies on an edge (or coincides with a vertex)
// The return value is the signed distance (positive stands for inside).
double pointPolygonTest(unsigned int i, cv::Point2f point);

void setThreshold(float thresholdValue);
void setAutoThreshold(bool autoThreshold);
Expand Down
37 changes: 34 additions & 3 deletions libs/ofxCv/include/ofxCv/Helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ namespace ofxCv {
cv::Point3_<T> intersectPointRay(cv::Point3_<T> point, cv::Point3_<T> ray) {
return ray * (point.dot(ray) / ray.dot(ray));
}
// morphological thinning, also called skeletonization, strangely missing from opencv
// here is a description of the algorithm http://homepages.inf.ed.ac.uk/rbf/HIPR2/thin.htm
// Note: it may produce wrong skeletons for some complex shapes.
template <class T>
void thin(T& img) {
cv::Mat mat = toCv(img);
Expand All @@ -194,6 +195,34 @@ namespace ofxCv {
for(int i=0;i<n;i++){int j=q[i];if(!p[j+ib1]&&!p[j+ia1]&&!p[j+ia2]&&p[j+ic2]&&p[j+ib3]){p[j]=0;}}
}

// Code for thinning a binary image using Zhang-Suen algorithm.
// Large areas to skeletonize may take considerable amount of time.
// Consider to rescale the input image and then upscale the skeleton for being computed every update()
// Otherwise use ofxCv::thin(), although it may lead to wrong skeletons for some shapes.
// Note: do not call thinningIteration() directly from your ofApp.
void thinningIteration( cv::Mat & img, int iter, cv::Mat & marker );
template<class T>
void thinning(T& img)
{
cv::Mat dst = toCv(img);
dst /= 255;
cv::Mat prev = cv::Mat::zeros(dst.size(), CV_8UC1);
cv::Mat marker = cv::Mat::zeros(dst.size(), CV_8UC1); // Re-uses allocated memory
cv::Mat diff;

do {
marker.setTo(cv::Scalar(0));
thinningIteration(dst, 0, marker);
marker.setTo(cv::Scalar(0));
thinningIteration(dst, 1, marker);
cv::absdiff(dst, prev, diff);
dst.copyTo(prev);
}
while (cv::countNonZero(diff) > 0);

dst *= 255;
}

// given a vector of lines, this function will find the average angle
float weightedAverageAngle(const std::vector<cv::Vec4i>& lines);

Expand All @@ -203,6 +232,7 @@ namespace ofxCv {
template <class S, class T, class D>
float autorotate(const S& src, D& dst, float threshold1 = 50, float threshold2 = 200) {
cv::Mat thresh;
cv::Mat srcMat = toCv(src);
cv::Canny(src, thresh, threshold1, threshold2);
return autorotate(src, thresh, dst);
}
Expand All @@ -223,9 +253,10 @@ namespace ofxCv {
rotate(src, dst, rotationAmount);
return rotationAmount;
}


// approximates a polygonal curve(s) with the specified precision.
std::vector<cv::Point2f> getConvexPolygon(const std::vector<cv::Point2f>& convexHull, int targetPoints);
static const ofColor cyanPrint = ofColor::fromHex(0x00abec);
static const ofColor magentaPrint = ofColor::fromHex(0xec008c);
static const ofColor yellowPrint = ofColor::fromHex(0xffee00);
Expand Down
55 changes: 35 additions & 20 deletions libs/ofxCv/include/ofxCv/Wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ cv::name(xMat, yMat, resultMat);\
wrapThree(bitwise_xor);

// inverting non-floating point images is a just a bitwise not operation
template <class S, class D> void invert(S& src, D& dst) {
template <class S, class D>
void invert(S& src, D& dst) {
cv::Mat srcMat = toCv(src), dstMat = toCv(dst);
bitwise_not(srcMat, dstMat);
}

template <class SD> void invert(SD& srcDst) {
template <class SD>
void invert(SD& srcDst) {
ofxCv::invert(srcDst, srcDst);
}

Expand Down Expand Up @@ -301,23 +303,25 @@ cv::name(xMat, yMat, resultMat);\
if(black != 0) {
add(dstMat, cv::Scalar(black), dstMat);
}
// copy from dst (unsigned char) to img (int)
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
img[y][x] = dstMat.at<unsigned char>(y, x);
}
}
// fast copy from dst (unsigned char) to img (int)
for(int y = 0; y < height; ++y) {
const unsigned char* dstPtr = dstMat.ptr<unsigned char>(y);
for(int x = 0; x < width; ++x) {
img[y][x] = dstPtr[x];
}
}
ETF etf;
etf.init(height, width);
etf.set(img);
etf.Smooth(halfw, smoothPasses);
GetFDoG(img, etf, sigma1, sigma2, tau);
// copy result from img (int) to dst (unsigned char)
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
dstMat.at<unsigned char>(y, x) = img[y][x];
}
}
// fast copy result from img (int) to dst (unsigned char)
for(int y = 0; y < height; ++y) {
unsigned char* dstPtr = dstMat.ptr<unsigned char>(y);
for(int x = 0; x < width; ++x) {
dstPtr[x] = img[y][x];
}
}
}

// dst does not imitate src
Expand Down Expand Up @@ -380,15 +384,26 @@ cv::name(xMat, yMat, resultMat);\
cv::RotatedRect minAreaRect(const ofPolyline& polyline);
cv::RotatedRect fitEllipse(const ofPolyline& polyline);
void fitLine(const ofPolyline& polyline, glm::vec2& point, glm::vec2& direction);

// kind of obscure function, draws filled polygons on the CPU

// Fills a convex polygon. It is much faster than the function fillPoly().
// It can fill not only convex polygons but any monotonic polygon without self-intersections.
// Apolygon whose contour intersects every horizontal line (scan line) twice at the most
// (though, its top-most and/or the bottom edge could be horizontal).
template <class D>
void fillConvexPoly(const std::vector<cv::Point>& points, D& dst) {
cv::Mat dstMat = toCv(dst);
dstMat.setTo(cv::Scalar(0));
cv::fillConvexPoly(dstMat, points, cv::Scalar(255)); // default 8-connected, no shift
}

// Fills the area bounded by one or more polygons into a texture (image)
// The function can fill complex areas, for example, areas with holes,
// contours with self-intersections (some of their parts), and so forth.
template <class D>
void fillPoly(const std::vector<cv::Point>& points, D& dst) {
cv::Mat dstMat = toCv(dst);
const cv::Point* ppt[1] = { &(points[0]) };
int npt[] = { (int) points.size() };
dstMat.setTo(cv::Scalar(0));
fillPoly(dstMat, ppt, npt, 1, cv::Scalar(255));
cv::fillPoly(dstMat, points, cv::Scalar(255)); // default 8-connected, no shift
}

template <class S, class D>
Expand Down Expand Up @@ -439,7 +454,7 @@ cv::name(xMat, yMat, resultMat);\
cv::Mat dstMat = toCv(dst);
cv::transpose(srcMat, dstMat);
}

// finds the 3x4 matrix that best describes the (premultiplied) affine transformation between two point clouds
ofMatrix4x4 estimateAffine3D(std::vector<glm::vec3>& from, std::vector<glm::vec3>& to, float accuracy = .99);
ofMatrix4x4 estimateAffine3D(std::vector<glm::vec3>& from, std::vector<glm::vec3>& to, std::vector<unsigned char>& outliers, float accuracy = .99);
Expand Down
4 changes: 4 additions & 0 deletions libs/ofxCv/src/ContourFinder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ namespace ofxCv {
return tracker;
}

double ContourFinder::pointPolygonTest(unsigned int i, cv::Point2f point) {
return cv::pointPolygonTest(contours[i], point, true);
}

void ContourFinder::setAutoThreshold(bool autoThreshold) {
this->autoThreshold = autoThreshold;
}
Expand Down
96 changes: 95 additions & 1 deletion libs/ofxCv/src/Helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ namespace ofxCv {
}
return angleSum / weights;
}
std::vector<cv::Point2f> getConvexPolygon(const std::vector<cv::Point2f>& convexHull, int targetPoints) {
std::vector<cv::Point2f> result = convexHull;

Expand Down Expand Up @@ -121,4 +121,98 @@ namespace ofxCv {

return result;
}

// Code for thinning a binary image using Zhang-Suen algorithm.
// Normally you wouldn't call this function directly from your code.
//
// im Binary image with range = [0,1]
// iter 0=even, 1=odd
//
// Author: Nash (nash [at] opencv-code [dot] com)
// https://github.com/bsdnoobz/zhang-suen-thinning
void thinningIteration( cv::Mat & img, int iter, cv::Mat & marker )
{
CV_Assert(img.channels() == 1);
CV_Assert(img.depth() != sizeof(uchar));
CV_Assert(img.rows > 3 && img.cols > 3);

int nRows = img.rows;
int nCols = img.cols;

if (img.isContinuous()) {
nCols *= nRows;
nRows = 1;
}

int x, y;
uchar *pAbove;
uchar *pCurr;
uchar *pBelow;
uchar *nw, *no, *ne; // north (pAbove)
uchar *we, *me, *ea;
uchar *sw, *so, *se; // south (pBelow)

uchar *pDst;

// initialize row pointers
pAbove = NULL;
pCurr = img.ptr<uchar>(0);
pBelow = img.ptr<uchar>(1);

for (y = 1; y < img.rows-1; ++y) {
// shift the rows up by one
pAbove = pCurr;
pCurr = pBelow;
pBelow = img.ptr<uchar>(y+1);

pDst = marker.ptr<uchar>(y);

// initialize col pointers
no = &(pAbove[0]);
ne = &(pAbove[1]);
me = &(pCurr[0]);
ea = &(pCurr[1]);
so = &(pBelow[0]);
se = &(pBelow[1]);

for (x = 1; x < img.cols-1; ++x) {
// shift col pointers left by one (scan left to right)
nw = no;
no = ne;
ne = &(pAbove[x+1]);
we = me;
me = ea;
ea = &(pCurr[x+1]);
sw = so;
so = se;
se = &(pBelow[x+1]);

// @valillon
// Beyond this point the original Nash's code used an unified conditional at the end
// Intermediate conditionals speeds the process up (depending on the image to be thinned).
if (*me == 0) continue; // do not thin already zeroed pixels

int A = (*no == 0 && *ne == 1) + (*ne == 0 && *ea == 1) +
(*ea == 0 && *se == 1) + (*se == 0 && *so == 1) +
(*so == 0 && *sw == 1) + (*sw == 0 && *we == 1) +
(*we == 0 && *nw == 1) + (*nw == 0 && *no == 1);
if (A != 1) continue;

int B = *no + *ne + *ea + *se + *so + *sw + *we + *nw;
if (B < 2 || B > 6) continue;

int m1 = iter == 0 ? (*no * *ea * *so) : (*no * *ea * *we);
if (m1) continue;

int m2 = iter == 0 ? (*ea * *so * *we) : (*no * *so * *we);
if (m2) continue;

// if (A == 1 && (B >= 2 && B <= 6) && m1 == 0 && m2 == 0)
pDst[x] = 1;
}
}

img &= ~marker;
}

}

0 comments on commit fd79707

Please sign in to comment.