Circular and Elliptical Hough Transforms

The Hough transform in its simplest form is a method to detect straight lines but it can also be used to detect circles or ellipses. The algorithm assumes that the edge is detected and it is robust against noise or missing points.

Circle detection

In the following example, the Hough transform is used to detect coin positions and match their edges. We provide a range of plausible radii. For each radius, two circles are extracted and we finally keep the five most prominent candidates. The result shows that coin positions are well-detected.

Algorithm overview

Given a black circle on a white background, we first guess its radius (or a range of radii) to construct a new circle. This circle is applied on each black pixel of the original picture and the coordinates of this circle are voting in an accumulator. From this geometrical construction, the original circle center position receives the highest score.

Note that the accumulator size is built to be larger than the original picture in order to detect centers outside the frame. Its size is extended by two times the larger radius.

import numpy as np
import matplotlib.pyplot as plt

from skimage import data, filter, color
from skimage.transform import hough_circle
from skimage.feature import peak_local_max
from skimage.draw import circle_perimeter
from skimage.util import img_as_ubyte


# Load picture and detect edges
image = img_as_ubyte(data.coins()[0:95, 70:370])
edges = filter.canny(image, sigma=3, low_threshold=10, high_threshold=50)

fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(5, 2))

# Detect two radii
hough_radii = np.arange(15, 30, 2)
hough_res = hough_circle(edges, hough_radii)

centers = []
accums = []
radii = []

for radius, h in zip(hough_radii, hough_res):
    # For each radius, extract two circles
    peaks = peak_local_max(h, num_peaks=2)
    centers.extend(peaks)
    accums.extend(h[peaks[:, 0], peaks[:, 1]])
    radii.extend([radius, radius])

# Draw the most prominent 5 circles
image = color.gray2rgb(image)
for idx in np.argsort(accums)[::-1][:5]:
    center_x, center_y = centers[idx]
    radius = radii[idx]
    cx, cy = circle_perimeter(center_y, center_x, radius)
    image[cy, cx] = (220, 20, 20)

ax.imshow(image, cmap=plt.cm.gray)

../_images/plot_circular_elliptical_hough_transform_1.png

Ellipse detection

In this second example, the aim is to detect the edge of a coffee cup. Basically, this is a projection of a circle, i.e. an ellipse. The problem to solve is much more difficult because five parameters have to be determined, instead of three for circles.

Algorithm overview

The algorithm takes two different points belonging to the ellipse. It assumes that it is the main axis. A loop on all the other points determines how much an ellipse passes to them. A good match corresponds to high accumulator values.

A full description of the algorithm can be found in reference [1].

References

[1]Xie, Yonghong, and Qiang Ji. “A new efficient ellipse detection method.” Pattern Recognition, 2002. Proceedings. 16th International Conference on. Vol. 2. IEEE, 2002

import matplotlib.pyplot as plt

from skimage import data, filter, color
from skimage.transform import hough_ellipse
from skimage.draw import ellipse_perimeter

# Load picture, convert to grayscale and detect edges
image_rgb = data.coffee()[0:220, 160:420]
image_gray = color.rgb2gray(image_rgb)
edges = filter.canny(image_gray, sigma=2.0,
                     low_threshold=0.55, high_threshold=0.8)

# Perform a Hough Transform
# The accuracy corresponds to the bin size of a major axis.
# The value is chosen in order to get a single high accumulator.
# The threshold eliminates low accumulators
result = hough_ellipse(edges, accuracy=20, threshold=250,
                       min_size=100, max_size=120)
result.sort(order='accumulator')

# Estimated parameters for the ellipse
best = result[-1]
yc = int(best[1])
xc = int(best[2])
a = int(best[3])
b = int(best[4])
orientation = best[5]

# Draw the ellipse on the original image
cy, cx = ellipse_perimeter(yc, xc, a, b, orientation)
image_rgb[cy, cx] = (0, 0, 255)
# Draw the edge (white) and the resulting ellipse (red)
edges = color.gray2rgb(edges)
edges[cy, cx] = (250, 0, 0)

fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(8, 4))

