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

基于OpenCV的答题卡自动识别与评分系统

引言

在教育考试场景中,手动批改答题卡效率低下且容易出错。本文将介绍如何使用Python和OpenCV实现一个答题卡自动识别与评分系统,通过计算机视觉技术完成答题卡的透视校正、选项识别和得分计算。该系统可广泛应用于学校考试、培训测评等场景,大幅提升批改效率。


环境准备

  • Python 3.7+
  • OpenCVpip install opencv-python
  • NumPypip install numpy

核心功能模块解析

整个系统分为以下核心步骤:
图像预处理 → 轮廓检测 → 透视变换 → 选项定位 → 答案匹配 → 结果输出

1. 导入依赖库

import numpy as np
import cv2
  • numpy:用于数值计算和数组操作。
  • cv2:OpenCV库,提供图像处理和计算机视觉算法。

2. 辅助函数定义

2.1 坐标点排序函数 order_points

答题卡的四个角点需要按左上→右上→右下→左下的顺序排列,否则透视变换会出错。
该函数通过计算坐标的和与差实现排序:

def order_points(pts):rect = np.zeros((4, 2), dtype="float32")s = pts.sum(axis=1)  # 计算(x+y),左上角点(x+y)最小,右下角点最大rect[0] = pts[np.argmin(s)]  # 左上rect[2] = pts[np.argmax(s)]  # 右下diff = np.diff(pts, axis=1)  # 计算(y-x),右上角点(y-x)最小(接近0),左下角点最大rect[1] = pts[np.argmin(diff)]  # 右上rect[3] = pts[np.argmax(diff)]  # 左下return rect
2.2 透视变换函数 four_point_transform

通过透视变换将倾斜的答题卡校正为正视图。
关键步骤:

  1. 计算变换前的四个角点(rect)。
  2. 确定变换后的目标尺寸(maxWidthmaxHeight)。
  3. 生成透视变换矩阵(M)并应用变换。
def four_point_transform(image, pts):rect = order_points(pts)(tl, tr, br, bl) = rect  # 解包四个角点# 计算变换后的宽度和高度(取两组对边的最大值)widthA = 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
2.3 轮廓排序函数 sort_contours

检测到的选项轮廓需要按从上到下、从左到右排序,以便逐题匹配答案。
支持多种排序方式(左→右、右→左、上→下、下→上):

def sort_contours(cnts, method="left-to-right"):reverse = Falsei = 0  # 排序依据:0为x轴(左右),1为y轴(上下)if method in ["right-to-left", "bottom-to-top"]:reverse = Trueif method in ["top-to-bottom", "bottom-to-top"]:i = 1# 计算每个轮廓的包围盒(x,y,w,h)boundingBoxes = [cv2.boundingRect(c) for c in cnts]# 按包围盒的指定维度排序(x或y坐标)(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse))return cnts, boundingBoxes
2.4 图像显示函数 cv_show

调试时用于显示中间结果:

def cv_show(name, img):cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()

3. 图像预处理与轮廓检测

3.1 读取图像并灰度化
image = cv2.imread("images/test_01.png")  # 替换为你的答题卡路径
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 灰度化减少计算量
3.2 高斯模糊去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0)  # 5x5核,σ=0
cv_show('blurred', blurred)  # 调试:观察模糊效果

高斯模糊可消除图像中的高频噪声(如纸张纹理、光照不均),避免后续边缘检测出现伪影。

3.3 Canny边缘检测
edged = cv2.Canny(blurred, 75, 200)  # 阈值75和200
cv_show('edged', edged)  # 调试:观察边缘轮廓

Canny算法通过梯度计算提取图像边缘,参数75200分别为低阈值和高阈值,用于区分强边缘和弱边缘。

