4

I have binarized images like this one:

enter image description here

I need to determine the center and radius of the inner solid disk. As you can see, it is surrounded by a textured area which touches it, so that simple connected component detection doesn't work. Anyway, there is a void margin on a large part of the perimeter.

A possible cure could be by eroding until all the texture disappears or disconnects from the disk, but this can be time consuming and the number of iterations is unsure. (In addition, in some unlucky cases there are tiny holes in the disk, which will grow with erosion.)

Any better suggestion to address this problem in a robust and fast way ? (I tagged OpenCV, but this is not mandated, what matters is the approach.)

  • Cross-posted on Mathematics. –  Nov 29 '18 at 11:04
  • 1
    Nice question. Are there always those 4 black *"fingers"* pointing in towards the centre? – Mark Setchell Nov 29 '18 at 11:20
  • 1
    Yep, nearly always, but their size/position is unrelated to the disk radius (and by the way, their end is not visible). –  Nov 29 '18 at 11:25
  • 1
    Can you assume that the center of the disk is always in the same position? or the image can be "translated"? If the center is fixed, switching to polar coordinates can be a good start. Another approach could be to find the smaller circle on the inverted image (which would be the "contour" of the inner disk), but you already mentioned Hough is too slow... Also, to find the center, since the inner disk is quite thick wrt other connected components, it's probably the maximum of the distance transform... Just dropping thoughts ;) – Miki Nov 29 '18 at 11:37
  • @Miki: no, the center moves, and the radius can change, though the measurements must be accurate to two or three pixels. When texture touches, the centroid can be offset by much more. –  Nov 29 '18 at 11:43
  • @Miki: you would need a "largest enclosed circle" function, which unfortunately is not available. But maybe this can be emulated by taking the outline, performing a geometric inversion (a circle transforms to another circle), then smallest enclosing circle... –  Nov 29 '18 at 11:47
  • A simple 11x11 mean and 99% threshold does a pretty good job on this image - no idea what would happen with your others. – Mark Setchell Nov 29 '18 at 11:54
  • @MarkSetchell: interesting. In fact this mean is counting the percentage of white pixels in the window, and that indeed quickly drops. For more isotropy, a Gaussian. –  Nov 29 '18 at 12:49
  • Do you have access to the original images? Or only the already binarized ones? Seems like lots of data was thrown away. – Cris Luengo Nov 29 '18 at 16:53
  • @CrisLuengo: good remark. I do have the grayscale images, but they are textured and with variable contrasts. Sometimes terrible. The binarized images are more stable. –  Nov 29 '18 at 17:24
  • dude, you're clearly a computer vision expert...why are you manufacturing a question for SO? – user1269942 Nov 29 '18 at 20:14
  • @user1269942: nobody's infallible :-) –  Nov 29 '18 at 20:15

6 Answers6

6

You can:

  1. Invert the image
  2. Find the largest axis-aligned rectangle containing only zeros, (I used my C++ code from this answer). The algorithm is pretty fast.
  3. Get the center and radius of the circle from the rectangle

enter image description here

Code:

#include <opencv2\opencv.hpp>

using namespace std;
using namespace cv;

// https://stackoverflow.com/a/30418912/5008845
cv::Rect findMaxRect(const cv::Mat1b& src)
{
    cv::Mat1f W(src.rows, src.cols, float(0));
    cv::Mat1f H(src.rows, src.cols, float(0));

    cv::Rect maxRect(0,0,0,0);
    float maxArea = 0.f;

    for (int r = 0; r < src.rows; ++r)
    {
        for (int c = 0; c < src.cols; ++c)
        {
            if (src(r, c) == 0)
            {
                H(r, c) = 1.f + ((r>0) ? H(r-1, c) : 0);
                W(r, c) = 1.f + ((c>0) ? W(r, c-1) : 0);
            }

            float minw = W(r,c);
            for (int h = 0; h < H(r, c); ++h)
            {
                minw = std::min(minw, W(r-h, c));
                float area = (h+1) * minw;
                if (area > maxArea)
                {
                    maxArea = area;
                    maxRect = cv::Rect(cv::Point(c - minw + 1, r - h), cv::Point(c+1, r+1));
                }
            }
        }
    }

    return maxRect;
}


