所以这就是我想出的 - 这有点费力,但它最终确实让我们得到了正确的答案。我将直接使用您在上一张图片中显示的连接组件输出。
使用morphological image skeletonization 来获取blob 的骨架。这样,它将为我们提供最小的轮廓表示,以便我们获得一个像素宽的边界,该边界穿过每个厚边缘的中间。你可以通过 Scikit-image 的skeletonize 方法来实现。
-
使用Hough Transform,这是一种在骨架化图像上的线检测方法。总之,它参数化了极域中的线,输出将是一组rho 和theta,告诉我们在骨架化图像中检测到哪些线。为此,我们可以使用 OpenCV 的 cv2.HoughLines。在骨架化图像上执行此操作非常重要,否则我们将有很多候选线平行于边界框的真实轮廓,您将无法区分它们。
李>
选取每对线并找到它们的交点。我们预计,对于所有线对,将有 4 个主要的交点集群,为我们提供每个矩形的角。
由于轮廓中的噪声,我们可能会得到四个以上的交点。我们可以使用convex hull 最终得到矩形的 4 个交点。总之,凸包算法在一个点列表上运行,它定义了一个点子集,该子集可以最小化地包含点列表。我们可以使用cv2.convexHull。
最后,由于霍夫变换的量化,每个角的附近可能有多个点。因此,应用K-Means clustering 找到 4 个点簇,从而找到它们的质心。我们可以使用cv2.kmeans。
找到质心后,我们可以简单地循环遍历每对点,最终找到到每个角的距离,从而找到您关心的距离。
让我们逐点逐一介绍:
第 1 步 - 形态图像骨架化
使用 Scikit-image 的 skeletonize,我们可以骨架化您在上面显示的连接组件图像。请注意,在继续之前,您需要将图像转换为二进制。调用该方法后,我们需要在其余过程中转换回无符号 8 位整数。我已经下载了上面的图片并保存在本地。我们可以在之后运行skeletonize 方法:
from skimage.morphology import skeletonize
im = cv2.imread('K7ELI.png', 0)
out = skeletonize(im > 0)
# Convert to uint8
out = 255*(out.astype(np.uint8))
我们得到这张图片:
步骤 #2 - 使用霍夫变换
使用霍夫变换,我们可以检测到这张图片中最突出的线条:
lines = cv2.HoughLines(out,1,np.pi/180,60)
在这里我们指定搜索空间,以便我们查找 bin 大小的长度为 1 且角度的 bin 为 1 度或pi / 180 弧度的线。总之,霍夫变换查看每个边缘点并遍历从原点到每个边缘点的一系列角度theta,并根据 bin 大小计算 rho 的相应值。这对登录到 2D 直方图,然后我们进行投票。我们对这个 2D 直方图设置阈值,以便任何超过某个值的 bin 都是候选行。在上面的代码行中,将 bin 计数的阈值设置为 60。
此代码是可选的,但我想向您展示可视化线条的样子:
img_colour = np.dstack([im, im, im])
lines = cv2.HoughLines(edges,1,np.pi/180,60)
for rho,theta in lines[:,0]:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img_colour,(x1,y1),(x2,y2),(0,0,255),2)
这个代码是我拉的from the following tutorial。它将图像中的霍夫变换检测线绘制为红色。我得到以下图像:
我们可以看到,图像中有四个交点。找到这些交点是我们接下来的工作。
步骤 #3 - 找到交点
在霍夫变换中,我们可以将原点到图像中点(x, y) 的线的长度与角度theta 的对向关联:
rho = x*cos(theta) + y*sin(theta)
我们也可以用笛卡尔形式形成线y = m*x + c的方程。我们可以通过将rho 等式的两边除以sin(theta) 然后将相关项移到每一边来在两者之间进行转换:
因此,我们应该循环遍历所有唯一的线对,并使用上面的方程,我们可以通过将它们的笛卡尔形式设置为彼此相等来找到它们的交点。为了节省空间,我不会为您推导,而是简单地将两条笛卡尔形式的线设置为彼此相等并求解x的交点坐标。完成后,将此点替换为两行中的任何一行以找到y 坐标。在两条几乎平行的线的情况下,或者如果我们选择两对方向相同且不相交的线,我们显然应该跳过图像之外的交点。
pts = []
for i in range(lines.shape[0]):
(rho1, theta1) = lines[i,0]
m1 = -1/np.tan(theta1)
c1 = rho1 / np.sin(theta1)
for j in range(i+1,lines.shape[0]):
(rho2, theta2) = lines[j,0]
m2 = -1 / np.tan(theta2)
c2 = rho2 / np.sin(theta2)
if np.abs(m1 - m2) <= 1e-8:
continue
x = (c2 - c1) / (m1 - m2)
y = m1*x + c1
if 0 <= x < img.shape[1] and 0 <= y < img.shape[0]:
pts.append((int(x), int(y)))
pts 是一个元组列表,以便我们添加图像内所有未超出范围的交点。
第 4 步 - 使用凸包
我们可以使用这个元组列表并使用凸包,以便我们找到定义矩形外周长的点列表。请注意,定义矩形的点的顺序是逆时针的。这对这一步无关紧要,但稍后会很重要:
pts = np.array(pts)
pts = pts[:,None] # We need to convert to a 3D numpy array with a singleton 2nd dimension
hull = cv2.convexHull(pts)
hull 包含一个 3D NumPy 数组,它是创建图像外边界的原始交点的子集。我们可以使用这些点来绘制它们在图像中的位置以进行说明
out2 = np.dstack([im, im, im])
for pt in hull[:,0]:
cv2.circle(out2, tuple(pt), 2, (0, 255, 0), 2)
我已经拍摄了原始图像并用绿色绘制了角点。我们得到这张图片:
第 5 步 - 应用 K-Means 聚类
如上图所示,有多个点映射到每个角。如果我们可以将每个角落的多个点合并为一个点,那就太好了。一种方法是平均每个角落的所有点,最简单的方法是使用 K-Means 聚类。我们需要质心从而给我们矩形的最终角点。我们需要确保我们指定了 4 个要查找的集群。
从K-Means clustering tutorial from the OpenCV docs,我们可以使用这个代码:
# Define criteria = ( type, max_iter = 10 , epsilon = 1.0 )
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
# Set flags (Just to avoid line break in the code)
flags = cv2.KMEANS_RANDOM_CENTERS
# Apply KMeans
# The convex hull points need to be float32
z = hull.copy().astype(np.float32)
compactness,labels,centers = cv2.kmeans(z,4,None,criteria,10,flags)
第一个参数是算法要求的需要在float32中的点的凸包。第二个参数指定我们要搜索的集群的数量,在我们的例子中是 4。第三个参数可以跳过。
它是每个点分配到的最佳集群 ID 的占位符,但我们不需要使用它。 criteria 是用于算法机制的 K-Means 参数,第五个参数告诉我们应该运行多少次尝试才能找到最佳集群。我们选择 10,这意味着我们运行了 10 次 K-Means,并选择了误差最小的集群配置。错误存储在算法输出的compactness 变量中。最后,最后一个变量是可选标志,我们设置它以便简单地从点中随机选择算法的初始质心。
labels 提供分配给每个点的集群 ID,centers 是我们需要的关键变量,因此返回:
array([[338.5 , 152.5 ],
[302.6667, 368.6667],
[139. , 340. ],
[178.5 , 127. ]], dtype=float32)
这些是矩形的四个角点。我们可以通过直接在原始图像上绘制它们来查看它们的排列位置,并且我们还得到了这个图像:
out3 = np.dstack([im, im, im])
for pt in centers:
cv2.circle(out3, tuple(pt), 2, (0, 255, 0), 2)
第 6 步 - 现在测量长度
最后,我们可以循环遍历每一对线,找到对应的维度。请注意,由于算法的随机性,K-Means 具有随机顺序的质心,我们可以在这些质心上运行凸包以确保顺序是循环的。
centers = cv2.convexHull(centers)[:,0]
for (i, j) in zip(range(4), [1, 2, 3, 0]):
length = np.sqrt(np.sum((centers[i] - centers[j])**2.0))
print('Length of side {}: {}'.format(i+1, length))
因此我们得到:
Length of side 1: 219.11654663085938
Length of side 2: 166.1582489013672
Length of side 3: 216.63160705566406
Length of side 4: 162.019287109375
如果您想从透视图中查看边界框是如何排列的,让我们在图像上实际绘制这些线,这些线是在这些中心定义的:
out4 = np.dstack([im, im, im])
for (i, j) in zip(range(4), [1, 2, 3, 0]):
cv2.line(out4, tuple(centers[i]), tuple(centers[j]), (0, 0, 255), 2)
我们得到:
要查看它与原始图像的对齐位置,让我们重复上面的代码,但在原始图像上绘制线条。为此,我下载了原始图像的副本:
out5 = cv2.imread('no8BP.png') # Note - grayscale image read in as colour
for (i, j) in zip(range(4), [1, 2, 3, 0]):
cv2.line(out5, tuple(centers[i]), tuple(centers[j]), (0, 0, 255), 2)
为了完整起见,这里是从头到尾的完整代码,没有所有调试输出 - 我们从读取图像到在原始图像中绘制线条,并打印检测到的矩形中每一边的长度。
from skimage.morphology import skeletonize
import cv2
import numpy as np
# Step #1 - Skeletonize
im = cv2.imread('K7ELI.png', 0)
out = skeletonize(im > 0)
# Convert to uint8
out = 255*(out.astype(np.uint8))
# Step #2 - Hough Transform
lines = cv2.HoughLines(out,1,np.pi/180,60)
# Step #3 - Find points of intersection
pts = []
for i in range(lines.shape[0]):
(rho1, theta1) = lines[i,0]
m1 = -1/np.tan(theta1)
c1 = rho1 / np.sin(theta1)
for j in range(i+1,lines.shape[0]):
(rho2, theta2) = lines[j,0]
m2 = -1 / np.tan(theta2)
c2 = rho2 / np.sin(theta2)
if np.abs(m1 - m2) <= 1e-8:
continue
x = (c2 - c1) / (m1 - m2)
y = m1*x + c1
if 0 <= x < img.shape[1] and 0 <= y < img.shape[0]:
pts.append((int(x), int(y)))
# Step #4 - Find convex hull
pts = np.array(pts)
pts = pts[:,None] # We need to convert to a 3D numpy array with a singleton 2nd dimension
hull = cv2.convexHull(pts)
# Step #5 - K-Means clustering
# Define criteria = ( type, max_iter = 10 , epsilon = 1.0 )
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
# Set flags (Just to avoid line break in the code)
flags = cv2.KMEANS_RANDOM_CENTERS
# Apply KMeans
# The convex hull points need to be float32
z = hull.copy().astype(np.float32)
compactness,labels,centers = cv2.kmeans(z,4,None,criteria,10,flags)
# Step #6 - Find the lengths of each side
centers = cv2.convexHull(centers)[:,0]
for (i, j) in zip(range(4), [1, 2, 3, 0]):
length = np.sqrt(np.sum((centers[i] - centers[j])**2.0))
print('Length of side {}: {}'.format(i+1, length))
# Draw the sides of each rectangle in the original image
out5 = cv2.imread('no8BP.png') # Note - grayscale image read in as colour
for (i, j) in zip(range(4), [1, 2, 3, 0]):
cv2.line(out5, tuple(centers[i]), tuple(centers[j]), (0, 0, 255), 2)
# Show the image
cv2.imshow('Output', out5); cv2.waitKey(0); cv2.destroyAllWindows()