Python OpenCV Answer Sheet Recognition: Preprocessing, Contour Detection, Perspective Transform, and Option Detection

This tutorial explains how to use Python and OpenCV to automatically process answer sheet images by performing preprocessing, contour detection, perspective transformation, and option analysis to extract selected answers as a structured array.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Python OpenCV Answer Sheet Recognition: Preprocessing, Contour Detection, Perspective Transform, and Option Detection

The article demonstrates a step‑by‑step method for automatically reading answer sheet images using Python and OpenCV, covering image preprocessing, contour detection, perspective correction, and option detection to produce a 5×5 answer matrix.

1. Preprocessing (denoise, grayscale, binarization)

img = cv2.imread("1.png",1)
#高斯去噪
img_gs = cv2.GaussianBlur(img,[5,5],0)
# 转灰度
img_gray = cv2.cvtColor(img_gs,cv2.COLOR_BGR2GRAY)
# 自适应二值化
_,binary_img = cv2.threshold(img_gray,0,255,cv2.THRESH_OTSU|cv2.THRESH_BINARY)

Note: cv2.THRESH_OTSU|cv2.THRESH_BINARY applies Otsu's adaptive thresholding with inverse binary output.

2. Contour Detection

#找轮廓
contours, hierarchy = cv2.findContours(binary_img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
#按照轮廓的面积从大到小排序
cnts = sorted(contours,key = cv2.contourArea,reverse=True)
#画轮廓
draw_img = cv2.drawContours(img.copy(),cnts[0],-1,(0,255,255),2)

The findContours function expects a binary image; cv2.RETR_EXTERNAL extracts only external contours, and cv2.CHAIN_APPROX_NONE returns all contour points.

3. Perspective Transform

#透视变换
#矩形的四个顶点为approxCurve[0][0],approxCurve[1][0],approxCurve[2][0],approxCurve[3][0]
#分别表示矩形的TL,BL,BR,TR四个点
a1 = list(approxCurve[0][0])
a2 = list(approxCurve[1][0])
a3 = list(approxCurve[2][0])
a4 = list(approxCurve[3][0])
#原始矩阵
mat1 = np.array([a1,a2,a3,a4],dtype = np.float32)
#计算矩形的w和h
w1 = int(np.sqrt((a1[0]-a4[0])**2+(a1[1]-a4[1])**2))
w2 = int(np.sqrt((a2[0]-a3[0])**2+(a2[1]-a3[1])**2))
h1 = int(np.sqrt((a1[0]-a2[0])**2+(a1[1]-a2[1])**2))
h2 = int(np.sqrt((a3[0]-a4[0])**2+(a3[1]-a4[1])**2))
w,h = max(w1,w2),max(h1,h2)
#目标矩阵
new_a1 = [0,0]
new_a2 = [0,h]
new_a3 = [w,h]
new_a4 = [w,0]
mat2 = np.array([new_a1,new_a2,new_a3,new_a4],dtype = np.float32)
#透视变换矩阵
mat = cv2.getPerspectiveTransform(mat1,mat2)
#进行透视变换
res = cv2.warpPerspective(img,mat,(w,h))
imshow((res))

The transformation aligns the answer sheet, removing surrounding background and correcting perspective distortion.

4. Detecting Each Option Contour

res_gray = cv2.cvtColor(res,cv2.COLOR_BGR2GRAY)
_,binary_res = cv2.threshold(res_gray,0,255,cv2.THRESH_OTSU|cv2.THRESH_BINARY_INV)
contours = cv2.findContours(binary_res,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)[0]
#挑选合适的轮廓
def check(contours):
    ans = []
    for i in contours:
        area = float(cv2.contourArea(i))
        length = float(cv2.arcLength(i,True))
        if area<=0 or length<=0:
            continue
        if area/length > 7.05 and area/length < 10.5:
            ans.append(i)
    return ans
ans_contours = check(contours)

5. Drawing Enclosing Circles, Sorting, and Locating Options

#遍历每一个圆形轮廓,画外接圆
circle = []
for i in ans_contours:
    (x,y),r = cv2.minEnclosingCircle(i)
    center = (int(x),int(y))
    r = int(r)
    circle.append((center,r))
#按照外接圆的水平坐标排序center[1](y坐标)
circle.sort(key = lambda x:x[0][1])
A = []
for i in range(1,6):
    now = circle[(i-1)*5:i*5]
    now.sort(key = lambda x:x[0][0])
    A.extend(now)

The sorted list A now holds the option circles from left‑to‑right and top‑to‑bottom.

6. Option Detection

def dots_distance(dot1,dot2):
    #计算二维空间中两个点的距离
    return ((dot1[0]-dot2[0])**2+(dot1[1]-dot2[1])**2)**0.5

def count_dots(center,radius):
    #输入圆的中心点与半径,返回圆内所有的坐标
    dots = []
    for i in range(-radius,radius+1):
        for j in range(-radius,radius+1):
            dot2 = (center[0]+i,center[1]+j)
            if dots_distance(center,dot2) <= radius:
                dots.append(dot2)
    return dots

da = []
for i in A:
    dots = count_dots(i[0],i[1])
    all_dots = len(dots)
    white_dots = 0
    for j in dots:
        if binary_res[j[1]][j[0]] == 255:
            white_dots += 1
    if white_dots/all_dots >= 0.4:
        da.append(1)
    else:
        da.append(0)

da = np.array(da)
da = np.reshape(da,(5,5))

The resulting 5×5 NumPy array da represents the selected answers, where 1 indicates a marked option and 0 indicates an unmarked one.

image-processinganswer-sheet-recognitioncomputer-vision
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.