Python OpenCV Answer Sheet Recognition: A Step‑by‑Step Tutorial
This tutorial explains how to use Python and OpenCV to read an answer‑sheet image, preprocess it, detect contours, apply perspective transformation, locate each option, and determine selected answers by analyzing pixel intensity within detected circles.
This guide demonstrates a complete workflow for recognizing multiple‑choice answer sheets using Python's OpenCV library. The process includes image loading, preprocessing, contour detection, perspective correction, option localization, and answer determination.
Step 1: Pre‑processing (denoising, grayscale, binarization)
<code>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)</code>Note: cv2.THRESH_OTSU|cv2.THRESH_BINARY performs Otsu's adaptive thresholding with inverse binary output.
Step 2: Contour detection
<code># 找轮廓
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)</code>The largest contour corresponds to the answer‑sheet region.
Step 3: Contour approximation
<code># 轮廓近似
alpha = 0.02 * cv2.arcLength(cnts[0], True)
approxCurve = cv2.approxPolyDP(cnts[0], alpha, True)
cv2.drawContours(img.copy(),[approxCurve],-1,(255,0,0),2)</code>Approximation reduces the polygon to its corner points, which are later used for perspective transformation.
Step 4: Perspective transformation
<code># 透视变换
# 四个顶点 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)
# 计算宽高
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, new_a2, new_a3, new_a4 = [0,0], [0,h], [w,h], [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)</code>The transformed image isolates the answer‑sheet and corrects its orientation.
Step 5: Detect contours of each option
<code>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]
cv2.drawContours(res.copy(), contours, -1, (0,0,255), 1)</code>Only contours that satisfy a specific area‑to‑perimeter ratio are kept:
<code>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 7.05 < area/length < 10.5:
ans.append(i)
return ans
ans_contours = check(contours)
cv2.drawContours(res.copy(), ans_contours, -1, (0,255,255), 3)</code>Step 6: Fit enclosing circles, sort, and determine selected options
<code># 计算每个选项的外接圆
circle = []
for i in ans_contours:
(x,y), r = cv2.minEnclosingCircle(i)
center = (int(x), int(y))
r = int(r)
circle.append((center, r))
# 按 y 坐标排序(行),再按 x 坐标排序(列)
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)
</code>Finally, for each circle the algorithm counts white pixels inside the binary image; if the proportion exceeds a threshold (e.g., 40%), the option is marked as selected:
<code>def dots_distance(p1, p2):
return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**0.5
def count_dots(center, radius):
dots = []
for i in range(-radius, radius+1):
for j in range(-radius, radius+1):
pt = (center[0]+i, center[1]+j)
if dots_distance(center, pt) <= radius:
dots.append(pt)
return dots
da = []
for center, r in A:
dots = count_dots(center, r)
total = len(dots)
white = sum(1 for pt in dots if binary_res[pt[1]][pt[0]] == 255)
da.append(1 if white/total >= 0.4 else 0)
answers = np.array(da).reshape(5,5)
</code>The resulting 5×5 array represents the detected answers for the sheet.
Images illustrating each processing stage are omitted for brevity but are included in the original article.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.