如何在 OpenCV - Python 中获得没有冗余的内部轮廓点

Ham*_*msi 2 opencv computer-vision python-3.x canny-operator

我是 OpenCV 新手,问题是我需要获取所有轮廓点。在 findContours 方法中设置 cv2.RETR_TREE 模式很容易。问题是这样会返回多余的坐标。因此,例如,在这个多边形中,我不想得到这样的轮廓点:

绿色是找到的轮廓(3),红色是根据这些轮廓找到的点

但像这样: 在此输入图像描述

因此,根据第一张图像,绿色是使用 RETR_TREE 模式检测到的轮廓,而点 1-2、3-5、4-6... 是多余的,因为它们彼此非常接近。我需要将这些冗余点组合成一个,并将其附加到 customContours 数组中。目前,我只有第一张图片的代码,设置点之间的距离和点坐标:

def getContours(img, minArea=20000, cThr=[100, 100]):
  font = cv2.FONT_HERSHEY_COMPLEX
  imgColor = img
  imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  imgBlur = cv2.GaussianBlur(imgGray, (5, 5), 1)
  imgCanny = cv2.Canny(imgBlur, cThr[0], cThr[1])
  kernel = np.ones((5, 5))
  imgDial = cv2.dilate(imgCanny, kernel, iterations=3)
  imgThre = cv2.erode(imgDial, kernel, iterations=2)
  cv2.imshow('threshold', imgThre)
  contours, hierachy = cv2.findContours(imgThre, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

  customContours = []
  for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > minArea:
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.009*peri, True)
        bbox = cv2.boundingRect(approx)
        customContours.append([len(approx), area, approx, bbox, cnt])
        print('points: ', len(approx))
        n = approx.ravel()
        i = 0
        for j in n:
            if i % 2 == 0:
                x = n[i]
                y = n[i + 1]
                string = str(x)+" " + str(y)
                cv2.putText(imgColor, str(i//2+1) + ': ' + string, (x, y), font, 2, (0, 0, 0), 2)
            i = i + 1
  customContours = sorted(customContours, key=lambda x: x[1], reverse=True)
  for cnt in customContours:
    cv2.drawContours(imgColor, [cnt[2]], 0, (0, 0, 255), 5)
  return imgColor, customContours
Run Code Online (Sandbox Code Playgroud)

您能帮我了解第二张图片的真正要点吗?

(编辑 01/07/21 )

我想要一个通用的解决方案,因为图像可能更复杂,如下图: 在此输入图像描述 注意:请注意,中间的箭头(点 17 和 18)没有封闭区域,因此不是要研究的多边形。那么,那个地区就没有兴趣获得他的积分了。另外,请注意,点的顺序并不重要,但如果条目是孔图像,则它应该知道有 4 个多边形,因此对于每个多边形点,从 0 开始,然后是 1,依此类推。

sta*_*ine 6

这是我的方法。它主要是基于形态学的。它涉及使用特殊内核对图像进行卷积。该卷积识别三角形的端点以及中线所在的交点。这将产生一个点蒙版,其中包含与您要查找的点相匹配的像素。之后,我们可以应用一点形态学来连接可能的重复点。剩下的就是获取这些点的坐标列表以进行进一步处理。

这些是步骤:

  1. 通过 Otsu 阈值处理获取输入的二值图像
  2. 获取二值图像的骨架
  3. 定义特殊并对骨架图像进行卷积
  4. 应用形态扩张来连接可能的重复点
  5. 获取点的质心并将它们存储在列表中

这是代码:

# Imports:
import numpy as np
import cv2

# image path
path = "D://opencvImages//"
fileName = "triangle.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Prepare a deep copy for results:
inputImageCopy = inputImage.copy()

# Convert BGR to Grayscale
grayImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)

# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
Run Code Online (Sandbox Code Playgroud)

第一位计算二值图像。非常简单。我使用这张图片作为基础,它只是您发布的内容的清理版本,没有注释。这是生成的二值图像:

现在,要执行卷积,我们必须首先获得图像“骨架”。骨架是二值图像的一个版本,其中线条已标准化为宽度为1 pixel。这很有用,因为我们可以将图像与3 x 3内核进行卷积并寻找特定的像素模式。让我们使用 OpenCV 的扩展图像处理模块来计算骨架:

# Get image skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
Run Code Online (Sandbox Code Playgroud)

这是获得的图像:

我们现在可以应用卷积。该方法基于Mark Setchell在这篇文章中提供的信息。这篇文章主要展示了查找形状端点的方法,但我将其扩展为还可以识别线相交点,例如三角形的中间部分。主要思想是,卷积产生一个非常具体的值,其中在输入图像中找到黑白像素的图案。请参阅该帖子了解此想法背后的理论,但在这里,我们正在寻找两个值:11040。第一个发生在找到终点时。第二个是当发现一条线相交时。让我们设置卷积:

# Threshold the image so that white pixels get a value of 0 and
# black pixels a value of 10:
_, binaryImage = cv2.threshold(skeleton, 128, 10, cv2.THRESH_BINARY)

# Set the convolution kernel:
h = np.array([[1, 1, 1],
              [1, 10, 1],
              [1, 1, 1]])

# Convolve the image with the kernel:
imgFiltered = cv2.filter2D(binaryImage, -1, h)

# Create list of thresholds:
thresh = [110, 40]
Run Code Online (Sandbox Code Playgroud)

第一部分已经完成。我们将分两个单独的步骤检测端点和交叉点。每一步都会产生一个部分结果,我们可以通过OR这两个结果得到最终的掩码:

# Prepare the final mask of points:
(height, width) = binaryImage.shape
pointsMask = np.zeros((height, width, 1), np.uint8)

# Perform convolution and create points mask:
for t in range(len(thresh)):
    # Get current threshold:
    currentThresh = thresh[t]
    # Locate the threshold in the filtered image:
    tempMat = np.where(imgFiltered == currentThresh, 255, 0)
    # Convert and shape the image to a uint8 height x width x channels
    # numpy array:
    tempMat = tempMat.astype(np.uint8)
    tempMat = tempMat.reshape(height,width,1)
    # Accumulate mask:
    pointsMask = cv2.bitwise_or(pointsMask, tempMat)
Run Code Online (Sandbox Code Playgroud)

这是点的最终掩码:

请注意,白色像素是与我们的目标图案匹配的位置。这些就是我们正在寻找的点。由于形状不是完美的三角形,因此某些点可能会重复。我们可以通过应用形态膨胀来“合并”相邻的斑点:

# Set kernel (structuring element) size:
kernelSize = 7
# Set operation iterations:
opIterations = 3
# Get the structuring element:
morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))

