简体   繁体   English

用 opencv 找到手绘线的终点

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

I'm trying to find the two end points of a hand drawn line I have written this snippet which finds the contours, but the end points is not correct:我试图找到手绘线的两个端点我写了这个找到轮廓的片段,但端点不正确:

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)

Original image:原图:

在此处输入图像描述

I have also tried cornerHarris but it gave me some extra points,我也试过cornerHarris但它给了我一些额外的分数,

Can someone please suggest an accurate and better approach?有人可以建议一种准确且更好的方法吗?

This solution uses a Python implementation of this approach .此解决方案使用 Python 实现此方法 The idea is to convolve the image with a special kernel that identifies starting/ending points in a line.这个想法是用一个特殊的 kernel 对图像进行卷积,它可以识别一条线的起点/终点。 These are the steps:这些是步骤:

  1. Resize your image a little bit, because its too big.稍微调整一下图像的大小,因为它太大了。
  2. Convert the image to grayscale将图像转换为灰度
  3. Get the skeleton获取骨架
  4. Convolve the skeleton with the endpoint kernel将骨架与端点kernel卷积
  5. Get the coordinates of the endpoints获取端点坐标

Now, that would be first iteration of the proposed algorithm.现在,这将是所提出算法的第一次迭代。 However, depending on the input image, there could be duplicated endpoints - individual points that are too close to each other and could be joined.但是,根据输入图像,可能存在重复的端点 - 彼此太近并且可以连接的单个点。 So, let's incorporate some additional processing to get rid of such duplicated points.所以,让我们结合一些额外的处理来摆脱这些重复的点。

  1. Identify possible duplicated points识别可能的重复点
  2. Join the duplicated points加入重复的点
  3. Compute final points计算最终点

These last steps are too general, let me further elaborate on the idea behind the duplicates elimination when we get to that step.这些最后的步骤太笼统了,当我们到达那一步时,让我进一步详细说明消除重复项背后的想法。 Let's see the code for the first part:让我们看看第一部分的代码:

# 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

So far I've resized the image (to a 0.5 of the original scale) and converted it to grayscale (in reality an inverted binary image).到目前为止,我已经调整了图像的大小(到原始比例的0.5 )并将其转换为灰度(实际上是一个倒置的二进制图像)。 Now, the first step to detect endpoints is to normalize the lines width to 1 pixel .现在,检测端点的第一步是将width标准化1 pixel This is achieved by computing the skeleton , which can be implemented using OpenCV's Extended Image Processing module :这是通过计算skeleton来实现的,可以使用OpenCV 的扩展图像处理模块来实现:

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

This is the skeleton:这是骨架:

Now, let's run the endpoint detection portion:现在,让我们运行端点检测部分:

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

Check out the original link for information on this method, but the general gist is that the kernel is such that convolution with ending points in a line will produce values of 110 , as a result of neighborhood summation.查看原始链接以获取有关此方法的信息,但一般要点是 kernel 使得与线中的端点的卷积将产生110的值,这是邻域求和的结果。 There are float operations involved, so gotta be careful with data types and conversions.涉及float操作,因此必须小心数据类型和转换。 The result of the procedure can be observed here:该过程的结果可以在这里观察到:

Those are the endpoints, however, note that some points could be joined, IF they are too close.这些是端点,但是请注意,如果它们太靠近,可以连接一些点。 Now comes the duplicate elimination steps.现在是重复消除步骤。 Let's first define the criteria to check if a point is a duplicate or not.让我们首先定义检查点是否重复的标准。 If the points are too close, we will join them.如果这些点太接近,我们将加入它们。 Let's propose a morphology-based approach to points closeness.让我们提出一种基于形态学的点接近度方法。 I'll dilate the endpoints mask with a rectangular kernel of size 3 and 3 iterations.我将使用大小为33次迭代的rectangular kernel扩展端点掩码。 If two or more points are too close, their dilation will produce a big, unique, blob:如果两个或多个点太接近,它们的膨胀将产生一个大的、独特的 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)

This is the result of the dilation.这是膨胀的结果。 I refer to this image as the groupsMask :我将此图像称为groupsMask

Note how some of the points now share adjacency.注意一些点现在是如何共享邻接的。 I will use this mask as a guide to produce the final centroids.我将使用这个掩码作为生成最终质心的指南。 The algorithm is like this: Loop through the endPointsMask , for every point, produce a label.算法是这样的:遍历endPointsMask ,对于每个点,产生一个 label。 Using a dictionary , store the label and all centroids sharing that label - use the groupsMask for propagating the label between different points via flood-filling .使用dictionary ,存储 label 和共享 label 的所有质心 - 使用groupsMask通过flood-filling在不同点之间传播 label。 Inside the dictionary we will store the centroid cluster label, the accumulation of the centroids sum and the count of how many centroids where accumulated, so we can produce a final average.dictionary中,我们将存储质心簇 label、质心总和的累积以及累积多少质心的计数,因此我们可以生成最终平均值。 Like this:像这样:

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

