拟合圆形到二值图像中

6
我一直在使用Skimage的阈值算法来得到一些二值掩模,例如,我获得了像这样的二进制图像:Binary image obtained as a result of Otsu thresholding 我想知道的是如何将一个圆拟合到这个二值掩模中。限制条件是圆应该尽可能地覆盖白色区域,并且整个圆周应完全位于白色部分。
我一直在努力想出如何高效地完成这个任务,但一直没有找到可行的解决方案。
我的一个想法是:
1. 找到图像/圆的某些最优中心(我还不确定如何实现这一点,可能需要一些栅格扫描等方法)。
2. 为不断增加的半径计算圆形,并找出它何时开始移出白色区域或超出图像。
3. 然后圆心和半径将描述圆。

2
一个非常简单的方法是将图像调整为单列像素(即宽度=1),然后查找该列中最亮的像素,这将告诉您哪一行具有最多的白色像素。然后将原始图像调整为单行像素(即高度=1),并查找该行中最亮的像素,这将告诉您哪一列具有最多的白色像素。然后您就可以得到圆的中心,因为它是两个直径相交的地方。 - Mark Setchell
我会尝试一下,只是出于好奇心 :-) - Luca
5个回答

8
这里有一个解决方案,尝试通过最小化来实现最佳圆形拟合。 很快就会发现气泡不是一个圆 :) 注意使用"regionprops"轻松确定区域的面积、质心等。 Circle fit to bubble
from skimage import io, color, measure, draw, img_as_bool
import numpy as np
from scipy import optimize
import matplotlib.pyplot as plt


image = img_as_bool(io.imread('bubble.jpg')[..., 0])
regions = measure.regionprops(measure.label(image))
bubble = regions[0]

y0, x0 = bubble.centroid
r = bubble.major_axis_length / 2.

def cost(params):
    x0, y0, r = params
    coords = draw.disk((y0, x0), r, shape=image.shape)
    template = np.zeros_like(image)
    template[coords] = 1
    return -np.sum(template == image)

x0, y0, r = optimize.fmin(cost, (x0, y0, r))

import matplotlib.pyplot as plt

f, ax = plt.subplots()
circle = plt.Circle((x0, y0), r)
ax.imshow(image, cmap='gray', interpolation='nearest')
ax.add_artist(circle)
plt.show()

这太棒了。我猜这是正确的图像处理方式!我一整天都在试错... - Luca
我稍微修改了它以适应椭圆。结果证明这是最符合我的需求的。感谢您提供这个方法。这个 region props 方法非常有用。 - Luca
@Luca 看看我的回答。我认为它应该更加健壮,并且给出更准确的结果。 - user2970139
我给你的解决方案点了赞@user2970139——我认为这是一个非常优雅的方法。 - Stefan van der Walt
1
@StefanvanderWalt 我们可能应该将这个例子添加到圆拟合的示例页面中...(顺便说一下,我是 Johannes) - user2970139
优雅地尝试着弄清楚如何将矩形适配到像这样的二进制图像中。 - Irtaza

6

通常情况下,这应该会产生非常好且可靠的结果:

import numpy as np
from skimage import measure, feature, io, color, draw
import matplotlib.pyplot as plt

img = color.rgb2gray(io.imread("circle.jpg"))
img = feature.canny(img).astype(np.uint8)
img[img > 0] = 255

coords = np.column_stack(np.nonzero(img))

model, inliers = measure.ransac(coords, measure.CircleModel,
                                min_samples=3, residual_threshold=1,
                                max_trials=500)

print(model.params)

rr, cc = draw.disk((model.params[0], model.params[1]), model.params[2],
                   shape=img.shape)

img = img * 0.5
img[rr, cc] += 128

plt.imshow(img)
plt.show()

circle fit to speech bubble


这也是一个有趣的方法。我也要试一试,但可能只能明天才行。我猜这些特征只会在圆周上被检测到? - Luca
这在处理这张图片时效果很好,但当你有更多椭圆形的图片时,似乎会出现问题。 - Luca
3
请使用适当的模型来处理椭圆形图片。具体可参考 skimage.measure.EllipseModel - user2970139
1
这个能推广到拟合矩形吗? - Irtaza

3
这实际上是图像处理中一个大部分已经解决的问题。看起来你想要的是霍夫变换,特别是圆形或椭圆形方面。我认为在一般情况下,圆形变换的计算量较小。 这里有一些scikit-image的代码示例,几乎完全展示了你想要做的事情。此外,这里还有文档链接

4
这里提供了一种基于脊线边缘和霍夫变换的更快速的环形检测器:https://github.com/eldad-a/ridge-directed-ring-detector - Stefan van der Walt
2
更新了 scikit-image Hough 变换示例 的链接。 - lanery

1

更新的答复

实际上,如果您使用连通组件分析,也称为斑点分析,您可以更简洁、更准确地使用ImageMagick完成此操作:

convert 3J3qz.jpg                                  \
   -define connected-components:verbose=true       \
   -define connected-components:area-threshold=100 \
   -connected-components 8 null:

输出:

Objects (id: bounding-box centroid area mean-color):
  0: 720x576+0+0 370.6,322.1 213779 srgb(0,0,0)
  13: 488x513+104+0 347.7,250.7 200941 srgb(255,255,255)   <-- answer

这段文字描述了你的最大斑点(即漫画气泡)的质心位于左上角坐标为347,250处,并给出了其边界框尺寸为488x513像素,左上角坐标为104,0,可以由此推导出半径。

我可以使用ImageMagick这样标记它们:

convert 3J3qz.jpg \
   -fill red -draw "rectangle 342,245 352,255" 
   -stroke red -fill none -draw "rectangle 104,0 592,513" 
   out.png

enter image description here

原始答案

如果你很好奇... 你可以用 ImageMagick 在两行代码中实现我建议的功能:

convert 3J3qz.jpg -resize 1x! -colorspace gray txt:

