user1146904
user1146904

Reputation: 263

Remove circles using opencv

I'm working on opencv problem to figure out which circles are filled. However, sometimes edge of circles are cause of false positive. It makes my wonder if I can remove these circles by turning all pixels white that have high R value in RGB. My approach is to create a mask of pixels that are pinkish and then subtract mask from original image to remove circles. As of now I am getting black mask. I'm doing something wrong. Please guide.

    rgb = cv2.imread(img, cv2.CV_LOAD_IMAGE_COLOR)
    rgb_filtered = cv2.inRange(rgb, (200, 0, 90), (255, 110, 255))
    cv2.imwrite('mask.png',rgb_filtered) 

enter image description here

Upvotes: 5

Views: 7809

Answers (3)

agmangas
agmangas

Reputation: 669

I've tried to come up with a solution in Python. Basically the process is the following:

  • Gaussian blur to reduce noise.
  • Otsu's threshold.
  • Find contours that have no parents, those contours should be the circles.
  • Check the ratio of white-to-black pixels inside each contour.

You may need to tune up the white ratio threshold to fit your application. I've used 0.7 as it seems a reasonable value.

import cv2
import numpy

# Read image and apply gaussian blur

img = cv2.imread("circles.png", cv2.CV_LOAD_IMAGE_GRAYSCALE)
img = cv2.GaussianBlur(img, (5, 5), 0)

# Apply OTSU thresholding and reverse it so the circles are in the foreground (white)

_, otsu = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
otsu = cv2.bitwise_not(otsu).astype("uint8")

# Find contours that have no parent

