【问题标题】:How to detect the custom shape in the given binary image using OpenCV?如何使用 OpenCV 检测给定二进制图像中的自定义形状?
【发布时间】:2021-06-17 20:51:11
【问题描述】:

我正在寻找一种使用 OpenCV 在给定二进制图像中检测自定义形状的方法。

我的自定义形状如下:

我正在尝试查找上述形状(或近似形状)是否存在于给定的 512 x 512 二值图像中。

我尝试了不同的方法,例如使用 cv::matchTemplate() 但这不适用于具有缩放和旋转形状的变体。

我需要一些解决方案来识别给定二进制图像中的这些形状。

提前致谢

关于形状的一些信息 :shapes 很简单,三个连接的组件带有线分隔。 这些是踝关节处胫骨、腓骨和距骨的横截面形状。 更多形状图片

编辑: 512by512 具有形状的图像

512 x 512 不带形状的图像

【问题讨论】:

  • 顶行和底行图像是镜像对象吗?中心和外部对象之间的大“洞”一次在左侧,一次在右侧?!?您能否定义对象的外观,因为在某些方面它们看起来并不相似。它们与图像中的其他结构有何不同?我会在归一化区域上使用紧密二元区域的聚类 + cv::minAreaRect + 大小/旋转归一化 + 倒角匹配。
  • 实际上我尝试创建 Haar 级联分类器(HCC)来检测这种形状。为此,我做了一些研究,了解到我需要大量的正面和负面数据来训练 HCC。所以我收集了一些这些形状的图像,并试图让它们旋转/镜像并创建更多的正数据(这是数据增强)用于训练目的但是当我测试 Hcc 以测试数据时,它在负面场景中失败了(HCC 甚至检测形状虽然它不存在)
  • 一般我提到的形状只是 CT 扫描中骨骼的图像。(你可以在这里看到它google.com/…)通常每个白色区域是一个单独的骨骼(胫骨、腓骨和距骨),即由细黑色区域(或线)分隔。
  • 级联分类器也可能存在旋转问题,我想这对于二值图像来说不是很好。如果你真的想使用昂贵的检测器,可以试试像 YOLO 这样的深度神经网络。如果您真的想继续使用级联分类器,请使用尽可能多的负图像并调整训练参数以减少误报的数量。训练需要很长时间,以便在后期可以找到足够多的额外错误样本(实际用例和足够多的图像通常需要 2-3 周的训练时间)。
  • 是的,级联分类器对我帮助不大。我对任何用于检测该形状的方法持开放态度。关于二进制图像,实际可用的 CT 扫描是灰度图像,我只是将它们转换为二进制,希望它可以在某种程度上对级联训练有所帮助。 请提出任何合适的方法。这对我很有帮助。

标签: c++ opencv image-processing


【解决方案1】:

您可以对图像进行迭代,并计算中心、大小和其他特征来找出您想要的对象。

【讨论】:

  • 你能分享任何示例代码吗
【解决方案2】:

这是我的算法。这个想法是对轮廓进行聚类(在示例算法中只是通过膨胀/腐蚀),并为每个轮廓标准化大小并测试不同旋转中的形状相似性。然后将图像区域与模板进行比较。

我正在使用这张图片作为模板:

与此轮廓

