【问题标题】:Extracting the dimensions of a rectangle提取矩形的尺寸
【发布时间】:2019-05-27 19:45:27
【问题描述】:

所以我正在处理一个带有声纳图像的图像处理项目。更具体地说,我正在尝试提取声纳扫描仪拍摄的水池图像的尺寸。我能够提取池的矩形区域,但我无法弄清楚如何以像素为单位获取每个边缘的尺寸。我正在 Python 中使用 OpenCV。

如果有人能就如何做到这一点给我任何建议,我将不胜感激。

我已经尝试找到霍夫线的交点,但结果并不理想。

到目前为止的代码。

import cv2 
import numpy as np 
from scipy import ndimage as ndi
from scipy.ndimage.measurements import label

def largest_component(indices):
    #this function takes a list of indices denoting
    #the white regions of the image and returns the largest 
    #white object of connected indices 

    return_arr = np.zeros((512,512), dtype=np.uint8)
    for index in indeces:
        return_arr[index[0]][index[1]] = 255

    return return_arr

image = cv2.imread('sonar_dataset/useful/sonarXY_5.bmp', 0)
image_gaussian = ndi.gaussian_filter(image, 4)
image_gaussian_inv = cv2.bitwise_not(image_gaussian)
kernel = np.ones((3,3),np.uint8)

# double thresholding extracting the sides of the rectangle
ret1, img1 = cv2.threshold(image_gaussian_inv, 120, 255, cv2.THRESH_BINARY)
ret2, img2 = cv2.threshold(image_gaussian_inv, 150, 255, cv2.THRESH_BINARY)

double_threshold = img1 - img2
closing = cv2.morphologyEx(double_threshold, cv2.MORPH_CLOSE, kernel1)

labeled, ncomponents = label(closing, kernel)
indices = np.indices(closing.shape).T[:,:,[1, 0]]
twos = indices[labeled == 2]
area =[np.sum(labeled==val) for val in range(ncomponents+1)]

rectangle = largest_component(twos)

cv2.imshow('rectangle', rectangle)
cv2.waitKey(0)

原始图像和提取的对象如下。

【问题讨论】:

  • 您能否提供您编写的生成上述图像的代码?它会给我们一个跳跃点。
  • 当然可以!很抱歉没有早点提供。起初,我只是想听听一些意见,但当然,代码本身可以提供更多帮助!请在编辑后的问题中找到它。
  • 完全不用担心。阅读代码向我展示了您尝试过的内容,以便在我写答案时有所帮助。我有几个想法。我会回复你的。
  • 请放回所有import 语句,以便其他人可以运行您的代码,而不必猜测ndi.gaussian_filter 可能是什么。谢谢。
  • 如果可以填充外轮廓,可以尝试裁剪掉凸面缺陷:stackoverflow.com/questions/35226993/… 后跟 minAreaRect 得到一个矩形。

标签: python opencv image-processing


【解决方案1】:

所以这就是我想出的 - 这有点费力,但它最终确实让我们得到了正确的答案。我将直接使用您在上一张图片中显示的连接组件输出。

  1. 使用morphological image skeletonization 来获取blob 的骨架。这样,它将为我们提供最小的轮廓表示,以便我们获得一个像素宽的边界,该边界穿过每个厚边缘的中间。你可以通过 Scikit-image 的skeletonize 方法来实现。

  2. 使用Hough Transform,这是一种在骨架化图像上的线检测方法。总之,它参数化了极域中的线,输出将是一组rho 和theta,告诉我们在骨架化图像中检测到哪些线。为此,我们可以使用 OpenCV 的 cv2.HoughLines。在骨架化图像上执行此操作非常重要,否则我们将有很多候选线平行于边界框的真实轮廓,您将无法区分它们。

    李>
  3. 选取每对线并找到它们的交点。我们预计,对于所有线对,将有 4 个主要的交点集群,为我们提供每个矩形的角。

  4. 由于轮廓中的噪声,我们可能会得到四个以上的交点。我们可以使用convex hull 最终得到矩形的 4 个交点。总之,凸包算法在一个点列表上运行,它定义了一个点子集,该子集可以最小化地包含点列表。我们可以使用cv2.convexHull。

  5. 最后,由于霍夫变换的量化,每个角的附近可能有多个点。因此,应用K-Means clustering 找到 4 个点簇,从而找到它们的质心。我们可以使用cv2.kmeans。

  6. 找到质心后,我们可以简单地循环遍历每对点,最终找到到每个角的距离,从而找到您关心的距离。

