4

I want to reduce the amount of white pixels in this image to just some candidate or representative points in the output image (goal is to model different types of shapes)

Input Image

Output Image

if you just connect the gray points in the output image together you have the same path but with less white pixels. this path should have only one starting point and one ending point and covers all path from starting to ending.

I can solve it using CCA(connected component analysis) and some if then else rules ! but it seems slow.

I need this Algorithm to reduce the amount of pixels needed to describe shapes.

what is the fastest and accurate algorithm here ?

I also welcome those methods that can increase the accuracy of shape modeling by increasing the candidate points.

PsP
  • 696
  • 3
  • 10
  • 34
  • 2
    Did you try finding the contour and then approximating it with a polyline (with [approxPolyDP](http://docs.opencv.org/2.4/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html?highlight=findcontours#approxpolydp))? Is it too slow for your application? – dhanushka May 29 '17 at 16:50
  • polyline or skeletonization algorithms are what you want. If you want to go the other way around: Having some representative points and the aim is to draw a smooth curve from that, you might want to have a look at spline-interpolation techniques. – Micka Jun 02 '17 at 11:43

3 Answers3

6
  • skeletonize the shape to get a single-pixel-wide path (see https://en.wikipedia.org/wiki/Topological_skeleton)

  • represent the path as a chain of pixels.

  • select a number of pixels along the path, regularly spaced.

  • generate a Cardinal spline through these points (see https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline). A cubic spline is also possible.

  • for every section of the spline, estimate the deviation between the path in the image and the curve. This can be done by sampling a few points along the curve and finding the closest point on the section of the path (by trying all pixels).

  • when the deviation is too large, add one or more pixels in that section.

  • recompute the whole spline and repeat, until you don't need to insert points anymore.

    [1]: https://i.stack.imgur.com/1IAenter image description here

By adjusting the deviation threshold, you can trade curve smoothness for matching accuracy.

It is possible to avoid recomputing the curve where no point insertion occurred, but this requires some care.

  • let me check it and see if it can handle complex shapes – PsP Jun 06 '17 at 04:49
  • @PsP: it can, and with an adjustable smoothness level. Reconstructing the thick curve after compression can be challenging. As an alternative you can work like in dhanushka's answer, by fitting the outlines of the thick curve. He uses a polyline, which can give a good level of approximation. A free-form curve will perform better compression, unless you want to keep the noise. –  Jun 06 '17 at 06:37
  • @Brans: no but this description should be sufficient. –  Jul 04 '20 at 21:47
4

I experimented with approximating the contour with polylines using approxPolyDP. When the shape is closed, this is simple and always produce a good approximation. But when the shape is open, like the sample image in question, sometimes the ordering of approximated points is not maintained (I've given an example of this below). It's difficult to describe every detail of the approach, so I've commented the c++ code as best as I can. Hope this helps.

Gradient direction at polyline approximated points (red dots are polyline approx. points offset by some amount in the opposite gradient direction, and blue dots are in gradient direction):

grad

Final approximated points (a point within a red blob) and order of navigation (blue arrows)

approx

Example shapes and gradient directions. Note the difference between closed and open shapes. For the closed shape, the red dots are all located to one side of the stroke. For open shapes, red dots are located on both sides of the stroke.

misc

Shape on the top left gets the order of navigation wrong because the contour doesn't start from a tip of the shape. However, it does a good pointwise approximation.

shape approx

int _tmain(int argc, _TCHAR* argv[])
{
    Mat im = imread("1.png", 0);
    Mat roi = Mat::zeros(im.size(), CV_8UC1);
    /* find the gradient at every point */
    Mat dx, dy;
    Sobel(im, dx, CV_32F, 1, 0, 7);
    Sobel(im, dy, CV_32F, 0, 1, 7);
    /* take the distance transform */
    Mat dist;
    distanceTransform(im, dist, CV_DIST_L2, 5);
    /* max stroke radius */
    double th;
    minMaxLoc(dist, NULL, &th);

    /* for display/debug purposes */
    Mat rgb = Mat::zeros(im.size(), CV_8UC3);
    Mat rgb2;
    cvtColor(im, rgb2, CV_GRAY2BGR);
    Mat rgb3 = Mat::zeros(im.size(), CV_8UC3);

    Mat tmp = im.clone();
    // find contours, get every point with CV_CHAIN_APPROX_NONE
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(tmp, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE, Point(0, 0));

    for(int idx = 0; idx >= 0; idx = hierarchy[idx][0])
    {
        /* draw contours */
        drawContours(rgb, contours, idx, Scalar(0, 255, 255), 2, 8);
        drawContours(rgb2, contours, idx, Scalar(0, 255, 255), 1, 8);
        drawContours(rgb3, contours, idx, Scalar(0, 255, 255), 1, 8);

        /* polyline approximztion of the contour */
        vector<Point> poly;
        approxPolyDP(contours[idx], poly, th, false);
        /* 
            now we'll sample the gradient along the points in the polyline, 
            find another opposite point in the coitour in the gradient direction, 
            then find the peak location in the distance image (looks like the
            mid point should also work, but I didn't try it).
        */
        for (Point& pt: poly)
        {
            /* sample dx, dy at our point of interest */
            float x = dx.at<float>(pt);
            float y = dy.at<float>(pt);
            float n = sqrtf(x*x + y*y);
            /*  
                select another point in the direction of the gradient that intersects the stroke:
                by choosing a point that's around 2.5 times the max stroke radius, we hope
                to cross the stroke with this line 
            */
            Point pt2(pt.x + 2.5*th*x/n, pt.y + 2.5*th*y/n);
            /* offset the first point a bit in the opposite gradient direction */
            Point pt1(pt.x - .5*th*x/n, pt.y - .5*th*y/n);
            /* draw a thick line */
            line(roi, pt1, pt2, Scalar(255, 255, 255), 2, 8);
            /*
                display the points
            */
            line(rgb3, pt1, pt2, Scalar(255, 255, 255), 2, 8);
            line(rgb2, pt1, pt2, Scalar(0, 255, 255), 2, CV_AA);
            circle(rgb2, pt1, 3, Scalar(0, 0, 255), -1, CV_AA);
            circle(rgb2, pt2, 3, Scalar(255, 0, 0), -1, CV_AA);
        }   

        /* dilate the lines so that nearby lines can merge */
        Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
        morphologyEx(roi, roi, CV_MOP_DILATE, kernel, Point(-1, -1), 1);
        /* only for debug */
        morphologyEx(rgb3, rgb3, CV_MOP_DILATE, kernel, Point(-1, -1), 1);
        /* we are interested in lines segments that are within the shape */
        roi &= im;
        /* collect all these line segments */
        vector<vector<Point>> regions;
        findContours(roi, regions, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
        /* 
            now that we have all the info about lines segments, 
            we can use the image for other purposes such as a mask
        */
        roi.setTo(Scalar(0, 0, 0));
        /* our points of interest when we approximate the shape */
        vector<Point> shapeApprox;
        /*
            for each point on the shape contour, see if it is within a line segment
            that we found using gradients. if so, find the peak location from the distance image.
            it is a point in the skeleton
        */
        for (Point& pt: contours[idx])
        {
            for (size_t i = 0; i < regions.size(); i++)
            {
                if (-1 != pointPolygonTest(regions[i], pt, false))
                {
                    /* using roi as a mask to find the peak location from distance image */
                    drawContours(roi, regions, i, Scalar(255, 255, 255), -1);
                    double mx;
                    Point mxLoc;
                    minMaxLoc(dist, NULL, &mx, NULL, &mxLoc, roi);
                    /* 
                        if this point is not already in the list, add it. 
                        as the gradient line can intersect the shape contour at two
                        points most of the time, we'll find the same peak twice
                    */
                    if (shapeApprox.end() == find(shapeApprox.begin(), shapeApprox.end(), mxLoc))
                    {
                        //cout << mx << " @ " << mxLoc << endl;
                        shapeApprox.push_back(mxLoc);
                    }
                    /* no need to test other gradient lines */
                    break;
                }
            }
            /* reset the mask */
            roi.setTo(Scalar(0, 0, 0));
        }
        /* draw the (possibly merged) gradient line segments */
        drawContours(rgb, regions, -1, Scalar(0, 0, 255), -1);
        /* draw the shape approximation */
        for (size_t i = 1; i < shapeApprox.size(); i++)
        {
            arrowedLine(rgb, shapeApprox[i-1], shapeApprox[i], Scalar(255, 0, 0), 2, CV_AA, 0, .1);
            //imshow("approx", rgb);
            //waitKey(0);
        }
    }

    imshow("debug", rgb3);
    imshow("points", rgb2);
    imshow("approx", rgb);
    waitKey(0);

    return 0;
}
dhanushka
  • 10,492
  • 2
  • 37
  • 47
0

Have you checked skeletonization techniques? There are some examples available... Check https://sites.google.com/site/rameyarnaud/research/c/voronoi for example

TFreitas
  • 290
  • 1
  • 12
  • yeah ofcourse, skeletonization based on distance transform and also morphology operations. By using this technique you get another image, so what's the next step ? how to move along the way and choose the candidate points ? – PsP Jun 02 '17 at 10:05
  • The resultant skeleton is already a "compressed" representation of your binary image. So why not using those as candidates for your modulation? If you want even less points you could try sampling the resultant points... I guess that would need previous ordering of your contour points – TFreitas Jun 02 '17 at 10:12
  • yes but how do you want to get the starting and ending points using the skeleton ? (suppose every kind of complex shape in your mind) – PsP Jun 02 '17 at 10:31
  • I think the proposal of Yves Daoust is quite what you want. Good Luck! – TFreitas Jun 03 '17 at 11:27