如何识别一组像素是否近似为矩形?

4
什么是将左侧像素组视为矩形的数学逻辑?

2
  1. 提取边缘
  2. 拟合直线(矢量化)
  3. 检查连通性创建多边形
  4. 检查角度
- Spektre
5
一种方法是1)找到旋转后的矩形边界框,2)计算该边界框的面积,然后3)将其与轮廓的面积进行比较。如果轮廓的面积至少为旋转边界框面积的80%(或任何任意数字),那么我们认为它是矩形的。另一种方法是使用线条和角度。您可以1)找到两条最长的线,并比较它们之间的角度。2)如果角度在某个阈值内,则它是矩形的。 - nathancy
谢谢您的回复。我想我会尝试实现@nathancy提到的边界框算法。 - user6895869
1
@NehalKalita 我已经实现了下面的边界框想法。 - nathancy
@nathancy,哇,你真的很快。我没想到你能在这么短的时间内写出代码。不管怎样,非常感谢。 - user6895869
显示剩余4条评论
2个回答

7
以下是评论中该想法的实现方法:
  1. 找到旋转矩形边界框
  2. 计算此旋转边界框和轮廓的面积
  3. 比较两者。如果轮廓的面积至少为旋转边界框面积的80%(或任何任意阈值),则将其视为矩形。

以下是图像处理流程的可视化展示:

输入图像 -> 阈值化 -> 检测到的旋转矩形边界框 -> 掩码

Contour area: 17719.0
Mask area: 20603.0
Compared area percentage: 86.002%
It is a rectangle!

另一张图片的处理流程如下:

输入图像 -> 阈值化 -> 检测旋转矩形边界框 -> 掩膜

Contour area: 13395.5
Mask area: 19274.5
Compared area percentage: 69.499%
It is not a rectangle!

您没有指定语言,这里提供一个使用Python OpenCV的简单实现。
import cv2
import numpy as np

# Load image, convert to grayscale, Otsu's threshold for binary image
image = cv2.imread('1.png')
mask = np.zeros(image.shape, dtype=np.uint8)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Find contours, find rotated rectangle, find contour area
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
contour_area = cv2.contourArea(cnts[0])
print('Contour area: {}'.format(contour_area))
rect = cv2.minAreaRect(cnts[0])
box = np.int0(cv2.boxPoints(rect))
cv2.drawContours(image, [box], 0, (36,255,12), 3)

# Find area of rotated bounding box and draw onto mask image
mask_area = cv2.contourArea(box)
cv2.drawContours(mask, [box], 0, (255,255,255), -1)
print('Mask area: {}'.format(mask_area))

# Compare areas and calculate percentage
rectangular_threshold = 80
percentage = (contour_area / mask_area) * 100
print('Compared area percentage: {:.3f}%'.format(percentage))
if percentage > rectangular_threshold:
    print('It is a rectangle!')
else:
    print('It is not a rectangle!')

# Display
cv2.imshow('image', image)
cv2.imshow('thresh', thresh)
cv2.imshow('mask', mask)
cv2.waitKey()

1
将矩形绘制到掩模上,然后对其进行轮廓处理,最后计算轮廓面积似乎有些多余(与使用旋转矩形大小属性计算面积相比)。这样做是否增加了我所忽略的鲁棒性? - Sneaky Polar Bear
2
@SneakyPolarBear 嘿,你是对的!那一部分完全是多余的。你可以直接找到区域并将其绘制到掩码上,而不必执行所有这些额外步骤。我想掩码也是不必要的,但为了可视化我会留下它。 - nathancy

2

这与其中一个评论(边界框和检查内部区域)非常相似。我建议您使用面积和周长的方程来制作更强大且可配置的形状测试器。

-获取所需形状周围的适合边界框。

-检查面积:斑点像素计数应大致等于A(斑点):L * W(旋转边界框)

-检查周长:运行边缘变换(如Canny或等效物),并将边缘像素数量与矩形周长方程进行比较:P(斑点):2W + 2H(旋转边界框)


-在边缘图像上运行霍夫线,并检查正交性,这可能是另一个要考虑的适应度估计器,如果这是您关心的重要参数。

基本上,每当您检查形状健身时,通常希望检查基于您关心的形状的各种适应度估计器(例如清晰的边缘,垂直的角落,内部填充,直边等),并且您会对每个适应度估计器进行加权或限制,以创建所需的组合特异性。

编辑:(代码和其他说明)。因此,为了清楚起见,对于您的示例,我同意面积是最强的适应度估计。但是,在形状识别中通常有用的是跟踪多个适应度估计器,以便您可以真正调整您关心和不关心的内容。我在您的样本中添加了一个附加的(完全虚构的)形状,以演示仅使用面积可能不太可靠的情况(在干净的矩形中切割)。如您所见,面积对于突出部等外部形状错误非常敏感,但是周长对于内部形状错误更敏感。(再次,这可能或可能不重要,但我认为它很有趣,可能会有所帮助)。我添加了一个额外的参数(周长与面积比),这是一种用于估计形状复杂性的巧妙指标(特别是在叶子中)。

