python - 用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 实现。这个想法是用一个特殊的内核对图像进行卷积,该内核识别一条线的起点/终点。这些是步骤:
- 稍微调整一下图像的大小,因为它太大了。
- 将图像转换为灰度
- 获取骨架
- 将骨架与端点内核卷积
- 获取端点坐标
现在,这将是所提出算法的第一次迭代。但是,根据输入图像,可能存在重复的端点 - 彼此太近并且可以连接的单个点。所以,让我们结合一些额外的处理来摆脱这些重复的点。
- 识别可能的重复点
- 加入重复的点
- 计算最终点
这些最后的步骤太笼统了,当我们到达那一步时,让我进一步详细说明消除重复项背后的想法。让我们看看第一部分的代码:
# 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
原始比例的 a)并将其转换为灰度(实际上是一个倒置的二进制图像)。现在,检测端点的第一步是将线标准化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)
查看原始链接以获取有关此方法的信息,但一般要点是内核使得与线中的端点的卷积将产生 的值110
,作为邻域求和的结果。涉及float
操作,因此必须小心数据类型和转换。该过程的结果可以在这里观察到:
这些是端点,但是请注意,如果它们太靠近,可以连接一些点。现在是重复消除步骤。让我们首先定义检查点是否重复的标准。如果这些点太接近,我们将加入它们。让我们提出一种基于形态学的点接近度方法。我将使用大小和迭代来扩展端点掩码。如果两个或多个点太接近,它们的膨胀将产生一个大的、独特的 blob:rectangular kernel
3
3
# 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
,对于每个点,产生一个标签。使用 a 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)
这是最终图像,显示了线条的终点/起点:
现在,端点检测方法,或者更确切地说,卷积步骤,正在曲线上产生一个明显的额外点,这可能是因为线上的一段与其邻域分离得太远 - 将曲线分成两部分。也许在卷积之前应用一点形态学可以解决这个问题。
推荐阅读
- javascript - 应用程序负载均衡器存在 CORS 问题
- java - 使用 PowerMockito 时出现 NullPointerException
- php - 仅使用西里尔字母搜索后,搜索表单重定向到主页
- flutter - 如何将类型从一个类“传递”到另一个类?
- random - 强化学习:序列中样本的 SGD 使用和独立性
- ruby-on-rails - 为什么我的 Rails `db/schema.rb` 函数的前缀是 `public`?
- c# - 请求被中止:无法创建 SSL/TLS 安全通道。在本地工作但不住在
- sql - 在同一个表的列中返回具有唯一值的行
- html - 无论网格布局如何,都放置一个按钮 bootstrap4
- .net - 服务器响应是:smtp23.relay.iad3b.emailsrvr.com