# ImageMagick pixel enumeration: 1,576,255,gray
0,0: (66,66,66)  #424242  gray(66)
0,1: (70,70,70)  #464646  gray(70)
0,2: (72,72,72)  #484848  gray(72)
0,3: (76,76,76)  #4C4C4C  gray(76)
...
0,152: (176,176,176)  #B0B0B0  gray(176)
0,153: (176,176,176)  #B0B0B0  gray(176)
0,154: (177,177,177)  #B1B1B1  gray(177)
0,155: (177,177,177)  #B1B1B1  gray(177)
0,156: (177,177,177)  #B1B1B1  gray(177)
0,157: (177,177,177)  #B1B1B1  gray(177)
0,158: (178,178,178)  #B2B2B2  gray(178)
0,159: (178,178,178)  #B2B2B2  gray(178)
0,160: (179,179,179)  #B3B3B3  gray(179)
0,161: (179,179,179)  #B3B3B3  gray(179)
0,162: (179,179,179)  #B3B3B3  gray(179)
0,163: (179,179,179)  #B3B3B3  gray(179)
0,164: (179,179,179)  #B3B3B3  gray(179)
0,165: (179,179,179)  #B3B3B3  gray(179)
0,166: (179,179,179)  #B3B3B3  gray(179)
0,167: (179,179,179)  #B3B3B3  gray(179)
0,168: (180,180,180)  #B4B4B4  gray(180)
0,169: (180,180,180)  #B4B4B4  gray(180)
0,170: (180,180,180)  #B4B4B4  gray(180)
0,171: (180,180,180)  #B4B4B4  gray(180)
0,172: (180,180,180)  #B4B4B4  gray(180)
0,173: (180,180,180)  #B4B4B4  gray(180)
0,174: (180,180,180)  #B4B4B4  gray(180)
0,175: (180,180,180)  #B4B4B4  gray(180)
0,176: (181,181,181)  #B5B5B5  gray(181)
0,177: (181,181,181)  #B5B5B5  gray(181)
0,178: (182,182,182)  #B6B6B6  gray(182)
0,179: (182,182,182)  #B6B6B6  gray(182)
0,180: (182,182,182)  #B6B6B6  gray(182)
0,181: (182,182,182)  #B6B6B6  gray(182)
0,182: (182,182,182)  #B6B6B6  gray(182)
0,183: (182,182,182)  #B6B6B6  gray(182)
0,184: (183,183,183)  #B7B7B7  gray(183)
0,185: (183,183,183)  #B7B7B7  gray(183)
0,186: (183,183,183)  #B7B7B7  gray(183)
0,187: (183,183,183)  #B7B7B7  gray(183)
0,188: (183,183,183)  #B7B7B7  gray(183)
0,189: (183,183,183)  #B7B7B7  gray(183)
0,190: (183,183,183)  #B7B7B7  gray(183)
0,191: (183,183,183)  #B7B7B7  gray(183)
0,192: (184,184,184)  #B8B8B8  gray(184)
0,193: (184,184,184)  #B8B8B8  gray(184)
0,194: (184,184,184)  #B8B8B8  gray(184)
0,195: (184,184,184)  #B8B8B8  gray(184)
0,196: (184,184,184)  #B8B8B8  gray(184)
0,197: (184,184,184)  #B8B8B8  gray(184)
0,198: (184,184,184)  #B8B8B8  gray(184)
0,199: (184,184,184)  #B8B8B8  gray(184)
0,200: (185,185,185)  #B9B9B9  gray(185)
0,201: (185,185,185)  #B9B9B9  gray(185)
0,202: (185,185,185)  #B9B9B9  gray(185)
0,203: (185,185,185)  #B9B9B9  gray(185)
0,204: (185,185,185)  #B9B9B9  gray(185)
0,205: (185,185,185)  #B9B9B9  gray(185)
0,206: (185,185,185)  #B9B9B9  gray(185)
0,207: (185,185,185)  #B9B9B9  gray(185)
0,208: (186,186,186)  #BABABA  gray(186)
0,209: (186,186,186)  #BABABA  gray(186)
0,210: (186,186,186)  #BABABA  gray(186)
0,211: (186,186,186)  #BABABA  gray(186)
0,212: (185,185,185)  #B9B9B9  gray(185)
0,213: (186,186,186)  #BABABA  gray(186)
0,214: (186,186,186)  #BABABA  gray(186)
0,215: (186,186,186)  #BABABA  gray(186)
0,216: (186,186,186)  #BABABA  gray(186)
0,217: (186,186,186)  #BABABA  gray(186)
0,218: (186,186,186)  #BABABA  gray(186)
0,219: (186,186,186)  #BABABA  gray(186)
0,220: (186,186,186)  #BABABA  gray(186)
0,221: (186,186,186)  #BABABA  gray(186)
0,222: (186,186,186)  #BABABA  gray(186)
0,223: (186,186,186)  #BABABA  gray(186)
0,224: (186,186,186)  #BABABA  gray(186)
0,225: (186,186,186)  #BABABA  gray(186)
0,226: (186,186,186)  #BABABA  gray(186)
0,227: (186,186,186)  #BABABA  gray(186)
0,228: (187,187,187)  #BBBBBB  gray(187)
0,229: (187,187,187)  #BBBBBB  gray(187)
0,230: (187,187,187)  #BBBBBB  gray(187)
0,231: (187,187,187)  #BBBBBB  gray(187)
0,232: (187,187,187)  #BBBBBB  gray(187)
0,233: (187,187,187)  #BBBBBB  gray(187)
0,234: (187,187,187)  #BBBBBB  gray(187) <---- max=234
0,235: (187,187,187)  #BBBBBB  gray(187)
0,236: (187,187,187)  #BBBBBB  gray(187)
0,237: (187,187,187)  #BBBBBB  gray(187)
0,238: (187,187,187)  #BBBBBB  gray(187)
0,239: (187,187,187)  #BBBBBB  gray(187)
0,240: (187,187,187)  #BBBBBB  gray(187)
0,241: (187,187,187)  #BBBBBB  gray(187)
0,242: (187,187,187)  #BBBBBB  gray(187)
0,243: (187,187,187)  #BBBBBB  gray(187)
0,244: (187,187,187)  #BBBBBB  gray(187)
0,245: (187,187,187)  #BBBBBB  gray(187)
0,246: (187,187,187)  #BBBBBB  gray(187)
0,247: (187,187,187)  #BBBBBB  gray(187)
0,248: (187,187,187)  #BBBBBB  gray(187)
0,249: (187,187,187)  #BBBBBB  gray(187)
0,250: (187,187,187)  #BBBBBB  gray(187)
...
0,573: (0,0,0)  #000000  gray(0)
0,574: (0,0,0)  #000000  gray(0)
0,575: (0,0,0)  #000000  gray(0)

另一方面

convert 3J3qz.jpg -resize x1! -colorspace gray txt: 