3.4 轮廓检测与筛选
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]  # 获取外层轮廓
# 按面积降序排序(最大的轮廓通常是答题卡)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
# 寻找近似四边形(答题卡的四个角点)
docCnt = None
for c in cnts:peri = cv2.arcLength(c, True)  # 计算轮廓周长approx = cv2.approxPolyDP(c, 0.02 * peri, True)  # 多边形近似(精度0.02倍周长)if len(approx) == 4:  # 筛选四边形docCnt = approxbreak
  • cv2.findContours:检测图像中的轮廓,RETR_EXTERNAL表示只检测外层轮廓。
  • approxPolyDP:通过道格拉斯-普克算法简化轮廓,保留关键顶点(答题卡的四个角点)。

4. 透视变换校正答题卡

warped_t = four_point_transform(image, docCnt.reshape(4, 2))  # 应用透视变换
cv_show('warped', warped_t)  # 调试:观察校正后的答题卡

通过four_point_transform函数,倾斜的答题卡被校正为正视图,便于后续选项定位。


5. 选项区域定位与识别

5.1 二值化处理
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)  # 转为灰度图
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]  # Otsu自适应阈值
cv_show('thresh', thresh)  # 调试:观察二值化结果
  • THRESH_BINARY_INV:反转二值化结果(选项填涂区域为白色,背景为黑色)。
  • THRESH_OTSU:自动计算最佳阈值,适应不同光照条件。
5.2 筛选选项轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]  # 检测选项轮廓
questionCnts = []
for c in cnts:(x, y, w, h) = cv2.boundingRect(c)ar = w / float(h)  # 计算宽高比# 筛选条件:尺寸足够大且接近正方形(0.9≤宽高比≤1.1)if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:questionCnts.append(c)
print(f"检测到{len(questionCnts)}个选项轮廓")

通过宽高比(接近1)和最小尺寸(避免噪声)筛选出有效选项轮廓。

5.3 按题目分组排序
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]  # 按从上到下排序

假设每道题有5个选项,按行分组后逐题处理。


6. 答案匹配与评分

6.1 定义正确答案
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}  # 键为题号,值为正确选项索引(0~4)

根据实际题目修改ANSWER_KEY,例如第0题正确选项是第1个(索引从0开始)。

6.2 逐题识别答案
correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):  # 每5个选项为一题cnts = sort_contours(questionCnts[i:i+5])[0]  # 当前题的5个选项(按左→右排序)bubbled = None  # 记录当前题填涂最深的选项for (j, c) in enumerate(cnts):  # 遍历每个选项# 创建掩膜(仅保留当前选项区域)mask = np.zeros(thresh.shape, dtype="uint8")cv2.drawContours(mask, [c], -1, 255, -1)  # -1表示填充轮廓内部# 计算掩膜区域的非零像素数(填涂程度)thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)total = cv2.countNonZero(thresh_mask_and)# 更新填涂最深的选项if bubbled is None or total > bubbled[0]:bubbled = (total, j)# 匹配正确答案k = ANSWER_KEY[q]  # 当前题的正确选项索引if k == bubbled[1]:  # 填涂选项与正确答案一致color = (0, 255, 0)  # 绿色标记正确correct += 1else:color = (0, 0, 255)  # 红色标记错误# 在校正后的图像上绘制结果cv2.drawContours(warped_t, [cnts[k]], -1, color, 3)
  • 掩膜技术:通过mask仅保留当前选项的区域,统计该区域的白色像素数(填涂程度),像素数最多的选项即为填涂答案。
  • 结果可视化:正确选项用绿色框标记,错误选项用红色框标记。
