将圆圈拟合到二值图像

fitting a circle to a binary image

我一直在使用 skim age 的阈值算法来获得一些二进制掩码。例如,我获得这样的二进制图像:

我想弄清楚的是如何将一个圆圈拟合到这个二进制掩码上。约束条件是圆圈应尽可能多地覆盖白色区域,并且圆圈的整个圆周应完全位于白色部分。

我一直在绞尽脑汁思考如何才能有效地做到这一点,但没有想出有效的解决方案。

我认为可能有用的一种方法是:

这其实是图像处理中解决最多的问题。看起来你想要的是 Hough Transform,特别是圆形或椭圆形的那种。我相信圆形的一般来说计算量要小一些。

Here are some code examples for scikit-image that show pretty much exactly what you're trying to do. And here is a link to the documentation.

更新答案

实际上,如果您使用 连通分量分析、a.k.a Blob 分析,您可以使用 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

原答案

正如你好奇的那样......你可以用两行来完成我对 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 中对 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)

当您的形状不是球形并且沿轴的折叠可以有多个最大值时,此代码会很小心。那样的话,就取中间的那个(当你拟合圆的时候能给你最大半径的那个)。

这是一个尝试通过最小化来实现最佳圆拟合的解决方案。很快就会发现气泡不是圆形 :) 请注意使用“regionprops”来轻松确定区域的面积、质心等。

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

这通常会给出非常好的和可靠的结果:

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