让我们逐点逐一介绍:

第 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()

【讨论】:

  • 这太完美了!感谢您花时间在这个非常详细的答案上!仅通过您的方法,我就学到了很多东西。在我的其他项目中使用它会很有趣。再次,非常感谢您!
  • @rockmyboat 不客气!对不起,我花了这么长时间。需要进行一些实验才能得到答案。这实际上很有趣!
  • 您对答案的反复修改也说服了我解决这个问题。您在这里有一个很好且强大的解决方案。我的有点短,但可能没有那么健壮。
  • @CrisLuengo 哈哈!有了这样的问题和答案,我总能找到对语言的改进,或者我没有正确解释的算法。我的最佳答案与编辑数量之间也存在相关性。我在 MATLAB 中写的关于互信息的那篇有大约 20 处编辑!顺便说一句,你的快速解决方案。当我检查边的长度以及它们与我的相似之处时,它让我更加确信它或多或少是正确的。
  • 我认为您不断改进的答案是您获得乐趣并可能自己学习的答案。我确信这会转化为更有趣的帖子,人们更有可能投票。另外,你正在撞帖...... :)
【解决方案2】:

它并不完美,但这种简单的方法应该是你的一个很好的起点:

import cv2, math
import numpy as np

img = cv2.imread(R'D:\dev\projects\stackoverflow\dimensions_of_rectangle\img1.png')
print(img.shape)
img_moments=cv2.moments(img[:,:,0]) #use only one channel here (cv2.moments operates only on single channels images)
print(img_moments)
# print(dir(img_moments))

# calculate centroid (center of mass of image)
x = img_moments['m10'] / img_moments['m00']
y = img_moments['m01'] / img_moments['m00']

# calculate orientation of image intensity (it corresponds to the image intensity axis)
u00 = img_moments['m00']
u20 = img_moments['m20'] - x*img_moments['m10']
u02 = img_moments['m02'] - y*img_moments['m01']
u11 = img_moments['m11'] - x*img_moments['m01']

u20_prim = u20/u00
u02_prim = u02/u00
u11_prim = u11/u00

angle = 0.5 * math.atan(2*u11_prim / (u20_prim - u02_prim))
print('The image should be rotated by: ', math.degrees(angle) / 2.0, ' degrees')

cols,rows = img.shape[:2]
# rotate the image by half of this angle
rotation_matrix = cv2.getRotationMatrix2D((cols/2,rows/2), math.degrees(angle / 2.0), 1)
img_rotated = cv2.warpAffine(img, rotation_matrix ,(cols,rows))
# print(img_rotated.shape, img_rotated.dtype)

cv2.imwrite(R'D:\dev\projects\stackoverflow\dimensions_of_rectangle\img1_rotated.png', img_rotated)

img_rotated_clone = np.copy(img_rotated)
img_rotated_clone2 = np.copy(img_rotated)

# first method - just calculate bounding rect
bounding_rect = cv2.boundingRect(img_rotated[:, :, 0])
cv2.rectangle(img_rotated_clone, (bounding_rect[0], bounding_rect[1]), 
    (bounding_rect[0] + bounding_rect[2], bounding_rect[1] + bounding_rect[3]), (255,0,0), 2)

# second method - find columns and rows with biggest sums
def nlargest_cols(a, n):
    col_sums = [(np.sum(col), idx) for idx, col in enumerate(a.T)]
    return sorted(col_sums, key=lambda a: a[0])[-n:]

def nlargest_rows(a, n):
    col_sums = [(np.sum(col), idx) for idx, col in enumerate(a[:,])]
    return sorted(col_sums, key=lambda a: a[0])[-n:]

top15_cols_indices = nlargest_cols(img_rotated[:,:,0], 15)
top15_rows_indices = nlargest_rows(img_rotated[:,:,0], 15)

for a in top15_cols_indices:
    cv2.line(img_rotated_clone, (a[1], 0), (a[1], rows), (0, 255, 0), 1)

for a in top15_rows_indices:
    cv2.line(img_rotated_clone, (0, a[1]), (cols, a[1]), (0, 0, 255), 1)

cv2.imwrite(R'D:\dev\projects\stackoverflow\dimensions_of_rectangle\img2.png', img_rotated_clone)

当然你需要调整路径。 img1.png 是您问题中的第二张图片, img1_rotated 是旋转图片的结果:

而img2是最终输出:

蓝色矩形是方法 1(只是一个边界矩形),绿线和红线(15 条红线和 15 条绿线 - 都是 1 像素宽)是第二种方法。

算法很简单:

  1. 计算图像矩以确定图像强度的主轴(我不知道如何描述它-查看维基页面https://en.wikipedia.org/wiki/Image_moment#Examples_2)。基本上,这是您必须旋转图像以使白色像素水平或垂直分布的角度。
  2. 知道角度后 - 旋转图像(并保存结果)。
  3. 方法 1 - 计算并绘制所有像素的旋转矩形。
  4. 方法 2 - 找到总和最大的 15 行和 15 列(== 白色像素的最大计数)并在这些行/列中绘制水平/垂直线。请注意,数字 15 是通过反复试验选择的,但应该很容易选择 2 列(和行)的总和并不接近彼此。这些列/行很适合作为矩形边界。

希望这是您要找的,如果您有任何问题,请告诉我。

【讨论】:

  • 优秀的解决方案。如果您假设矩形围绕图像的中间,您可以简化最后一步。如果是这样,您可以在图像中间画一条水平线,找到它与矩形相交的两个点,然后用垂直线重复。
  • 这是一个很好的解决方案,但它缺少需要测量每边长度的最后一步。第二种方法更准确。把它放到第二种方法中,你就有我的投票权。
【解决方案3】:

已经有两个很好的解决方案,我想提出一个更简单的基于检测矩形的不同方法。

(我在这里使用带有 DIPimage 的 MATLAB,因为这对我来说比 Python 更快地进行概念验证,但是 Python 中提供了相同的确切功能,请参阅文章末尾。免责声明:我是 DIPimage 的作者。)

由于矩形是较暗背景上的明亮形状,并且(我假设)保证围绕图像中心,我们可以在图像中心生成种子,在外围生成种子,并使用分水岭找到矩形。在这种情况下,分水岭可以保证生成单个 1 像素厚的闭合轮廓。

img = readim('https://i.stack.imgur.com/no8BP.png');
seeds = clone(img);
seeds(rr(seeds)<50) = 1;
seeds(rr(seeds)>250) = 2;
rect = waterseed(seeds,gaussf(img));
overlay(img,rect) % for display only

请注意,我稍微平滑了输入图像。但是矩形还是比较吵的,会影响我们后面的尺寸测量。我们可以使用带有大圆形结构元素的形态开口来平滑它。此操作也会切掉边角,但圆角不会影响测量结果。

rect = opening(fillholes(rect),35);
overlay(img,rect-berosion(rect)) % for display only

现在我们有了一个适合测量的漂亮形状。 Feret 直径是形状投影的长度。我们测量最短投影的长度(等于矩形的宽度)和垂直于最短投影的长度(等于矩形的长度)。有关计算这些长度的算法的详细说明,请参阅 this blog post of mine。

msr = measure(rect,[],'feret');
sz = msr(1).feret(2:3)

这将返回sz = [162.7506, 215.0775]。


这是上面代码的 Python 等价物(运行完全相同的算法实现)。 PyDIP 是 DIPlib 库的 Python 绑定,不如我在上面使用的 DIPimage 工具箱成熟,并且一些语法有点冗长(尽管主要是故意的)。一位同事正在打包 PyDIP 的二进制发行版,在此之前您必须构建它 from sources,如果您关注 the instructions,希望这相当简单。

import PyDIP as dip

img = dip.ImageRead('no8BP.png')

seeds = img.Similar()
seeds.Fill(0)
rr = dip.CreateRadiusCoordinate(seeds.Sizes())
seeds[rr<50] = 1
seeds[rr>250] = 2

rect = dip.SeededWatershed(dip.Gauss(img), seeds)
dip.viewer.Show(dip.Overlay(img,rect))
dip.viewer.Spin()

rect = dip.Opening(dip.FillHoles(rect),35)
dip.viewer.Show(dip.Overlay(img,rect-dip.BinaryErosion(rect,1,1)))
dip.viewer.Spin()

msr = dip.MeasurementTool.Measure(dip.Label(rect),features=['Feret'])
sz = (msr[1]['Feret'][1],msr[1]['Feret'][2])
print(sz)

您也可以在 OpenCV 中实现这一点,但它可能涉及更多。例如,我们这里计算的两个 Feret 度量等价于 OpenCV 的 minAreaRect 返回的结果,种子分水岭包含在 OpenCV 的 watershed 中。

【讨论】:

  • 也是一个很好的解决方案!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-09
  • 2023-03-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多