John
John

Reputation: 1828

How can I find the center the hole with hough circles

For this image, I tried to use hough cirlce to find the center of the "black hole".

enter image description here

After playing with the parameters of cv2.HoughCircles for a long time, the following is the best I can get.

raw image: enter image description here

enter image description here

# reproducible code for stackoverflow
import cv2
import os
import sys
from matplotlib import pyplot as plt
import numpy as np

# read image can turn it gray
img = cv2.imread(FILE)
cimg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_gray = dst = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.figure(figsize = (18,18))
plt.imshow(cimg, cmap = "gray")

# removing noises
element = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
closing = cv2.morphologyEx(y, cv2.MORPH_CLOSE, element,  iterations = 7)
plt.figure(figsize = (12,12))
plt.imshow(closing, cmap = "gray")

# try to find the circles

circles = cv2.HoughCircles(closing,cv2.HOUGH_GRADIENT,3,50,
                            param1=50,param2=30,minRadius=20,maxRadius=50)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
    # draw the outer circle
    cv2.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
    # draw the center of the circle
    cv2.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
plt.figure(figsize = (12,12))
plt.imshow(cimg)

Update::

The one with Canny:

edges = cv2.Canny(closing, 100, 300)
plt.figure(figsize = (12,12))
plt.imshow(edges, cmap = "gray")
circles = cv2.HoughCircles(edges,cv2.HOUGH_GRADIENT,2,50,
                            param1=50,param2=30,minRadius=20,maxRadius=60)

circles = np.uint16(np.around(circles))
cimg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

for i in circles[0,:]:
    # draw the outer circle
    cv2.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
    # draw the center of the circle
    cv2.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
plt.figure(figsize = (12,12))
plt.imshow(cimg)

enter image description here enter image description here

Still not the right circle that is wanted.


Update:

@crackanddie

Sometimes there is 6 or 9 in the identity number. The circle in 6 or 9 is not very round. Is there any way to filter that out?

enter image description here

enter image description here

enter image description here

Upvotes: 0

Views: 1010

Answers (3)

Knight Forked
Knight Forked

Reputation: 1619

If you want to use a "thinking out of the box" solution then check this solution out. Remember this might have a few false positives in some cases and would only work in cases where circle contour is complete or joined.

import numpy as np
import cv2
import matplotlib.pyplot as plt
from math import pi

pi_eps = 0.1

rgb = cv2.imread('/path/to/your/image/find_circle.jpg')
gray = cv2.cvtColor(rgb, cv2.COLOR_BGR2GRAY)

th = cv2.adaptiveThreshold(gray,255, cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY_INV,21,5)