代码:

#include <stdio.h>
#include <opencv2/opencv.hpp>
#include <Windows.h>
#include <string>

using namespace std;
using namespace cv;

void drawOrientedBoundingBox(Mat img, RotatedRect obb, Scalar color)
{
    Point2f vertices[4];
    obb.points(vertices);
    for (int i = 0; i < 4; i++)
        line(img, vertices[i], vertices[(i + 1) % 4], color, 2);
}

int main(int argc, char** argv)
{
    //play with these to dial in what you care about (across multiple sample images)
    double areaFitHigh = 1.2;
    double areaFitLow = 0.8;
    double perimFitHigh = 1.05;
    double perimFitLow = 0.85;

    std::string fileName = "C:/Local Software/voyDICOM/resources/images/RectTester.jpg";
    Mat tempImage = imread(fileName, cv::IMREAD_GRAYSCALE);

    Mat bwImg;
    cv::threshold(tempImage, bwImg, 0, 255, cv::THRESH_OTSU);

    Mat contImg = Mat(bwImg.rows, bwImg.cols, CV_8UC3, Scalar(0, 0, 0));

    vector<vector<Point> > contours;
    findContours(bwImg, contours, RETR_LIST, CHAIN_APPROX_NONE);

    bool areaFitnessFlag = false;
    bool perimFitnessFlag = false;

    for (size_t i = 0; i < contours.size(); i++)
    {
        std::string contourName = "S_" + std::to_string(i);
        std::cout << "-------------Contour Detected------------" << std::endl;
        std::cout << contourName << std::endl;

        if (contours[i].size() >= 2 * bwImg.cols + 2 * bwImg.rows - 4)
        {
            std::cout << "image boundary... don't know how to make findContours not detect this up front" << std::endl;
            continue;
        }

        RotatedRect obb = minAreaRect(contours[i]);

        //draw graphics for debug purposes
        drawOrientedBoundingBox(contImg, obb, Scalar(255, 0, 0));
        drawContours(contImg, contours, static_cast<int>(i), Scalar(0, 0, 255), 2);
        putText(contImg, contourName, obb.center, cv::FONT_HERSHEY_DUPLEX, 1, cv::Scalar(0, 255, 0), 1, false);

        //perform fitness checks
        double areaBlob = contourArea(contours[i]);
        double areaOBB = obb.size.area();

        double perimeterBlob = contours[i].size();
        double perimeterOBB = 2 * obb.size.width + 2 * obb.size.height;

        double perimToArea = 0;
        if (areaBlob > 0) { perimToArea = perimeterBlob / areaBlob; }

        std::cout << "area: " << areaBlob << " , " << areaOBB << std::endl;
        std::cout << "perimeter: " << perimeterBlob << " , " << perimeterOBB << std::endl;
        std::cout << "Perimeter to Area Ratio: " << perimToArea << std::endl;

        double areaFitness = 0;
        if (areaOBB > 0) { areaFitness = areaBlob / areaOBB; }
        std::cout << "Area Fitness: " << areaFitness << std::endl;

        double perimeterFitness = 0;
        if (perimeterOBB > 0) { perimeterFitness = perimeterBlob / perimeterOBB; }
        std::cout << "Perimeter Fitness: " << perimeterFitness << std::endl;


        if (areaFitness > areaFitHigh || areaFitness < areaFitLow)
        { areaFitnessFlag = false; }
        else
        { areaFitnessFlag = true; }

        if (perimeterFitness > perimFitHigh || perimeterFitness < perimFitLow)
        { perimFitnessFlag = false; }
        else
        { perimFitnessFlag = true; }

        if (areaFitnessFlag && perimFitnessFlag)
        { std::cout << "This is a rectangle!" << std::endl; }
        else
        { std::cout << "This is not a rectangle..." << std::endl; }
    }

    namedWindow("Original", WINDOW_AUTOSIZE);
    imshow("Original", tempImage);

    namedWindow("Thresh", WINDOW_AUTOSIZE);
    imshow("Thresh", bwImg);

    namedWindow("Contours", WINDOW_AUTOSIZE);
    imshow("Contours", contImg);

    waitKey(0);
    system("pause");
    return 0;
}

结果图片:

enter image description here

有用链接:

https://docs.opencv.org/master/df/dee/samples_2cpp_2minarea_8cpp-example.html

https://docs.opencv.org/master/db/dd6/classcv_1_1RotatedRect.html

https://learnopencv.com/contour-detection-using-opencv-python-c/

在OpenCV C++中绘制旋转矩形


如果需要的话,我可以用OpenCV C++编写一些示例代码,以帮助您理解这些步骤中的任何一个。 - Sneaky Polar Bear
有趣的想法。当边界框以角度旋转时,你如何计算blob像素数 A = L * W - nathancy
轮廓面积用于blob,使用RotatedRect的大小属性来获取矩形。 - Sneaky Polar Bear

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接