15

I have successfully applied the method cv::approxPolyDP on contours (cv::findContours), in order to represent a contour with a simpler polygon and implicitly do some denoising.

I would like to do the same thing on an edge map acquired from an RGBD camera (which is in general very noisy), but with not much success up to now and I cannot find relative examples online. The reason I need this, is that by means of an edge map one can also use the edges between fingers, edges created by finger occlusion or edges created in the palm.

Is this method applicable to general edge maps, other than contours?

Could someone pinpoint me to an example?

Some images attached:

Successful example for contours: enter image description here

Problematic case for edge maps:

Most probably I draw things in the wrong way, but drawing just the pixels returned by the method shows that probably large areas are not represented in the end result (and this doesn't change much according to the epsilon-parameter).

enter image description here

I attach also a depth image, similar to the ones I use in the experimental pipeline descibed above. This depth image was not acquired by a depth camera, but was synthetically generated by reading the depth buffer of the gpu, using OpenGL.

enter image description here

Just for reference, this is also the edge map of the depth image acquired straight from the depth camera (using the raw image, no smoothing etc applied)

enter image description here

(hand as viewd from a depth camera, palm facing upwards, fingers "closing" towards the palm)

rwong
  • 6,062
  • 1
  • 23
  • 51
dim_tz
  • 1,451
  • 5
  • 20
  • 37
  • My suspicion is that there are gaps in the edge map found by `cv::findContours`. Could you post one of the original images from the RGBD camera? (Possibly the depth image). If denoising is required, it would need to be applied before contour/edge finding. – rwong Mar 02 '14 at 20:10
  • Hi rwong, in this case I don't use cv::findContours, I apply Canny Edge Detection on the edge image. At the moment I don't denoise the edge image beforehand, as I first wanted to see the quality of the real data acquired, but you are right, one should also denoise a little bit before the edge detection. I have to note however that, as a first step, I use synthetically generated data (depth buffer of OpenGL), so no denoising is necessary in the examples that I posted. I will edit the question to add the synthetic depth image. – dim_tz Mar 02 '14 at 20:18
  • For Canny Edge Detection, lowering the lower threshold (while keeping the higher threshold same) will result in more edge pixels being flagged, and thus reduce the chance of gaps in edge chains. I haven't used `approxPolyDP` before so I couldn't comment on that. The lesson is that if there's imperfection in one step, it's usually difficult to fix in subsequent steps. – rwong Mar 02 '14 at 23:28
  • This is true, but the images show that the canny edge detection is sufficient enough, providing a meaningful edge map (plus, lowering the threshold gives more inner-edges, with no significant influence on the outer edges). So there is no artifact at this stage I guess, it's either that I use approxPpolyDP in a wrong way, or it cannot handle non-closed-contour maps (which I think is not the case, based also on the input parameters of the method). – dim_tz Mar 03 '14 at 00:12

1 Answers1

35

Your issue with approxPolyDP is due to the formatting of the input into approxPolyDP.

Explanation

approxPolyDP expects its input to be a vector of Points. These points define a polygonal curve that will be processed by approxPolyDP. The curve could be open or closed, which can be controlled by a flag.

The ordering of the points in the list is important. Just as one traces out a polygon by hand, each subsequent point in the vector must be the next vertex of the polygon, clockwise or counter-clockwise.

If the list of points is stored in raster order (sorted by Y and then X), then the point[k] and point[k+1] do not necessarily belong to the same curve. This is the cause of the problem.

This issue is explained with illustrations in OpenCV - How to extract edges form result of Canny Function? . Quote from Mikhail: "Canny doesn't connect pixels into chains or segments."


Illustration of "raster order" that is generated by Canny.

Raster order


Illustration of "contour order" that is expected by approxPolyDP

Contour order


What is needed

What you need is a list of "chains of edge pixels". Each chain must contain edge pixels that are adjacent to each other, just like someone tracing out an object's outline by a pencil, without the tip of the pencil leaving the paper.

This is not what is returned from edge detection methods, such as Canny. Further processing is needed to convert an edge map into chains of adjacent (continuous) edge pixels.

Suggested solutions

(1) Use binary threshold instead of edge detection as the input to findContours

This would be applicable if there exists a threshold value that separates the hand from the background, and that this value works for the whole hand (not just part of the hand).

(2) Scan the edge map, and build the list of adjacent pixels by examining the neighbors of each edge pixel.

This is similar to the connected-components algorithm, except that instead of finding a blob (where you only need to know each pixel's membership), you try to find chains of pixels such that you can tell the previous and next edge pixels along the chain.

(3) Use an alternative edge detection algorithm, such as Edge Drawing.

Details can be found at http://ceng.anadolu.edu.tr/cv/EdgeDrawing/

Unfortunately, this is not provided out-of-the-box from OpenCV, so you may have to find an implementation elsewhere.


Sample code for option #1.

#include <stdint.h>
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main()
{
    Mat matInput = imread("~/Data/mA9EE.png", false);

    // ---- Preprocessing of depth map. (Optional.) ----

    GaussianBlur(matInput, matInput, cv::Size(9, 9), 4.0);

    // ---- Here, we use cv::threshold instead of cv::Canny as explained above ----

    Mat matEdge;

    //Canny(matInput, matEdge, 0.1, 1.0);

    threshold(matInput, matEdge, 192.0, 255.0, THRESH_BINARY_INV);

    // ---- Use findContours to find chains of consecutive edge pixels ----

    vector<vector<Point> > contours;
    findContours(matEdge, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);

    // ---- Code below is only used for visualizing the result. ----

    Mat matContour(matEdge.size(), CV_8UC1);

    for (size_t k = 0; k < contours.size(); ++k)
    {
        const vector<Point>& contour = contours[k];
        for (size_t k2 = 0; k2 < contour.size(); ++k2)
        {
            const Point& p = contour[k2];
            matContour.at<uint8_t>(p) = 255;
        }
    }

    imwrite("~/Data/output.png", matContour);
    cout << "Done!" << endl;
    return 0;
}
Community
  • 1
  • 1
rwong
  • 6,062
  • 1
  • 23
  • 51
  • 1
    Hi rwong, wow, this is a great answer all in all! I thought that I was doing something wrong with the output, while the input was wrong for exactly the reason that you describe. Thank you for listing 3 possible options as a solution! Regarding the first solution, the "Contour Example" image, in my 1st post, is exactly about this case, using a simple threshold to segment the whole hand and feed the findcontours method. This gives as output the silhouette, and the (depicted) result is very nice. In cases of Edges you cannot use the method threshold though, so in practice what you can do... – dim_tz Mar 13 '14 at 14:08
  • 1
    for a quick (hacky!) test is use canny edge detector, invert it, then use the threshold method and feed with the result the findcontours method. There is going to be the problem of a double contour around each edge though, which, according to your application, might or might-not be ok. In the end I decided not to use any smoothing/filtering or approximation of contours since it does not help for the current project, but the answer is great and really analytical. For future reference, people with a similar problem should consider as the best solution either the second or the third option. – dim_tz Mar 13 '14 at 14:08
  • That Turkish university site is a PITA - the CompVis reseach team's sub-site (the /cv/ bit of the URL) has given a 503 for hours. I don't much feel like paying $35 for a copy of the paper from Elsevier, either... there's a precis paper here -> http://www.researchgate.net/publication/220932363_Edge_Drawing_A_Heuristic_Approach_to_Robust_Real-Time_Edge_Detection that seems promising: I've got ~2 million images for which I'm trying to implement some feature detection, so a few msec saved here and there add up to meaningful time (fingers crossed, even more if I use GPUs) – GT. May 08 '15 at 03:56
  • 1
    Hi. I'm a little confused about explanation above. Both Canny and threshold return a 2D array (image) no? Where is the order of pixels there? – Marumba Feb 23 '21 at 17:14
  • @Marumba The issue is with the input expected by `approxPolyDP`. – rwong Feb 24 '21 at 05:52
  • @rwong, in the above code, matEdge (the input to approxPolyDP) is a 2D array representing the image (both in canny and threshold). I don't see how it contains any kind of order (except the standard raster ordering of the pixel). – Marumba Feb 25 '21 at 07:20
  • @Marumba You didn't provide enough context for me to understand what your question is about. If your question isn't directly related to the current question, please consider opening a new question. My sample code didn't use `approxPolyDP`. If you want to use it, the steps will be (roughly): (1) `Canny` (or) `threshold`, (2) `findContours` (3) for each contour `approxPolyDP`. It's not possible to use `approxPolyDP` without something *similar to* `findContours` because `approxPolyDP` expects a vector of `Point2`, not an image. – rwong Feb 25 '21 at 09:44
  • @Marumba If you find an inaccuracy or ambiguity in my answer, please use the "Suggest Edit" function to remove that inaccuracy or ambiguity. Otherwise it is difficult for me to see exactly which part of the (somewhat old) answer is unclear. – rwong Feb 25 '21 at 09:47
  • “_Illustration of ‘raster **order’ that is generated by `Canny`**._” You said so in your answer, while **`Canny` does _not_ generate an order** of points. [It generates an image, `OpenCV::Mat`.](https://docs.opencv.org/5.x/dd/d1a/group__imgproc__feature.html#ga04723e007ed888ddf11d9ba04e2232de) That’s what @Marumba’s talking about. – Константин Ван Dec 06 '22 at 23:08