int main()
{
    cv::Mat1b img = cv::imread("path/to/img", cv::IMREAD_GRAYSCALE);

    // Correct image
    img = img > 127;

    cv::Rect r = findMaxRect(~img);

    cv::Point center ( std::round(r.x + r.width / 2.f), std::round(r.y + r.height / 2.f));
    int radius = std::sqrt(r.width*r.width + r.height*r.height) / 2;

    cv::Mat3b out;
    cv::cvtColor(img, out, cv::COLOR_GRAY2BGR);
    cv::rectangle(out, r, cv::Scalar(0, 255, 0));
    cv::circle(out, center, radius, cv::Scalar(0, 0, 255));

    return 0;
}
Miki
  • 40,887
  • 13
  • 123
  • 202
  • 2
    this is cool! I would've tried to somehow find the interior of the circle, then shoot rays to the outside until a black pixel is hit and then use a RANSAC to fit a circle. But your approach is very simple and looks quite robust to me. – Micka Nov 29 '18 at 12:32
  • Interesting. Needs to be a little adapted for when there are small holes in the disk. –  Nov 29 '18 at 12:52
  • @YvesDaoust yep, holes must be removed for this approach to work. I can't come up with an easy fix – Miki Nov 29 '18 at 13:24
2

My method is to use morph-open, findcontours, and minEnclosingCircle as follow:

enter image description here

#!/usr/bin/python3
# 2018/11/29 20:03 
import cv2

fname = "test.png"
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

th, threshed = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
morphed = cv2.morphologyEx(threshed, cv2.MORPH_OPEN, kernel, iterations = 3)

