Sometimes I prowl around StackOverflow to find questions about OpenCVs. No, I’m not computer vision expert, it’s just that there are many questions there that I can use as programming exercise, hopefully I can learn more about it. Here’s one of the simpler problems, finding filled circles among answer sheet.

Here’s the original image:

Since my assumption is that, the circles should be relatively similar sized, with similar spacing, and not much noise or smudges on it. Additional assumption is that the picture of the answer sheet is taken directly from bird’s eye view. A lot of assumptions, you can see that my code will fail when the condition doesn’t fit the assumption.

Here’s how I did it:

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

And here’s the result:

And here’s the code

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(); }

It still requires tuning of parameters though, so it’s not a good solution. Have fun.