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

OpenCV经典案例:01 答题卡识别

目录

透视变换矫正

选项识别匹配

QT 界面设计


引言:随着信息化的发展,计算机阅卷已经成为一种常规操作。在大型考试中,客观题基本不再 需要人工阅卷。本项目旨在开发一个基于OpenCV的高效答题卡识别系统,通过先进的图像处理和模式识别技术,实现对答题卡的快速准确分析。

文章所有资源请看文末!

透视变换矫正

假如有一张答题卡平放在地面上,那我们怎样去找到答题卡的边界轮廓呢?

答案是透视变换。首先我们需要找到答题卡的轮廓才能对选项做各种处理呀,接下来就是对透视变换的方法说明了。

假设原始图像中的点为(x,y),目标图像中的对应点为(X,Y)。透视变换可以用一个 3x3 的矩阵M来描述:

\begin{bmatrix} X\\ Y\\ 1 \end{bmatrix}=M\times \begin{bmatrix} x\\ y\\ 1 \end{bmatrix}

其中,矩阵M的元素取决于原始四边形和目标四边形顶点的坐标。其核心原理在于通过建立原始图像和目标图像之间的对应点关系,来计算一个变换矩阵

综上所述,使用透视变换扫描得到答题卡边界具体步骤如下:

  1. 找到原始图像的4个顶点和目标图像的4个顶点
  2. 根据8个顶点构造原始图像到目标图像的变换矩阵
  3. 依据变换矩阵,实现原始图像到目标图像的变换,完成倾斜矫正

注意:用于构造变换矩阵使用的原始图像的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计算变换矩阵Mcv2.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 界面设计 引言&#xff1a;随着信息化的发展&#xff0c;计算机阅卷已经成为一种常规操作。在大型考试中&#xff0c;客观题基本不再 需要人工阅卷。本项目旨在开发一个基于OpenCV的高效答题卡识别系统&#xff0c;通过先进的图像处理和模…...

进程的管理与控制详解:创建、终止、阻塞等待与非阻塞等待

目录 一、进程创建 1、实例 2、fork函数详解 (1)fork函数模板 (2). fork() 函数的工作原理 (3). fork() 返回值和错误处理 3、如何理解进程创建过程 二、进程终止 1、终止是在做什么&#xff1f; 2、进程终止&#xff0c;有三种情况 3、进程如何终止&#xff1f; 三…...

【从零开始一步步学习VSOA开发】开发环境搭建

开发环境搭建 开发 VSOA 首先需要搭建开发环境&#xff0c;这里讲解 Windows 下 C/C 开发环境搭建方法。 下载 IDE 并申请授权码 SylixOS 的开发和部署需要 RealEvo-IDE 的支持&#xff0c;因此您需要先获取 RealEvo-IDE 的安装包和注册码。 RealEvo-IDE 分为体验版和商业版…...

一篇文章让你用我的世界中的红石搞懂什么是ALU!

目录 1.一些在开始的约定 2.七大逻辑门电路 1、 与门 2、 或门 3、 非门 5、 或非门 6、 异或门 7、 同或门 3.半加器 4.全加器 5.ALU 1.一些在开始的约定 相同的概念&#xff1a;相同的概念&#xff1a;高电平低电平逻辑真逻辑假 开关的开 开关的关 灯的亮 灯…...

硬盘数据恢复:所需时长、全面指南及注意事项

在数字化时代&#xff0c;硬盘作为我们存储重要数据的核心设备&#xff0c;其重要性不言而喻。然而&#xff0c;由于各种原因&#xff0c;如误删除、格式化、硬盘故障等&#xff0c;我们时常面临数据丢失的困境。数据恢复不仅关乎个人隐私和信息安全&#xff0c;更可能影响到我…...

基于SpringBoot+Vue的科研管理系统(带1w+文档)

基于SpringBootVue的科研管理系统(带1w文档) 基于SpringBootVue的科研管理系统(带1w文档) 科研的管理系统设计过程中采用Java开发语言,B/S结构&#xff0c;采取springboot框架&#xff0c;并以MySql为数据库进行开发。结合以上技术&#xff0c;对本系统的整体、数据库、功能模块…...

计算机组成原理 —— 五段式指令流水线

计算机组成原理 —— 五段式指令流水线 五段式指令流水线运算类指令LOAD指令的执行过程STORE指令的执行过程条件转移指令执行过程无条件转移指令的执行过程 我们今天来看看五段式指令流水线&#xff1a; 五段式指令流水线 五段式指令流水线是一种常见的处理器架构设计中采用的…...

【Bigdata】什么是关系联机分析处理

这是我父亲 日记里的文字 这是他的生命 留下留下来的散文诗 几十年后 我看着泪流不止 可我的父亲已经 老得像一个影子 &#x1f3b5; 许飞《父亲写的散文诗》 关系联机分析处理&#xff08;Relational Online Analytical Processing&#xff0c;简称 ROLA…...

