【问题标题】:How to ignore/remove contours that touch the image boundaries如何忽略/删除接触图像边界的轮廓
【发布时间】:2016-11-15 16:57:10
【问题描述】:

我有以下代码使用cvThresholdcvFindContours 检测图像中的轮廓:

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) );

我想以某种方式扩展此代码以过滤/忽略/删除任何触及图像边界的轮廓。但是我不确定如何去做。我应该过滤阈值图像还是可以在之后过滤轮廓?希望有人知道一个优雅的解决方案,因为令人惊讶的是我无法通过谷歌搜索找到解决方案。

【问题讨论】:

  • 帮自己一个忙,停止使用过时的 C 语法!改用 C++ api。
  • 感谢您的评论,我很清楚这一点。那么关于实际问题:您将如何做到这一点(无论是在过时的 C 语法中还是在更现代的 C++ 语法中)?

标签: c++ opencv


【解决方案1】:

2021 年 11 月 25 日更新

  • 更新代码示例
  • 修复了图像边框的错误
  • 添加更多图片
  • 添加支持 CMake 的 Github 存储库以构建示例应用程序

可在此处找到完整的开箱即用示例: C++ application with CMake

一般信息

  • 我正在使用 OpenCV 3.0.0
  • 使用cv::findContours 实际上会更改输入图像,因此请确保您在专门为此功能的单独副本上工作,或者根本不进一步使用该图像

2019-03-07 更新:“因为 opencv 3.2 源图像没有被这个函数修改。”(见corresponding OpenCV documentation

一般解决方案

您只需要知道轮廓是否有任何点触及图像边框。可以通过以下两个过程之一轻松提取此信息:

  • 检查轮廓的每个点的位置。如果它位于图像边界(x = 0 或 x = 宽度 - 1 或 y = 0 或 y = 高度 - 1),请忽略它。
  • 围绕轮廓创建一个边界框。如果边界框位于图像边框上,您就知道轮廓也是如此。

第二种解决方案(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})

第二种解决方案的代码(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;
}

通过以下方式调用它:

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

完整的 C++ 示例:

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;
}

图像边界附近的轮廓检测问题

OpenCV 似乎在正确找到图像边界附近的轮廓方面存在问题。

对于两个对象,检测到的轮廓是相同的(见图)。但是,在图像 2 中,检测到的轮廓不正确,因为对象的一部分位于 x = 0 处,但轮廓位于 x = 1 处。

这对我来说似乎是一个错误。 这里有一个未解决的问题:https://github.com/opencv/opencv/pull/7516

似乎也有使用 cv::copyMakeBorder (https://github.com/opencv/opencv/issues/4374) 的解决方法,但它似乎有点复杂。

如果您能有点耐心,我建议您等待 OpenCV 3.2 的发布,这应该会在接下来的 1-2 个月内发布。

新的示例图片: 左上单像素,对象左、右、上、下,每个触摸和不触摸(1px 距离)


示例图片

  • 对象触摸图像边框
  • 对象未触及图像边框
  • 对象触摸图像边框的轮廓
  • 对象不接触图像边框的轮廓

【讨论】:

  • 使用 copymakeborder 解决方法实际上非常简单。在图像周围添加 1 黑色的边框,并使用 (-1,-1) 偏移调用 findcontours
  • 我认为 bb.width >= xMax 应该是 bb.width + bb.x >= xMax 和类似的高度,因为边界框的宽度/高度需要放在坐标系中进行比较最大 x 和 y。
  • @Jacob 非常感谢您指出这一点。我在重新访问/测试期间调整了代码、添加了示例并修复了一个额外的错误。我已经扩展了代码/cmets 并添加了一个 Github 存储库,以便更容易地使用 CMake 支持。
【解决方案2】:

虽然这个问题是用 C++ 编写的,但同样的问题会影响 Python 中的 openCV。 Python中openCV“0像素”边界问题的解决方案(也可能在C++中使用)是在每个边界上用1个像素填充图像,然后用填充的图像调用openCV,然后删除之后的边界。比如:

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

【讨论】:

  • 这就像一个魅力。但是,要删除触摸轮廓(到图像边界),我必须按照这个答案的建议pad 2 pixels,然后发出findContourschop the padded 2 pixels,因为openCV 会从第一个像素开始找到轮廓。否则,由于openCV 的限制,您将无法移除接触轮廓。
【解决方案3】:

如果有人在 MATLAB 中需要这个,这里是函数。

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

【讨论】:

    猜你喜欢
    • 2020-06-17
    • 1970-01-01
    • 1970-01-01
    • 2019-12-17
    • 1970-01-01
    • 2019-05-23
    • 1970-01-01
    • 2010-10-23
    • 2021-07-27
    相关资源
    最近更新 更多