目录

1.答题卡自动批阅整体实现思路

2.关键技术步骤与原理

答题卡区域提取

①轮廓检测并排序

②执行透视变换

③找到每一个圆圈轮廓

④先对所有圆圈轮廓从上到下排序

⑤再通过循环每次只提取出五个轮廓再进行从左到右的排序

3.完整代码


1.答题卡自动批阅整体实现思路

  • 首先,通过图像处理技术将含答题卡的图片中的答题区域单独提取出来,使其变为一个标准的矩形。
  • 然后,针对处理后的答题卡区域进行分析,逐一识别每一道题目的各个选项(A, B, C, D, E)是否被学生正确涂黑。
  • 最终,根据判断结果计算出学生的得分,并以百分比和具体答案串等形式展示。

2.关键技术步骤与原理

答题卡区域提取

该过程采用“轮廓查找”与“透视变换”。通过灰度化、二值化等预处理,利用cv2.findContours找到答题卡的最外层轮廓,随后对轮廓进行近似得到四个角点,最后应用透视变换将答题卡区域整齐地裁剪出来。

image=cv2.imread(r'./images/test_04.png')
contours_img=image.copy()
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
blurred=cv2.GaussianBlur(gray,(5,5),0)
cv_show('blurred',blurred)
edged=cv2.Canny(blurred,75,200)
cv_show('edged',edged)

简单的预处理操作

①轮廓检测并排序
  • 首先通过轮廓的边界框(bounding box)对所有选项轮廓进行从大到小的初步排序。
#轮廓检测
cnts=cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show('contours_img',contours_img)
donCnt=None
#根据轮廓大小排序,准备透视变换
cnts=sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:#遍历每一个轮廓peri=cv2.arcLength(c,True)approx=cv2.approxPolyDP(c,0.02*peri,True)if len(approx)==4:donCnt=approxbreak

approx的长度必须为四,得到答题卡区域轮廓的四个近似点坐标矩阵数组

②执行透视变换
#执行透视变换
warped_t=four_point_transform(image,donCnt.reshape(4,2))
warped_new=warped_t.copy()
cv_show('warped_t',warped_t)

二值化

thresh=cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
thresh_Contours=thresh.copy()

③找到每一个圆圈轮廓

先找到所有轮廓

#找到每一个圆圈轮廓
cnts=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours=cv2.drawContours(warped_t,cnts,-1,(0,255,0),1)
cv_show('warped_Contours',warped_Contours)

对所有轮廓做外接矩形条件判断,筛选出所有圆形答题轮廓共25个

questionCnts=[]
for c in cnts:(x,y,w,h)=cv2.boundingRect(c)ar=w/float(h)if w>=20 and h>=20 and 0.9<=ar<=1.1:questionCnts.append(c)
print(len(questionCnts))#25
④先对所有圆圈轮廓从上到下排序
questionCnts=sort_contours(questionCnts,method='top-to-bottom')[0]
correct=0
⑤再通过循环每次只提取出五个轮廓再进行从左到右的排序
#每排有五个选项
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)):cnts=sort_contours(questionCnts[i:i+5])[0]bubbled=None#遍历每一个结果for (j,c) in enumerate(cnts):#使用mask来判读结果mask=np.zeros(thresh.shape,dtype='uint8')cv2.drawContours(mask,[c],-1,255,-1)#-1表示填充cv_show('mask',mask)#通过计算非零点数量来算是否选择这个答案#利用掩膜(mask)进行与操作,只保留mask位置中的内容thresh_mask_and=cv2.bitwise_and(thresh,thresh,mask=mask)cv_show('thresh_mask_and',thresh_mask_and)total=cv2.countNonZero(thresh_mask_and)#统计灰度值不为零的像素数量if bubbled is None or total>bubbled[0]:bubbled=(total,j)#对比正确答案color=(0,0,255)answer=ANSWER_KRY[q]if answer==bubbled[1]:#判断正确color=(0,255,0)correct+=1cv2.drawContours(warped_new,[cnts[answer]],-1,color,3)cv_show('warpeding',warped_new)

