Filling an outlined circle
Use cv2.fillPoly()
to fill the circle contour
import cv2
image = cv2.imread('1.png', 0)
thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cv2.fillPoly(image, cnts, [255,255,255])
cv2.imshow('image', image)
cv2.waitKey()
Note: The Otsu's threshold could be removed for slightly faster performance since the input image is already a binary image, you could directly find contours on the grayscale image
I tried finding the bounding box of the white outline, and getting its centre, then floodfilling with white from there outwards.
#!/usr/bin/env python3
import cv2
def findfill(image):
thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cv2.fillPoly(image, cnts, [255,255,255])
def me(image):
x,y,w,h = cv2.boundingRect(image)
cv2.floodFill(image,None,(int(x+w/2),int(y+h/2)),255)
return image
image = cv2.imread('BLYmz.png', 0)
%timeit findfill(image)
%timeit me(image)
This seems to give the same results and run 2.5x faster:
findfill
810 µs ± 2.94 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
me
343 µs ± 1.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Of course, if you have 1.5 million to do, I would recommend some parallel processing too :-)
For a truly arbitrary shape, I'd recommend flood fill. However, since you have a guaranteed convex shape, you can make some optimizations. Specifically, every row/column of the image will follow one of three patterns:
- All black
- Black, white, black
- Black, white, black, white, black
Technically there are more options since either or both of the black margins in options 2 and 3 may be missing. The goal is to fill in the middle black region in option 3. This can be done with some simple numpy masking and fancy indexing.
The basic algorithm is:
- Compute the start index of each white segment
- Make a row mask of rows containing two start indices
- Make a full mask containing the original data, with elements between the indices set to
True
as well.
def fill_convex(image):
mask = image.astype(np.bool)
# mask out elements that are 1, but the previous is 0
start = (mask[:, 1:] & ~mask[:, :-1])
# find rows that have exactly two runs of True
row_mask = (np.count_nonzero(start, axis=1) == 2)
# get the pairs of column indices that correspond to the masked elements
cols = np.nonzero(start[row_mask, :])[1].reshape(-1, 2)
# create a row of column indices the same size as a row
count = np.arange(image.shape[1])
# fill in the elements between start and stop indices for each row
# the None indices are used to trigger broadcasting
to_fill = ((count[None, :] >= cols[:, 0, None]) & (count[None, :] <= cols[:, 1, None]))
# update the mask
mask[row_mask, :] |= to_fill
# fill in the image
image[mask] = 255
return image
Timing
This method is about twice as slow as @nathancy's and more than 10 times slower than @MarkSetchell's. I'm basically leaving it here for fun at this point.
$ python -m timeit -s 'import q58174115' 'q58174115.nathancy(q58174115.image)'
500 loops, best of 5: 437 usec per loop
$ python -m timeit -s 'import q58174115' 'q58174115.MarkSetchell(q58174115.image.copy())'
5000 loops, best of 5: 62.9 usec per loop
$ python -m timeit -s 'import q58174115' 'q58174115.MadPhysicist(q58174115.image.copy())'
500 loops, best of 5: 779 usec per loop
Here, q58174115.py
is
import cv2
import numpy as np
def nathancy(image):
thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cv2.fillPoly(image, cnts, [255,255,255])
return image
def MarkSetchell(image):
x,y,w,h = cv2.boundingRect(image)
cv2.floodFill(image,None,(int(x+w/2),int(y+h/2)),255)
return image
def MadPhysicist(image):
mask = image.astype(np.bool)
# mask out elements that are 1, but the previous is 0
start = (mask[:, 1:] & ~mask[:, :-1])
# find rows that have exactly two runs of True
row_mask = (np.count_nonzero(start, axis=1) == 2)
# get the pairs of column indices that correspond to the masked elements
cols = np.nonzero(start[row_mask, :])[1].reshape(-1, 2)
# create a row of column indices the same size as a row
count = np.arange(image.shape[1])
# fill in the elements between start and stop indices for each row
# the None indices are used to trigger broadcasting
to_fill = ((count[None, :] >= cols[:, 0, None]) & (count[None, :] <= cols[:, 1, None]))
# update the mask
mask[row_mask, :] |= to_fill
# fill in the image
image[mask] = 255
return image
image = cv2.imread('58174115.png', 0)