【问题标题】:Finding the end points of a hand drawn line with opencv用opencv查找手绘线的端点
【发布时间】:2021-04-19 04:08:28
【问题描述】:

我正在尝试找到手绘线的两个端点 我写了这个找到轮廓的sn-p, 但端点不正确:

img = cv2.imread("my_img.jpeg")

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Binary Threshold:
_, thr_img = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

cv2.imshow(winname="after threshold", mat=thr_img)
cv2.waitKey(0)

contours, _ = cv2.findContours(image=thr_img, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_SIMPLE)

for idx, cnt in enumerate(contours):
    print("Contour #", idx)
    cv2.drawContours(image=img, contours=[cnt], contourIdx=0, color=(255, 0, 0), thickness=3)
    cv2.circle(img, tuple(cnt[0][0]), 5, (255, 255, 0), 5) # Result in wrong result
    cv2.circle(img, tuple(cnt[-1][0]), 5, (0, 0, 255), 5)  # Result in wrong result
    cv2.imshow(winname="contour" + str(idx), mat=img)
    cv2.waitKey(0)

原图:

我也试过cornerHarris,但它给了我一些额外的分数,

有人可以提出一个准确和更好的方法吗?

【问题讨论】:

    标签: python opencv image-processing


    【解决方案1】:

    我想建议一种更简单、更有效的方法,更重要的是,它不会产生错误的端点:

    思路很简单,细化后,统计相邻像素(8-connectivity)if neighbours count equals 1 --> the point is an end point

    代码不言自明:

    def get_end_pnts(pnts, img):
        extremes = []    
        for p in pnts:
            x = p[0]
            y = p[1]
            n = 0        
            n += img[y - 1,x]
            n += img[y - 1,x - 1]
            n += img[y - 1,x + 1]
            n += img[y,x - 1]    
            n += img[y,x + 1]    
            n += img[y + 1,x]    
            n += img[y + 1,x - 1]
            n += img[y + 1,x + 1]
            n /= 255        
            if n == 1:
                extremes.append(p)
        return extremes
    

    主要:

    img = cv2.imread(p, cv2.IMREAD_GRAYSCALE)
    img = cv2.threshold(img, 128, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
    img = cv2.ximgproc.thinning(img)
    pnts = cv2.findNonZero(img)
    pnts = np.squeeze(pnts)
    ext = get_end_pnts(pnts, img)
    for p in ext:
        cv2.circle(img, (p[0], p[1]), 5, 128)
    

    输出:

    编辑:您可能有兴趣访问我对this similar question 的回答。它有一些额外的功能,它还能检测端点和连接器点。

    【讨论】:

      【解决方案2】:

      此解决方案使用this approach 的 Python 实现。这个想法是用一个特殊的内核对图像进行卷积,该内核识别一条线的起点/终点。这些是步骤:

      1. 稍微调整您的图片,因为它太大了。
      2. 将图像转换为灰度
      3. 获取骨架
      4. 卷积骨架与端点内核
      5. 获取端点的坐标

      现在,这将是所提议算法的第一次迭代。但是,根据输入图像,可能存在重复的端点 - 彼此太近并且可以连接的单个点。所以,让我们结合一些额外的处理来去除这些重复的点。

      1. 识别可能的重复点
      2. 加入重复点
      3. 计算最终点

      这些最后的步骤太笼统了,当我们到达那个步骤时,让我进一步详细说明消除重复项背后的想法。我们来看第一部分的代码:

      # imports:
      import cv2
      import numpy as np
      
      # image path
      path = "D://opencvImages//"
      fileName = "hJVBX.jpg"
      
      # Reading an image in default mode:
      inputImage = cv2.imread(path + fileName)
      
      # Resize image:
      scalePercent = 50  # percent of original size
      width = int(inputImage.shape[1] * scalePercent / 100)
      height = int(inputImage.shape[0] * scalePercent / 100)
      
      # New dimensions:
      dim = (width, height)
      
      # resize image
      resizedImage = cv2.resize(inputImage, dim, interpolation=cv2.INTER_AREA)
      
      # Color conversion
      grayscaleImage = cv2.cvtColor(resizedImage, cv2.COLOR_BGR2GRAY)
      grayscaleImage = 255 - grayscaleImage
      

      到目前为止,我已经调整了图像的大小(到原始比例的0.5)并将其转换为灰度(实际上是一个倒置的二进制图像)。现在,检测端点的第一步是规范化width1 pixel。这是通过计算skeleton 来实现的,可以使用OpenCV 的扩展图像处理模块

      # Compute the skeleton:
      skeleton = cv2.ximgproc.thinning(grayscaleImage, None, 1)
      

      这是骨架:

      现在,让我们运行端点检测部分:

      # 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 end-points 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)
      
      # Extract only the end-points pixels, those with
      # an intensity value of 110:
      endPointsMask = np.where(imgFiltered == 110, 255, 0)
      
      # The above operation converted the image to 32-bit float,
      # convert back to 8-bit uint
      endPointsMask = endPointsMask.astype(np.uint8)
      

      查看原始链接以获取有关此方法的信息,但一般要点是内核使得与一行中的端点的卷积将产生110 的值,这是邻域求和的结果。涉及float 操作,因此必须小心数据类型和转换。该过程的结果可以在这里观察到:

      这些是端点,但是请注意,有些点可以连接,如果它们太近了。现在是重复消除步骤。让我们首先定义检查点是否重复的标准。如果这些点太接近,我们将加入它们。让我们提出一种基于形态的方法来处理点的接近度。我将使用大小为33 迭代的rectangular kernel扩张端点掩码。如果两个或多个点太接近,它们的膨胀会产生一个大的、独特的 blob:

      # RGB copy of this:
      rgbMask = endPointsMask.copy()
      rgbMask = cv2.cvtColor(rgbMask, cv2.COLOR_GRAY2BGR)
      
      # Create a copy of the mask for points processing:
      groupsMask = endPointsMask.copy()
      
      # Set kernel (structuring element) size:
      kernelSize = 3
      # Set operation iterations:
      opIterations = 3
      # Get the structuring element:
      maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
      # Perform dilate:
      groupsMask = cv2.morphologyEx(groupsMask, cv2.MORPH_DILATE, maxKernel, None, None, opIterations, cv2.BORDER_REFLECT101)
      

      这是膨胀的结果。我将此图像称为groupsMask

      注意一些点现在如何共享邻接。我将使用这个掩码作为生成最终质心的指南。算法是这样的:循环遍历endPointsMask,为每个点生成一个标签。使用dictionary,存储标签和共享该标签的所有质心 - 使用groupsMask 通过flood-filling 在不同点之间传播标签。在dictionary 中,我们将存储质心簇标签、质心总和的累积以及累积多少质心的计数,因此我们可以生成最终平均值。像这样:

      # Set the centroids Dictionary:
      centroidsDictionary = {}
      
      # Get centroids on the end points mask:
      totalComponents, output, stats, centroids = cv2.connectedComponentsWithStats(endPointsMask, connectivity=8)
      
      # Count the blob labels with this:
      labelCounter = 1
      
      # Loop through the centroids, skipping the background (0):
      for c in range(1, len(centroids), 1):
      
          # Get the current centroids:
          cx = int(centroids[c][0])
          cy = int(centroids[c][1])
      
          # Get the pixel value on the groups mask:
          pixelValue = groupsMask[cy, cx]
      
          # If new value (255) there's no entry in the dictionary
          # Process a new key and value:
          if pixelValue == 255:
      
              # New key and values-> Centroid and Point Count:
              centroidsDictionary[labelCounter] = (cx, cy, 1)
      
              # Flood fill at centroid:
              cv2.floodFill(groupsMask, mask=None, seedPoint=(cx, cy), newVal=labelCounter)
              labelCounter += 1
      
          # Else, the label already exists and we must accumulate the
          # centroid and its count:
          else:
      
              # Get Value:
              (accumCx, accumCy, blobCount) = centroidsDictionary[pixelValue]
      
              # Accumulate value:
              accumCx = accumCx + cx
              accumCy = accumCy + cy
              blobCount += 1
      
              # Update dictionary entry:
              centroidsDictionary[pixelValue] = (accumCx, accumCy, blobCount)
      

      这里是过程的一些动画,首先,质心被一个一个地处理。我们正在尝试加入那些似乎彼此接近的点:

      组掩码被大量新标签填充。共享一个标签的点被加在一起以产生最终的平均点。有点难看,因为我的标签从 1 开始,但你几乎看不到被填充的标签:

      现在,剩下的就是产生最后的分数了。遍历字典并检查质心及其计数。如果计数大于1,则质心代表一个累加,必须通过它的计数来生成最终点:

      # Loop trough the dictionary and get the final centroid values:
      for k in centroidsDictionary:
          # Get the value of the current key:
          (cx, cy, count) = centroidsDictionary[k]
          # Process combined points:
          if count != 1:
              cx = int(cx/count)
              cy = int(cy/count)
          # Draw circle at the centroid
          cv2.circle(resizedImage, (cx, cy), 5, (0, 0, 255), -1)
      
      cv2.imshow("Final Centroids", resizedImage)
      cv2.waitKey(0)
      

      这是最终图像,显示了线条的终点/起点:

      现在,端点检测方法,或者更确切地说,卷积步骤,正在曲线上产生一个明显的额外点,这可能是因为线上的一段距离其邻域太远 - 分割曲线分两部分。也许在卷积之前应用一点形态学可以解决这个问题。

      【讨论】:

      • 这篇文章已经付出了很多努力
      猜你喜欢
      • 2018-03-15
      • 1970-01-01
      • 2019-10-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-08
      • 2012-09-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多