Filling an outlined circle

Use cv2.fillPoly() to fill the circle contour

enter image description here

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:

  1. All black
  2. Black, white, black
  3. 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:

  1. Compute the start index of each white segment
  2. Make a row mask of rows containing two start indices
  3. 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)