Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fill contours that touch the image border?

Tags:

c++

opencv

Say I have the following binary image created from the output of cv::watershed():

enter image description here

Now I want to find and fill the contours, so I can separate the corresponding objects from the background in the original image (that was segmented by the watershed function).

To segment the image and find the contours I use the code below:

cv::Mat bgr = cv::imread("test.png");

// Some function that provides the rough outline for the segmented regions.
cv::Mat markers = find_markers(bgr); 

cv::watershed(bgr, markers);

cv::Mat_<bool> boundaries(bgr.size());
for (int i = 0; i < bgr.rows; i++) {
    for (int j = 0; j < bgr.cols; j++) {
        boundaries.at<bool>(i, j) = (markers.at<int>(i, j) == -1);
    }
}

std::vector<std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;

cv::findContours(
    boundaries, contours, hierarchy,
    CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE
);

So far so good. However, if I pass the contours acquired above to cv::drawContours() as below:

cv::Mat regions(bgr.size(), CV_32S);
cv::drawContours(
    regions, contours, -1, cv::Scalar::all(255),
    CV_FILLED, 8, hierarchy, INT_MAX
);

This is what I get:

enter image description here

The leftmost contour was left open by cv::findContours(), and as a result it is not filled by cv::drawContours().

Now I know this is a consequence of cv::findContours() clipping off the 1-pixel border around the image (as mentioned in the documentation), but what to do then? It seems an awful waste to discard a contour just because it happened to brush off the image's border. And anyway how can I even find which contour(s) fall in this category? cv::isContourConvex() is not a solution in this case; a region can be concave but "closed" and thus not have this problem.

Edit: About the suggestion to duplicate the pixels from the borders. The problem is that my marking function is also painting all pixels in the "background", i.e. those regions I'm sure aren't part of any object:

enter image description here

This results in a boundary being drawn around the output. If I somehow avoid cv::findContours() to clip off that boundary:

enter image description here

The boundary for the background gets merged with that leftmost object:

enter image description here

Which results in a nice white-filled box.

like image 774
xperroni Avatar asked Dec 06 '13 07:12

xperroni


People also ask

What is contours of an image?

Contours are defined as the line joining all the points along the boundary of an image that are having the same intensity. Contours come handy in shape analysis, finding the size of the object of interest, and object detection.

What are contours lines in image processing?

What are contours? Contours can be explained simply as a curve joining all the continuous points (along the boundary), having same color or intensity. The contours are a useful tool for shape analysis and object detection and recognition. For better accuracy, use binary images.


2 Answers

Solution number 1: use image extended by one pixel in each direction:

Mat extended(bgr.size()+Size(2,2), bgr.type());
Mat markers = extended(Rect(1, 1, bgr.cols, bgr.rows));

// all your calculation part

std::vector<std::vector<Point> > contours;
findContours(boundaries, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);

Mat regions(bgr.size(), CV_8U);
drawContours(regions, contours, -1, Scalar(255), CV_FILLED, 8, Mat(), INT_MAX, Point(-1,-1));

Note that contours were extracted from extended image, i.e. their x and y values are bigger by 1 from what they should be. This is why I use drawContours with (-1,-1) pixel offset.

Solution number 2: add white pixels from boundary of image to the neighbor row/column:

bitwise_or(boundaries.row(0), boundaries.row(1), boundaries.row(1));
bitwise_or(boundaries.col(0), boundaries.col(1), boundaries.col(1));
bitwise_or(boundaries.row(bgr.rows()-1), boundaries.row(bgr.rows()-2), boundaries.row(bgr.rows()-2));
bitwise_or(boundaries.col(bgr.cols()-1), boundaries.col(bgr.cols()-2), boundaries.col(bgr.cols()-2));

Both solution are half-dirty workarounds, but this is all I could think about.

like image 93
Michael Burdinov Avatar answered Nov 19 '22 19:11

Michael Burdinov


Following Burdinov's suggestions I came up with the code below, which correctly fills all extracted regions while ignoring the all-enclosing boundary:

cv::Mat fill_regions(const cv::Mat &bgr, const cv::Mat &prospective) {
    static cv::Scalar WHITE = cv::Scalar::all(255);
    int rows = bgr.rows;
    int cols = bgr.cols;

    // For the given prospective markers, finds
    // object boundaries on the given BGR image.
    cv::Mat markers = prospective.clone();
    cv::watershed(bgr, markers);

    // Copies the boundaries of the objetcs segmented by cv::watershed().
    // Ensures there is a minimum distance of 1 pixel between boundary
    // pixels and the image border.
    cv::Mat borders(rows + 2, cols + 2, CV_8U);
    for (int i = 0; i < rows; i++) {
        uchar *u = borders.ptr<uchar>(i + 1) + 1;
        int *v = markers.ptr<int>(i);
        for (int j = 0; j < cols; j++, u++, v++) {
            *u = (*v == -1);
        }
    }

    // Calculates contour vectors for the boundaries extracted above.
    std::vector<std::vector<cv::Point> > contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(
        borders, contours, hierarchy,
        CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE
    );

    int area = bgr.size().area();
    cv::Mat regions(borders.size(), CV_32S);
    for (int i = 0, n = contours.size(); i < n; i++) {
        // Ignores contours for which the bounding rectangle's
        // area equals the area of the original image.
        std::vector<cv::Point> &contour = contours[i];
        if (cv::boundingRect(contour).area() == area) {
            continue;
        }

        // Draws the selected contour.
        cv::drawContours(
            regions, contours, i, WHITE,
            CV_FILLED, 8, hierarchy, INT_MAX
        );
    }

    // Removes the 1 pixel-thick border added when the boundaries
    // were first copied from the output of cv::watershed().
    return regions(cv::Rect(1, 1, cols, rows));
}
like image 40
xperroni Avatar answered Nov 19 '22 21:11

xperroni