ax1.set_title('Original picture')
ax1.imshow(image_rgb)

ax2.set_title('Edge (white) and result (red)')
ax2.imshow(edges)

plt.show()

../_images/plot_circular_elliptical_hough_transform_2.png

STDOUT


        

STDERR


        

Python source code: download (generated using skimage 0.11dev)

IPython Notebook: download (generated using skimage 0.11dev)

aW1wb3J0IG51bXB5IGFzIG5wCmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQKCmZyb20gc2tpbWFnZSBpbXBvcnQgZGF0YSwgZmlsdGVyLCBjb2xvcgpmcm9tIHNraW1hZ2UudHJhbnNmb3JtIGltcG9ydCBob3VnaF9jaXJjbGUKZnJvbSBza2ltYWdlLmZlYXR1cmUgaW1wb3J0IHBlYWtfbG9jYWxfbWF4CmZyb20gc2tpbWFnZS5kcmF3IGltcG9ydCBjaXJjbGVfcGVyaW1ldGVyCmZyb20gc2tpbWFnZS51dGlsIGltcG9ydCBpbWdfYXNfdWJ5dGUKCgojIExvYWQgcGljdHVyZSBhbmQgZGV0ZWN0IGVkZ2VzCmltYWdlID0gaW1nX2FzX3VieXRlKGRhdGEuY29pbnMoKVswOjk1LCA3MDozNzBdKQplZGdlcyA9IGZpbHRlci5jYW5ueShpbWFnZSwgc2lnbWE9MywgbG93X3RocmVzaG9sZD0xMCwgaGlnaF90aHJlc2hvbGQ9NTApCgpmaWcsIGF4ID0gcGx0LnN1YnBsb3RzKG5jb2xzPTEsIG5yb3dzPTEsIGZpZ3NpemU9KDUsIDIpKQoKIyBEZXRlY3QgdHdvIHJhZGlpCmhvdWdoX3JhZGlpID0gbnAuYXJhbmdlKDE1LCAzMCwgMikKaG91Z2hfcmVzID0gaG91Z2hfY2lyY2xlKGVkZ2VzLCBob3VnaF9yYWRpaSkKCmNlbnRlcnMgPSBbXQphY2N1bXMgPSBbXQpyYWRpaSA9IFtdCgpmb3IgcmFkaXVzLCBoIGluIHppcChob3VnaF9yYWRpaSwgaG91Z2hfcmVzKToKICAgICMgRm9yIGVhY2ggcmFkaXVzLCBleHRyYWN0IHR3byBjaXJjbGVzCiAgICBwZWFrcyA9IHBlYWtfbG9jYWxfbWF4KGgsIG51bV9wZWFrcz0yKQogICAgY2VudGVycy5leHRlbmQocGVha3MpCiAgICBhY2N1bXMuZXh0ZW5kKGhbcGVha3NbOiwgMF0sIHBlYWtzWzosIDFdXSkKICAgIHJhZGlpLmV4dGVuZChbcmFkaXVzLCByYWRpdXNdKQoKIyBEcmF3IHRoZSBtb3N0IHByb21pbmVudCA1IGNpcmNsZXMKaW1hZ2UgPSBjb2xvci5ncmF5MnJnYihpbWFnZSkKZm9yIGlkeCBpbiBucC5hcmdzb3J0KGFjY3VtcylbOjotMV1bOjVdOgogICAgY2VudGVyX3gsIGNlbnRlcl95ID0gY2VudGVyc1tpZHhdCiAgICByYWRpdXMgPSByYWRpaVtpZHhdCiAgICBjeCwgY3kgPSBjaXJjbGVfcGVyaW1ldGVyKGNlbnRlcl95LCBjZW50ZXJfeCwgcmFkaXVzKQogICAgaW1hZ2VbY3ksIGN4XSA9ICgyMjAsIDIwLCAyMCkKCmF4Lmltc2hvdyhpbWFnZSwgY21hcD1wbHQuY20uZ3JheSk=
CmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQKCmZyb20gc2tpbWFnZSBpbXBvcnQgZGF0YSwgZmlsdGVyLCBjb2xvcgpmcm9tIHNraW1hZ2UudHJhbnNmb3JtIGltcG9ydCBob3VnaF9lbGxpcHNlCmZyb20gc2tpbWFnZS5kcmF3IGltcG9ydCBlbGxpcHNlX3BlcmltZXRlcgoKIyBMb2FkIHBpY3R1cmUsIGNvbnZlcnQgdG8gZ3JheXNjYWxlIGFuZCBkZXRlY3QgZWRnZXMKaW1hZ2VfcmdiID0gZGF0YS5jb2ZmZWUoKVswOjIyMCwgMTYwOjQyMF0KaW1hZ2VfZ3JheSA9IGNvbG9yLnJnYjJncmF5KGltYWdlX3JnYikKZWRnZXMgPSBmaWx0ZXIuY2FubnkoaW1hZ2VfZ3JheSwgc2lnbWE9Mi4wLAogICAgICAgICAgICAgICAgICAgICBsb3dfdGhyZXNob2xkPTAuNTUsIGhpZ2hfdGhyZXNob2xkPTAuOCkKCiMgUGVyZm9ybSBhIEhvdWdoIFRyYW5zZm9ybQojIFRoZSBhY2N1cmFjeSBjb3JyZXNwb25kcyB0byB0aGUgYmluIHNpemUgb2YgYSBtYWpvciBheGlzLgojIFRoZSB2YWx1ZSBpcyBjaG9zZW4gaW4gb3JkZXIgdG8gZ2V0IGEgc2luZ2xlIGhpZ2ggYWNjdW11bGF0b3IuCiMgVGhlIHRocmVzaG9sZCBlbGltaW5hdGVzIGxvdyBhY2N1bXVsYXRvcnMKcmVzdWx0ID0gaG91Z2hfZWxsaXBzZShlZGdlcywgYWNjdXJhY3k9MjAsIHRocmVzaG9sZD0yNTAsCiAgICAgICAgICAgICAgICAgICAgICAgbWluX3NpemU9MTAwLCBtYXhfc2l6ZT0xMjApCnJlc3VsdC5zb3J0KG9yZGVyPSdhY2N1bXVsYXRvcicpCgojIEVzdGltYXRlZCBwYXJhbWV0ZXJzIGZvciB0aGUgZWxsaXBzZQpiZXN0ID0gcmVzdWx0Wy0xXQp5YyA9IGludChiZXN0WzFdKQp4YyA9IGludChiZXN0WzJdKQphID0gaW50KGJlc3RbM10pCmIgPSBpbnQoYmVzdFs0XSkKb3JpZW50YXRpb24gPSBiZXN0WzVdCgojIERyYXcgdGhlIGVsbGlwc2Ugb24gdGhlIG9yaWdpbmFsIGltYWdlCmN5LCBjeCA9IGVsbGlwc2VfcGVyaW1ldGVyKHljLCB4YywgYSwgYiwgb3JpZW50YXRpb24pCmltYWdlX3JnYltjeSwgY3hdID0gKDAsIDAsIDI1NSkKIyBEcmF3IHRoZSBlZGdlICh3aGl0ZSkgYW5kIHRoZSByZXN1bHRpbmcgZWxsaXBzZSAocmVkKQplZGdlcyA9IGNvbG9yLmdyYXkycmdiKGVkZ2VzKQplZGdlc1tjeSwgY3hdID0gKDI1MCwgMCwgMCkKCmZpZzIsIChheDEsIGF4MikgPSBwbHQuc3VicGxvdHMobmNvbHM9MiwgbnJvd3M9MSwgZmlnc2l6ZT0oOCwgNCkpCgpheDEuc2V0X3RpdGxlKCdPcmlnaW5hbCBwaWN0dXJlJykKYXgxLmltc2hvdyhpbWFnZV9yZ2IpCgpheDIuc2V0X3RpdGxlKCdFZGdlICh3aGl0ZSkgYW5kIHJlc3VsdCAocmVkKScpCmF4Mi5pbXNob3coZWRnZXMpCgpwbHQuc2hvdygp