Here are some animations of the procedure, first, the centroids being processed one by one.这是该过程的一些动画,首先,质心被一个一个地处理。 We are trying to join those points that seem to close to each other:我们正在尝试加入那些似乎彼此接近的点:

The groups mask being flood-filled with new labels.组掩码被新标签淹没。 The points that share a label are added together to produce a final average point.将共享 label 的点加在一起以产生最终平均点。 It is a little hard to see, because my label starts at 1 , but you can barely see the labels being populated:有点难以看到,因为我的 label 从1开始,但您几乎看不到正在填充的标签:

Now, what remains is to produce the final points.现在,剩下的就是产生最后的点了。 Loop through the dictionary and check out the centroids and their count.遍历字典并检查质心及其计数。 If the count is greater than 1 , the centroid represents an accumulation and must be diveded by its count to produce the final point:如果计数大于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)

This is the final image, showing the end/starting points of the lines:这是最终图像,显示了线条的终点/起点:

Now, the endpoints detection method, or rather, the convolution step, is producing an apparent extra point on the curve, that could be because of a segment on the lines is too separated from its neighborhood - splitting the curve in two parts.现在,端点检测方法,或者更确切地说,卷积步骤,正在曲线上产生一个明显的额外点,这可能是因为线上的一段与其邻域分离得太远 - 将曲线分成两部分。 Maybe applying a little bit of morphology before the convolution could get rid of the problem.也许在卷积之前应用一点形态学可以解决这个问题。

I would like to suggest a simpler, more efficient method and more importantly, it does not produce false endpoints:我想建议一种更简单、更有效的方法,更重要的是,它不会产生错误的端点:

The idea is quite simple, after thinning, count neighbouring pixels (8-connectivity) if neighbours count equals 1 --> the point is an end point这个想法很简单,细化后, if neighbours count equals 1 --> the point is an end point

The code is self-explanatory:代码是不言自明的:

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

main:主要的:

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: Output: 在此处输入图像描述

Edit: you might be interested in visiting my answer to this similar question .编辑:您可能有兴趣访问我对这个类似问题的回答。 It has some extra functionality, it detects end-points and connector-points as well.它有一些额外的功能,它也检测端点和连接器点。

I would like to propose another solution, involving the Hit-or-Miss Transform .我想提出另一种解决方案,涉及Hit-or-Miss Transform This transform helps identify certain patterns in a binary image.此转换有助于识别二进制图像中的某些模式。 The popular erosion and dilation operations form the basis of this transform.流行的腐蚀和膨胀操作构成了这种变换的基础。 See this page for more details, formula, illustration and code 有关详细信息、公式、插图和代码,请参阅此页面

One of the operations I performed on the binary image is skeletonization, which brings down the width of any shape down to just one pixel .我对二值图像执行的操作之一是骨架化,它将任何形状的宽度降低到只有一个像素 Using hit-or-miss transform finding endpoints is pretty easy, ie if you know how they can be found and what kernels need to be used to spot them.使用命中或未命中的变换查找端点非常容易,也就是说,如果您知道如何找到它们以及需要使用什么内核来发现它们。

Variations:变化:

These end points can be in eight different positions.这些端点可以位于八个不同的位置。 The end pixel is the one at the center of each kernel below:结束像素是下面每个 kernel 中心的像素:

  1. One variation that can occur is when the end pixel is between 2 other pixels like the following:可能发生的一种变化是当结束像素位于其他 2 个像素之间时,如下所示:

1 --> foreground pixel (white pixel) 1 --> 前景像素(白色像素)

0 --> background pixel (black pixel) 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. Another variation is when the end pixel is at the end of a diagonal;另一种变化是当结束像素位于对角线的末端时; like the following:像下面这样:
|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|

Code:代码:

Read the image, inversely binarize it and obtain its skeleton:读取图像,将其反二值化并获得其骨架:

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)  

According to the documentation in OpenCV:根据 OpenCV 中的文档

  • 1 -> foreground pixel 1 -> 前景像素
  • -1 -> background pixel -1 -> 背景像素
  • 0 -> foreground/background pixel 0 -> 前景/背景像素

We build 4 different kernels of variation 1 from k1 to k4 below:我们在下面从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")

Perform hit-or-miss transform using all the above kernels and sum the images:使用上述所有内核执行命中或未命中变换并对图像求和:

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

We build the next 4 kernels of variation 2 from k5 to k8 below:我们在下面构建从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

Add both the results and draw the end points:添加两个结果并绘制端点:

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