当前位置: 首页 > news >正文

计算机视觉(opencv)实战二十四——扫描答题卡打分

扫描答题卡打分系统原理与实现

答题卡自动阅卷是计算机视觉在教育领域的经典应用。本文将详细介绍如何使用 OpenCVNumPy 从扫描的答题卡图像中自动识别作答选项、比对标准答案并计算得分。

整体流程

整个答题卡打分系统可以拆分为以下环节:

  1. 图像预处理 – 灰度化、去噪、边缘检测。

  2. 答题卡定位与透视变换 – 提取答题卡轮廓,将倾斜的图像矫正。

  3. 二值化处理 – 将答题区域转为黑白图,便于检测圆圈。

  4. 轮廓提取与筛选 – 找出所有可能的选项圆圈。

  5. 按顺序排序选项 – 保证每一题的选项从左到右。

  6. 识别涂黑的选项 – 利用掩膜统计涂黑面积。

  7. 与答案比对、计算得分 – 给出总分并标记正确/错误选项。

下面我们逐步剖析代码和原理。


你给出的代码是答题卡自动打分的核心工具函数部分,负责几项关键操作:

  1. 四点排序 + 透视变换

  2. 轮廓排序

  3. 图像展示

我来帮你逐行解析一下,让你完全理解每个函数的作用和细节。


图片准备:

一、函数准备

1. 导入工具包与答案定义

import numpy as np
import cv2
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}  # 正确答案
  • NumPy:用于数组和数学运算(特别是点坐标计算)。

  • OpenCV:用于图像处理。

  • ANSWER_KEY:字典,表示每道题的正确选项。
    例如 {0: 1} 表示第0题的正确答案是第1个选项。


2. order_points – 四点排序

def order_points(pts):# 一共4个坐标点rect = np.zeros((4, 2), dtype="float32")  # 用来存储排序之后的坐标# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下s = pts.sum(axis=1)  # 对pts矩阵的每一行进行求和操作。(x+y)rect[0] = pts[np.argmin(s)]  # 左上角 -> x+y 最小rect[2] = pts[np.argmax(s)]  # 右下角 -> x+y 最大diff = np.diff(pts, axis=1)  # 对pts矩阵的每一行进行求差操作。(y - x)rect[1] = pts[np.argmin(diff)]  # 右上角 -> y-x 最小rect[3] = pts[np.argmax(diff)]  # 左下角 -> y-x 最大return rect

作用:
接收一个四边形的四个点,返回按 左上、右上、右下、左下 顺序排列的新数组。
这是透视变换的前提:如果点顺序错了,透视矫正就会失败。

原理:

  • x+y 最小的是左上点,因为它最靠近坐标原点。

  • x+y 最大的是右下点,因为它离原点最远。

  • y-x 最小的是右上点(因为 y 比 x 小)。

  • y-x 最大的是左下点。


3. four_point_transform – 透视变换

def four_point_transform(image, pts):rect = order_points(pts)(tl, tr, br, bl) = rectwidthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))maxWidth = max(int(widthA), int(widthB))heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))maxHeight = max(int(heightA), int(heightB))dst = np.array([[0, 0], [maxWidth - 1, 0],[maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]],dtype="float32")M = cv2.getPerspectiveTransform(rect, dst)warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))return warped

作用:
根据答题卡四个顶点的位置,将倾斜的答题卡矫正为正视图,方便后续处理。

关键步骤:

  1. 计算目标宽度、高度:

    • widthA:下边长度

    • widthB:上边长度

    • maxWidth:取两者最大值,避免变形

    • heightAheightB:左右边长度,取最大值作为目标高度

  2. 构建目标坐标:

    • 左上角 → (0,0)

    • 右上角 → (maxWidth-1,0)

    • 右下角 → (maxWidth-1,maxHeight-1)

    • 左下角 → (0,maxHeight-1)

  3. 计算透视矩阵
    cv2.getPerspectiveTransform 得到变换矩阵 M

  4. 执行透视变换
    cv2.warpPerspective 得到矫正后的图像。


4. sort_contours – 轮廓排序

def sort_contours(cnts, method='left-to-right'):reverse = Falsei = 0if method == 'right-to-left' or method == 'bottom-to-top':reverse = Trueif method == 'top-to-bottom' or method == 'bottom-to-top':i = 1boundingBoxes = [cv2.boundingRect(c) for c in cnts](cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),key=lambda b: b[1][i],reverse=reverse))return cnts, boundingBoxes

