0

I am building an OMR (Optical Mark Recognition) system in Python using OpenCV. My answer sheet has 180 bubbles arranged in 4 columns of 45 questions, with 4 options (A, B, C, D) per question. Each bubble is an empty circle with a colored outline and white fill, and when a student marks an answer it becomes a dark filled circle. I need to first detect all bubble positions whether they are filled or not, and then measure which ones are actually filled.

My current approach is to threshold the image and find contours. I convert the image to grayscale, apply CLAHE for contrast enhancement, then use Otsu thresholding with THRESH_BINARY_INV, and finally filter contours by area and circularity like this:

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
gray = clahe.apply(gray)
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

bubbles = []
for cnt in contours:
    area = cv2.contourArea(cnt)
    if not (60 <= area <= 3000):
        continue
    peri = cv2.arcLength(cnt, True)
    circularity = 4 * math.pi * area / (peri ** 2)
    if circularity < 0.45:
        continue
    bubbles.append(cnt)

The problem is that on a digital screenshot of the PDF, the bubbles are just thin circle outlines with white inside. After thresholding, the empty bubbles have almost no filled pixel area, so cv2.contourArea() returns a very small value and they all get filtered out, giving me zero detected bubbles. I also tried adaptive thresholding with cv2.ADAPTIVE_THRESH_GAUSSIAN_C but it still fails because the bubble outline is only 1 to 2 pixels wide after thresholding.
The sheet is generated using ReportLab where bubbles are drawn as canvas.circle(x, y, r, fill=1, stroke=1) with white fill and a red stroke, giving a bubble radius of about 3.6pt in the PDF which is roughly 5 to 8 pixels in a typical screenshot. The same sheet is also printed and filled by students with a pencil, then photographed, so the solution needs to work for both digital screenshots and real photos.
I have read about cv2.HoughCircles which seems like it could work since it detects circular shapes based on gradients rather than filled regions, but I am unsure how to tune param1, param2, minRadius, and maxRadius for this specific case. I also considered template matching using a programmatically generated circle template, but I am not sure which approach is more reliable here. What is the correct method to detect all bubble positions regardless of whether they are filled, and once the positions are known, what is the best way to measure the fill ratio inside each circle?

Environment: Python 3.11, OpenCV 4.8, Windows 11. Input is either a PNG screenshot of the PDF or a JPG photo of the printed and filled sheet.

New contributor
Sriram N Kulkarni is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
4
  • 1
    trying to locate the bubbles is the first mistake. that is not how OMR works. Commented Apr 28 at 8:25
  • in what way does this problem involve "d3.js"? Commented Apr 28 at 8:26
  • 2
    Please add a reproducible example in your question (an image) Commented Apr 28 at 9:25
  • 1
    a minimal reproducible example needs data to be reproducible. give us the data. Commented 2 days ago

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.