How can I read answer sheet with python opencv?

164 views Asked by At

I have an answer sheet. I want to identify the fields marked on it and check whether they are correct or incorrect.

I have come to a certain stage.

I get the results in the picture below.

But even though they are all correct, the result I found and marked is incorrect.

They are all correct in marking.

At this stage I got help from many examples and chatgpt but I can't move forward.

The situation I expect

If the correct answer is marked, circle it in green and increase the number of correct answers by 1.

If the wrong answer is marked, circle the correct answer in green and the wrong answer in red.

And increase the number of incorrect by one.

If there is no marking, do not take any action and move to the next row.

If there are two markings on a line, for example, if A and C are marked, circle them both in red and move to the next line as if they were empty.

Can you help me about the error in my code and how to fix it?

Here is my code



import cv2

import numpy as np

import imutils

from imutils import contours

# Load the image

image_path = 'mmm.jpg'

image = cv2.imread(image_path)

image_path2 = 'anabos.jpg'

image2 = cv2.imread(image_path2)

ANSWER_KEY = {0:0,1:3,2:1,3:2,4:0,5:4,6:3,7:4,8:0,9:0,10:0,11:0,12:2,13:4,14:3,15:0,16:1,17:1,18:3,19:1,20:2,21:1,22:2,23:3,24:0,25:1,26:1,27:1,28:0,29:4,30:4,31:0,32:1,33:1,34:3,35:3,36:1,37:4,38:1,39:3}

# Convert the image to grayscale

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

x=837

y=746

w=168

h=1335

img=image[y:y+h, x:x+w]

img2=image2[y:y+h, x:x+w]

lower=(0,0,0)

upper=(0,0,255)

thresh = cv2.inRange(img, lower, upper)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10,10))

morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)

morph = cv2.morphologyEx(morph, cv2.MORPH_OPEN, kernel)

centers = []

contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

contours = contours[0] if len(contours) == 2 else contours[1]

i = 1

for cntr in contours:

    M = cv2.moments(cntr)

    cx = int(M["m10"] / M["m00"])

    cy = int(M["m01"] / M["m00"])

    centers.append((cx,cy))

    cv2.circle(img2, (cx, cy), 15, (0, 0, 0), -1)

    pt = (cx,cy)

    print("circle #:",i, "center:",pt)

    i = i + 1

    # Extract the region of interest (ROI) for the selected rectangle

gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

paper = img2

#cv2.imshow("gray2", gray2)

#cv2.imshow("paper", paper)

thresh = cv2.threshold(gray2, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,

    cv2.CHAIN_APPROX_SIMPLE)

cnts = imutils.grab_contours(cnts)

questionCnts = []

# loop over the contours

for c in cnts:

    # compute the bounding box of the contour, then use the

    # bounding box to derive the aspect ratio

    (x, y, w, h) = cv2.boundingRect(c)

    

    ar = w / float(h)

    

    # in order to label the contour as a question, region

    # should be sufficiently wide, sufficiently tall, and

    # have an aspect ratio approximately equal to 1

    #print(str(w))

    

    if w >= 10 and h >= 10 and ar >= 0.9 and ar <= 1.1:

        questionCnts.append(c)

  

# sort the question contours top-to-bottom, then initialize

# the total number of correct answers

questionCnts = sorted(questionCnts, key=lambda x: cv2.boundingRect(x)[1])

correct = 0

# each question has 5 possible answers, to loop over the

# question in batches of 5

for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):

    # sort the contours for the current question from

    # left to right, then initialize the index of the

    # bubbled answer

    cnts = sorted(questionCnts[q:q + 5], key=lambda x: cv2.boundingRect(x)[0])

    bubbled = None

    # loop over the sorted contours

    for (j, c) in enumerate(cnts):

        # construct a mask that reveals only the current

        # "bubble" for the question

        mask = np.zeros(thresh.shape, dtype="uint8")

        cv2.drawContours(mask, [c], -1, 255, -1)

        # apply the mask to the thresholded image, then

        # count the number of non-zero pixels in the

        # bubble area

        mask = cv2.bitwise_and(thresh, thresh, mask=mask)

        total = cv2.countNonZero(mask)

        print(total)

        # if the current total has a larger number of total

        # non-zero pixels, then we are examining the currently

        # bubbled-in answer

        if bubbled is None or total > bubbled[0]:

            bubbled = (total, j)

    # initialize the contour color and the index of the

    # *correct* answer

    color = (0, 0, 255)

    k = ANSWER_KEY[q]

    #print(bubbled)

    # check to see if the bubbled answer is correct

    if k == bubbled[1]:

        color = (0, 255, 0)

        correct += 1

    cv2.drawContours(paper, [cnts[k]], -1, color, 3)

  # grab the test taker

score = (correct)

print("[INFO] score: {:.2f}%".format(score))

cv2.imshow("Original", image)

cv2.imshow("Exam", paper)

cv2.imshow("thresh", thresh)

cv2.imshow("img", img)

cv2.waitKey(0)

My main sheet is; Main sheet

Threshold image like this; thresh

My result is; result

My expected result is like this; Expected result

Can you show me the way? How can I fix this?

1

There are 1 answers

0
fmw42 On

Here is how to use HoughCircles in Python/OpenCV to detect your circles. I hope it helps you.

Input:

enter image description here

import cv2
import numpy as np

# read the large marked exam
img = cv2.imread('mmm_exam.jpg') 

# crop coords
x=837
y=746
w=168
h=1335

# crop img
crop = img[y:y+h, x:x+w]

# convert to gray
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)

# get Hough Circles
# minDist is the minimum estimate for space between centers
# param1 usually does not need changing
# param2 has been decreased to value I usually find is needed to bring out the circles
# minRadius is minimum estimate for radius of circles
# maxRadius is maximum estimate for radius of circles
# Values measured manually on test image
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, minDist=30, param1=150, param2=10, minRadius=12, maxRadius=15)

# draw circles on copy of cropped image
gray = cv2.merge([gray,gray,gray])
result = gray.copy()
for circle in circles[0]:
    (x,y,r) = circle
    x = int(x)
    y = int(y)
    r = int(r)
    cv2.circle(result, (x, y), r, (0, 0, 255), 2)

# save results
cv2.imwrite('mmm_exam_crop.jpg', crop)
cv2.imwrite('mmm_exam_gray.jpg', gray)
cv2.imwrite('mmm_exam_circles.jpg', result)

# show results
cv2.imshow('crop', crop)
cv2.imshow('gray', gray)
cv2.imshow('result', result)
cv2.waitKey(0)

cropped image:

enter image description here

gray image:

enter image description here

circles on gray image:

enter image description here

If you now follow the link I gave you in my comment. The next step would be to count the number of non-zero pixels in the contours inside an Otsu threshold gray image to determine which contours are mostly filled (empty circles) and which are hardly filled (marked circles)