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

news2025/4/25 22:34:11

        前边我们已经讲解了使用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,=rect
    cv2.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_score
        else:
            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_score
        else:
            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_score
        else:
            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_score
        else:
            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_score
        else:
            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,=rect
    cv2.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_score
        else:
            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_score
        else:
            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_score
        else:
            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_score
        else:
            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_score
        else:
            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.coloradmin.cn/o/2342749.html

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

相关文章

Java学习手册:JSON 数据格式基础知识

1. JSON 简介 JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;易于阅读和编写&#xff0c;也易于机器解析和生成。它最初来源于 JavaScript&#xff0c;但如今已被许多语言所采用&#xff0c;包括 Java、Python、C 等。JSON 以…

《重塑AI应用架构》系列: Serverless与MCP融合创新,构建AI应用全新智能中枢

在人工智能飞速发展的今天&#xff0c;数据孤岛和工具碎片化问题一直是阻碍AI应用高效发展的两大难题。由于缺乏统一的标准&#xff0c;AI应用难以无缝地获取和充分利用数据价值。 为了解决这些问题&#xff0c;2024年AI领域提出了MCP&#xff08;Model Context Protocol模型上…

深度图可视化

import cv2# 1.读取一张深度图 depth_img cv2.imread("Dataset_depth/images/train/1112_0-rgb.png", cv2.IMREAD_UNCHANGED) print(depth_img.shape) cv2.imshow("depth", depth_img) # (960, 1280) print(depth_img)# 读取一张rgb的图片做对比 input_p…

微软Edge浏览器字体设置

前言 时间&#xff1a;2025年4月 自2025年4月起&#xff0c;微软Edge浏览器的默认字体被微软从微软雅黑替换成了Noto Sans&#xff0c;如下图。Noto Sans字体与微软雅黑风格差不多&#xff0c;但在4K以下分辨率的显示器上较微软雅黑更模糊&#xff0c;因此低分辨率的显示器建议…

Vue生命周期详细解析

前言 Vue.js作为当前最流行的前端框架之一&#xff0c;其生命周期钩子函数是每个Vue开发者必须掌握的核心概念。本文将全面解析Vue的生命周期&#xff0c;帮助开发者更好地理解Vue实例的创建、更新和销毁过程。 一、Vue生命周期概述 Vue实例从创建到销毁的整个过程被称为Vue…

基于c#,wpf,ef框架,sql server数据库,音乐播放器

详细视频: 【基于c#,wpf,ef框架,sql server数据库&#xff0c;音乐播放器。-哔哩哔哩】 https://b23.tv/ZqmOKJ5

前端项目搭建集锦:vite、vue、react、antd、vant、ts、sass、eslint、prettier、浏览器扩展,开箱即用,附带项目搭建教程

前端项目搭建集锦&#xff1a;vite、vue、react、antd、vant、ts、sass、eslint、prettier、浏览器扩展&#xff0c;开箱即用&#xff0c;附带项目搭建教程 前言&#xff1a;一、Vue项目下载快速通道二、React项目下载快速通道三、BrowserPlugins项目下载快速通道四、项目搭建教…

什么是Maven

Maven的概念 Maven是一个一键式的自动化的构建工具。Maven 是 Apache 软件基金会组织维护的一款自动化构建工具&#xff0c;专注服务于Java 平台的项目构建和依赖管理。Maven 这个单词的本意是&#xff1a;专家&#xff0c;内行。Maven 是目前最流行的自动化构建工具&#xff0…

neo4j中节点内的名称显示不全解决办法(如何让label在节点上自动换行)

因为节点过多而且想让节点中所有文字都显示出来而放大节点尺寸 从neo4j中导出png,再转成PDF来查看时&#xff0c;要看清节点里面的文字就得放大5倍才行 在网上看了很多让里面文字换行的办法都不行 然后找到一个比较靠谱的办法是在要显示的标签内加换行符 但是我的节点上显示的是…

【GIT】github中的仓库如何删除?

你可以按照以下步骤删除 GitHub 上的仓库&#xff08;repository&#xff09;&#xff1a; &#x1f6a8; 注意事项&#xff1a; ❗️删除仓库是不可恢复的操作&#xff0c;所有代码、issue、pull request、release 等内容都会被永久删除。 &#x1f9ed; 删除 GitHub 仓库步骤…