还有这个对象图像(删除了白色背景,因为我假设只使用外部轮廓。

算法给出了这个结果:

found target shape with similarity 72.5144% and angle: 180 degrees
5 of 26
found target shape with similarity 73.1325% and angle: 0 degrees
6 of 26
found target shape with similarity 71.7287% and angle: 270 degrees
7 of 26
8 of 26
found target shape with similarity 72.3608% and angle: 90 degrees
9 of 26
10 of 26
11 of 26
12 of 26
13 of 26
14 of 26
15 of 26
16 of 26
found target shape with similarity 62.7371% and angle: 60 degrees
17 of 26
found target shape with similarity 62.6041% and angle: 240 degrees
18 of 26
19 of 26
20 of 26
found target shape with similarity 62.8935% and angle: 150 degrees
21 of 26
found target shape with similarity 62.39% and angle: 330 degrees
22 of 26
23 of 26
24 of 26
25 of 26

这是代码(带有一些用于保存图像等的脏帮手):

int glob_counter = 0;
double contourMaskSimilarity(float angleDiff, float scale, cv::Point2f cont_center, std::vector<cv::Point> contour, cv::Mat img, cv::Point2f template_center, cv::Mat img_templ)
{
    cv::Mat rotationMat = cv::getRotationMatrix2D(cont_center, angleDiff, scale);
    cv::Mat rotationMatPersp = cv::Mat::eye(3, 3, CV_64FC1);
    for (int y = 0; y < rotationMat.rows; ++y)
        for (int x = 0; x < rotationMat.cols; ++x)
        {
            rotationMatPersp.at<double>(y, x) = rotationMat.at<double>(y, x);
        }

    //cv::Mat img_tmp = img_color.clone();
    cv::Mat img_tmp_mask = cv::Mat::zeros(img.size(), img.type());
    std::vector < std::vector<cv::Point> >contours_img;
    contours_img.push_back(contour);
    cv::drawContours(img_tmp_mask, contours_img, 0, cv::Scalar::all(255), -1);

    //cv::circle(img_tmp, cont_center, 3, cv::Scalar(255, 0, 255), 2); // drawing

    std::vector<cv::Point2f> points;
    std::vector<cv::Point2f> warpedPoints;
    points.push_back(cont_center);

    cv::perspectiveTransform(points, warpedPoints, rotationMatPersp);

    cv::Mat translation = cv::Mat::eye(3, 3, CV_64FC1);
    translation.at<double>(0, 2) = template_center.x - warpedPoints[0].x; // x
    translation.at<double>(1, 2) = template_center.y - warpedPoints[0].y; // x

    cv::Mat transformation = translation * rotationMatPersp; // transformation after each other => 1. rotation 2. translation

    cv::Mat imgBin = img.clone();
    imgBin = imgBin & img_tmp_mask;
    cv::Mat imgBinWarped;
    // warp the image to same size and rotation as the template, according to angle and center
    //cv::warpPerspective(imgBin, imgBinWarped, transformation, cv::Size(img.size().width * scale, img.size().height * scale));
    cv::warpPerspective(imgBin, imgBinWarped, transformation, img_templ.size());

    cv::Rect subImage = cv::Rect(0, 0, img_templ.cols, img_templ.rows);
    cv::Mat imgSub = imgBinWarped(subImage);

    cv::Mat imgMul = imgSub.mul(img_templ); // 255 everywhere where template and current image-region are non-zero. 0 everywhere else

    double sum1 = cv::countNonZero(imgMul);
    double sum2 = cv::countNonZero(img_templ);
    double sum3 = cv::countNonZero(imgSub);
    //std::cout << sum1 << " " << sum2 << " " << sum3 << std::endl;

    // confidence similar to intersection over union.
    // use a better shape-similarity here, like a chamfer matching or a mean-hausdorff-distance?
    double conf = sum1 * sum1 / (sum2 * sum3);
    //std::cout << conf * 100 << " %" << std::endl;

    // TODO: remove!
    if (conf > 0.5)
    {
        cv::imwrite("C:/data/StackOverflow/bone_shapes/out_sub_" + std::to_string(glob_counter) + "_" + std::to_string(conf) + ".png", imgSub);
        cv::imwrite("C:/data/StackOverflow/bone_shapes/out_mul_" + std::to_string(glob_counter) + "_" + std::to_string(conf) + ".png", imgMul);
        glob_counter++;
    }

    return conf;
}

int main()
{
    cv::Mat img_templ = cv::imread("C:/data/StackOverflow/bone_shapes/bone_shape_template.png", cv::IMREAD_GRAYSCALE);
    // binarize the img (I guess it was binarized already?)
    cv::Mat templ = img_templ > 0;

    cv::Mat img_shapes = cv::imread("C:/data/StackOverflow/bone_shapes/bones_set_blackBG.png", cv::IMREAD_GRAYSCALE);
    // binarize the image (it was some grayscale gradients at the object borders...)
    cv::Mat img = img_shapes > 200;

    // 1. close-operator to merge all the parts of the shapes to a single contour. For other shapes you might need some kind of clustering.
    cv::Mat img_closed = img.clone();
    int nDilations = 2;
    cv::dilate(img_closed, img_closed, cv::getStructuringElement(cv::MorphShapes::MORPH_RECT, cv::Size(3, 3), cv::Point(1, 1)), cv::Point(1, 1), nDilations);
    cv::erode(img_closed, img_closed, cv::getStructuringElement(cv::MorphShapes::MORPH_RECT, cv::Size(3, 3), cv::Point(1, 1)), cv::Point(1, 1), nDilations);

    cv::Mat template_closed = templ.clone();
    int nDilationsTemplate = 2;
    cv::dilate(template_closed, template_closed, cv::getStructuringElement(cv::MorphShapes::MORPH_RECT, cv::Size(3, 3), cv::Point(1, 1)), cv::Point(1, 1), nDilations);
    cv::erode(template_closed, template_closed, cv::getStructuringElement(cv::MorphShapes::MORPH_RECT, cv::Size(3, 3), cv::Point(1, 1)), cv::Point(1, 1), nDilations);

    // 2. find contours (only necessary once for the template:)

    std::vector<std::vector<cv::Point> > contour_template;
    cv::findContours(template_closed, contour_template, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);


    std::vector<std::vector<cv::Point> > contours_img;
    cv::findContours(img_closed, contours_img, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);


    // display the results for debugging/sanity checks only
    cv::Mat img_color, template_color;
    cv::cvtColor(img_shapes, img_color, cv::COLOR_GRAY2BGR);
    cv::cvtColor(img_templ, template_color, cv::COLOR_GRAY2BGR);

    for (int i = 0; i < contour_template.size(); ++i)
        cv::drawContours(template_color, contour_template, i, cv::Scalar(255, 0, 255), 4);
    cv::imshow("template color", template_color);

    for (int i = 0; i < contours_img.size(); ++i)
        cv::drawContours(img_color, contours_img, i, cv::Scalar(0, 0, 255), 2);
    cv::imshow("img color", img_color);
    //cv::waitKey(1);


    // make sure the template only has one contour!
    if (contour_template.size() != 1)
    {
        std::cout << "closed template doesnt consist of a single contour" << std::endl;
        throw("closed template doesnt consist of a single contour");
    }

    // 3. get size and orientation of the shapes:
    cv::RotatedRect template_orientation = cv::minAreaRect(contour_template[0]);
    cv::Point2f template_center;    float template_size = 0;
    //cv::minEnclosingCircle(contour_template[0], template_center, template_size);
    template_center = template_orientation.center;
    template_size = (template_orientation.size.width > template_orientation.size.height) ? template_orientation.size.width : template_orientation.size.height;

    // now check every contour in the target image:
    for (int i = 0; i < contours_img.size(); ++i)
    {
        std::cout << i << " of " << contours_img.size() << std::endl;
        std::vector<cv::Point> cont = contours_img[i];

        cv::RotatedRect cont_orientation = cv::minAreaRect(cont);
        cv::Point2f cont_center;    float cont_size = 0;

        cont_center = cont_orientation.center;
        cont_size = (cont_orientation.size.width > cont_orientation.size.height) ? cont_orientation.size.width : cont_orientation.size.height;
        // angle difference according to rotated rectangle bounding boxes
        float angleDiff = template_orientation.angle - cont_orientation.angle;
        // scale according to rotated rectangle bounding boxes
        float scale = template_size / cont_size;

        double bestSimilarity = 0.0;
        float bestAngle = 0.0;
        float stepDegree = 15; // make smaller if you need a finer rotation resolution.

        // check various angles:
        for (float j = 0; j < 360.0f; j+=stepDegree)
        {
            //float angle = angleDiff + j * stepDegree; // initial guess for rotation. Works if shape is really similar
            float angle = j;
            // similarity computation is slow for small contours. Maybe because of the warping?
            double similarity = contourMaskSimilarity(angle, scale, cont_center, contours_img[i], img, template_center, templ);
            if (similarity > bestSimilarity)
            {
                bestSimilarity = similarity;
                bestAngle = angle;
            }
        }
        glob_counter++;

        if (bestSimilarity > 0.5)
        {
            cv::drawContours(img_color, contours_img, i, cv::Scalar(0, 255, 0), 2);
            std::cout << "found target shape with similarity " << 100*bestSimilarity << "% and angle: " << bestAngle << " degrees"<< std::endl;
        }

        //cv::waitKey(0);
    
        //cv::waitKey(0);
        /*
        std::cout << rotationMat << std::endl;

        cv::Point2f offsetTemplateImage;
        offsetTemplateImage.x = -template_center.x;
        offsetTemplateImage.y = -template_center.y;
        cv::Rect subImage = cv::Rect(cont_center.x + offsetTemplateImage.x, cont_center.y + offsetTemplateImage.y, img_templ.cols, img_templ.rows);
        */
    }

    cv::imshow("template", img_templ);
    cv::imshow("template binary", templ);
    cv::imshow("template closed", template_closed);

    cv::imshow("shapes", img_shapes);
    cv::imshow("shapes binary", img);
    cv::imshow("shapes closed", img_closed);

    cv::imshow("result", img_color);

    cv::imwrite("C:/data/StackOverflow/bone_shapes/out_template_color.png", template_color);
    cv::imwrite("C:/data/StackOverflow/bone_shapes/out_result.png", img_color);


    cv::waitKey(0);
}

以下是一些扭曲图像的示例以及与模板相乘的二进制文件:

【讨论】:

  • 非常感谢@Micka 的代码工作。我实际上是图像处理和opencv的新手。我会尝试给定的代码,让你知道我的想法。我实际上是在收集您在问题聊天中要求的图像。 我添加了一些有问题的图片。请检查。
  • 已经测试过了。该算法检测到正确/正面的骨骼并且不检测到负面的骨骼(很好),但还在两个正面图像中检测到额外的骨骼(不好)。所以形状大小/旋转部分看起来还可以,但匹配部分还不够复杂。
  • 嗨@Micka,似乎它正在工作,但并非在所有类型的情况下。不过目前还好。非常感谢您的支持
  • @V01 感谢您的赏金。我想通过使用更多的形状属性(目前更多的是关于面积)来改进相似度值计算步骤(这是非常原始的 atm)。不幸的是,由于项目截止日期,我目前没有太多时间。
猜你喜欢
  • 2015-04-10
  • 2017-04-21
  • 2020-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-14
  • 1970-01-01
相关资源
最近更新 更多