cnts = cv2.findContours(morphed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

cnt = max(cnts, key=cv2.contourArea)
pt, r = cv2.minEnclosingCircle(cnt)

pt = (int(pt[0]), int(pt[1]))
r = int(r)

print("center: {}\nradius: {}".format(pt, r))

The final result:

enter image description here

center: (184, 170)
radius: 103
Kinght 金
  • 17,681
  • 4
  • 60
  • 74
  • Yep, erosion works in many cases. (There is no real need of post-dilation, one can compensate on the radius.) –  Nov 29 '18 at 12:53
  • @YvesDaoust erosion is not enough, dilation is also needed. – Kinght 金 Nov 29 '18 at 12:55
  • 1
    In my case, centroid and radius from area are more robust, as there can remain irregularities on the outline, which are not averaged away by the enclosing circle. –  Nov 29 '18 at 12:55
  • 1
    I don't see why. Please expand. –  Nov 29 '18 at 12:56
1

Here is an example of using hough circle. It can work if you set the min and max radius to a proper range.

import cv2
import numpy as np

# load image in grayscale
image = cv2.imread('radius.png',0)
r , c = image.shape

# remove noise
dst = cv2.blur(image,(5,5))

# Morphological closing
dst = cv2.erode(dst,None,iterations = 3)
dst = cv2.dilate(dst,None,iterations = 3)

# Find Hough Circle
circles = cv2.HoughCircles(dst
    ,cv2.HOUGH_GRADIENT
    ,2
    ,minDist = 0.5* r
    ,param2 = 150
    ,minRadius = int(0.5 * r / 2.0) 
    ,maxRadius = int(0.75 * r / 2.0)
    )

# Display
edges_color = cv2.cvtColor(image,cv2.COLOR_GRAY2BGR)
for i in circles[0]:
    print(i)
    cv2.circle(edges_color,(i[0],i[1]),i[2],(0,0,255),1)



cv2.imshow("edges_color",edges_color)
cv2.waitKey(0)

Here is the result [185. 167. 103.6]

enter image description here

yapws87
  • 1,809
  • 7
  • 16
  • Thank you for doing the experiment. I need a more accurate fit, like achieved by Silencer. –  Nov 29 '18 at 16:59
1

My second attempt on this case. This time I am using morphological closing operation to weaken the noise and maintain the signal. This is followed by a simple threshold and a connectedcomponent analysis. I hope this code can run faster.

enter image description here

Using this method, i can find the centroid with subpixel accuracy

('center : ', (184.12244328746746, 170.59771290442544))

Radius is derived from the area of the circle.

('radius : ', 101.34704439389715)

Here is the full code

import cv2
import numpy as np

# load image in grayscale
image = cv2.imread('radius.png',0)
r,c = image.shape
# remove noise
blured = cv2.blur(image,(5,5))

# Morphological closing
morph = cv2.erode(blured,None,iterations = 3)
morph = cv2.dilate(morph,None,iterations = 3)
cv2.imshow("morph",morph)
cv2.waitKey(0)

# Get the strong signal
th, th_img = cv2.threshold(morph,200,255,cv2.THRESH_BINARY)

cv2.imshow("th_img",th_img)
cv2.waitKey(0)

# Get connected components
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(th_img)
print(num_labels)
print(stats)

# displat labels
labels_disp = np.uint8(255*labels/np.max(labels))
cv2.imshow("labels",labels_disp)
cv2.waitKey(0)

# Find center label
cnt_label = labels[r/2,c/2]

# Find circle center and radius
# Radius calculated by averaging the height and width of bounding box
area = stats[cnt_label][4]
radius = np.sqrt(area / np.pi)#stats[cnt_label][2]/2 + stats[cnt_label][3]/2)/2
cnt_pt = ((centroids[cnt_label][0]),(centroids[cnt_label][1]))
print('center : ',cnt_pt)
print('radius : ',radius)

# Display final result
edges_color = cv2.cvtColor(image,cv2.COLOR_GRAY2BGR)
cv2.circle(edges_color,(int(cnt_pt[0]),int(cnt_pt[1])),int(radius),(0,0,255),1)
cv2.circle(edges_color,(int(cnt_pt[0]),int(cnt_pt[1])),5,(0,0,255),-1)

x1 = stats[cnt_label][0]
y1 = stats[cnt_label][1]
w1 = stats[cnt_label][2]
h1 = stats[cnt_label][3]
cv2.rectangle(edges_color,(x1,y1),(x1+w1,y1+h1),(0,255,0))

cv2.imshow("edges_color",edges_color)
cv2.waitKey(0)
yapws87
  • 1,809
  • 7
  • 16
  • Why would you not use the area of the object to derive the radius? Square root of the number of pixels divided by pi. – Cris Luengo Nov 29 '18 at 18:12
  • This is very close to MarkSetchell's suggestion. The closing acts as a lowpass filter. Thanks. –  Nov 29 '18 at 19:18
  • @YvesDaoust Doesn't this method depend heavily on lighting conditions and the threshold values? Wouldn't Miki's method be more robust? – zindarod Dec 01 '18 at 12:11
  • 2
    @zindarod: there is an automatic threshold adjustment mechanism used here. Anyway, cases like this one are "unthresholdable", as there is some amount of contrast reversal. Miki's method lacks robustness as it only looks at the circumference at the four corners, where irregularities can cause errors of several pixels. –  Dec 01 '18 at 12:30
0

Have you tried something along the lines of the Circle Hough Transform? I see that OpenCv has its own implementation. Some preprocessing (median filtering?) might be necessary here, though.

leonbloy
  • 73,180
  • 20
  • 142
  • 190
  • 1
    No, this has little chance to work, it is slow, and there are several extra circles which will interfere. –  Nov 29 '18 at 11:23
0

Here is a simple approach:

  1. Erode the image (using a large, circular SE), then find the centroid of the result. This should be really close to the centroid of the central disk.
  2. Compute the mean as a function of the radius of the original image, using the computed centroid as the center.

The output looks like this:

Radial mean of the picture in the question

From here, determining the radius is quite simple.

Here is the code, I'm using PyDIP (we don't yet have a binary distribution, you'll need to download and build form sources):

import matplotlib.pyplot as pp
import PyDIP as dip
import numpy as np

img = dip.Image(pp.imread('/home/cris/tmp/FDvQm.png')[:,:,0])
b = dip.Erosion(img, 30)
c = dip.CenterOfMass(b)
rmean = dip.RadialMean(img, center=c)
pp.plot(rmean)
r = np.argmax(rmean < 0.5)

Here, r is 102, as the radius in integer number of pixels, I'm sure it's possible to interpolate to improve precision. c is [184.02, 170.45].

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • Thanks Cris. I mentioned explicitly that the disks can have small holes, so that large erosion has a catastrophic effect. –  Nov 29 '18 at 19:15
  • @Yves: Why don't you post one of the images with a small hole in it (or preferably, one of the original images you said you have). A simple smoothing before the erosion should take care of that, but I can't advice things like these without seeing your small holes first. – Cris Luengo Nov 29 '18 at 19:18
  • "A simple smoothing" will have a bad effect on the texture, and will fill the holes there as well. –  Nov 29 '18 at 19:19
  • @Yves: That depends on how you smooth. I'd probably suggest an area closing or something to that extent. – Cris Luengo Nov 29 '18 at 19:20