# ImageMagick pixel enumeration: 720,1,255,gray
0,0: (0,0,0)  #000000  gray(0)
1,0: (0,0,0)  #000000  gray(0)
2,0: (0,0,0)  #000000  gray(0)
3,0: (0,0,0)  #000000  gray(0)
4,0: (0,0,0)  #000000  gray(0)
...
241,0: (219,219,219)  #DBDBDB  gray(219)
242,0: (220,220,220)  #DCDCDC  gray(220)
243,0: (220,220,220)  #DCDCDC  gray(220)
244,0: (221,221,221)  #DDDDDD  gray(221)
245,0: (222,222,222)  #DEDEDE  gray(222)
246,0: (223,223,223)  #DFDFDF  gray(223)
247,0: (223,223,223)  #DFDFDF  gray(223)
248,0: (224,224,224)  #E0E0E0  gray(224)
249,0: (224,224,224)  #E0E0E0  gray(224)
250,0: (225,225,225)  #E1E1E1  gray(225)
251,0: (227,227,227)  #E3E3E3  gray(227)
252,0: (229,229,229)  #E5E5E5  gray(229)
253,0: (230,230,230)  #E6E6E6  gray(230)
254,0: (231,231,231)  #E7E7E7  gray(231)
255,0: (232,232,232)  #E8E8E8  gray(232)  <--- max=255
256,0: (231,231,231)  #E7E7E7  gray(231)
257,0: (231,231,231)  #E7E7E7  gray(231)
258,0: (231,231,231)  #E7E7E7  gray(231)
259,0: (231,231,231)  #E7E7E7  gray(231)
260,0: (230,230,230)  #E6E6E6  gray(230)
261,0: (230,230,230)  #E6E6E6  gray(230)
262,0: (230,230,230)  #E6E6E6  gray(230)
263,0: (230,230,230)  #E6E6E6  gray(230)
264,0: (230,230,230)  #E6E6E6  gray(230)
265,0: (230,230,230)  #E6E6E6  gray(230)
266,0: (230,230,230)  #E6E6E6  gray(230)
267,0: (230,230,230)  #E6E6E6  gray(230)
268,0: (229,229,229)  #E5E5E5  gray(229)
269,0: (230,230,230)  #E6E6E6  gray(230)
270,0: (229,229,229)  #E5E5E5  gray(229)
271,0: (229,229,229)  #E5E5E5  gray(229)
272,0: (229,229,229)  #E5E5E5  gray(229)
273,0: (229,229,229)  #E5E5E5  gray(229)
274,0: (229,229,229)  #E5E5E5  gray(229)
275,0: (229,229,229)  #E5E5E5  gray(229)
276,0: (229,229,229)  #E5E5E5  gray(229)
277,0: (229,229,229)  #E5E5E5  gray(229)
278,0: (229,229,229)  #E5E5E5  gray(229)
279,0: (229,229,229)  #E5E5E5  gray(229)
280,0: (229,229,229)  #E5E5E5  gray(229)
281,0: (229,229,229)  #E5E5E5  gray(229)
282,0: (229,229,229)  #E5E5E5  gray(229)
283,0: (229,229,229)  #E5E5E5  gray(229)
284,0: (229,229,229)  #E5E5E5  gray(229)
285,0: (229,229,229)  #E5E5E5  gray(229)
286,0: (229,229,229)  #E5E5E5  gray(229)
287,0: (230,230,230)  #E6E6E6  gray(230)
288,0: (230,230,230)  #E6E6E6  gray(230)
289,0: (230,230,230)  #E6E6E6  gray(230)
290,0: (230,230,230)  #E6E6E6  gray(230)
291,0: (230,230,230)  #E6E6E6  gray(230)
292,0: (230,230,230)  #E6E6E6  gray(230)
293,0: (230,230,230)  #E6E6E6  gray(230)
294,0: (230,230,230)  #E6E6E6  gray(230)
295,0: (231,231,231)  #E7E7E7  gray(231)
296,0: (231,231,231)  #E7E7E7  gray(231)
297,0: (231,231,231)  #E7E7E7  gray(231)
298,0: (231,231,231)  #E7E7E7  gray(231)
299,0: (231,231,231)  #E7E7E7  gray(231)
300,0: (231,231,231)  #E7E7E7  gray(231)
301,0: (231,231,231)  #E7E7E7  gray(231)
302,0: (231,231,231)  #E7E7E7  gray(231)
303,0: (231,231,231)  #E7E7E7  gray(231)
304,0: (232,232,232)  #E8E8E8  gray(232)
305,0: (231,231,231)  #E7E7E7  gray(231)
306,0: (231,231,231)  #E7E7E7  gray(231)
307,0: (231,231,231)  #E7E7E7  gray(231)
308,0: (231,231,231)  #E7E7E7  gray(231)
309,0: (232,232,232)  #E8E8E8  gray(232)
310,0: (232,232,232)  #E8E8E8  gray(232)
311,0: (232,232,232)  #E8E8E8  gray(232)
312,0: (233,233,233)  #E9E9E9  gray(233)
313,0: (232,232,232)  #E8E8E8  gray(232)
314,0: (232,232,232)  #E8E8E8  gray(232)
315,0: (232,232,232)  #E8E8E8  gray(232)
316,0: (232,232,232)  #E8E8E8  gray(232)
317,0: (232,232,232)  #E8E8E8  gray(232)
318,0: (232,232,232)  #E8E8E8  gray(232)
319,0: (232,232,232)  #E8E8E8  gray(232)
320,0: (232,232,232)  #E8E8E8  gray(232)
321,0: (233,233,233)  #E9E9E9  gray(233)
322,0: (233,233,233)  #E9E9E9  gray(233)
323,0: (233,233,233)  #E9E9E9  gray(233)
324,0: (233,233,233)  #E9E9E9  gray(233)
325,0: (233,233,233)  #E9E9E9  gray(233)
326,0: (233,233,233)  #E9E9E9  gray(233)
327,0: (233,233,233)  #E9E9E9  gray(233)
328,0: (233,233,233)  #E9E9E9  gray(233)
329,0: (233,233,233)  #E9E9E9  gray(233)
330,0: (233,233,233)  #E9E9E9  gray(233)
331,0: (233,233,233)  #E9E9E9  gray(233)
332,0: (233,233,233)  #E9E9E9  gray(233)
333,0: (233,233,233)  #E9E9E9  gray(233)
334,0: (233,233,233)  #E9E9E9  gray(233)
335,0: (233,233,233)  #E9E9E9  gray(233)
336,0: (233,233,233)  #E9E9E9  gray(233)
337,0: (233,233,233)  #E9E9E9  gray(233)
338,0: (233,233,233)  #E9E9E9  gray(233)
339,0: (233,233,233)  #E9E9E9  gray(233)
340,0: (233,233,233)  #E9E9E9  gray(233)
341,0: (233,233,233)  #E9E9E9  gray(233)
342,0: (233,233,233)  #E9E9E9  gray(233)
343,0: (233,233,233)  #E9E9E9  gray(233)
344,0: (233,233,233)  #E9E9E9  gray(233)
345,0: (233,233,233)  #E9E9E9  gray(233)
346,0: (233,233,233)  #E9E9E9  gray(233)
347,0: (233,233,233)  #E9E9E9  gray(233)
348,0: (233,233,233)  #E9E9E9  gray(233)
349,0: (233,233,233)  #E9E9E9  gray(233)
350,0: (233,233,233)  #E9E9E9  gray(233)
351,0: (233,233,233)  #E9E9E9  gray(233)
352,0: (233,233,233)  #E9E9E9  gray(233)
353,0: (233,233,233)  #E9E9E9  gray(233)
354,0: (233,233,233)  #E9E9E9  gray(233)
...
717,0: (0,0,0)  #000000  gray(0)
718,0: (0,0,0)  #000000  gray(0)
719,0: (0,0,0)  #000000  gray(0)

谢谢你的回答。我现在正在尝试用Python编写代码,让我们看看进展如何。 - Luca
1
该方法对示例图像中显示的裁剪不具有鲁棒性。 - Stefan van der Walt
1
@StefanvanderWalt 是的,你是对的 - 我完全误解了问题 - 但我会保留这个答案,因为它可能对其他试图解决我认为自己正在解决的问题的人有用 :-) - Mark Setchell
我展示的实现方法会处理裁剪,但是确实很不专业。 - Luca

0

对于想要用Python编写Mark的建议的人来说,这是非常容易的。

collapsed = np.sum(binary_array, axis=0)
# These indices will be already sorted
indices = np.where(collapsed == collapsed.max())[0]
c = indices[int(round((len(indices) - 1) / 2))]

# Same for rows
collapsed = np.sum(binary_array, axis=1)
# These indices will be already sorted
indices = np.where(collapsed == collapsed.max())[0]
r = indices[int(round((len(indices) - 1) / 2))]

# circle center is (r, c)

当您的形状不是球形且沿轴向折叠可能具有多个最大值时,此代码会进行处理。在这种情况下,它会选择中间值(可以在适配圆时给您最大半径的那个值)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接