Remove circles from an image in Python

2014-10-21

This post is a follow-up to a post by Steve on Image Processing on how to remove circles from an image by specifying their center and radius. Read Filling circles to see how to do it in Matlab. I'll show how to do it in Python with SciPy and OpenCV.

Some initial imports we'll need:

import cv2
import numpy as np

The starting point is an image with some circular objects:

coins.png

To read this file as a grayscale image I do:

img = cv2.imread("coins.png", cv2.IMREAD_GRAYSCALE)

Let's hide the coin at x=348, y=317 and radius 45 pixels. Similar to Matlab recipe, we create a mask first.

# location and size of the circle
xc, yc, r = 348, 317, 45
# size of the image
H, W = img.shape
# x and y coordinates per every pixel of the image
x, y = np.meshgrid(np.arange(W), np.arange(H))
# squared distance from the center of the circle
d2 = (x - xc)**2 + (y - yc)**2
# mask is True inside of the circle
mask = d2 < r**2

coin-mask.png

We may simply set image to black (0) or white (255) under the mask as in the Matlab recipe, or we may calculate an average color outside of the coin, and fill inner pixels with this color:

outside = np.ma.masked_where(mask, img)
average_color = outside.mean()
img[mask] = average_color

New image now looks like this:

coin-masked.png

The coin is missing, but the circle is clearly visible. A nicer way to fill the circle is to use OpenCV inpaint function. It will reconstruct the missing part from the pixels around it.

# we need to convert the mask to 8bit single channel image
bytemask = np.asarray(mask*255, dtype=np.uint8)
inpainted = cv2.inpaint(img, bytemask, inpaintRadius=10, flags=cv2.INPAINT_TELEA)

The inpainted image:

coin-inpainted.png

Now what if I wanted to find and remove all coins automatically? The approach suggested in Steve's article (apply binary threshold to mask all coins which happen to be brighter than the background) will not work, unless I carefully select a different background and take care of uniform illumination.

A different approach is to use Hough transform to detect circles:

circles = cv2.HoughCircles(img, cv2.cv.CV_HOUGH_GRADIENT,
                           dp=1.5, minDist=30, minRadius=15, maxRadius=60)

Hough transform is somewhat finicky as far as parameters should be selected on a case-by-case basis. Beyond the parameters in the example above you may want to adjust two other parameters: param1 (default value is 100), which more or less translates into sensitivity of the edge detection procedure (see OpenCV docs for a more techinal definition), and param2 (default value is 100) is a circle detection threshold. The smaller is param2, the more false circles will be detected.

To make sure only the right circles were selected we'll draw them over the original color image:

color_img = cv2.imread("coins.png")
red = (0,0,255)
for x, y, r in circles[0]:
    cv2.circle(color_img, (x,y), r, red, 2)

coins-detected.png

The detected circles are not very precise, they don't cover shadow and reflex zone around the coin, so we may want to make them slightly bigger when creating a mask.

black = np.zeros(img.shape)
for x, y, r in circles[0]:
    cv2.circle(black, (x,y), int(r+15), 255, -1)  # -1 to draw filled circles

coins-mask.png

Now we may simply inpaint all found coins:

bytemask = np.asarray(black, dtype=np.uint8)
inpainted = cv2.inpaint(img, bytemask, inpaintRadius=5, flags=cv2.INPAINT_TELEA)

And all coins magically disappear:

coins-all-inpainted.png

We used three OpenCV features:

  • Hough transform
  • inpaint function
  • primitive drawing procedures (cv2.circle)

If we skip inpainting, which is unique to OpenCV right now, we may also do everything in scikit-image. It tends to have a nicer Python interface. Let me know if you'd like to see some examples.

If you like this post, you may also like Counting objects and calculating objects' density in Python.