基于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://www.pswp.cn/diannao/99143.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/99143.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LLaMA-MoE v2:基于后训练混合专家模型的稀疏性探索与技术突破

重新定义大型语言模型的效率边界在人工智能飞速发展的今天&#xff0c;大型语言模型&#xff08;LLMs&#xff09;已成为推动技术进步的核心力量。然而&#xff0c;模型规模的不断扩大带来了惊人的计算成本和高昂的部署门槛&#xff0c;使得众多研究机构和中小型企业难以承担。…

R geo 然后读取数据的时候 make.names(vnames, unique = TRUE): invalid multibyte string 9

setwd("K:/download/geo") # 替换为实际工作目录 # 修改get_geo_data_local函数中的读取部分 #file_path <- "K:/download/geo/raw_data/GEO/GSE32967_series_matrix_fixed.txt" file_path <- "K:/download/geo/data/GSE32967_series_matrix.t…

深入理解 Spring @Async 注解:原理、实现与实践

在现代 Java 应用开发中&#xff0c;异步编程是提升系统吞吐量和响应速度的关键技术之一。Spring 框架提供的Async注解极大简化了异步方法的实现&#xff0c;让开发者无需手动管理线程即可轻松实现异步操作。本文将从底层原理到实际应用&#xff0c;全面解析Async注解的工作机制…

linux C 语言开发 (七) 文件 IO 和标准 IO

文章的目的为了记录使用C语言进行linux 开发学习的经历。开发流程和要点有些记忆模糊&#xff0c;赶紧记录&#xff0c;防止忘记。 相关链接&#xff1a; linux C 语言开发 (一) Window下用gcc编译和gdb调试 linux C 语言开发 (二) VsCode远程开发 linux linux C 语言开发 (…

maven , mvn 运行 项目

提示&#xff1a;环境搭建 文章目录前言一、使用步骤1. 以构建含有 pom.xml 的项目2.mvn 运行具体项目3.mvn 指定模块>运行具体项目前言 提示&#xff1a;版本 spirngboot 3.2 jdk 21 mvn 3.9 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、使…

JVM垃圾回收的时机是什么时候(深入理解 JVM 垃圾回收时机:什么时候会触发 GC?)

深入理解 JVM 垃圾回收时机&#xff1a;什么时候会触发 GC&#xff1f;在 Java 开发中&#xff0c;我们常听说 “JVM 会自动进行垃圾回收”&#xff0c;但很少有人能说清&#xff1a;GC 究竟在什么情况下会被触发&#xff1f;是到固定时间就执行&#xff1f;还是内存满了才会启…

在Vue项目中Axios发起请求时的小知识

在Vue项目中Axios发起请求时的小知识 在Vue项目开发中&#xff0c;Axios作为基于Promise的HTTP客户端&#xff0c;凭借其简洁的API设计和强大的功能&#xff08;如请求/响应拦截、自动JSON转换、取消请求等&#xff09;&#xff0c;已成为前端与后端通信的主流选择。本文将深入…

GeoHash分级索引技术

GeoHash分级索引技术是一种将二维地理坐标转换为一维字符串的空间索引方法,其核心是通过分级网格划分和前缀编码实现高效的空间数据检索。以下从技术原理、实现细节到工程优化展开详细解析: 一、编码原理与分级结构 1. 经纬度二进制化 GeoHash通过递归二分地球表面生成网格…

HTML HTML基础(4)

1.列表 (1).有序列表 概念&#xff1a;有顺序或侧重顺序的列表。 <h2>要把大象放冰箱总共分几步</h2> <ol> <li>把冰箱门打开</li> <li>把大象放进去</li> <li>把冰箱门关上</li> </ol> (2).无序列表 概念&a…

MySQL中的回表操作

在数据库查询&#xff08;尤其是基于 B树索引 的关系型数据库&#xff0c;如MySQL、PostgreSQL&#xff09;中&#xff0c;“回表”是一个核心且高频出现的概念&#xff0c;直接影响查询性能。要理解回表&#xff0c;需先理清索引结构与数据存储的关联&#xff0c;再拆解其发生…

