一种可能的解决方案涉及在this 帖子中应用该方法。它涉及将输入图像与用于识别端点的特殊内核进行卷积。这些是步骤:
- 将图像转换为灰度
- 通过对灰度图像应用 Otsu 阈值 来获取二值图像
- 应用一点形态学,以确保我们有连续和闭合的曲线
- 计算图像的骨架
-
卷积骨架与端点内核
-
在原始图像上绘制端点
让我们看看代码:
# Imports:
import cv2
import numpy as np
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Prepare a deep copy of the input for results:
inputImageCopy = inputImage.copy()
# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
第一位非常简单。只需使用 Otsu 的阈值获得二值图像。结果如下:
阈值可能会错过曲线内的一些像素,从而导致“间隙”。我们不希望这样,因为我们正在尝试识别端点,这些端点本质上是曲线上的间隙。让我们使用一点形态学来填补可能的空白 - closing 将有助于填补这些较小的空白:
# Set morph operation iterations:
opIterations = 2
# Get the structuring element:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# Perform Closing:
binaryImage = cv2.morphologyEx(binaryImage, cv2.MORPH_CLOSE, kernel, None, None, opIterations, cv2.BORDER_REFLECT101)
这是现在的结果:
好的,接下来是获取二进制图像的skeleton。 骨架 是二进制图像的一个版本,其中线条已被标准化为宽度为1 pixel。这很有用,因为我们可以将图像与3 x 3 内核进行卷积,并寻找特定的像素模式——那些识别端点的模式。让我们使用 OpenCV 的扩展图像处理模块来计算骨架:
# Compute the skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
没什么特别的,只需一行代码即可完成。结果是这样的:
这张图片很微妙,但是曲线现在有1 px的宽度,所以我们可以应用卷积。这种方法的主要思想是卷积产生一个非常具体的值,其中在输入图像中发现了黑白像素的模式。我们要找的值是110,但是我们需要在实际卷积之前执行一些操作。详情请参阅original post。这些是操作:
# 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)
如果我们imshowendPointsMask,我们会得到这样的结果:
在上图中,您可以看到已识别端点的位置。让我们获取这些白色像素的坐标:
# Get the coordinates of the end-points:
(Y, X) = np.where(endPointsMask == 255)
最后,让我们在这些位置画圈:
# Draw the end-points:
for i in range(len(X)):
# Get coordinates:
x = X[i]
y = Y[i]
# Set circle color:
color = (0, 0, 255)
# Draw Circle
cv2.circle(inputImageCopy, (x, y), 3, color, -1)
cv2.imshow("Points", inputImageCopy)
cv2.waitKey(0)
这是最终结果:
编辑:识别哪个 blob 产生每组点
由于您还需要知道哪个斑点/轮廓/曲线产生了每组端点,因此您可以使用其他一些函数重新编写下面的代码来实现这一点。在这里,我将主要依靠我之前编写的用于检测图像中最大的斑点的函数。两条曲线中的一条总是比另一条更大(即面积更大)。如果你提取这条曲线,处理它,然后迭代地从原始图像中减去它,你可以逐条曲线处理,每次你可以知道哪条曲线(当前最大的一条)产生了当前的端点。让我们修改代码来实现这些想法:
# Imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "w97nr.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# Set morph operation iterations:
opIterations = 2
# Get the structuring element:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# Perform Closing:
binaryImage = cv2.morphologyEx(binaryImage, cv2.MORPH_CLOSE, kernel, None, None, opIterations, cv2.BORDER_REFLECT101)
# Compute the skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)
直到骨架计算,一切都是一样的。现在,我们将提取当前最大的 blob 并对其进行处理以获得其端点,我们将继续提取当前最大的 blob,直到没有更多曲线可以提取。因此,我们只需修改先前的代码来管理这个想法的迭代性质。另外,让我们将端点存储在列表中。此列表的每一行将表示一条新曲线:
# Processing flag:
processBlobs = True
# Shallow copy for processing loop:
blobsImage = skeleton
# Store points per blob here:
blobPoints = []
# Count the number of processed blobs:
blobCounter = 0
# Start processing blobs:
while processBlobs:
# Find biggest blob on image:
biggestBlob = findBiggestBlob(blobsImage)
# Prepare image for next iteration, remove
# currrently processed blob:
blobsImage = cv2.bitwise_xor(blobsImage, biggestBlob)
# Count number of white pixels:
whitePixelsCount = cv2.countNonZero(blobsImage)
# If the image is completely black (no white pixels)
# there are no more curves to process:
if whitePixelsCount == 0:
processBlobs = False
# Threshold the image so that white pixels get a value of 0 and
# black pixels a value of 10:
_, binaryImage = cv2.threshold(biggestBlob, 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)
# Get the coordinates of the end-points:
(Y, X) = np.where(endPointsMask == 255)
# Prepare random color:
color = (np.random.randint(low=0, high=256), np.random.randint(low=0, high=256), np.random.randint(low=0, high=256))
# Prepare id string:
string = "Blob: "+str(blobCounter)
font = cv2.FONT_HERSHEY_COMPLEX
tx = 10
ty = 10 + 10 * blobCounter
cv2.putText(inputImageCopy, string, (tx, ty), font, 0.3, color, 1)
# Store these points in list:
blobPoints.append((X,Y, blobCounter))
blobCounter = blobCounter + 1
# Draw the end-points:
for i in range(len(X)):
x = X[i]
y = Y[i]
cv2.circle(inputImageCopy, (x, y), 3, color, -1)
cv2.imshow("Points", inputImageCopy)
cv2.waitKey(0)
这个循环提取最大的 blob 并对其进行处理,就像在帖子的第一部分中一样 - 我们将图像与端点内核进行卷积并定位匹配点。对于原始输入,结果如下:
如您所见,每组点都使用一种唯一颜色(随机生成)绘制。还有当前的blob“ID”(只是一个升序计数)以与每组点相同的颜色绘制在文本中,因此您知道哪个blob产生了每组端点。该信息存储在blobPoints 列表中,我们可以打印它的值,如下所示:
# How many blobs where found:
blobCount = len(blobPoints)
print("Found: "+str(blobCount)+" blobs.")
# Let's check out each blob and their end-points:
for b in range(blobCount):
# Fetch data:
p1 = blobPoints[b][0]
p2 = blobPoints[b][1]
id = blobPoints[b][2]
# Print data for each blob:
print("Blob: "+str(b)+" p1: "+str(p1)+" p2: "+str(p2)+" id: "+str(id))
哪些打印:
Found: 2 blobs.
Blob: 0 p1: [39 66] p2: [ 42 104] id: 0
Blob: 1 p1: [129 119] p2: [25 49] id: 1
这是findBiggestBlob 函数的实现,它只使用其面积计算图像上最大的斑点。它返回一个孤立的最大 blob 的图像,这来自 C++ implementation 我写的相同想法:
def findBiggestBlob(inputImage):
# Store a copy of the input image:
biggestBlob = inputImage.copy()
# Set initial values for the
# largest contour:
largestArea = 0
largestContourIndex = 0
# Find the contours on the binary image:
contours, hierarchy = cv2.findContours(inputImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Get the largest contour in the contours list:
for i, cc in enumerate(contours):
# Find the area of the contour:
area = cv2.contourArea(cc)
# Store the index of the largest contour:
if area > largestArea:
largestArea = area
largestContourIndex = i
# Once we get the biggest blob, paint it black:
tempMat = inputImage.copy()
cv2.drawContours(tempMat, contours, largestContourIndex, (0, 0, 0), -1, 8, hierarchy)
# Erase smaller blobs:
biggestBlob = biggestBlob - tempMat
return biggestBlob