提取矩形的尺寸

时间:2019-05-27 19:45:28

标签: python opencv image-processing

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

如果有人能提出建议,我将不胜感激。

我已经尝试过找到hough线的线相交点,但这并没有令人满意的结果。

到目前为止的代码。

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)

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

Original Image

Extracted Object

3 个答案:

答案 0 :(得分:11)

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

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

  2. 使用Hough Transform,这是在骨架化图像上的线检测方法。总而言之,它在极域中对线进行参数化,并且输出将是一组rhotheta,它们告诉我们在骨架化图像中检测到了哪些线。我们可以为此使用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))

我们得到这张图片:

fig1

步骤2-使用霍夫变换

使用霍夫变换,我们可以检测到该图像中最突出的线条:

lines = cv2.HoughLines(out,1,np.pi/180,60)

在这里,我们指定搜索空间,以便我们查找bin大小为1且角度为1度或pi / 180弧度的线。总而言之,霍夫变换查看每个边缘点,并遍历从原点到每个边缘点的一系列角度theta,并根据面元大小计算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的这段代码。它在图像中将霍夫变换检测到的线条绘制为红色。我得到以下图像:

fig2

我们可以看到,图像中有四个相交点。找到这些交叉点是我们的下一步。

步骤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)

我已经拍摄了原始图像,并以绿色绘制了拐角点。我们得到这张图片:

fig3

步骤#5-应用K均值聚类

如上图所示,每个角都有多个点。如果我们可以将每个角上的多个点合并为一个点,那将是很好的。一种方法是对每个角上的所有点求平均值,而开箱即用的最简单方法是使用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,这意味着我们运行K-Means 10次,并选择错误量最小的集群配置。错误存储在算法输出的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)

fig5

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

我们得到:

fig5

要查看其与原始图像的对齐方式,我们只需重复上面的代码,然后在原始图像上绘制线条即可。我下载了原始图像的副本:

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)

fig6


为了完整起见,这是从头到尾的全部代码,没有所有调试输出-我们从读取图像到在原始图像中绘制线条,并在检测到的矩形中打印每边的长度。

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

答案 1 :(得分:5)

这并不完美,但是这种简单的方法对您来说应该是一个很好的起点:

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是旋转图片的结果:

enter image description here

和img2是最终输出:

enter image description here 蓝色矩形是method1(仅是矩形边界),绿线和红线(15条红色和15条绿色-均为1像素宽)是第二种方法。

算法非常简单:

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

希望这是您想要的,让我知道您会有任何疑问。

答案 2 :(得分:5)

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

(我在此处将MATLAB与DIPimage一起使用,因为与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

OP's image with a red rectangle drawn over the rectangle to be detected

请注意,我对输入图像进行了一些平滑处理。但是矩形仍然很嘈杂,这将影响我们稍后进行的尺寸测量。我们可以使用带有大型圆形结构元素的形态学开口来平滑它。此操作也会切掉拐角,但是圆角不会影响测量结果。

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

Same image, but the rectangle is smoother and with rounded corners

现在,我们拥有适合测量的漂亮形状。费雷特直径是形状的突起的长度。我们测量最短投影的长度(等于矩形的宽度)和垂直于最短投影的长度(等于矩形的长度)。有关计算这些长度的算法的详细说明,请参见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中。