QT子线程与GUI线程安全交互

在Qt应用程序开发中&#xff0c;涉及到多线程处理时&#xff0c;如何安全地从子线程更新UI界面是一个常见的问题。Qt的UI界面并不是线程安全的&#xff0c;意味着你不能直接在子线程中操作UI组件&#xff08;比如按钮、标签等&#xff09;。如果不遵循线程安全的规则&#xff0…

RL【10-2】:Actor - Critic

系列文章目录 Fundamental Tools RL【1】&#xff1a;Basic Concepts RL【2】&#xff1a;Bellman Equation RL【3】&#xff1a;Bellman Optimality Equation Algorithm RL【4】&#xff1a;Value Iteration and Policy Iteration RL【5】&#xff1a;Monte Carlo Learnin…

开源大模型天花板?DeepSeek-V3 6710亿参数MoE架构深度拆解

文章目录认知解构&#xff1a;DeepSeek的定位与核心价值模型概述与发展历程创立初期与技术奠基&#xff08;2023年7月-2024年11月&#xff09;里程碑一&#xff1a;MoE架构规模化突破&#xff08;2024年12月&#xff09;里程碑二&#xff1a;推理成本革命性优化&#xff08;202…

10 训练中的一些问题

&#x1f31f; 大背景&#xff1a;训练神经网络 下山寻宝 训练神经网络就像你蒙着眼在一座大山里&#xff0c;想找最低点&#xff08;最小损失&#xff09;。你只能靠脚下的坡度&#xff08;梯度&#xff09;来决定往哪儿走。 你的位置 模型参数&#xff08;权重 www&#xf…

synchronized锁升级的过程(从无锁到偏向锁,再到轻量级锁,最后到重量级锁的一个过程)

锁升级是 Java 中 synchronized 锁 的核心优化机制&#xff08;基于 JVM 的 对象头 Mark Word 实现&#xff09;&#xff0c;指锁的状态从 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 逐步升级的过程。其目的是通过 “按需升级”&#xff0c;在不同并发场景下选择最优的锁实现&am…

HOT100--Day25--84. 柱状图中最大的矩形,215. 数组中的第K个最大元素,347. 前 K 个高频元素

HOT100–Day25–84. 柱状图中最大的矩形&#xff0c;215. 数组中的第K个最大元素&#xff0c;347. 前 K 个高频元素 每日刷题系列。今天的题目是《力扣HOT100》题单。 题目类型&#xff1a;栈&#xff0c;堆。 84. 柱状图中最大的矩形 思路&#xff1a; class Solution {publ…

基于 Apache Doris 的用户画像数据模型设计方案

一、 需求分析与设计目标数据源&#xff1a;用户基本信息&#xff1a;用户ID、性别、出生日期、注册时间、常驻地域&#xff08;省、市、区&#xff09;、职业等。用户体检报告&#xff1a;每次体检的报告ID、体检时间、各项指标&#xff08;如血压、血糖、血脂、BMI等&#xf…

Python的深度学习

深入理解Python高级特性掌握Python的高级特性是进阶的关键&#xff0c;包括装饰器、生成器、上下文管理器、元类等。这些特性能够提升代码的灵活性和效率。例如&#xff0c;装饰器可以用于实现AOP&#xff08;面向切面编程&#xff09;&#xff0c;生成器可以处理大数据流而无需…

数据库范式(Normalization)

一个设计混乱的数据库就像一个杂乱的房间&#xff0c;用起来非常不方便&#xff1a;东西到处乱放&#xff08;数据冗余&#xff09;&#xff0c;找件东西要翻遍所有角落&#xff08;查询困难&#xff09;&#xff0c;扔掉一把旧椅子时&#xff0c;可能会把搭在上面的唯一一件外…

数据结构---循环队列

基于循环数组实现的循环队列解决了顺序队列中的假溢出导致的空间浪费问题操作&#xff1a;&#xff08;1&#xff09;初始化//循环队列 typedef struct {int *data;//指针模拟声明数组int head,tail;//队头&#xff0c;队尾 }Queue; //初始化 Queue *InitQueue() {Queue *q (Q…