计算机视觉cv入门之答题卡自动批阅

        前边我们已经讲解了使用cv2进行图像预处理与边缘检测等方面的知识,这里我们以答题卡自动批阅这一案例来实操一下。

大致思路

        答题卡自动批阅的大致流程可以分为这五步:图像预处理-寻找考试信息区域与涂卡区域-考生信息区域OCR识别-涂卡区域填涂答案判断-图像中标记结果

接下来我们按照这五步来进行讲解。

图像预处理

答题卡获取首先,在网上随便找一张答题卡图片

由于这里我只需要考生信息与填途题目,所以只是截取了左上角这一部分作为我们后续的目标。 

 接着,我们使用图像编辑软件将考生信息填入,并将10道题目进行填涂。

读取图像 

# #读取答题卡图片
import cv2
import matplotlib.pyplot as plt
src_image=cv2.imread(filename='answercard4.jpg',flags=cv2.IMREAD_COLOR_RGB)
height,width=src_image.shape[:2]
plt.xticks(range(0,width,10),minor=True)
plt.yticks(range(0,height,10),minor=True)
plt.imshow(src_image)

        这里我使用matplotlib的imshow函数来显示图像,这样在jupyter环境中可以不打开任何弹窗直接显示图像,比较方便。

转为灰度图

#转为灰度图
gray_image=cv2.cvtColor(src=src_image,code=cv2.COLOR_RGB2GRAY)
plt.title('原始图像(灰度图)')
plt.imshow(gray_image,cmap='gray')

        

        将原始图像转化为灰度图是为了后续的检测等操作,在计算机视觉任务中,基本上所有的操作都是针对灰度图来进行的,灰度图是将原始图像的多个通道按照一定权重求和叠加而来,这样一来多通道变成了单通道(Gray=w_1*B+w_2*G+w_3*R),在计算量上也会比较友好。

 阈值化

#阈值化
thresh,binary_image=cv2.threshold(src=gray_image,thresh=128,maxval=255,type=cv2.THRESH_OTSU+cv2.THRESH_BINARY)
plt.imshow(binary_image,cmap='gray')

        阈值化是为了更好的查找轮廓。这里阈值化我们使用cv2.THRESH+cv2.THRESH-OTSU方法来自动对图像进行二值化阈值分割。 

考生信息与答题区域分割