选项识别与答案分析

  • 使用逐个建立掩膜(mask)的方式,从原图中精确抠取出每个选项的图像。
  • 计算抠取出的每个选项区域内的白色像素点数量。
  • 通过比较不同选项的白色像素点数量,确定学生实际作答的选项,数量最多的即为作答的区域。

 标准答案比对与评分

  • 系统内部维护一个标准答案对照表(answer key),记录每道题的正确选项。
    ANSWER_KRY={0:1,1:4,2:0,3:3,4:1}#正确答案
  • 将程序识别出的答案与标准答案进行比对。
  • 答案正确,则计为1分,并使用绿色边框对正确选项进行标注;若答案错误,则不加分,并用红色边框标注正确选项。
  • 最终,系统将根据总分和题目数量计算出最终得分并在图片上显示。
score=(correct/5.0)*100
print('[INFO] score:{:.2f}%'.format(score))
cv2.putText(warped_new,'{:.2f}%'.format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)
cv_show('Original',image)
cv_show('Exam',warped_new)#[INFO] score:20.00%

3.完整代码

import cv2
import numpy as npdef order_points(pts):rect=np.zeros((4,2),dtype='float32')#按顺序找到对应坐标0123即左上右上右下左下s=pts.sum(axis=1)#每行求和rect[0]=pts[np.argmin(s)]rect[2]=pts[np.argmax(s)]diff=np.diff(pts,axis=1)#每行求差rect[1]=pts[np.argmin(diff)]rect[3]=pts[np.argmax(diff)]return rect
def four_point_transform(image,pts):#获取输入坐标点rect=order_points(pts)(tl,tr,br,bl)=rect#计算输入的w和h值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)#M是获取到的转换之间的关系warped=cv2.warpPerspective(image,M,(maxWidth,maxHeight))#返回变换后结果return warped
def sort_contours(cnts,method='left-to-right'):reverse=Falsei=0if method=='right-to-left'or method=='bottom-to-top':reverse=Trueif method=='top-to-bottom'or method=='bottom-to-top':i=1boundingBoxes=[cv2.boundingRect(c) for c in cnts](cnts,boundingBoxes)=zip(*sorted(zip(cnts,boundingBoxes),key=lambda a:a[1][i],reverse=reverse))return cnts,boundingBoxes
def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)ANSWER_KRY={0:1,1:4,2:0,3:3,4:1}#正确答案image=cv2.imread(r'./images/test_04.png')
contours_img=image.copy()
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
blurred=cv2.GaussianBlur(gray,(5,5),0)
cv_show('blurred',blurred)
edged=cv2.Canny(blurred,75,200)
cv_show('edged',edged)
#轮廓检测
cnts=cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show('contours_img',contours_img)
donCnt=None
#根据轮廓大小排序,准备透视变换
cnts=sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:#遍历每一个轮廓peri=cv2.arcLength(c,True)approx=cv2.approxPolyDP(c,0.02*peri,True)if len(approx)==4:donCnt=approxbreak
#执行透视变换
warped_t=four_point_transform(image,donCnt.reshape(4,2))
warped_new=warped_t.copy()
cv_show('warped_t',warped_t)
warped=cv2.cvtColor(warped_t,cv2.COLOR_BGR2GRAY)
thresh=cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
thresh_Contours=thresh.copy()
#找到每一个圆圈轮廓
cnts=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours=cv2.drawContours(warped_t,cnts,-1,(0,255,0),1)
cv_show('warped_Contours',warped_Contours)questionCnts=[]
for c in cnts:(x,y,w,h)=cv2.boundingRect(c)ar=w/float(h)if w>=20 and h>=20 and 0.9<=ar<=1.1:questionCnts.append(c)
print(len(questionCnts))
#按照从上到下进行排序
questionCnts=sort_contours(questionCnts,method='top-to-bottom')[0]
correct=0
#每排有五个选项
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)):cnts=sort_contours(questionCnts[i:i+5])[0]bubbled=None#遍历每一个结果for (j,c) in enumerate(cnts):#使用mask来判读结果mask=np.zeros(thresh.shape,dtype='uint8')cv2.drawContours(mask,[c],-1,255,-1)#-1表示填充cv_show('mask',mask)#通过计算非零点数量来算是否选择这个答案#利用掩膜(mask)进行与操作,只保留mask位置中的内容thresh_mask_and=cv2.bitwise_and(thresh,thresh,mask=mask)cv_show('thresh_mask_and',thresh_mask_and)total=cv2.countNonZero(thresh_mask_and)#统计灰度值不为零的像素数量if bubbled is None or total>bubbled[0]:bubbled=(total,j)#对比正确答案color=(0,0,255)answer=ANSWER_KRY[q]if answer==bubbled[1]:#判断正确color=(0,255,0)correct+=1cv2.drawContours(warped_new,[cnts[answer]],-1,color,3)cv_show('warpeding',warped_new)
score=(correct/5.0)*100
print('[INFO] score:{:.2f}%'.format(score))
cv2.putText(warped_new,'{:.2f}%'.format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)
cv_show('Original',image)
cv_show('Exam',warped_new)

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

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

