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

问题描述

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

green colors are contours found (3),red color are points found according to those contours

但是像这样:

enter image description here

所以根据第一张图片,绿色是用 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,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),2)
            i = i + 1
  customContours = sorted(customContours,key=lambda x: x[1],reverse=True)
  for cnt in customContours:
    cv2.drawContours(imgColor,[cnt[2]],255),5)
  return imgColor,customContours

你能帮我了解一下关于第二张图片的真实要点吗?

(编辑 01/07/21)

我想要一个通用的解决方案,因为图像可能更复杂,例如下图:

enter image description here

注意: 请注意,中间箭头(点 17 和 18)没有封闭区域,因此不是要研究的多边形。那么,那个地区就没有兴趣获得他的积分了。另外,注意点的顺序并不重要,但如果入口是洞图像,它应该知道有4个多边形,所以对于每个多边形点从0开始,然后是1,等等。

解决方法

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

步骤如下:

  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,255,cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

第一位计算二进制图像。很直接。我使用 this image 作为基础,它只是您发布的没有注释的内容的清理版本。这是生成的二进制图像:

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

# Get image skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage,None,1)

这是获得的图像:

我们现在可以应用卷积。该方法基于 Mark Setchell 在此 post 上的信息。这篇文章主要展示了寻找形状端点的方法,但我将其扩展为也识别线交点,例如三角形的中间部分。主要思想是卷积产生一个非常具体的值,其中在输入图像中找到黑白像素的模式。有关此想法背后的理论,请参阅帖子,但在这里,我们正在寻找两个值: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,1]])

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

# Create list of thresholds:
thresh = [110,40]

第一部分完成。我们将在两个独立的步骤中检测端点和交叉点。每一步都会产生一个部分结果,我们可以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,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,1)
    # Accumulate mask:
    pointsMask = cv2.bitwise_or(pointsMask,tempMat)

这是点的最终掩码:

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

# 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,opIterations,cv2.BORDER_REFLECT101)

结果如下:

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

# 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,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,font,0.5,(255,0),1)

    # Show image:
    cv2.imshow("Circles",inputImageCopy)
    cv2.waitKey(0)

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

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

# Print the list of points:
print(pointsList)

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

[(717,971),(22,960),(183,587),(568,586),(388,98)]