3台CentOS虚拟机部署 StarRocks 1 FE+ 3 BE集群

背景&#xff1a;公司最近业务数据量上去了&#xff0c;需要做一个漏斗分析功能&#xff0c;实时性要求较高&#xff0c;mysql已经已经不在适用&#xff0c;做了个大数据技术栈选型调研后&#xff0c;决定使用StarRocks StarRocks官网&#xff1a;StarRocks | A High-Performa…

【HCIA】简易的两个VLAN分别使用DHCP分配IP

前言 之前我们通过 静态ip地址实现了Vlan间通信 &#xff0c;现在我们添加一个常用的DHCP功能。 文章目录 前言1. 配置交换机2. 接口模式3. 全局模式后记修改记录 1. 配置交换机 首先&#xff0c;使用DHCP&#xff0c;需要先启动DHCP服务&#xff1a; [Huawei]dhcp enable I…

艾蒙顿桌面app下载-Emotn UI下载安装-emotn ui官方tv版安卓固件

在智能电视桌面应用的领域里&#xff0c;Emotn UI 凭借其简洁无广告、可自定义等特点&#xff0c;赢得了不少用户的关注。然而&#xff0c;小编深入了解后发现了一款更好用的电视桌面——乐看家桌面在诸多方面更具优势&#xff0c;能为你带来更优质的大屏体验。 乐看家桌面内置…

3、ArkTS语言介绍

目录 基础知识函数函数声明可选参数Rest参数返回类型箭头函数&#xff08;又名Lambda函数&#xff09;闭包 类字段字段初始化getter和setter继承父类访问方法重写方法重载签名可见性修饰符&#xff08;Public、Private、protected&#xff09; 基础知识 ArkTS是一种为构建高性…

修改了Element UI中组件的样式,打包后样式丢失

修改了Element UI中组件的样式&#xff0c;在本地运行没有问题&#xff0c;但是打包到线上发现样式丢失&#xff08;样式全部不生效、或者有一部分生效&#xff0c;一部分不生效&#xff09;&#xff0c;问题在于css的加载顺序导致代码编译后样式被覆盖了&#xff0c; 解决办法…

【springsecurity oauth2授权中心】jwt令牌更换成自省令牌 OpaqueToken P4

前言 前面实现了授权中心授权&#xff0c;客户端拿到access_token后就能请求资源服务器接口 权限的校验都是在资源服务器上进行的&#xff0c;授权服务器颁发的access_token有限期是2小时&#xff0c;也就是说在2小时之内&#xff0c;不管授权服务器那边用户的权限如何变更都…

诱骗协议芯片支持PD2.0/3.0/3.1/PPS协议,支持使用一个Type-C与电脑传输数据和快充取电功能

快充是由充电器端的充电协议和设备端的取电协议进行握手通讯进行协议识别来完成的&#xff0c;当充电器端的充电协议和设备端的取电协议握手成功后&#xff0c;设备会向充电器发送电压请求&#xff0c;充电器会根据设备的需求发送合适的电压给设备快速供电。 设备如何选择快充…

变量在template里不好使,在setup好使?

问题&#xff1a; 自定义的一个函数 &#xff0c;import导入后 setup里面使用正常 &#xff0c;在template里面说未定义 作用域问题 在 Vue 的模板语法中&#xff0c;模板&#xff08;template &#xff09;里能直接访问的是组件实例上暴露的属性和方法。从代码看&#xff0c…

OpenCV 图形API(53)颜色空间转换-----将 RGB 图像转换为灰度图像函数RGB2Gray()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 将图像从 RGB 色彩空间转换为灰度。 R、G 和 B 通道值的常规范围是 0 到 255。生成的灰度值计算方式如下&#xff1a; dst ( I ) 0.299 ∗ src…

Trae+DeepSeek学习Python开发MVC框架程序笔记(四):使用sqlite存储查询并验证用户名和密码

继续通过Trae向DeepSeek发问并修改程序&#xff0c;实现程序运行时生成数据库&#xff0c;用户在系统登录页面输入用户名和密码后&#xff0c;控制器通过模型查询用户数据库表来验证用户名和密码&#xff0c;验证通过后显示登录成功页面&#xff0c;验证失败则显示登录失败页面…