作用:
对轮廓按照 左右或上下顺序 排列,保证题目和选项的顺序一致。

  • i=0 → 按 x 坐标排序 (left-to-right)

  • i=1 → 按 y 坐标排序 (top-to-bottom)

返回值是排序后的轮廓和它们的外接矩形信息。


5. cv_show – 显示图像

def cv_show(name, img):cv2.imshow(name, img)cv2.waitKey(0)
  • 封装了 cv2.imshowcv2.waitKey,用于调试时查看每个阶段的图像效果。

  • 便于逐步验证每个处理步骤的结果。


要不要我帮你把这部分工具函数和后续的预处理、识别、打分流程整合在一起,写成一个可以直接运行的完整脚本?这样你可以直接跑通从图片到得分的全流程。

二、图像预处理

# 预处理
image = cv2.imread("./images/test_01.png")
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)

关键步骤:

  • 灰度化:将彩色图像转为灰度图,降低维度,减少噪声干扰。

  • 高斯模糊:平滑图像,去除高频噪声,否则后续边缘检测会产生虚假边缘。

  • Canny边缘检测:提取出答题卡外轮廓,为后续轮廓检测打下基础。


三、答题卡定位与透视变换

答题卡可能存在拍摄倾斜,需要通过轮廓检测找到卡片边界并执行透视变换。

# 轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 3)
cv_show('contours_img', contours_img)docCnt = None
# 根据轮廓大小进行排序,准备透视变换
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:  # 遍历每一个轮廓peri = cv2.arcLength(c, True)approx = cv2.approxPolyDP(c, 0.02 * peri, True)  # 轮廓近似if len(approx) == 4:docCnt = approxbreak# 执行透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)

原理解析:

  • 轮廓检测:通过 cv2.findContours 找出所有外部轮廓。

  • 面积排序:假设答题卡是图像中最大的矩形物体。

  • 多边形逼近:用 approxPolyDP 将轮廓近似为多边形,只保留四个点的轮廓。

  • 透视变换:利用四点坐标执行投影变换,得到俯视图,避免倾斜影响。


四、二值化处理


warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)# 阈值处理
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()

核心思想:

  • 将图像转为黑白二值图,背景变白,涂黑的选项变黑。

  • 使用 cv2.THRESH_BINARY_INV 反转颜色,方便后续统计像素点。


五、轮廓提取与筛选

# 找到每一个圆圈轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0, 255, 0), 1)
cv_show('warped_Contours', warped_Contours)questionCnts = []
for c in cnts:  # 遍历轮廓并计算比例和大小(x, y, w, h) = cv2.boundingRect(c)ar = w / float(h)# 根据实际情况指定标准if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:questionCnts.append(c)
print(len(questionCnts))

关键点:

  • 我们只保留大小合理、接近正方形的轮廓,以排除杂点。

  • 通过宽高比(aspect ratio)筛选圆形/方形的选项框。


六、按顺序排序选项

questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]
  • 先按纵向排序,保证从第一题到最后一题。

  • 每道题再按横向排序,保证选项顺序与答题卡一致。


七、识别涂黑的选项

原理:

  • 掩膜(mask):只保留当前选项的像素区域。

  • 统计黑色像素数量:涂得越满,非零像素数越大。

  • 选择涂得最满的选项:作为该题的最终答案。

八、与答案比对并计算分数

  • 绿色标记正确选项,红色标记错误选项。

  • 根据题目数量计算总分。

# 按照从上到下进行排序
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]
correct = 0
# 每排有5个选项
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):cnts = sort_contours(questionCnts[i:i + 5])[0]  # 排序bubbled = None# 遍历每一个结果for (j, c) in enumerate(cnts):# 使用mask来判断结果mask = np.zeros(thresh.shape, dtype="uint8")cv2.drawContours(mask, [c], -1, color=255, thickness=-1)  # -1表示填充cv_show('mask', mask)# 通过计算非零点数量来算是否选择这个答案# 利用掩膜(mask)进行“与”操作,只保留mask位置中的内容thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)cv_show('thresh_mask_and', thresh_mask_and)total = cv2.countNonZero(thresh_mask_and)  # 统计灰度值不为0的像素数if bubbled is None or total > bubbled[0]:  # 通过阈值判断,保存灰度值最大的序号bubbled = (total, j)# 对比正确答案color = (0, 0, 255)k = ANSWER_KEY[q]if k == bubbled[1]:  # 判断正确color = (0, 255, 0)correct += 1cv2.drawContours(warped_new, [cnts[k]], -1, color, thickness=3)  # 绘图cv_show('warpeding', warped_new)
score = (correct / 5.0) * 100

九、结果展示

最后将分数写在图像上,显示原图与判分结果。

print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), org=(10, 30),fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.9, color=(0, 0, 255), thickness=2)
cv2.imshow("Original", mat=image)
cv2.imshow("Exam", mat=warped_new)
cv2.waitKey(0)

运行结果:


十、总结

这套答题卡打分系统的核心优势:

  • 通用性强:可处理拍照角度不同、大小不同的答题卡。

  • 鲁棒性高:通过轮廓筛选和涂黑面积统计,能抵抗一定程度的噪声。

  • 易扩展:可以轻松修改 ANSWER_KEY,支持更多题目和多种题型。

未来可进一步优化:

  • 适配多行多列答题卡自动分区。

  • 使用深度学习模型识别异常涂卡(多选、未选)。

  • 增加UI和批量处理功能,支持学校实际批改场景。


文章转载自:

http://zfpvfWyC.rkhhL.cn
http://ENMRhG8F.rkhhL.cn
http://n7mfKs3A.rkhhL.cn
http://tU8Ktxos.rkhhL.cn
http://KNHACaEW.rkhhL.cn
http://cCDHK1wz.rkhhL.cn
http://xUAUaqJS.rkhhL.cn
http://Cy4F8dKT.rkhhL.cn
http://UxVFxMww.rkhhL.cn
http://kPi3lwuU.rkhhL.cn
http://6CMdKM5z.rkhhL.cn
http://t2iM5vMh.rkhhL.cn
http://3Gl8DFoX.rkhhL.cn
http://YuZGco0L.rkhhL.cn
http://VUTfAUGn.rkhhL.cn
http://SYshOpaO.rkhhL.cn
http://LcIPNNhE.rkhhL.cn
http://q502F9Kp.rkhhL.cn
http://DUBcksRF.rkhhL.cn
http://K9ttoNaL.rkhhL.cn
http://wJOcmxh8.rkhhL.cn
http://nnZM7PF0.rkhhL.cn
http://w6qo389W.rkhhL.cn
http://jkeThp9g.rkhhL.cn
http://nHpJAO2p.rkhhL.cn
http://ujiwnyWG.rkhhL.cn
http://JlPEU6jV.rkhhL.cn
http://unBD1z0z.rkhhL.cn
http://SsDi7jkx.rkhhL.cn
http://vtvwMqKT.rkhhL.cn
http://www.dtcms.com/a/387031.html

相关文章:

  • 居住证申请:线上照片回执办理!
  • Roo Code 的差异_快速编辑功能
  • 【深度学习】基于深度学习算法的图像版权保护数字水印技术
  • mcp初探
  • 深入C++对象生命周期:从构造到析构的奥秘
  • 视频上传以及在线播放
  • Powershell and Python are very similar
  • 鸿蒙Next离线Web组件实战:轻松实现离线加载与缓存优化
  • deepseek原理
  • 力扣复盘 之“移动零”
  • 任务管理系统常用平台整理:适合多项目团队
  • docker安装华为openGauss数据库
  • AI的设计图,神经网络架构
  • abaqus仿真完后如何把受力曲线显示出来
  • 核心硬件面试题目详解和回答策略之1
  • [MySQL]Order By:排序的艺术
  • Android创建新的自定义系统分区实现OTA内容修改
  • Linux内存管理章节十三:打通外设与内存的高速通道:深入Linux DMA与一致性内存映射
  • DIV居中
  • 扩散模型对齐:DMPO 让模型更懂人类偏好
  • nvidia jetson nano 连接蓝牙音响
  • 用Postman实现自动化接口测试和默认规范
  • [栈模拟]2197. 替换数组中的非互质数
  • 从零到一使用开源Keepalived配置实现高可用的集群教程
  • RAG与Fine-tuning-面试
  • Syslog服务
  • git clone vllm
  • 物联网的发展展望
  • PySpark处理超大规模数据文件:Parquet格式的使用
  • Spring Boot项目通过tomcat部署项目(包含jar包、war包)