使用Python在OpenCV中检测线条和形状

13

我一直在尝试使用OpenCV(cv2)来检测线条和形状。比如说,我的女儿画了一幅图像:

enter image description here

我正在尝试编写一个Python脚本,可以分析这个图像并将其转换为硬线条/形状,类似于:

enter image description here

话虽如此,我已经安装了OpenCV并尝试运行它,但除了能够在图像中画出一条垂直线外,还没有什么进展。以下是我目前的代码,如果有任何指针或建议,关于如何使用OpenCV进行此操作,将不胜感激。

import cv2
import numpy as np

class File(object):
    def __init__(self, filename):
        self.filename = filename

    def open(self, filename=None, mode='r'):
        if filename is None:
            filename = self.filename

        return cv2.imread(filename), open(filename, mode)

    def save(self, image=None, filename_override=None):
        filename = "output/" + self.filename.split('/')[-1]

        if filename_override:
            filename = "output/" + filename_override

        return cv2.imwrite(filename, image)

class Image(object):
    def __init__(self, image):
        self.image = image

    def grayscale(self):
        return cv2.cvtColor(self.image, cv2.COLOR_BGR2GRAY)

    def edges(self):
        return cv2.Canny(self.image, 0, 255)

    def lines(self):
        lines = cv2.HoughLinesP(self.image, 1, np.pi/2, 6, None, 50, 10)
        for line in lines[0]:
            pt1 = (line[0],line[1])
            pt2 = (line[2],line[3])
            cv2.line(self.image, pt1, pt2, (0,0,255), 2)

if __name__ == '__main__':
    File = File('images/a.png')
    Image = Image(File.open()[0])
    Image.image = Image.grayscale()
    Image.lines()
    File.save(Image.image)

很遗憾,对于简单的正方形绘图,我只能得到以下结果:

在此输入图片描述

其中方框内垂直线条是代码的输出结果。


你想使用OpenCV C++吗? - Miki
@Miki 如果我的 C++ 不那么生疏,我肯定会做的。不过最终,如果这个概念在 Python 中行得通的话,我想将这个应用迁移到 C 或 C++。 - jsanc623
好的,我会在有空的时候尽快发布我的C++答案。然后我会让你决定它是否足够好。顺便说一下,你可以在这里看一下(http://www.emgu.com/wiki/index.php/Shape_(Triangle,_Rectangle,_Circle,_Line)_Detection_in_CSharp)。虽然是C#,但很容易理解。 - Miki
再次感谢你,Miki。任何语言的代码都可以帮助我,因为我精通几种语言(尽管在许多语言上都很生疏)。目前正在查看下面回答中rexroni提到的《Practical OpenCV》。 - jsanc623
2个回答

14

这是我的尝试。代码是用C++编写的,但由于大多数是OpenCV函数,因此可以轻松移植到Python。

方法简要概述,代码中的注释也应该有所帮助。

  1. 加载图像
  2. 转换为灰度
  3. 对图像二值化(阈值处理)
  4. 细化,获得细线轮廓并帮助findContours
  5. 获取轮廓
  6. 对于每个轮廓,获取凸包(以处理开放式轮廓),并根据圆度进行分类。以不同方式处理每种形状。

    • 圆形:找到最小外接圆或最佳匹配椭圆
    • 矩形:查找边界框或最小定向边界框
    • 三角形:搜索最小外接圆与原始形状的交点,因为它们将在三角形的三个顶点相交。

注意:

  • 我需要将原始图像从具有透明度的png修改为3通道RGB。
  • 细化代码来自这里。也有Python版本。
  • 圆度定义为:A测量形状接近圆形的程度。例如,正六边形的圆度比正方形高。被定义为(\frac{4*\pi*Area}{perimeter * perimeter})。这意味着圆的圆度为1,正方形的圆度为0.785等等。
  • 由于轮廓,每种形状可能会有多个检测结果。可以根据重叠区域条件进行过滤。我现在没有将此部分插入代码中,因为它需要附加的逻辑与找到形状的主要任务并没有直接关系。

更新-刚刚注意到在OpenCV 3.0.0中有函数minEnclosingTriangle。这可能有助于用来查找三角形的顶点而不是使用我的过程。但是,由于将此函数插入代码将是微不足道的,因此我将保留我的过程以防一个人没有OpenCV 3.0.0。

代码:

#include <opencv2\opencv.hpp>
#include <vector>
#include <iostream>

using namespace std;
using namespace cv;

/////////////////////////////////////////////////////////////////////////////////////////////
// Thinning algorithm from here:
// https://github.com/bsdnoobz/zhang-suen-thinning
/////////////////////////////////////////////////////////////////////////////////////////////

void thinningIteration(cv::Mat& img, int iter)
{
    CV_Assert(img.channels() == 1);
    CV_Assert(img.depth() != sizeof(uchar));
    CV_Assert(img.rows > 3 && img.cols > 3);

    cv::Mat marker = cv::Mat::zeros(img.size(), CV_8UC1);

    int nRows = img.rows;
    int nCols = img.cols;

    if (img.isContinuous()) {
        nCols *= nRows;
        nRows = 1;
    }

    int x, y;
    uchar *pAbove;
    uchar *pCurr;
    uchar *pBelow;
    uchar *nw, *no, *ne;    // north (pAbove)
    uchar *we, *me, *ea;
    uchar *sw, *so, *se;    // south (pBelow)

    uchar *pDst;

    // initialize row pointers
    pAbove = NULL;
    pCurr = img.ptr<uchar>(0);
    pBelow = img.ptr<uchar>(1);

    for (y = 1; y < img.rows - 1; ++y) {
        // shift the rows up by one
        pAbove = pCurr;
        pCurr = pBelow;
        pBelow = img.ptr<uchar>(y + 1);

        pDst = marker.ptr<uchar>(y);

        // initialize col pointers
        no = &(pAbove[0]);
        ne = &(pAbove[1]);
        me = &(pCurr[0]);
        ea = &(pCurr[1]);
        so = &(pBelow[0]);
        se = &(pBelow[1]);

        for (x = 1; x < img.cols - 1; ++x) {
            // shift col pointers left by one (scan left to right)
            nw = no;
            no = ne;
            ne = &(pAbove[x + 1]);
            we = me;
            me = ea;
            ea = &(pCurr[x + 1]);
            sw = so;
            so = se;
            se = &(pBelow[x + 1]);

            int A = (*no == 0 && *ne == 1) + (*ne == 0 && *ea == 1) +
                (*ea == 0 && *se == 1) + (*se == 0 && *so == 1) +
                (*so == 0 && *sw == 1) + (*sw == 0 && *we == 1) +
                (*we == 0 && *nw == 1) + (*nw == 0 && *no == 1);
            int B = *no + *ne + *ea + *se + *so + *sw + *we + *nw;
            int m1 = iter == 0 ? (*no * *ea * *so) : (*no * *ea * *we);
            int m2 = iter == 0 ? (*ea * *so * *we) : (*no * *so * *we);

            if (A == 1 && (B >= 2 && B <= 6) && m1 == 0 && m2 == 0)
                pDst[x] = 1;
        }
    }

    img &= ~marker;
}

void thinning(const cv::Mat& src, cv::Mat& dst)
{
    dst = src.clone();
    dst /= 255;         // convert to binary image

    cv::Mat prev = cv::Mat::zeros(dst.size(), CV_8UC1);
    cv::Mat diff;

    do {
        thinningIteration(dst, 0);
        thinningIteration(dst, 1);
        cv::absdiff(dst, prev, diff);
        dst.copyTo(prev);
    } while (cv::countNonZero(diff) > 0);

    dst *= 255;
}


int main()
{
    RNG rng(123);

    // Read image
    Mat3b src = imread("path_to_image");

    // Convert to grayscale
    Mat1b gray;
    cvtColor(src, gray, COLOR_BGR2GRAY);

    // Binarize
    Mat1b bin;
    threshold(gray, bin, 127, 255, THRESH_BINARY_INV);

    // Perform thinning
    thinning(bin, bin);

    // Create result image
    Mat3b res = src.clone();

    // Find contours
    vector<vector<Point>> contours;
    findContours(bin.clone(), contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

    // For each contour
    for (vector<Point>& contour : contours)
    {
        // Compute convex hull
        vector<Point> hull;
        convexHull(contour, hull);

        // Compute circularity, used for shape classification
        double area = contourArea(hull);
        double perimeter = arcLength(hull, true);
        double circularity = (4 * CV_PI * area) / (perimeter * perimeter);

        // Shape classification

        if (circularity > 0.9)
        {
            // CIRCLE

            //{
            //  // Fit an ellipse ...
            //  RotatedRect rect = fitEllipse(contour);
            //  Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
            //  ellipse(res, rect, color, 5);
            //}
            {
                // ... or find min enclosing circle
                Point2f center;
                float radius;
                minEnclosingCircle(contour, center, radius);
                Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
                circle(res, center, radius, color, 5);
            }
        }
        else if (circularity > 0.75)
        {
            // RECTANGLE

            //{
            //  // Minimum oriented bounding box ...
            //  RotatedRect rect = minAreaRect(contour);
            //  Point2f pts[4];
            //  rect.points(pts);

            //  Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
            //  for (int i = 0; i < 4; ++i)
            //  {
            //      line(res, pts[i], pts[(i + 1) % 4], color, 5);
            //  }
            //}
            {
                // ... or bounding box
                Rect box = boundingRect(contour);
                Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
                rectangle(res, box, color, 5);
            }
        }
        else if (circularity > 0.7)
        {
            // TRIANGLE

            // Select the portion of the image containing only the wanted contour
            Rect roi = boundingRect(contour);
            Mat1b maskRoi(bin.rows, bin.cols, uchar(0));
            rectangle(maskRoi, roi, Scalar(255), CV_FILLED);
            Mat1b triangle(roi.height, roi.height, uchar(0));
            bin.copyTo(triangle, maskRoi);

            // Find min encolsing circle on the contour
            Point2f center;
            float radius;
            minEnclosingCircle(contour, center, radius);

            // decrease the size of the enclosing circle until it intersects the contour
            // in at least 3 different points (i.e. the 3 vertices)
            vector<vector<Point>> vertices;
            do
            {
                vertices.clear();
                radius--;

                Mat1b maskCirc(bin.rows, bin.cols, uchar(0));
                circle(maskCirc, center, radius, Scalar(255), 5);

                maskCirc &= triangle;
                findContours(maskCirc.clone(), vertices, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

            } while (vertices.size() < 3);

            // Just get the first point in each vertex blob.
            // You could get the centroid for a little better accuracy

            Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
            line(res, vertices[0][0], vertices[1][0], color, 5);
            line(res, vertices[1][0], vertices[2][0], color, 5);
            line(res, vertices[2][0], vertices[0][0], color, 5);

        }
        else
        {
            cout << "Some other shape..." << endl;
        }

    }

    return 0;
}

结果 (minEnclosingCircleboundingRect): 输入图像描述

结果 (fitEllipseminAreaRect): 输入图像描述


这真是太完美了。谢谢Miki!我会用API作为参考将代码转录成Python,并在POC完成后将您的代码用于迁移到C++。非常非常感谢! - jsanc623
你的回答一如既往地棒极了,@Miki :) 你的实现非常出色 :) - sagar suri

1

您可以查看一些资源。

首先,您可以考虑在answers.opencv.org上提问。那里可能有更多的opencv专家。

其次,Samarth Brahmbhatt的《Practical OpenCV》一书可作为免费PDF文件,并且可以轻松在Google上找到。它包含许多与您寻找的内容相关的示例。

例如,您可以分离不同(非重叠)轮廓,如第68页的示例6.1所示。他在第78页的示例6.4中有一个简单的程序用于查找圆和线。您还可以在第82页的示例6.5中找到基于RANSAC的椭圆查找器(更复杂,但在这里非常有用)。

该书使用C++编写,但我想它将非常相关,您只需要API参考即可将其翻译成Python。

就您的项目而言,我个人会逐个分析每个轮廓,从他的椭圆查找器开始,如果找不到合适的椭圆,则可以使用可调阈值的Hough变换,并在其交点处截断结果线,然后你就有了多边形。


谢谢@rexroni的评论,我会查看Brahmbhatt的书籍,看看它是否有帮助! - jsanc623

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