# Perform Dilate:
morphoImage = cv2.morphologyEx(pointsMask, cv2.MORPH_DILATE, morphKernel, None, None, opIterations, cv2.BORDER_REFLECT101)
Run Code Online (Sandbox Code Playgroud)

这是结果:

非常好,我们现在有了大的像素簇(或斑点)。要获取它们的坐标,一种可能的方法是获取这些轮廓的边界矩形并计算它们的质心:

# Look for the outer contours (no children):
contours, _ = cv2.findContours(morphoImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# Store the points here:
pointsList = []

# Loop through the contours:
for i, c in enumerate(contours):

    # Get the contours bounding rectangle:
    boundRect = cv2.boundingRect(c)

    # Get the centroid of the rectangle:
    cx = int(boundRect[0] + 0.5 * boundRect[2])
    cy = int(boundRect[1] + 0.5 * boundRect[3])

    # Store centroid into list:
    pointsList.append( (cx,cy) )

    # Set centroid circle and text:
    color = (0, 0, 255)
    cv2.circle(inputImageCopy, (cx, cy), 3, color, -1)
    font = cv2.FONT_HERSHEY_COMPLEX
    string = str(cx) + ", " + str(cy)
    cv2.putText(inputImageCopy, str(i) + ':' + string, (cx, cy), font, 0.5, (255, 0, 0), 1)

    # Show image:
    cv2.imshow("Circles", inputImageCopy)
    cv2.waitKey(0)
Run Code Online (Sandbox Code Playgroud)

这些是位于原始输入中的点:

另请注意,我已将它们的坐标存储在pointsList列表中:

# Print the list of points:
print(pointsList)
Run Code Online (Sandbox Code Playgroud)

这将质心打印为元组(centroidX, centroidY)

[(717, 971), (22, 960), (183, 587), (568, 586), (388, 98)]
Run Code Online (Sandbox Code Playgroud)