OpenCV经典案例:01 答题卡识别
目录
透视变换矫正
选项识别匹配
QT 界面设计
引言:随着信息化的发展,计算机阅卷已经成为一种常规操作。在大型考试中,客观题基本不再 需要人工阅卷。本项目旨在开发一个基于OpenCV的高效答题卡识别系统,通过先进的图像处理和模式识别技术,实现对答题卡的快速准确分析。
文章所有资源请看文末!
透视变换矫正
假如有一张答题卡平放在地面上,那我们怎样去找到答题卡的边界轮廓呢?
答案是透视变换。首先我们需要找到答题卡的轮廓才能对选项做各种处理呀,接下来就是对透视变换的方法说明了。
假设原始图像中的点为,目标图像中的对应点为
。透视变换可以用一个 3x3 的矩阵
来描述:
其中,矩阵的元素取决于原始四边形和目标四边形顶点的坐标。其核心原理在于通过建立原始图像和目标图像之间的对应点关系,来计算一个变换矩阵。
综上所述,使用透视变换扫描得到答题卡边界具体步骤如下:
- 找到原始图像的4个顶点和目标图像的4个顶点
- 根据8个顶点构造原始图像到目标图像的变换矩阵
- 依据变换矩阵,实现原始图像到目标图像的变换,完成倾斜矫正
注意:用于构造变换矩阵使用的原始图像的4个顶点和目标图像的4个顶点的位置必须是匹配的,也就是说,要将左上、右上、左下、右下4个顶点按照相同的顺序排列。
OK,下面我们直接根据代码来进行说明。
import cv2
import math
import numpy as np# x坐标
def sortBy_x(pt):return pt[0]# y坐标
def sortBy_y(pt):return pt[1]def correct(path):try:answerSheet = cv2.imread(path)gray = cv2.cvtColor(answerSheet, cv2.COLOR_BGR2GRAY)blurred = cv2.GaussianBlur(gray, (3, 3), 0)canny = cv2.Canny(blurred, 75, 200)contours, Hierarchy = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)if len(contours) == 1:result_contour = contours[0]else:max_length = -1index = -1for i, contour in enumerate(contours):length = cv2.arcLength(contour, True)if length > max_length:max_length = lengthindex = iresult_contour = contours[index]pts = cv2.approxPolyDP(result_contour, 0.02 * cv2.arcLength(result_contour, True), True)if len(pts) != 4:raise ValueError("透视变换需要四个点,但检测到的点数量为{}".format(len(pts)))pts = np.array([pt[0] for pt in pts]) # 提取点坐标print(pts)pts = sorted(pts, key=sortBy_x)print(pts)pts = sorted(pts, key=sortBy_y)print(pts)print(pts[0][0])width1 = math.sqrt((pts[0][0] - pts[1][0]) ** 2 + (pts[0][1] - pts[1][1]) ** 2)width2 = math.sqrt((pts[2][0] - pts[3][0]) ** 2 + (pts[2][1] - pts[3][1]) ** 2)width = int(max(width1, width2))height1 = math.sqrt((pts[0][0] - pts[3][0]) ** 2 + (pts[0][1] - pts[3][1]) ** 2)height2 = math.sqrt((pts[2][0] - pts[1][0]) ** 2 + (pts[2][1] - pts[1][1]) ** 2)height = int(max(height1, height2))pts_dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype="float32")pts_src = np.array(pts, dtype="float32")M = cv2.getPerspectiveTransform(pts_src, pts_dst)birdMat = cv2.warpPerspective(answerSheet, M, (width, height))return birdMatexcept Exception as e:print(f"Error in correct: {e}")return None
1、首先对读取的图像进行一系列预处理操作:灰度转换、滤波、边缘检测等以凸显图像特征
2、使用cv2.findContours查找图像轮廓
当轮廓数量为1时,直接将其结果作为轮廓。
否则通过计算 每个轮廓的弧长,找到弧长最长的轮廓作为结果轮廓。
3、使用cv2.approxPolyDP函数对结果轮廓进行多边形逼近,得到近似的顶点坐标
4、将顶点坐标提取出来,并分别按照x坐标和y坐标进行排序,同时计算相邻两点之间的距离,取最大值作为宽度和高度,并据此计算目标顶点
5、cv2.getPerspectiveTransfor计算变换矩阵,cv2.warpPerspective根据变换矩阵对原始图像进行透视变换,得到矫正后的图像
效果如下:

选项识别匹配
答题卡轮廓边界得到之后就是对选项的处理了。
import cv2
import numpy as np
import mathdef sortBy_x(pt):return pt[0]def sortBy_y(pt):return pt[1]def recognition(path, imageIndex):try:answerSheet = cv2.imread(path)gray = cv2.cvtColor(answerSheet, cv2.COLOR_BGR2GRAY)blurred = cv2.GaussianBlur(gray, (3, 3), 0)canny = cv2.Canny(blurred, 75, 200)contours, _ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)if len(contours) == 1:result_contour = contours[0]else:max_length = -1index = -1for i, contour in enumerate(contours):length = cv2.arcLength(contour, True)if length > max_length:max_length = lengthindex = iresult_contour = contours[index]pts = cv2.approxPolyDP(result_contour, 0.02 * cv2.arcLength(result_contour, True), True)if len(pts) != 4:raise ValueError("识别需要四个点,但检测到的点数量为{}".format(len(pts)))pts = np.array([pt[0] for pt in pts])pts = sorted(pts, key=sortBy_x)pts = sorted(pts, key=sortBy_y)width1 = math.sqrt((pts[0][0] - pts[1][0]) ** 2 + (pts[0][1] - pts[1][1]) ** 2)width2 = math.sqrt((pts[2][0] - pts[3][0]) ** 2 + (pts[2][1] - pts[3][1]) ** 2)width = int(max(width1, width2))height1 = math.sqrt((pts[0][0] - pts[3][0]) ** 2 + (pts[0][1] - pts[3][1]) ** 2)height2 = math.sqrt((pts[2][0] - pts[1][0]) ** 2 + (pts[2][1] - pts[1][1]) ** 2)height = int(max(height1, height2))pts_dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype="float32")pts_src = np.array(pts, dtype="float32")M = cv2.getPerspectiveTransform(pts_src, pts_dst)birdMat = cv2.warpPerspective(answerSheet, M, (width, height))cv2.imshow("original", birdMat)################# 识别 ##############################gray_birdMat = cv2.cvtColor(birdMat, cv2.COLOR_BGR2GRAY)_, target = cv2.threshold(gray_birdMat, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)cv2.imshow("Img", target)element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))target = cv2.dilate(target, element)cv2.imshow("image", target)# 提取选项contours, _ = cv2.findContours(target, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# print(contours)selected_contours = [c for c in contours if cv2.boundingRect(c)[2] > 20 and cv2.boundingRect(c)[3] > 20]answerSheet_con = cv2.cvtColor(target, cv2.COLOR_GRAY2BGR)cv2.drawContours(answerSheet_con, selected_contours, -1, (0, 0, 255), 2)# 选项定位、二维数组存储radius = []center = []for contour in selected_contours:(x, y), r = cv2.minEnclosingCircle(contour)radius.append(r)center.append((int(x), int(y)))x_min = min(center, key=lambda x: x[0])[0]x_max = max(center, key=lambda x: x[0])[0]x_interval = (x_max - x_min) // 4y_min = min(center, key=lambda x: x[1])[1]y_max = max(center, key=lambda x: x[1])[1]y_interval = (y_max - y_min) // 4classed_contours = [[[] for _ in range(5)] for _ in range(5)]for i, point in enumerate(center):index_x = round((point[0] - x_min) / x_interval)index_y = round((point[1] - y_min) / y_interval)classed_contours[index_y][index_x] = selected_contours[i]colors = [(0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 0, 0), (0, 255, 0)]test_result = cv2.cvtColor(target, cv2.COLOR_GRAY2BGR)for i in range(5):for j in range(5):if len(classed_contours[i][j]) > 0:cv2.drawContours(test_result, classed_contours[i][j], -1, colors[i], 2)# 答案自定义,只有 5个选项correct_answers = [0, 4, 4, 2, 1]# 定义选项位置result_count = np.zeros((5, 5), dtype=int)re_rect = [[cv2.boundingRect(contour) for contour in row] for row in classed_contours]count_roi = np.zeros((5, 5), dtype=np.float32)min_count = 999max_count = -1for i in range(5):for j in range(5):if len(classed_contours[i][j]) > 0:rect = re_rect[i][j]tem = target[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]]count = cv2.countNonZero(tem)if count > max_count:max_count = countif count < min_count:min_count = countcount_roi[i][j] = countmean = (max_count - min_count) // 2option_diff = np.abs(count_roi - max_count)for i in range(5):for j in range(5):if option_diff[i][j] < mean:result_count[i][j] += 1# 进行审阅label_answer = birdMat.copy()correct_count = 0wrong_answers = {}for i in range(5):selected = []for j in range(5):if result_count[i][j] == 1:selected.append(j)if j == correct_answers[i]:cv2.drawContours(label_answer, classed_contours[i][j], -1, (255, 0, 0), 2)else:cv2.drawContours(label_answer, classed_contours[i][j], -1, (0, 0, 255), 2)# 记录题目数量、正确题数、错题if len(selected) == 0:continue # 未作答,不做任何处理elif len(selected) == 1:if selected[0] == correct_answers[i]:correct_count += 1else:wrong_answers[i + 1] = chr(65 + selected[0]) # 错误选项else:blue_count = sum(1 for j in selected if j == correct_answers[i])red_count = len(selected) - blue_countif blue_count > 0 and red_count > 0:wrong_answers[i + 1] = '多选'total_questions = len(correct_answers)score = correct_count / total_questions * 100data = {"序号": "{:02}".format(imageIndex + 1),"成绩": score,"题目总数": total_questions,"错题": str(wrong_answers),"正确题数": correct_count}return label_answer, dataexcept Exception as e:print(f"Error in recognition: {e}")return None, None
1、首先仍就是图像预处理,这通常会使得我们更易于提取选项,得到其位置。将变换后的图像转为灰度图并进行反二阈值化凸显选项,随后进行膨胀操作以连接断开的部分或填充小的空洞。
2、提取选项轮廓。通过cv2.findContours得到所有轮廓,随后对每个轮廓进行筛选,只有宽度和高度均大于20像素的轮廓才会被保留下来,这样就能够得到选项了。
3、选项定位与分类。计算每个符合条件的轮廓的最小外接圆的圆心和半径。根据圆心坐标,将选项按照水平和垂直方向进行分类并存储到二维数组中。
4、答案识别与审阅。
- 自定义正确答案,用数字标识答案位置,默认从0开始。
- 为每个选项区域计算非零像素的数量。
- 通过计算得到的数量与平均值,确定每个选项的选择情况并存储到二维数组中。
5、审阅结果展示与数据统计。比较二维数组与正确答案,绘制正确和错误选项的轮廓,正确为蓝色,错误为红色;同时统计正确题数、计算分数,并将相关数据存储到字典中。
效果如下:

QT 界面设计
本次界面设计使用的是pyqt5,我也只是初学,所以做的界面不是很好,但也勉强还算看的过眼吧。这个界面其实就是把变换后的图像和识别检测的结果弄到展示窗口,然后把记录的数据信息这些保存到excel表而已,说实在的还是太简陋了呀。OK,下面我们直接看效果吧。
答题卡识别
好的,以上就是本次项目的所有内容了,希望对大家有所帮助呀,有疑问的可以评论或私聊我解答哟!
文章所有资源有需要的可自取
百度网盘链接: https://pan.baidu.com/s/1pFeaKRGAwF1zfip_wqt_dQ 提取码: 0bw7
相关文章:
OpenCV经典案例:01 答题卡识别
目录 透视变换矫正 选项识别匹配 QT 界面设计 引言:随着信息化的发展,计算机阅卷已经成为一种常规操作。在大型考试中,客观题基本不再 需要人工阅卷。本项目旨在开发一个基于OpenCV的高效答题卡识别系统,通过先进的图像处理和模…...
进程的管理与控制详解:创建、终止、阻塞等待与非阻塞等待
目录 一、进程创建 1、实例 2、fork函数详解 (1)fork函数模板 (2). fork() 函数的工作原理 (3). fork() 返回值和错误处理 3、如何理解进程创建过程 二、进程终止 1、终止是在做什么? 2、进程终止,有三种情况 3、进程如何终止? 三…...
【从零开始一步步学习VSOA开发】开发环境搭建
开发环境搭建 开发 VSOA 首先需要搭建开发环境,这里讲解 Windows 下 C/C 开发环境搭建方法。 下载 IDE 并申请授权码 SylixOS 的开发和部署需要 RealEvo-IDE 的支持,因此您需要先获取 RealEvo-IDE 的安装包和注册码。 RealEvo-IDE 分为体验版和商业版…...
一篇文章让你用我的世界中的红石搞懂什么是ALU!
目录 1.一些在开始的约定 2.七大逻辑门电路 1、 与门 2、 或门 3、 非门 5、 或非门 6、 异或门 7、 同或门 3.半加器 4.全加器 5.ALU 1.一些在开始的约定 相同的概念:相同的概念:高电平低电平逻辑真逻辑假 开关的开 开关的关 灯的亮 灯…...
硬盘数据恢复:所需时长、全面指南及注意事项
在数字化时代,硬盘作为我们存储重要数据的核心设备,其重要性不言而喻。然而,由于各种原因,如误删除、格式化、硬盘故障等,我们时常面临数据丢失的困境。数据恢复不仅关乎个人隐私和信息安全,更可能影响到我…...
基于SpringBoot+Vue的科研管理系统(带1w+文档)
基于SpringBootVue的科研管理系统(带1w文档) 基于SpringBootVue的科研管理系统(带1w文档) 科研的管理系统设计过程中采用Java开发语言,B/S结构,采取springboot框架,并以MySql为数据库进行开发。结合以上技术,对本系统的整体、数据库、功能模块…...
计算机组成原理 —— 五段式指令流水线
计算机组成原理 —— 五段式指令流水线 五段式指令流水线运算类指令LOAD指令的执行过程STORE指令的执行过程条件转移指令执行过程无条件转移指令的执行过程 我们今天来看看五段式指令流水线: 五段式指令流水线 五段式指令流水线是一种常见的处理器架构设计中采用的…...
【Bigdata】什么是关系联机分析处理
这是我父亲 日记里的文字 这是他的生命 留下留下来的散文诗 几十年后 我看着泪流不止 可我的父亲已经 老得像一个影子 🎵 许飞《父亲写的散文诗》 关系联机分析处理(Relational Online Analytical Processing,简称 ROLA…...
svd在求解最小二乘中的应用
文章目录 线性最小二乘的直接解法(正规方程解法)什么是伪逆?伪逆矩阵的一般形式伪逆矩阵与SVD的关系 线性最小二乘的直接解法(正规方程解法) 对于 A x b \boldsymbol{A}xb Axb的线性最小二乘问题,有直解析…...
JVM—垃圾收集算法和HotSpot算法实现细节
参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明 1、分代回收策略 分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取…...
nvidia系列教程-AGX-Orin基础环境搭建
目录 前言 一、Agx-Orin(32GB)介绍 1.1 GPU 1.2 CPU 1.3 NVDLA 1.4 内存 1.5 存储 二、安装JetPack SDK 三、基础环境配置 四、jetpack软件版本 总结 前言 NVIDIA Jetson AGX Orin 是一款功能强大的嵌入式AI平台,专为需要高性能和低…...
使用SpringAOP实现公共字段填充
文章目录 概要整体架构流程技术细节小结 概要 在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表…...
c++初阶-----适配器---priority_queue
作者前言 🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂 🎂 作者介绍: 🎂🎂 🎂 🎉🎉🎉…...
VSCode上安装C#环境教程
本章教程,教你如何在vscode上,可以快速运行一些基础的c#代码。 1、下载 .NET Code SDK 下载地址:https://dotnet.microsoft.com/zh-cn/download/dotnet/sdk-for-vs-code?utm_source=vs-code&utm_medium=referral&utm_campaign=sdk-install 根据自己的操作系统,选择…...
VS Code 和 Visual Studio 哪个更好
文章目录 VS Code 和 Visual Studio 哪个更好Visual Studio Code简介Visual Studio简介相同点差异点总结 VS Code 和 Visual Studio 哪个更好 Visual Studio Code简介 Visual Studio Code(简称 VS Code)是一款开源的、免费的、跨平台的、轻量级的代码编…...
FCA-数据分析理论试卷
其他参考: https://segmentfault.com/a/1190000043363073 https://blog.csdn.net/CSDN_WYY/article/details/137082340 Part.1:判断题(总分:8分 得分:8) 第1题 判断题 对任意事件A和B,必有 …...
WPF程序通过CadLib4加载CAD .dwg格式文件
1、下载CadLib相关dll文件,主要用到的:WW.dll、WW.Cad.dll、WW.GL.dll 2、程序中引用dll库。 3、创建WPF程序,使用Canvas来加载dwg文件,支持拖动和放大缩小。 4、部分代码: public void Init(string filename) {tr…...
图表全能王(ChartStudio) 上架VisionPro!
图表全能王(ChartStudio) - 终极图表制作工具!支持条形图、折线图、面积图、柱形图、条形图、饼图、玫瑰图、雷达图、牛肉图、风琴图、旭日图、桑基图等图表。 https://apps.apple.com/app/chartstudio-data-analysis/id6474099675 https://apps.apple.com/cn/app/…...
【云原生】Job一次性任务详解
Job一次性任务 文章目录 Job一次性任务一、Job介绍二、运行示例Job 一、Job介绍 Job会创建一个或者多个Pod,并将继续重试Pod的执行,直到指定数量的Pod成功终止。随着Pod成功借宿,Job跟踪记录成功完成的Pod个数。当数量达到指定的成功个数阈值…...
化工厂人员定位采用多种定位技术的融合定位系统的好处
由于化工厂内环境的复杂性和危险性,通常单一的定位技术很难满足全厂区的人员定位需求,如果能将不同定位技术融合在一起,发挥出它们各自的优势,那么就能解决以上问题。 融合定位技术诞生背景 随着科技的不断发展,多种定…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