#考生信息区域与答题区域分割
contours,hiercahy=cv2.findContours(binary_image,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
possible_rectangles=[]
answer_rectangle=[]
for points in contours:x,y,w,h=cv2.boundingRect(points)if 800<w*h<120000:possible_rectangles.append((x,y,w,h))
information_rectangles=[rect for rect in possible_rectangles if 100<rect[2]<140 and 30<rect[3]<60]#长在100~~60
answer_rectangle=sorted(possible_rectangles,key=lambda x:x[2]*x[3])[-2]
marked_img=src_image.copy()
information_images=[]
for rect in information_rectangles:x, y, w, h,=rectcv2.rectangle(marked_img, (x, y), (x+w, y+h), (0, 255, 0), 3)information_images.append(marked_img[y:y+h,x:x+w])
x,y,w,h=answer_rectangle
answer_area=marked_img[y:y+h,x:x+w]
answer_area=cv2.cvtColor(src=answer_area,code=cv2.COLOR_RGB2GRAY)
cv2.rectangle(marked_img,(x,y),(x+w,y+h),(255,0,0),3)
plt.xticks(range(0,marked_img.shape[1],10),minor=True)
plt.yticks(range(0,marked_img.shape[0],10),minor=True)
plt.imshow(marked_img)

         查找轮廓时我们通常使用findContours函数来进行查找(返回值为所有可能的轮廓点contours以及这些点之间的拓扑结构hierachy),考虑到要分割的区域都是矩形,因此我们可以在查找到的轮廓点中使用cv2.boundingrectangle函数来对查找到的轮廓进行矩形拟合

       然后,我们再使用cv2.drawContours函数将其在原始图像中标记出来即可。

OCR识别

   这里我使用现成的OCR字符识别库,这里我使用的是paddleocr

获取方式

pip install paddlepaddle paddleocr

OCR识别

#使用paddleocr识别考生信息
student_information=[]
import torch
from paddleocr import PaddleOCR
ocr=PaddleOCR(lang="ch")
for image in information_images:result=ocr.ocr(image,cls=True)for line in result[0]:text=line[1][0]student_information.append(text)    
print(student_information)     

 结果:

答题区域答案识别

         这一步是整个任务的关键,但其实也比较简单,就是按照查找到的填涂过的黑色矩形的位置来判断,首先我们要在这个填涂答案的区域内定位所有黑色矩形的位置以及长和宽,然后根据以下的关系来判断每一列的答案是ABCDE的哪一个,其中filled_area_top是指整个填涂答案中最顶部的位置,即A的位置(我的答案中有A,倘若没有的话,也可以完全根据y坐标自行指定一个ABCDE所在的范围),filled_area_bottom是整个填入答案中最底部的位置,即E的位置。


thresh,binary_answer_area=cv2.threshold(src=answer_area,thresh=128,maxval=255,type=cv2.THRESH_BINARY+cv2.THRESH_OTSU)
contours,hiercahy=cv2.findContours(image=binary_answer_area,mode=cv2.RETR_TREE,method=cv2.CHAIN_APPROX_SIMPLE)
filled_areas=[]
answers=[]
epsilon=5
true_answers=['C','A','D','A','C','C','B','E','A','D']
for points in contours:x,y,w,h=cv2.boundingRect(points)if 300<w*h<500:filled_areas.append((x,y,w,h))
filled_areas=sorted(filled_areas,key=lambda point:point[1])
filled_area_top,filled_area_bottom=filled_areas[0][1],filled_areas[-1][1]
filled_areas=sorted(filled_areas,key=lambda point:point[0])
score=0
total_num=len(filled_areas)
avg_score=100/total_num
plt.imshow(marked_img)
for i in range(len(filled_areas)):x,y,w,h=filled_areas[i]if 0<=(y-filled_area_top)<=epsilon:answers.append('A')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='A',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if epsilon<abs(y-filled_area_top)<=h+epsilon:answers.append('B')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='B',color='blue',size=15)if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if h+epsilon<abs(y-filled_area_top)<=2*h+epsilon:answers.append('C')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='C',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 2*h+epsilon<abs(y-filled_area_top)<=3*h+epsilon:answers.append('D')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='D',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 0<=filled_area_bottom-y<=epsilon:answers.append('E')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='E',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)
plt.text(x=width-100,y=50,s=score,color='red',size='20')
plt.text(x=width-100,y=70,s='———',color='red',size='15')
plt.text(x=width-100,y=75,s='———',color='red',size='15')
for info in student_information:print(info)
print(f'你的答案是:{answers}')
print(f'正确答案是:{true_answers}')
print(f'考试成绩:{score}')

结果: 

完整代码