contours, hierarchy = cv2.findContours(numpy.copy(otsu), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
parent_contours = [contours[idx] for idx, val in enumerate(hierarchy[0]) if val[3] == -1]

# Loop through all contours to check the ratio of white to black pixels inside each one

filled_circles_contours = list()

for contour in parent_contours:

    contour_mask = numpy.zeros(img.shape).astype("uint8")
    cv2.drawContours(contour_mask, [contour], -1, 1, thickness=-1)
    white_len_mask = len(cv2.findNonZero(contour_mask))
    white_len_thresholded = len(cv2.findNonZero(contour_mask * otsu))
    white_ratio = float(white_len_thresholded) / white_len_mask

    if white_ratio > 0.7:
        filled_circles_contours.append(contour)

# Show image with detected circles

cv2.drawContours(img, filled_circles_contours, -1, (0, 0, 0), thickness=2)
cv2.namedWindow("Result")
cv2.imshow("Result", img)
cv2.waitKey(0)

This is the result I obtained from applying the code above to your image:

Filled circles detection result

Upvotes: 1

Micka
Micka

Reputation: 20130

Here is my solution. Unfortunately it's in C++ too and this is how it works:

  1. threshold the image to find out which parts are background (white paper)
  2. find the circles by extracting contours.
  3. now each contour is assumed to be a circle, so compute the minimum circle enclosing that contour. No parameter tuning necessary if the input is ok (that means every circle is a single contour, so circle may not be connected by drawing for example)
  4. check for each circle, whether there are more foreground (drawing) or background (white paper) pixel inside (by some ratio threshold).

    int main()
    {
    cv::Mat colorImage = cv::imread("countFilledCircles.png");
    cv::Mat image = cv::imread("countFilledCircles.png", CV_LOAD_IMAGE_GRAYSCALE);
    
    // threshold the image!
    cv::Mat thresholded;
    cv::threshold(image,thresholded,0,255,CV_THRESH_BINARY_INV | CV_THRESH_OTSU);
    
    // save threshold image for demonstration:
    cv::imwrite("countFilledCircles_threshold.png", thresholded);
    
    
    // find outer-contours in the image these should be the circles!
    cv::Mat conts = thresholded.clone();
    std::vector<std::vector<cv::Point> > contours;
    std::vector<cv::Vec4i> hierarchy;
    
    cv::findContours(conts,contours,hierarchy, CV_RETR_EXTERNAL, CV_C    HAIN_APPROX_SIMPLE, cv::Point(0,0));
    
    
    
    // colors in which marked/unmarked circle outlines will be drawn:
    cv::Scalar colorMarked(0,255,0);
    cv::Scalar colorUnMarked(0,0,255);
    
    // each outer contour is assumed to be a circle
    // TODO: you could first find the mean radius of all assumed circles and try to find outlier (dirt etc in the image)
    for(unsigned int i=0; i<contours.size(); ++i)
    {
            cv::Point2f center;
            float radius;
            // find minimum circle enclosing the contour
            cv::minEnclosingCircle(contours[i],center,radius);
    
            bool marked = false;
    
        cv::Rect circleROI(center.x-radius, center.y-radius, center.x+radius, center.y+radius);
        //circleROI = circleROI & cv::Rect(0,0,image.cols, image.rows);
    
        // count pixel inside the circle
        float sumCirclePixel = 0;
        float sumCirclePixelMarked = 0;
        for(int j=circleROI.y; j<circleROI.y+circleROI.height; ++j)
            for(int i=circleROI.x; i<circleROI.x+circleROI.width; ++i)
            {
                cv::Point2f current(i,j);
                // test if pixel really inside the circle:
            if(cv::norm(current-center) < radius)
                {
                    // count total number of pixel in the circle
                    sumCirclePixel = sumCirclePixel+1.0f;
                    // and count all pixel in the circle which hold the segmentation threshold
                    if(thresholded.at<unsigned char>(j,i))
                        sumCirclePixelMarked = sumCirclePixelMarked + 1.0f;
                }
            }
    
        const float ratioThreshold = 0.5f;
        if(sumCirclePixel)
            if(sumCirclePixelMarked/sumCirclePixel > ratioThreshold) marked = true;
    
        // draw the circle for demonstration
        if(marked)
            cv::circle(colorImage,center,radius,colorMarked,1);
        else
            cv::circle(colorImage,center,radius,colorUnMarked,1);
        }
    
    
    cv::imshow("thres", thresholded);
    cv::imshow("colorImage", colorImage);
    cv::imwrite("countFilledCircles_output.png", colorImage);
    
    cv::waitKey(-1);
    }
    

giving me these results:

after otsu thresholding:

enter image description here

final image:

enter image description here

Upvotes: 5

sub_o
sub_o

Reputation: 2802

Here's how I did it:

  1. Convert to grayscale, apply gaussian blur to remove noises
  2. Apply otsu thresholding, it's quite good to separate fore and background, you should read about it
  3. Apply Hough circle transform to find candidate circles, sadly this requires heavy tuning. Maybe watershed segmentation is a better alternative
  4. Extract the ROI from the candidate circles, and find the ratio of black and white pixels.

Here's my sample result: enter image description here

When we draw our result on original image: enter image description here

Here's the sample code (sorry in C++):

void findFilledCircles( Mat& img ){
    Mat gray;
    cvtColor( img, gray, CV_BGR2GRAY );

    /* Apply some blurring to remove some noises */
    GaussianBlur( gray, gray, Size(5, 5), 1, 1);

    /* Otsu thresholding maximizes inter class variance, pretty good in separating background from foreground */
    threshold( gray, gray, 0.0, 255.0, CV_THRESH_OTSU );
    erode( gray, gray, Mat(), Point(-1, -1), 1 );

    /* Sadly, this is tuning heavy, adjust the params for Hough Circles */
    double dp       = 1.0;
    double min_dist = 15.0;
    double param1   = 40.0;
    double param2   = 10.0;
    int min_radius  = 15;
    int max_radius  = 22;

    /* Use hough circles to find the circles, maybe we could use watershed for segmentation instead(?) */
    vector<Vec3f> found_circles;
    HoughCircles( gray, found_circles, CV_HOUGH_GRADIENT, dp, min_dist, param1, param2, min_radius, max_radius );

    /* This is just to draw coloured circles on the 'originally' gray image */
    vector<Mat> out = { gray, gray, gray };
    Mat output;
    merge( out, output );

    float diameter  = max_radius * 2;
    float area      = diameter * diameter;

    Mat roi( max_radius, max_radius, CV_8UC3, Scalar(255, 255, 255) );
    for( Vec3f circ: found_circles ) {
    /* Basically we extract the region of the circles, and count the ratio of black pixels (0) and white pixels (255) */
        Mat( gray, Rect( circ[0] - max_radius, circ[1] - max_radius, diameter, diameter ) ).copyTo( roi );
        float filled_percentage = 1.0 - 1.0 * countNonZero( roi ) / area;

        /* If more than half is filled, then maybe it's filled */
        if( filled_percentage > 0.5 )
            circle( output, Point2f( circ[0], circ[1] ), max_radius, Scalar( 0, 0, 255), 3 );
        else
            circle( output, Point2f( circ[0], circ[1] ), max_radius, Scalar( 255, 255, 0), 3 );
    }


    namedWindow("");
    moveWindow("", 0, 0);
    imshow("", output );
    waitKey();
}

Upvotes: 0

Related Questions