Luca
Luca

Reputation: 10996

fitting a circle to a binary image

I have been using skim age's thresholding algorithms to get some binary mask. For example, I obtain binary images like this:

Binary image obtained as a result of Otsu thresholding

What I am trying to figure out is how can I fit a circle to this binary mask. The constraint is the circle should cover as much of the white areas as possible and the whole circumference of the circle should lie entirely on the white parts.

I have been wrecking my head on how I can do this efficiently but have come up with no solution that works.

One approach I thought that might be something is:

Upvotes: 6

Views: 10978

Answers (5)

user2970139
user2970139

Reputation: 559

This should in general give very good and robust results:

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

Upvotes: 6

Stefan van der Walt
Stefan van der Walt

Reputation: 7253

Here is a solution that tries to make an optimal circle fit via minimization. It soon becomes apparent that the bubble isn't a circle :) Note the use of "regionprops" for easily determining area, centroid, etc. of regions.

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

Upvotes: 8

Mali Akmanalp
Mali Akmanalp

Reputation: 141

This is actually a mostly solved problem in image processing. Looks like what you want is a Hough Transform, specifically the circular or elliptical kind. I believe the circular one is a bit less computationally intensive in general.

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.

Upvotes: 3

Luca
Luca

Reputation: 10996

For someone looking to code Mark's suggestion in python, it is quite easy.

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)

This code takes care when your shape is not spherical and the collapsing along an axes can have multiple maxima. In that case, it takes the middle one (the one that can give you the largest radius when you fit the circle).

Upvotes: 0

Mark Setchell
Mark Setchell

Reputation: 207465

Updated Answer

Actually, if you use Connected Components Analysis, a.k.a Blob Analysis, you can do it with ImageMagick much more succinctly and accurately like this:

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

Output:

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

which shows your largest blob (the speech bubble) has its centroid at coordinates 347,250 from the top-left corner, and also gives you the bounding box which measures 488x513 pixels and its top-left corner is at 104,0 from which you can derive a radius.

I can mark these with ImageMagick like this:

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

Original Answer

As you are curious... you can do what I was suggesting with ImageMagick in two lines:

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)

And the other side

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)

Upvotes: 1

Related Questions