# #读取答题卡图片
import cv2
import matplotlib.pyplot as plt
src_image=cv2.imread(filename='answercard4.jpg',flags=cv2.IMREAD_COLOR_RGB)
height,width=src_image.shape[:2]
plt.xticks(range(0,width,10),minor=True)
plt.yticks(range(0,height,10),minor=True)
plt.imshow(src_image)
#转为灰度图
gray_image=cv2.cvtColor(src=src_image,code=cv2.COLOR_RGB2GRAY)
plt.imshow(gray_image,cmap='gray')
thresh,binary_image=cv2.threshold(src=gray_image,thresh=128,maxval=255,type=cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
plt.imshow(binary_image,cmap='gray')
#考生信息区域与答题区域分割
contours,hiercahy=cv2.findContours(binary_image,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
possible_rectangles=[]
answer_rectangle=[]
for points in contours:x,y,w,h=cv2.boundingRect(points)if 800<w*h<120000:possible_rectangles.append((x,y,w,h))
information_rectangles=[rect for rect in possible_rectangles if 100<rect[2]<140 and 30<rect[3]<60]#长在100~~60
answer_rectangle=sorted(possible_rectangles,key=lambda x:x[2]*x[3])[-2]
marked_img=src_image.copy()
information_images=[]
for rect in information_rectangles:x, y, w, h,=rectcv2.rectangle(marked_img, (x, y), (x+w, y+h), (0, 255, 0), 3)information_images.append(marked_img[y:y+h,x:x+w])
x,y,w,h=answer_rectangle
answer_area=marked_img[y:y+h,x:x+w]
answer_area=cv2.cvtColor(src=answer_area,code=cv2.COLOR_RGB2GRAY)
cv2.rectangle(marked_img,(x,y),(x+w,y+h),(255,0,0),3)
plt.xticks(range(0,marked_img.shape[1],10),minor=True)
plt.yticks(range(0,marked_img.shape[0],10),minor=True)
plt.imshow(marked_img)
#使用paddleocr识别考生信息
student_information=[]
import torch
from paddleocr import PaddleOCR
ocr=PaddleOCR(lang="ch")
for image in information_images:result=ocr.ocr(image,cls=True)for line in result[0]:text=line[1][0]student_information.append(text)    
print(student_information)     thresh,binary_answer_area=cv2.threshold(src=answer_area,thresh=128,maxval=255,type=cv2.THRESH_BINARY+cv2.THRESH_OTSU)
contours,hiercahy=cv2.findContours(image=binary_answer_area,mode=cv2.RETR_TREE,method=cv2.CHAIN_APPROX_SIMPLE)
filled_areas=[]
answers=[]
epsilon=5
true_answers=['C','A','D','A','C','C','B','E','A','D']
for points in contours:x,y,w,h=cv2.boundingRect(points)if 300<w*h<500:filled_areas.append((x,y,w,h))
filled_areas=sorted(filled_areas,key=lambda point:point[1])
filled_area_top,filled_area_bottom=filled_areas[0][1],filled_areas[-1][1]
filled_areas=sorted(filled_areas,key=lambda point:point[0])
score=0
total_num=len(filled_areas)
avg_score=100/total_num
plt.imshow(marked_img)
for i in range(len(filled_areas)):x,y,w,h=filled_areas[i]if 0<=(y-filled_area_top)<=epsilon:answers.append('A')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='A',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if epsilon<abs(y-filled_area_top)<=h+epsilon:answers.append('B')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='B',color='blue',size=15)if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if h+epsilon<abs(y-filled_area_top)<=2*h+epsilon:answers.append('C')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='C',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 2*h+epsilon<abs(y-filled_area_top)<=3*h+epsilon:answers.append('D')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='D',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 0<=filled_area_bottom-y<=epsilon:answers.append('E')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='E',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)
plt.text(x=width-100,y=50,s=score,color='red',size='20')
plt.text(x=width-100,y=70,s='———',color='red',size='15')
plt.text(x=width-100,y=75,s='———',color='red',size='15')
for info in student_information:print(info)
print(f'你的答案是:{answers}')
print(f'正确答案是:{true_answers}')
print(f'考试成绩:{score}')

总结 

 

        以上便是计算机视觉cv2入门之答题卡自动批阅的所有内容,所有代码作者纯手敲无任何AI,如果本文对你有用,还劳驾各位一键三连支持一下博主。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78102.shtml

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

相关文章

语音合成之一TTS技术发展史综述

TTS技术发展史综述 引言TTS技术的起源与早期探索基于规则的TTS系统&#xff1a;原理与发展共振峰合成技术&#xff1a;作用与影响拼接合成技术&#xff1a;发展与应用统计参数语音合成&#xff1a;以隐马尔可夫模型&#xff08;HMM&#xff09;为例深度学习驱动的TTS&#xff1…

