packoman
packoman

Reputation: 1282

How to ignore/remove contours that touch the image boundaries

I have the following code to detect contours in an image using cvThreshold and cvFindContours:

CvMemStorage* storage = cvCreateMemStorage(0);
CvSeq* contours = 0;

cvThreshold( processedImage, processedImage, thresh1, 255, CV_THRESH_BINARY );
nContours = cvFindContours(processedImage, storage, &contours, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, cvPoint(0,0) );

I would like to somehow extend this code to filter/ignore/remove any contours that touch the image boundaries. However I am unsure how to go about this. Should I filter the threshold image or can I filter the contours afterwards? Hope somebody knows an elegant solution, since surprisingly I could not come up with a solution by googling.

Upvotes: 5

Views: 10842

Answers (3)

Baiz
Baiz

Reputation: 1091

Update 2021-11-25

  • updates code example
  • fixes bugs with image borders
  • adds more images
  • adds Github repo with CMake support to build example app

Full out-of-the-box example can be found here: C++ application with CMake

General info

  • I am using OpenCV 3.0.0
  • Using cv::findContours actually alters the input image, so make sure that you work either on a separate copy specifically for this function or do not further use the image at all

Update 2019-03-07: "Since opencv 3.2 source image is not modified by this function." (see corresponding OpenCV documentation)

General solution

All you need to know of a contour is if any of its points touches the image border. This info can be extracted easily by one of the following two procedures:

  • Check each point of your contour regarding its location. If it lies at the image border (x = 0 or x = width - 1 or y = 0 or y = height - 1), simply ignore it.
  • Create a bounding box around the contour. If the bounding box lies along the image border, you know the contour does, too.

Code for the second solution (CMake):

cmake_minimum_required(VERSION 2.8)

project(SolutionName)

find_package(OpenCV REQUIRED)

set(TARGETNAME "ProjectName")

add_executable(${TARGETNAME} ./src/main.cpp)

include_directories(${CMAKE_CURRENT_BINARY_DIR} ${OpenCV_INCLUDE_DIRS} ${OpenCV2_INCLUDE_DIR})
target_link_libraries(${TARGETNAME} ${OpenCV_LIBS})

Code for the second solution (C++):

bool contourTouchesImageBorder(const std::vector<cv::Point>& contour, const cv::Size& imageSize)
{
    cv::Rect bb = cv::boundingRect(contour);

    bool retval = false;

    int xMin, xMax, yMin, yMax;

    xMin = 0;
    yMin = 0;
    xMax = imageSize.width - 1;
    yMax = imageSize.height - 1;

    // Use less/greater comparisons to potentially support contours outside of 
    // image coordinates, possible future workarounds with cv::copyMakeBorder where
    // contour coordinates may be shifted and just to be safe.
    // However note that bounding boxes of size 1 will have their start point
    // included (of course) but also their and with/height values set to 1 
    // but should not contain 2 pixels.
    // Which is why we have to -1 the "search grid"
    int bbxEnd = bb.x + bb.width - 1;
    int bbyEnd = bb.y + bb.height - 1;
    if (bb.x <= xMin ||
        bb.y <= yMin ||
        bbxEnd >= xMax ||
        bbyEnd >= yMax)
    {
        retval = true;
    }

    return retval;
}

Call it via:

...
cv::Size imageSize = processedImage.size();
for (auto c: contours)
{
    if(contourTouchesImageBorder(c, imageSize))
    {
        // Do your thing...
        int asdf = 0;
    }
}
...

Full C++ example:

void testContourBorderCheck()
{
    std::vector<std::string> filenames =
    {
        "0_single_pixel_top_left.png",
        "1_left_no_touch.png",
        "1_left_touch.png",
        "2_right_no_touch.png",
        "2_right_touch.png",
        "3_top_no_touch.png",
        "3_top_touch.png",
        "4_bot_no_touch.png",
        "4_bot_touch.png"
    };

    // Load example image
    //std::string path = "C:/Temp/!Testdata/ContourBorderDetection/test_1/";
    std::string path = "../Testdata/ContourBorderDetection/test_1/";

    for (int i = 0; i < filenames.size(); ++i)
    {
        //std::string filename = "circle3BorderDistance0.png";
        std::string filename = filenames.at(i);
        std::string fqn = path + filename;
        cv::Mat img = cv::imread(fqn, cv::IMREAD_GRAYSCALE);

        cv::Mat processedImage;
        img.copyTo(processedImage);

        // Create copy for contour extraction since cv::findContours alters the input image
        cv::Mat workingCopyForContourExtraction;
        processedImage.copyTo(workingCopyForContourExtraction);

        std::vector<std::vector<cv::Point>> contours;
        // Extract contours 
        cv::findContours(workingCopyForContourExtraction, contours, cv::RetrievalModes::RETR_EXTERNAL, cv::ContourApproximationModes::CHAIN_APPROX_SIMPLE);

        // Prepare image for contour drawing
        cv::Mat drawing;
        processedImage.copyTo(drawing);
        cv::cvtColor(drawing, drawing, cv::COLOR_GRAY2BGR);

        // Draw contours
        cv::drawContours(drawing, contours, -1, cv::Scalar(255, 255, 0), 1);

        //cv::imwrite(path + "processedImage.png", processedImage);
        //cv::imwrite(path + "workingCopyForContourExtraction.png", workingCopyForContourExtraction);
        //cv::imwrite(path + "drawing.png", drawing);

        const auto imageSize = img.size();
        bool liesOnBorder = contourTouchesImageBorder(contours.at(0), imageSize);
        // std::cout << "lies on border: " << std::to_string(liesOnBorder);
        std::cout << filename << " lies on border: "
            << liesOnBorder;
        std::cout << std::endl;
        std::cout << std::endl;

        cv::imshow("processedImage", processedImage);
        cv::imshow("workingCopyForContourExtraction", workingCopyForContourExtraction);
        cv::imshow("drawing", drawing);
        cv::waitKey();

        //cv::Size imageSize = workingCopyForContourExtraction.size();
        for (auto c : contours)
        {
            if (contourTouchesImageBorder(c, imageSize))
            {
                // Do your thing...
                int asdf = 0;
            }
        }
        for (auto c : contours)
        {
            if (contourTouchesImageBorder(c, imageSize))
            {
                // Do your thing...
                int asdf = 0;
            }
        }
    }
}

int main(int argc, char** argv)
{
    testContourBorderCheck();
    return 0;
}

Problem with contour detection near image borders

OpenCV seems to have a problem with correctly finding contours near image borders.

For both objects, the detected contour is the same (see images). However, in image 2 the detected contour is not correct since a part of the object lies along x = 0, but the contour lies in x = 1.

This seem like a bug to me. There is an open issue regarding this here: https://github.com/opencv/opencv/pull/7516

There also seems to be a workaround with cv::copyMakeBorder (https://github.com/opencv/opencv/issues/4374), however it seems a bit complicated.

If you can be a bit patient, I'd recommend waiting for the release of OpenCV 3.2 which should happen within the next 1-2 months.

New example images: Single pixel top left, objects left, right, top, bottom, each touching and not touching (1px distance)

image

image

image

image

image

image

image

image


Example images

  • Object touching image border
  • Object not touching image border
  • Contour for object touching image border
  • Contour for object not touching image border

Object touching image border

Object not touching image border

Contour for object touching image border

Contour for object not touching image border

Upvotes: 10

Prefect
Prefect

Reputation: 1777

If anyone needs this in MATLAB, here is the function.

function [touch] = componentTouchesImageBorder(C,im_row_max,im_col_max)
    %C is a bwconncomp instance
    touch=0;
    S = regionprops(C,'PixelList');

    c_row_max = max(S.PixelList(:,1));
    c_row_min = min(S.PixelList(:,1));
    c_col_max = max(S.PixelList(:,2));
    c_col_min = min(S.PixelList(:,2));

    if (c_row_max==im_row_max || c_row_min == 1 || c_col_max == im_col_max || c_col_min == 1)
        touch = 1;
    end
end

Upvotes: 0

Liam Finnie
Liam Finnie

Reputation: 169

Although this question is in C++, the same issue affects openCV in Python. A solution to the openCV '0-pixel' border issue in Python (and which can likely be used in C++ as well) is to pad the image with 1 pixel on each border, then call openCV with the padded image, and then remove the border afterwards. Something like:

img2 = np.pad(img.copy(), ((1,1), (1,1), (0,0)), 'edge')
# call openCV with img2, it will set all the border pixels in our new pad with 0
# now get rid of our border
img = img2[1:-1,1:-1,:]
# img is now set to the original dimensions, and the contours can be at the edge of the image

Upvotes: 5

Related Questions