svd在求解最小二乘中的应用

文章目录 线性最小二乘的直接解法&#xff08;正规方程解法&#xff09;什么是伪逆&#xff1f;伪逆矩阵的一般形式伪逆矩阵与SVD的关系 线性最小二乘的直接解法&#xff08;正规方程解法&#xff09; 对于 A x b \boldsymbol{A}xb Axb的线性最小二乘问题&#xff0c;有直解析…...

JVM—垃圾收集算法和HotSpot算法实现细节

参考资料&#xff1a;深入理解Java虚拟机&#xff1a;JVM高级特性与最佳实践&#xff08;第3版&#xff09;周志明 1、分代回收策略 分代的垃圾回收策略&#xff0c;是基于这样一个事实&#xff1a;不同的对象的生命周期是不一样的。因此&#xff0c;不同生命周期的对象可以采取…...

nvidia系列教程-AGX-Orin基础环境搭建

目录 前言 一、Agx-Orin&#xff08;32GB&#xff09;介绍 1.1 GPU 1.2 CPU 1.3 NVDLA 1.4 内存 1.5 存储 二、安装JetPack SDK 三、基础环境配置 四、jetpack软件版本 总结 前言 NVIDIA Jetson AGX Orin 是一款功能强大的嵌入式AI平台&#xff0c;专为需要高性能和低…...

使用SpringAOP实现公共字段填充

文章目录 概要整体架构流程技术细节小结 概要 在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段&#xff0c;在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段&#xff0c;也就是也就是在我们的系统中很多表…...

c++初阶-----适配器---priority_queue

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…...

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&#xff08;简称 VS Code&#xff09;是一款开源的、免费的、跨平台的、轻量级的代码编…...

FCA-数据分析理论试卷

其他参考&#xff1a; https://segmentfault.com/a/1190000043363073 https://blog.csdn.net/CSDN_WYY/article/details/137082340 Part.1&#xff1a;判断题&#xff08;总分&#xff1a;8分 得分&#xff1a;8&#xff09; 第1题 判断题 对任意事件A和B&#xff0c;必有 …...

WPF程序通过CadLib4加载CAD .dwg格式文件

1、下载CadLib相关dll文件&#xff0c;主要用到的&#xff1a;WW.dll、WW.Cad.dll、WW.GL.dll 2、程序中引用dll库。 3、创建WPF程序&#xff0c;使用Canvas来加载dwg文件&#xff0c;支持拖动和放大缩小。 4、部分代码&#xff1a; public void Init(string filename) {tr…...

图表全能王(ChartStudio) 上架VisionPro!

图表全能王(ChartStudio) - 终极图表制作工具&#xff01;支持条形图、折线图、面积图、柱形图、条形图、饼图、玫瑰图、雷达图、牛肉图、风琴图、旭日图、桑基图等图表。 https://apps.apple.com/app/chartstudio-data-analysis/id6474099675 https://apps.apple.com/cn/app/…...

【云原生】Job一次性任务详解

Job一次性任务 文章目录 Job一次性任务一、Job介绍二、运行示例Job 一、Job介绍 Job会创建一个或者多个Pod&#xff0c;并将继续重试Pod的执行&#xff0c;直到指定数量的Pod成功终止。随着Pod成功借宿&#xff0c;Job跟踪记录成功完成的Pod个数。当数量达到指定的成功个数阈值…...

化工厂人员定位采用多种定位技术的融合定位系统的好处

由于化工厂内环境的复杂性和危险性&#xff0c;通常单一的定位技术很难满足全厂区的人员定位需求&#xff0c;如果能将不同定位技术融合在一起&#xff0c;发挥出它们各自的优势&#xff0c;那么就能解决以上问题。 融合定位技术诞生背景 随着科技的不断发展&#xff0c;多种定…...

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…...

从WWDC看苹果产品发展的规律

WWDC 是苹果公司一年一度面向全球开发者的盛会&#xff0c;其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具&#xff0c;对过去十年 WWDC 主题演讲内容进行了系统化分析&#xff0c;形成了这份…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂

蛋白质结合剂&#xff08;如抗体、抑制肽&#xff09;在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上&#xff0c;高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术&#xff0c;但这类方法普遍面临资源消耗巨大、研发周期冗长…...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

微信小程序 - 手机震动

一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注&#xff1a;文档 https://developers.weixin.qq…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

爬虫基础学习day2

# 爬虫设计领域 工商&#xff1a;企查查、天眼查短视频&#xff1a;抖音、快手、西瓜 ---> 飞瓜电商&#xff1a;京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空&#xff1a;抓取所有航空公司价格 ---> 去哪儿自媒体&#xff1a;采集自媒体数据进…...

初探Service服务发现机制

1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能&#xff1a;服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源&#xf…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...