简体   繁体   English

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

[英]Detecting lines and shapes in OpenCV using Python

I've been playing around with OpenCV (cv2) and detecting lines and shapes. 我一直在玩OpenCV(cv2)并检测线条和形状。 Say that my daughter drew a drawing, like so: 假设我的女儿画了一幅画,就像这样:

在此输入图像描述

I am trying to write a Python script that would analyze the drawing and convert it into hard lines/shapes, something like: 我正在尝试编写一个Python脚本来分析绘图并将其转换为硬线/形状,如:

在此输入图像描述

That being said, I have installed opencv and tried to play around with it, but have had no luck aside from being able to draw a single vertical line through the image. 话虽这么说,我已经安装了opencv并尝试使用它,但除了能够在图像中绘制一条垂直线之外没有运气。 Below is my code so far, any pointers or suggestions as to how I should go about doing this with opencv would be greatly appreciated. 下面是我的代码到目前为止,任何关于如何使用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)

Unfortunately, for a simple square drawing, all I get back is: 不幸的是,对于一个简单的方形绘图,我得到的全部是:

在此输入图像描述

where the vertical line in the box is the output from the code. 框中的垂直线是代码的输出。

Here is my attempt. 这是我的尝试。 It's in C++, but can be easily ported to python since most are OpenCV functions. 它是在C ++中,但可以很容易地移植到python,因为大多数是OpenCV函数。

A brief outline of the method, comments in the code should help, too. 该方法的简要概述,代码中的注释也应该有所帮助。

  1. Load the image 加载图像
  2. Convert to grayscale 转换为灰度
  3. Binaryze the image (threshold) 二进制图像(阈值)
  4. Thinning, to have thin contours and help findContours 变薄,有薄的轮廓和帮助findContours
  5. Get contours 获得轮廓
  6. For each contour, get convex hull (to handle open contours), and classify according to circularity . 对于每个轮廓,获得凸包(以处理开放的轮廓),并根据圆度进行分类。 Handle each shape differently. 处理每个形状的方式不同

    • Circle : find the minimum encolsing circle, or the best fitting ellipse :找到最小的包围圆,或最佳拟合椭圆
    • Recrangle : find the boundinx box, or the minimum oriented bounding box. Recrangle :找到boundinx框,或最小的方向边界框。
    • Triangle : search for the intersection of the minimum enclosing circle with the original shape, as they would intersect in the three vertices of the triangle. 三角形 :搜索最小包围圆与原始形状的交点,因为它们将在三角形的三个顶点中相交。

NOTES: 笔记:

  • I needed to modify the original image to 3 channel RGB from a png with transparency. 我需要从具有透明度的png将原始图像修改为3通道RGB。
  • The thinning code is from here . 细化代码来自这里 There is also the Python version. 还有Python版本。
  • Circularity is defined as: A measures how close to a circle the shape is. 度定义为:A测量形状与圆形的接近程度。 Eg a regular hexagon has higher circularity than say a square. 例如,正六边形具有比正方形更高的圆度。 Is defined as (\\frac{4*\\pi*Area}{perimeter * perimeter}). 定义为(\\ frac {4 * \\ pi * Area} {perimeter * perimeter})。 This means that a circle has a circularity of 1, circularity of a square is 0.785, and so on. 这意味着圆的圆度为1,正方形的圆度为0.785,依此类推。
  • Because of the contours, there may be multiple detection for each shape. 由于轮廓,每个形状可能有多个检测。 These can be filtered out according to, for example, intersection over union condition. 这些可以根据例如交叉结合条件过滤掉。 I did't inserted this part in the code for now, since it requires additional logic that isn't strictly related to the main task of finding the shapes. 我暂时没有在代码中插入此部分,因为它需要额外的逻辑,这与查找形状的主要任务并不严格相关。

UPDATE - Just noticed that in OpenCV 3.0.0 there is the function minEnclosingTriangle . 更新 - 注意到在OpenCV 3.0.0中有minEnclosingTriangle函数。 This might be helpful to use instead of my procedure to find the triangle vertices. 这可能有助于使用而不是我的过程来找到三角形顶点。 However, since inserting this function in the code would be trivial, I'll leave my procedure in the code in case one doesn't have OpenCV 3.0.0. 但是,由于在代码中插入此函数将是微不足道的,所以我将在代码中保留我的过程,以防一个人没有OpenCV 3.0.0。

The code: 编码:

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

The results ( minEnclosingCircle and boundingRect ): 结果( minEnclosingCircleboundingRect ): 在此输入图像描述

The results ( fitEllipse and minAreaRect ): 结果( fitEllipseminAreaRect ): 在此输入图像描述

You might check out a couple of resources. 您可以查看几个资源。

First, you might consider asking questions at answers.opencv.org. 首先,您可以考虑在answers.opencv.org上提问。 There is probably a higher concentration of opencv specialists there. 那里的opencv专家可能更集中。

Second, the book Practical OpenCV by Samarth Brahmbhatt is available as a free pdf and is easily found on google. 其次,Samarth Brahmbhatt的Practical OpenCV一书以免费pdf格式提供,在谷歌上很容易找到。 It is contains many examples related to what you are looking for. 它包含许多与您要查找的内容相关的示例。

For example, you can separate different (non-overlapping) contours, as is shown in example 6.1 on page 68. He has a simple program for finding circles and lines in example 6.4 on page 78. You can also find a RANSAC-based ellipse-finder (much more complicated, but would be very useful here) in example 6.5 on page 82. 例如,您可以分隔不同的(非重叠)轮廓,如第68页的示例6.1所示。他有一个简单的程序,用于查找第78页的示例6.4中的圆和线。您还可以找到基于RANSAC的椭圆-finder(更复杂,但在这里非常有用)在第82页的示例6.5中。

The book is in C++, but I imagine it will be very relevant, only you will need an API reference to translate it to python. 这本书是用C ++编写的,但我认为它非常相关,只需要一个API引用就可以将它翻译成python。

Personally, for your proejct, I would analyze one contour at a time, starting with his ellipse finder, and where a suitable ellipse can not be found, you could use a Hough transform of adjustable threshold, and truncate the resulting lines at their intersections, and bam! 就个人而言,对于你的项目,我会一次分析一个轮廓,从他的椭圆查找器开始,并且在找不到合适的椭圆的地方,你可以使用可调阈值的霍夫变换,并截断它们交叉处的结果线,和巴姆! You have polygons. 你有多边形。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM