所以我正在处理声纳图像的图像处理项目。更具体地说,我正在尝试提取声纳扫描仪拍摄的水池图像的尺寸。我能够提取池的矩形区域,但无法弄清楚如何以像素为单位获取每个边缘的尺寸。我正在使用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)
原始图像和提取的对象在下面。
答案 0 :(得分:11)
所以这是我想出的-有点费力,但最终确实使我们得到了正确的答案。我将直接使用您在上一张图像中显示的连接的组件输出。
使用morphological image skeletonization,以便获得斑点的骨架。这样,它将为我们提供最小的轮廓表示,从而使我们得到一个像素宽的边界,该边界穿过每个粗边的中间。您可以通过Scikit-image的skeletonize
方法来实现。
使用Hough Transform,这是在骨架化图像上的线检测方法。总而言之,它在极域中对线进行参数化,并且输出将是一组rho
和theta
,它们告诉我们在骨架化图像中检测到了哪些线。我们可以为此使用OpenCV的cv2.HoughLines
。在骨架图像上执行此操作非常重要,否则我们将有许多与边界框的真实轮廓平行的候选行,并且您将无法对其进行区分。
取每对线并找到它们的交点。我们希望在所有成对的线中,将有4个主要的相交簇,这些簇使我们获得每个矩形的角。
由于轮廓中的噪声,我们可能会得到四个以上的交点。我们可以使用convex hull最终获得矩形的4个交点。总而言之,凸包算法在点列表上运行,其中定义了可以最小化包含点列表的点子集。我们可以使用cv2.convexHull
。
最后,由于霍夫变换的量化,每个角附近可能存在多个点。因此,应用K-Means clustering来找到4个点的群集,从而找到它们的质心。我们可以为此使用cv2.kmeans
。
找到质心后,我们可以简单地以循环方式遍历每对点,最终找到到每个角的距离,从而找到您关心的距离。
让我们一步一步地完成每一点:
使用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))
我们得到这张图片:
使用霍夫变换,我们可以检测到该图像中最突出的线条:
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的这段代码。它在图像中将霍夫变换检测到的线条绘制为红色。我得到以下图像:
我们可以看到,图像中有四个相交点。找到这些交叉点是我们的下一步。
在霍夫变换中,我们可以通过以下方式将从原点到直线(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
是一个元组列表,这样我们就可以添加图像内所有不超出边界的交点。
我们可以使用此元组列表并使用凸包,以便我们找到定义矩形外围的点列表。请注意,定义矩形的点的顺序是逆时针方向。此步骤无关紧要,但稍后将重要:
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)
我已经拍摄了原始图像,并以绿色绘制了拐角点。我们得到这张图片:
如上图所示,每个角都有多个点。如果我们可以将每个角上的多个点合并为一个点,那将是很好的。一种方法是对每个角上的所有点求平均值,而开箱即用的最简单方法是使用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)
最后,我们可以遍历每对线并找到相应的尺寸。请注意,由于算法的随机性,由于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()
答案 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是旋转图片的结果:
和img2是最终输出:
蓝色矩形是method1(仅是矩形边界),绿线和红线(15条红色和15条绿色-均为1像素宽)是第二种方法。
算法非常简单:
希望这是您想要的,让我知道您会有任何疑问。
答案 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
请注意,我对输入图像进行了一些平滑处理。但是矩形仍然很嘈杂,这将影响我们稍后进行的尺寸测量。我们可以使用带有大型圆形结构元素的形态学开口来平滑它。此操作也会切掉拐角,但是圆角不会影响测量结果。
rect = opening(fillholes(rect),35);
overlay(img,rect-berosion(rect)) % for display only
现在,我们拥有适合测量的漂亮形状。费雷特直径是形状的突起的长度。我们测量最短投影的长度(等于矩形的宽度)和垂直于最短投影的长度(等于矩形的长度)。有关计算这些长度的算法的详细说明,请参见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
中。