相关文章

C#实现通过POST实现读取数据

C# POST请求与MySQL数据存储实现下面是一个完整的C#解决方案&#xff0c;用于发送POST请求、接收响应数据&#xff0c;并将数据保存到MySQL数据库中。完整代码实现 using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.J…

Java 字符编码问题,怎么优雅地解决?

网罗开发&#xff08;小红书、快手、视频号同名&#xff09;大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等方…

STL之string类(C++)

1.string类核心定位std::string 本质是对 “字符序列” 的封装&#xff0c;内部通过动态数组存储字符&#xff0c;并自动管理内存&#xff08;分配、扩容、释放&#xff09;&#xff0c;对外提供了简洁的接口用于字符串的创建、修改、拼接、查找等操作。1.1 使用前提头文件包含…

[Maven 基础课程]第一个 Maven 项目

idea 新建一个项目&#xff1a; 来到 New Project 页面&#xff1a; 这里我们有两种方式创建 maven 项目&#xff0c;一种是自定义创建&#xff0c;另一种是使用 maven 模版项目创建。 自定义创建 maven 项目 基本配置 Name: first_maven_project 项目名称&#xff0c;设为 …

uni小程序中使用Echarts图表

前言 今天鸡米花给大家带来的是在uni里面使用echarts&#xff0c;能够完美支持和PC端一样的效果&#xff0c;我这边的工程是uni转为微信小程序&#xff0c;用的是vue3vite来写的&#xff0c;然后实现了竖屏和横屏的展示方式&#xff0c;好了献上效果图。 效果图 一、引入插件 这…

从FOTA测试到汽车电子安全体系的启蒙之旅

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

stm32中 中断和事件的区别

一、核心概念比喻想象一下工厂里的一个报警系统&#xff1a;​中断 (Interrupt)​​&#xff1a;就像火警警报器响了。它的目的是通知管理员&#xff08;CPU&#xff09;​​&#xff1a;“着火了&#xff01;”。管理员听到后&#xff0c;会停下手中的工作&#xff08;保存现场…

深入理解MySQL主从架构中的Seconds_Behind_Master指标

问题&#xff1a;主从延迟与写后读不一致 在典型的 MySQL 主从架构下&#xff0c;所有写操作都会直接进入主库&#xff0c;而读操作大多分流到从库&#xff0c;从而实现读写分离&#xff0c;缓解主库压力。 然而 MySQL 的复制机制是异步的&#xff1a;主库先写入 binlog&#…

MySQL安装(linux版本)

MySQL安装&#xff08;linux版本&#xff09; 课程地址 08. 进阶-MySQL安装(linux版本)_哔哩哔哩_bilibili 安装过程中所有需要的程序都放在网盘里了 通过网盘分享的文件&#xff1a;虚拟机 链接: https://pan.baidu.com/s/1eLMD2iq1uEujNN7mWs2dIg?pwdckmh 提取码: ckmh …

OpenCV 图像双三次BSpline插值

文章目录 一、简介 二、实现代码 三、实现效果 参考资料 一、简介 之前我们介绍过BSpline曲线,一条B样条曲线可以被定义成 n + 1 n+1 n+1个控制点的集合 { Q i } i = 0 n {\{Q_i\}}^{n}_{i=0}

Prometheus+Grafana构建企业级监控方案

1.prometheus工作原理&#xff1a; Prometheus将指标收集并存储为时间序列数据库&#xff08;时序数据库&#xff09;&#xff0c;即指标信息与记录它的时间戳一起存储&#xff0c;以及称为标签的可选键值对。 特性&#xff1a; 具有由指标名称和键/值对识别的时间序列数据的…

第23课:行业解决方案设计

第23课:行业解决方案设计 课程目标 掌握金融、医疗、教育等行业应用 学习领域特定Agent设计 了解行业标准集成 实践设计行业解决方案 课程内容 23.1 金融行业解决方案 金融Agent系统 class FinancialAgentSystem {constructor() {this.agents =

Go语言快速入门教程(JAVA转go)——2 环境搭建与入门

安装go Go官网下载地址&#xff1a;https://golang.org/dl/ 中国区官方镜像站&#xff08;推荐&#xff09;&#xff1a;https://golang.google.cn/dl/ windows安装 下载好后选择安装路径即可&#xff0c;安装完成后&#xff0c;winr 输入cmd调出命令行窗口&#xff0c;输入…

ffplay播放pcm

用 ffplay 播放 PCM 裸流时&#xff0c;必须手动告诉它“没有封装头、采样率、声道数、采样格式”四个关键点。命令模板如下&#xff1a; ffplay -f <采样格式> -ar <采样率> -ac <声道数> -i <pcm文件>常用组合示例 48 kHz、16 bit、小端、双声道 ffp…

【LLM】大模型训练中的稳定性问题

训练稳定性问题 &#x1f4cb; 概述 本文档详细介绍了在项目中解决训练稳定性问题的方法、原理分析以及实际应用。涵盖了梯度裁剪、损失函数优化、数值稳定化处理和学习率调度等关键技术。&#x1f6a8; 问题描述 现象: 训练过程中出现数值不稳定&#xff0c;损失函数波动剧烈 …

【linux系统】6. 基础开发工具(一)

一. 软件包管理器 1&#xff09;Linux下安装软件的常用方法 1. 源代码安装 下载程序的源代码&#xff0c;本地编译成二进制文件&#xff0c;拷贝到系统指定路径下。 2. rpm包安装 已经编译好的安装包&#xff0c;使用rpm对应的指令去安装&#xff0c;也比较麻烦。 3. 包…

ffplay数据结构分析

struct VideoState 播放器封装 typedef struct VideoState {SDL_Thread *read_tid; // 读线程句柄AVInputFormat *iformat; // 指向demuxerint abort_request; // 1时请求退出播放int force_refresh; // 1时刷新画面&#xff0c;请求立即刷新画面的意思int paused; …

OpenCV:银行卡号识别

目录 一、项目原理与核心技术 二、环境准备与工具包导入 1. 环境依赖 2. 工具包导入 三、自定义工具类 myutils.py 实现 四、主程序核心流程&#xff08;银行卡识别.py&#xff09; 1. 命令行参数设置 2. 银行卡类型映射 3. 辅助函数&#xff1a;图像展示 五、步骤 1…

基于spark的澳洲光伏发电站选址预测

基于spark的澳洲光伏发电站选址预测项目概况 [&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;] 点这里,查看所有项目 [&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x…

Kibana 双栈网络(Dual-Stack)支持能力评估

#作者&#xff1a;Unstopabler 文章目录一&#xff0e;测试目标二&#xff0e;测试环境三&#xff0e;Kibana1、查询 Kibana pod信息2、查询Kibana service信息3、Kibana service 设置四&#xff0e;验证测试1、Kibana 监听参数设置2、Kibana节点IPv4状态检查3、Kibana节点IPv6…