contours, hier = cv2.findContours(th.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
out_img = rgb.copy()

for i in range(len(contours)):        
    x,y,w,h = cv2.boundingRect(contours[i])
    ar = min(w,h)/max(w,h)
    # For a circle aspect ratio is close to 1.0
    # In your use case circle diameter is between 40px-100px
    if ar < 0.9 or \
    w < 40 or w > 100:
        continue
    
    # P = 2 * PI * r
    perimeter = cv2.arcLength(contours[i], True)
    if perimeter == 0:
        continue
    # Second level confirmation could be done using PI = P * P / (4 * A)
    # A = PI * r * r
    area = cv2.contourArea(contours[i])
    if area == 0:
        continue
    # d = (w+h) / 2 average diameter
    # A contour is a circle if (P / d) = PI
    ctr_pi = perimeter / ((w+h) / 2)
    
    if abs(ctr_pi - pi) < pi_eps * pi:
        cv2.circle(out_img, (int(x+w/2), int(y+h/2)), int(max(w,h)/2), (0, 255, 0), 1)
        print("Center of the circle: ", x + w/2, y+h/2)

plt.imshow(out_img)

Detected Circle

Upvotes: 0

stateMachine
stateMachine

Reputation: 5805

This is an alternative method if you do not want to implement or fiddle with Hough's parameters. You must be sure there's at least one circle visible in your picture. The idea is to create a segmentation mask based on the CMYK color space and filter the blobs of interest by circularity and area. These are the steps:

  1. Convert the image from BGR to CMYK
  2. Threshold the K channel to get a binary mask
  3. Filter blobs by circularity and area
  4. Approximate the filtered blobs as circles

I'm choosing the CMYK color space because the circle is mostly black. The K (key) channel (in this case - black) should do a good job of representing the blob of interest, albeit, with some noise - as usual. Let's see the code:

# Imports:
import cv2
import numpy as np

# image path
path = "D://opencvImages//"
fileName = "dyj3O.jpg"

# load image
bgr = cv2.imread(path + fileName)

Alright, we need to convert the image from BGR to CMYK. OpenCV does not offer the conversion, so we need to do it manually. The formula is very straightforward. I'm just interested on the K channel, so I just calculate it like this:

# Make float and divide by 255:
bgrFloat = bgr.astype(np.float) / 255.

# Calculate K as (1 - whatever is biggest out of bgrFloat)
kChannel = 1 - np.max(bgrFloat, axis=2)

# Convert back to uint 8:
kChannel = 255 * kChannel
kChannel = kChannel.astype(np.uint8)

Gotta keep en eye on the data types, because there are float operations going on. This is the result:

As you see, the hole is almost 100% white, that's cool, we can threshold this image via Otsu like this:

# Compute binary mask of the hole via Otsu:
_, binaryImage = cv2.threshold(kChannel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Which gives you this nice binary mask:

Now, here comes the laborious part. Let's find contours on this image. For every contour/blob compute circularity and area. Use this info to filter noise and get the contour of interest, keep in mind that a perfect circle should have circularity close to 1.0. Once you get a contour of interest, approximate a circle to it. This is the process:

# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

# Store the detected circles here:
detectedCircles = []

# Look for the potential contours of interest:
for _, c in enumerate(contours):

    # Get the blob's area and perimeter:
    contourArea = cv2.contourArea(c)
    contourPerimeter = cv2.arcLength(c, True)

    # Compute circularity:
    if contourPerimeter > 0:
        circularity = (4 * 3.1416 * contourArea) / (pow(contourPerimeter, 2))
    else:
        circularity = 0.0

    # Set the min threshold values to identify the
    # blob of interest:
    minCircularity = 0.7
    minArea = 2000

    if circularity >= minCircularity and contourArea >= minArea:

        # Approximate the contour to a circle:
        (x, y), radius = cv2.minEnclosingCircle(c)

        # Compute the center and radius:
        center = (int(x), int(y))

        # Cast radius to in:
        radius = int(radius)

        # Store the center and radius:
        detectedCircles.append([center, radius])

        # Draw the circles:
        cv2.circle(bgr, center, radius, (0, 255, 0), 2)
        cv2.imshow("Detected Circles", bgr)

print("Circles Found: " + str(len(detectedCircles)))

Additionally, I have stored the circle (center and radius) in the detectedCircles list. This is the final result:

Circles Found: 1

Upvotes: 1

crackanddie
crackanddie

Reputation: 708

Here it is:

import numpy as np
import cv2


def threshold_gray_const(image_, rang: tuple):
    return cv2.inRange(image_, rang[0], rang[1])


def binary_or(image_1, image_2):
    return cv2.bitwise_or(image_1, image_2)


def negate_image(image_):
    return cv2.bitwise_not(image_)


def particle_filter(image_, power):
    # Abdrakov's particle filter
    nb_components, output, stats, centroids = cv2.connectedComponentsWithStats(image_, connectivity=8)
    sizes = stats[1:, -1]
    nb_components = nb_components - 1

    min_size = power

    img2 = np.zeros(output.shape, dtype=np.uint8)
    for i in range(0, nb_components):
        if sizes[i] >= min_size:
            img_to_compare = threshold_gray_const(output, (i + 1, i + 1))
            img2 = binary_or(img2, img_to_compare)

    img2 = img2.astype(np.uint8)
    return img2


def reject_borders(image_):
    # Abdrakov's border rejecter
    out_image = image_.copy()
    h, w = image_.shape[:2]
    for row in range(h):
        if out_image[row, 0] == 255:
            cv2.floodFill(out_image, None, (0, row), 0)
        if out_image[row, w - 1] == 255:
            cv2.floodFill(out_image, None, (w - 1, row), 0)
    for col in range(w):
        if out_image[0, col] == 255:
            cv2.floodFill(out_image, None, (col, 0), 0)
        if out_image[h - 1, col] == 255:
            cv2.floodFill(out_image, None, (col, h - 1), 0)
    return out_image


src = cv2.imread("your_image")
img_gray = dst = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
closing = cv2.morphologyEx(img_gray, cv2.MORPH_CLOSE, element,  iterations=2)
tv, thresh = cv2.threshold(closing, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
neg = negate_image(thresh)
rej = reject_borders(neg)
filtered = particle_filter(rej, 300)
edges = cv2.Canny(filtered, 100, 200)

circles = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, 3, 50, param1=50, param2=30, minRadius=20, maxRadius=50)
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
    # draw the outer circle
    cv2.circle(src, (i[0], i[1]), i[2], (0, 255, 0), 2)
    # draw the center of the circle
    cv2.circle(src, (i[0], i[1]), 2, (0, 0, 255), 3)

cv2.imshow("closing", closing)
cv2.imshow("edges", edges)
cv2.imshow("out", src)
cv2.waitKey(0)

I changed cv2.morphologyEx parameters a bit, because they were too strong. And after this noise removing I made a binary image using cv2.THRESH_OTSU parameter, negated it, rejected borders and filtered a bit. Then I used cv2.Canny to find edges and this 'cannied' image I passed into cv2.HoughCircles. If any questions - ask me :)

Upvotes: 0

Related Questions