6.3 计算得分并输出
score = (correct / len(ANSWER_KEY)) * 100  # 总题数为ANSWER_KEY的长度
print(f"[INFO] 得分: {score:.2f}%")
# 在图像上显示得分
cv2.putText(warped_t, f"{score:.2f}%", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Result", warped_t)
cv2.waitKey(0)

运行结果示例

假设测试图像test_01.png是一张5题的答题卡,其中3题正确,2题错误,则输出:

检测到25个选项轮廓
[INFO] 得分: 60.00%

最终图像会显示校正后的答题卡,正确选项为绿色框,错误选项为红色框,并标注得分。


注意事项与改进方向

  1. 图像质量:确保答题卡光照均匀、无遮挡,否则可能导致轮廓检测失败。
  2. 答案键配置:需根据实际题目修改ANSWER_KEY字典。
  3. 鲁棒性优化:可添加轮廓面积过滤、倾斜角度校正等功能,适应更复杂的拍摄场景。
  4. 多题型支持:当前仅支持单选题,可扩展支持多选题(通过统计多个最高像素数的选项)。


文章转载自:

http://ScLazpMU.xwgbr.cn
http://A5gtiIwH.xwgbr.cn
http://fveMwmiF.xwgbr.cn
http://I8AZqtRK.xwgbr.cn
http://hE8RRN2X.xwgbr.cn
http://8RoIXGOw.xwgbr.cn
http://CEYAq334.xwgbr.cn
http://9Hq4vFUb.xwgbr.cn
http://hZPRcCIF.xwgbr.cn
http://fstiM8L6.xwgbr.cn
http://5is6tiV7.xwgbr.cn
http://vSb7ZJSi.xwgbr.cn
http://gHzgQdw7.xwgbr.cn
http://jVsxfPgw.xwgbr.cn
http://lFe5u0jR.xwgbr.cn
http://ZY5g5L5Q.xwgbr.cn
http://rP9Wq8Yt.xwgbr.cn
http://DpAm4iYZ.xwgbr.cn
http://iE5TrtXa.xwgbr.cn
http://zrCFVUmI.xwgbr.cn
http://5Hck731U.xwgbr.cn
http://mn22ZayN.xwgbr.cn
http://EbiwcBt5.xwgbr.cn
http://E8zAxP4e.xwgbr.cn
http://wTnWVsvl.xwgbr.cn
http://cfDiGvfy.xwgbr.cn
http://4a9YS8lV.xwgbr.cn
http://tlt4qPQA.xwgbr.cn
http://b6vMCGOd.xwgbr.cn
http://LLeRzkap.xwgbr.cn
http://www.dtcms.com/a/382893.html

相关文章:

  • 贪心算法应用:出租车调度问题详解
  • 【RK3576】【Android14】如何在Android14下单独编译kernel-6.1?
  • FlashAttention(V2)深度解析:从原理到工程实现
  • ​Prometheus+Grafana监控系统配置与部署全解
  • 电路调试过程中辨认LED正负极并焊接
  • ubuntu24.04 缺少libwebkit2gtk-4.0和libssl.so.1.1
  • eslint-config-encode 使用指南
  • MySQL高阶查询语句与视图实战指南
  • 金融数学与应用数学(金融方向)课程重合度高吗?
  • 知识沉淀过于碎片化如何形成体系化框架
  • 第二十篇|SAMU教育学院的教育数据剖析:制度阈值、能力矩阵与升学网络
  • 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第十章知识点问答(10题)
  • dockercompose和k8s区别
  • HENGSHI SENSE 6.0技术解密:边缘计算+Serverless架构如何重构企业级BI实时性
  • Delphi - IndyHttpServer接收上传文件
  • 1.linux环境配置+ssh远程连接vscode调试(问题:无法联网,无法共享粘贴板,不满足运行vscode服务器的先决条件)
  • unity导入blender动画
  • 【杂谈】-备份革命:解锁AI时代的“死数据“金矿
  • npm 发布流程——从创建组件到发布到 npm 仓库
  • 单变量单步时序预测 | TCN-GRU时间卷积神经网络结合门控循环单元
  • 分布式协议与算法实战-理论篇
  • 《sklearn机器学习——数据预处理》生成多项式特征
  • XLua教程之入门篇
  • java学习笔记----标识符与变量
  • C7.1:谐振和调谐的含义
  • 代码随想录学习(一)——数组理论基础
  • Windows 平台上基于 MCP 构建“文心一言+彩云天气”服务实战
  • leetcode38(二叉树的最大深度)
  • PyTorch实战(7)——循环神经网络
  • 【LeetCode hot100|Week2】滑动窗口,子串