繁体   English   中英

用 opencv 找到手绘线的终点

[英]Finding the end points of a hand drawn line with opencv

我试图找到手绘线的两个端点我写了这个找到轮廓的片段,但端点不正确:

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 实现此方法 这个想法是用一个特殊的 kernel 对图像进行卷积,它可以识别一条线的起点/终点。 这些是步骤:

  1. 稍微调整一下图像的大小,因为它太大了。
  2. 将图像转换为灰度
  3. 获取骨架
  4. 将骨架与端点kernel卷积
  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 )并将其转换为灰度(实际上是一个倒置的二进制图像)。 现在,检测端点的第一步是将width标准化1 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)

查看原始链接以获取有关此方法的信息,但一般要点是 kernel 使得与线中的端点的卷积将产生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 ,对于每个点,产生一个 label。 使用dictionary ,存储 label 和共享 label 的所有质心 - 使用groupsMask通过flood-filling在不同点之间传播 label。 dictionary中,我们将存储质心簇 label、质心总和的累积以及累积多少质心的计数,因此我们可以生成最终平均值。 像这样:

# 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)

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

组掩码被新标签淹没。 将共享 label 的点加在一起以产生最终平均点。 有点难以看到,因为我的 label 从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)

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

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

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

这个想法很简单,细化后, 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)

Output: 在此处输入图像描述

编辑:您可能有兴趣访问我对这个类似问题的回答。 它有一些额外的功能,它也检测端点和连接器点。

我想提出另一种解决方案,涉及Hit-or-Miss Transform 此转换有助于识别二进制图像中的某些模式。 流行的腐蚀和膨胀操作构成了这种变换的基础。 有关详细信息、公式、插图和代码,请参阅此页面

我对二值图像执行的操作之一是骨架化,它将任何形状的宽度降低到只有一个像素 使用命中或未命中的变换查找端点非常容易,也就是说,如果您知道如何找到它们以及需要使用什么内核来发现它们。

变化:

这些端点可以位于八个不同的位置。 结束像素是下面每个 kernel 中心的像素:

  1. 可能发生的一种变化是当结束像素位于其他 2 个像素之间时,如下所示:

1 --> 前景像素(白色像素)

0 --> 背景像素(黑色像素)

|1 1 1|
|0 1 0|
|0 0 0|

|0 0 1|
|0 1 1|
|0 0 1|

|0 0 0|
|0 1 0|
|1 1 1|

|1 0 0|
|1 1 0|
|1 0 0|
  1. 另一种变化是当结束像素位于对角线的末端时; 像下面这样:
|0 0 0|
|0 1 0|
|1 0 0|

|1 0 0|
|0 1 0|
|0 0 0|

|0 0 1|
|0 1 0|
|0 0 0|

|0 0 0|
|0 1 0|
|0 0 1|

代码:

读取图像,将其反二值化并获得其骨架:

img = cv2.imread(r'C:\Users\524316\Desktop\Stack\Code\Find_endpoints\lines.jpg')
img2 = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]    
skeleton = cv2.ximgproc.thinning(th, None, 1)  

根据 OpenCV 中的文档

  • 1 -> 前景像素
  • -1 -> 背景像素
  • 0 -> 前景/背景像素

我们在下面从k1k4构建了 4 个不同的变体 1 内核:

k1 = np.array(([ 0, 0,  0], 
               [-1, 1, -1], 
               [-1,-1, -1]), dtype="int")

k2 = np.array(([0, -1, -1], 
               [0,  1, -1], 
               [0, -1, -1]), dtype="int")

k3 = np.array(([-1, -1, 0],  
               [-1,  1, 0], 
               [-1, -1, 0]), dtype="int")

k4 = np.array(([-1, -1, -1], 
               [-1,  1, -1], 
               [ 0,  0,  0]), dtype="int")

使用上述所有内核执行命中或未命中变换并对图像求和:

o1 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k1)
o2 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k2)
o3 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k3)
o4 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k4)
out1 = o1 + o2 + o3 + o4

我们在下面构建从k5k8的变体 2 的下 4 个内核:

k5 = np.array(([-1, -1, -1], 
               [-1,  1, -1], 
               [ 0, -1, -1]), dtype="int")

k6 = np.array(([-1, -1, -1], 
               [-1,  1, -1], 
               [-1, -1,  0]), dtype="int")

k7 = np.array(([-1, -1,  0], 
               [-1,  1, -1], 
               [-1, -1, -1]), dtype="int")

k8 = np.array(([ 0, -1, -1], 
               [-1,  1, -1], 
               [-1, -1, -1]), dtype="int")               

o5 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k5)
o6 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k6)
o7 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k7)
o8 = cv2.morphologyEx(skeleton, cv2.MORPH_HITMISS, k8)
out2 = o5 + o6 + o7 + o8

添加两个结果并绘制端点:

out = cv2.add(out1, out2)
pts = np.argwhere(out == 255)
for pt in pts:
    img2 = cv2.circle(img2, (pt[1], pt[0]), 15, (0,255,0), -1)

在此处输入图像描述

暂无
暂无

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

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