目标检测中的损失函数(一) | IoU GIoU DIoU CIoU EIoU Focal-EIoU

&#x1f680;该系列将会持续整理和更新BBR相关的问题&#xff0c;如有错误和不足恳请大家指正&#xff0c;欢迎讨论&#xff01;&#xff01;&#xff01; &#x1f4e6;目标检测的损失函数一般包含三个部分&#xff0c;分别是边界框损失也可称为定位损失、置信度损失和分类损…

结构型模式:适配器模式

什么是适配器模式&#xff1f; 适配器模式&#xff08;Adapter Pattern&#xff09;是一种常用的结构型设计模式&#xff0c;它的主要作用是将一个类的接口转换成客户端期望的另一个接口。就像现实生活中的各种转接头一样&#xff0c;适配器模式使得原本因接口不兼容而无法一起…

AI Agent认知框架(ReAct、函数调用、计划与执行、自问自答、批判修正、思维链、思维树详解和对比,最后表格整理总结

以下是主流AI Agent认知框架的详细说明、对比及表格总结&#xff1a; 1. 各认知框架详解 (1) ReAct (Reasoning Action) 定义&#xff1a;结合推理&#xff08;Reasoning&#xff09;和行动&#xff08;Action&#xff09;的循环过程。核心机制&#xff1a; 模型先推理&…

特征存储的好处:特征存储在机器学习开发中的优势

随着企业寻求提升机器学习生产力和运营能力 (MLOps),特征存储 (Feature Store) 的普及度正在迅速提升。随着 MLOps 技术的进步,特征存储正成为机器学习基础设施的重要组成部分,帮助企业提升模型的性能和解释能力,并加速新模型与生产环境的集成。这些存储充当集中式存储库,…

SPRING-AI 官方事例

springAI 关于最近看了很多SpringAi&#xff0c;阅读很多代码都感觉特别陌生 SpringAI依赖的springBoot版本都是3.3以上, 以及很多SpringAi都是依赖JDK版本最低17, 并且出现了很多新关键字例如 var,record 等写法, 烟花缭乱得lambda 表达式&#xff0c; 到处都是使用build 构…

Visual Studio Code 使用tab键往左和往右缩进内容

使用VSCode写东西&#xff0c;经常遇到多行内容同时缩进的情况&#xff0c;今天写文档的时候就碰到&#xff0c;记录下来&#xff1a; 往右缩进 选中多行内容&#xff0c;点tab键&#xff0c;会整体往右缩进&#xff1a; 往左缩进 选中多行内容&#xff0c;按shifttab&am…

机器学习(7)——K均值聚类

文章目录 1. K均值&#xff08;K-means&#xff09;聚类是什么算法&#xff1f;2. 核心思想2. 数学目标3. 算法步骤3.1. 选择K个初始质心&#xff1a;3.2.迭代优化3.3. 重复步骤2和步骤3&#xff1a; 4. 关键参数5. 优缺点6. 改进变种7. K值选择方法8. Python示例9. 应用场景10…

爬虫案例-爬取某企数据

文章目录 1、准备要爬取企业名称数据表2、爬取代码3、查看效果 1、准备要爬取企业名称数据表 企业名称绍兴市袍江王新国家庭农场绍兴市郑杜粮油专业合作社绍兴市越城区兴华家庭农场绍兴市越城区锐意家庭农场绍兴市越城区青甸畈家庭农场绍兴市袍江王新国家庭农场绍兴市袍江月明…

足球 AI 智能体技术解析:从数据采集到比赛预测的全链路架构

一、引言 在足球运动数字化转型的浪潮中&#xff0c;AI 智能体正成为理解比赛、预测赛果的核心技术引擎。本文从工程实现角度&#xff0c;深度解析足球 AI 的技术架构&#xff0c;涵盖数据采集、特征工程、模型构建、实时计算到决策支持的全链路技术方案&#xff0c;揭示其背后…

怎么配置一个kubectl客户端访问多个k8s集群

怎么配置一个kubectl客户端访问多个k8s集群 为什么有的客户端用token也访问不了k8s集群&#xff0c;因为有的是把~/.kube/config文件&#xff0c;改为了~/.kube/.config文件&#xff0c;文件设置成隐藏文件了。 按照kubectl的寻找配置的逻辑&#xff0c;kubectl找不到要访问集群…

[QMT量化交易小白入门]-四十六、年化收益率118%的回测参数,如何用贪心算法挑选50个两两相关性最小的ETF组合

本专栏主要是介绍QMT的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写100篇左右。 QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。 文章目录 相关阅读准备工作安装所需库导入所需模块下载所有ETF数…

几何编码:启用矢量模式地理空间机器学习

在 ML 模型中使用点、线和多边形&#xff0c;将它们编码为捕捉其空间属性的向量。 自地理信息系统 (GIS) 诞生之初&#xff0c;“栅格模式”和“矢量模式”之间就存在着显著的区别。在栅格模式下&#xff0c;数据以值的形式呈现在规则的网格上。这包括任何形式的图像&#xff0…

Leetcode98、230:二叉搜索树——递归学习

什么是二叉搜索树&#xff1a;右子树节点 > 根节点 > 左子树节点&#xff0c; 二叉搜索树中的搜索&#xff0c;返回给定值val所在的树节点 终止条件为传进来的节点为空、或者节点的值 val值&#xff0c;返回这个节点&#xff1b; 单程递归逻辑&#xff1a;定义一个resu…

每天学一个 Linux 命令(30):cut

​​可访问网站查看,视觉品味拉满: http://www.616vip.cn/30/index.html cut 命令用于从文件或输入流中提取文本的特定部分(如列、字符或字节位置)。它常用于处理结构化数据(如 CSV、TSV)或按固定格式分割的文本。以下是详细说明和示例: 命令格式 cut [选项] [文件...]…

Tauri 2.3.1+Leptos 0.7.8开发桌面应用--Sqlite数据库选中数据的表格输出

在前期工作的基础上&#xff08;Tauri 2.3.1Leptos 0.7.8开发桌面应用--Sqlite数据库的写入、展示和选择删除_tauri leptos sqlite 选择删除-CSDN博客&#xff09;&#xff0c;实现将选中的数据实时用表格展示出来&#xff0c;效果如下&#xff1a; 1. 后台invoke调用命令 Tau…

使用Tauri 2.3.1+Leptos 0.7.8开发桌面小程序汇总

近期断断续续学习了Rust编程&#xff0c;使用Tauri 2.3.1Leptos 0.7.8开发了一个自用的桌面小程序。Win10操作系统&#xff0c;使用VS Code及rust analyzer插件搭建的开发环境&#xff0c;后期开始使用Roo Code绑定DeepSeek API 辅助编程&#xff0c;对我这个初学者编程帮助很大…

考研英一学习笔记

2024 年全国硕士研究生招生考试 英语&#xff08;一&#xff09;试题 &#xff08;科目代码&#xff1a;201&#xff09; Section Ⅰ Use of English Directions: Read the following text. Choose the best word(s) for each numbered blank and mark A, B, C or D on the ANS…

【技术笔记】Cadence实现Orcad与Allegro软件交互式布局设置

【技术笔记】Cadence实现Orcad与Allegro软件交互式布局设置 更多内容见专栏&#xff1a;【硬件设计遇到了不少问题】、【Cadence从原理图到PCB设计】 在做硬件pcb设计的时候&#xff0c;原理图选中一个元器件&#xff0c;希望可以再PCB中可以直接选中。 为了达到原理图和PCB两两…