OpenCV+Python识别机读卡

news2024/11/15 5:02:30

背景介绍

正常机读卡是通过读卡机读取识别结果的,目前OpenCV已经这么强大了,尝试着用OpenCV+Python来识别机读卡。要识别的机读卡长这样:

我们做以下操作:

1.识别答题卡中每题选中项结果。

不做以下操作:

1.不识别准考证号。(暂不识别,后面有需要再补充此部分)

2.不识别101-106题(这些题实际情况下经常用不到,如果要识别原理也一样)

实现思路

通过分析答题卡特征:

1.答题区域为整张图片最大轮廓,先找出答题区域。

2.答题区域分为6行,每行4组,第6行只有1组,我们暂不处理第6行,只处理前面5行。

3.给定每一行第一个选项中心点坐标,该行其余选项的中心点坐标可以推算出来。

4.通过找到每个选项中心点坐标,再加上选项宽高,就可以在答题区域绘出每个选项的范围。

5.通过计算每个选项范围图像里非0像素点个数:

   单选题非0像素点最少的既是答案。

   多选题结合阈值判断该选项是否选中。

6.输出完整答案。

实现步骤

1.图像预处理

将图片转灰度图、黑帽运算:移除干扰项、二值化突出轮廓、查找轮廓、找出答题区域。

import cv2
import numpy as np

# 1.读取图片并缩放
orginImg = cv2.imread("1.jpg")
size = ((int)(650*1.8), (int)(930*1.8))  # 尽可能将图片弄大一点,下面好处理
img = cv2.resize(orginImg, size)

#显示图像
def imshow(name,image):
    scale_percent = 50  # 缩放比例
    width = int(image.shape[1] * scale_percent / 100)
    height = int(image.shape[0] * scale_percent / 100)
    dim = (width, height)
    resized_image = cv2.resize(image, dim, interpolation=cv2.INTER_AREA)
    cv2.imshow(name, resized_image)

imshow("1.orgin", img)

# 2.转灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
imshow("2.gray", gray)

# 3.黑帽运算:移除干扰项
cvblackhat = cv2.morphologyEx(
    gray, cv2.MORPH_BLACKHAT, cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)))
imshow("3.black", cvblackhat)

# 4.二值化突出轮廓,自动阈值范围 cv2.THRESH_BINARY|cv2.THRESH_OTSU
thresh = cv2.threshold(
    cvblackhat, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
imshow("4.thresh", thresh)

# 5.提取轮廓,并在图上标记轮廓
cnts, hierarchy = cv2.findContours(
    thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mark = img.copy()
cv2.drawContours(mark, cnts, -1, (0, 0, 255), 2)
imshow("5.contours", mark)

# 6.提取我们感兴趣的部分(这里我们只需要答题部分) img[y:y+h, x:x+w]
roi = None
for (i, c) in enumerate(cnts):
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w/float(h)
    if w > 500 and h > 500 and ar > 0.9 and ar < 1.1:
        roi = img[y:y+h, x:x+w]
        break
imshow("5.roi", roi)

运行结果

2.查找每个选项的中心点坐标

这里每行起始点坐标和每个选项宽高,都是写的固定值(手动量出来的,本来想通过图像提取轮廓来取每个选项中心坐标点的,可是由于机读卡填图不规范,可能会影响轮廓提取,不是很靠谱,暂时没想到更好的办法)


# 7.查找每个选项的中心点坐标
# 思路:
# 通过分析:
# 1.答题区域分为6行,每行4组,第6行只有1组,我们暂不处理第6行,只处理前面5行。
# 2.只要给定每一行第一个选项中心坐标,该行其余选项的中心坐标可以推算出来。
# 3.通过找到每个选项中心点坐标,再加上选项宽高,就可以在答题区域绘出每个选项的范围。
# 4.通过计算每个选项范围图像里非0像素点个数,结合阈值判断该选项是否选中。
# 5.结合题目个数,遍历每个选项,构造出最终答案。
item = [34, 20]  # 每个选项宽度(跟图形缩放有关系)
x_step = 44  # x方向行距(两个选项水平方向距离)
y_step = 28  # y方向行距(两个选项垂直方向距离)
blank = 92  # 每组间距(5个一组)水平方向距离
centers = []  # 每个选项的中心点坐标,用来框选选项
# 答题区域有5行多1组,这里只处理前面5行,最后一组暂不处理
startPonits = [(25, 44), (25, 216), (26, 392), (26, 566), (28, 744)]
for (i, p) in enumerate(startPonits):
    temp = []  # 暂存该组选项坐标
    start = list(p)  # 该行起始点坐标
    for g in range(0, 4, 1):  # 每行有4组
        if len(temp) > 0:
            startx = temp[len(temp)-1][0] + blank  # 最后一个选项的x坐标+每组间距
        else:
            startx = start[0]
        start[0] = startx
        for i in range(start[0], start[0]+5*x_step, x_step):  # 水平5个选项
            for j in range(start[1], start[1]+4*y_step, y_step):  # 垂直4个选项
                temp.append((i, j))
    for (i, c) in enumerate(temp):
        centers.append(c)

# 8.将选项绘制到答题区域
show = roi.copy()
for (i, (x, y)) in enumerate(centers):
    left = x-(int)(item[0]/2)
    top = y-(int)(item[1]/2)
    # 绘选项区域矩形
    cv2.rectangle(show, (left, top), (left +
                  item[0], top + item[1]), (0, 0, 255), -1)
    # 绘制序号标签
    cv2.putText(show, str(i+1), (left, top+10),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
imshow("5.show", show)

运行结果

3.遍历每个选项,计算非0像素点个数

# 9.截取每个答题选项,并计算非0像素点个数
map = roi.copy()
map = cv2.cvtColor(map, cv2.COLOR_BGR2GRAY)
map = cv2.threshold(map, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
points = []
for (i, (x, y)) in enumerate(centers):
    left = x-(int)(item[0]/2)
    top = y-(int)(item[1]/2)

    # 截取每个选项小图
    item_img = map[top:top+item[1], left:left+item[0]]
    count = cv2.countNonZero(item_img)
    points.append(count)
    cv2.imwrite("item/"+str(i+1)+"-"+str(count)+".jpg", item_img)

这里为了方便观看,将每个选项截取另存为成图像了,以索“引号+非0像素点个数”命名。实际过程中可以不另存为图像。

大概像这样:

4.整理答案

# 10.整理答案
answer = []  # 二维数组:保存每个题ABCD4个选项值
group = []  # 将点分组,每4个1组,对应每题的4个选项
for i in range(0, len(points), 1):
    group.append(points[i])
    if (i+1) % 4 == 0:
        answer.append(group)
        group = []

def printItem(i, optoins):
    question = {0: "A", 1: "B", 2: "C", 3: "D"}
    index = 0
    if i < 80:
        # 单选题(非0像素点最少的既是答案)
        an = min(optoins)
        index = optoins.index(an)
        print("第"+str(i+1)+"题"+question.get(index))
    else:
        # 多选题(根据阈值来判断是否选中)
        ans = ""
        for (j, p) in enumerate(options):
            if p < 400:
                index = optoins.index(p)
                ans += question.get(index)
        print("第"+str(i+1)+"题"+ans)

# 打印题目答案
for (i, options) in enumerate(answer):
    printItem(i, options)

输出结果:

完整代码

import cv2
import numpy as np

# 1.读取图片并缩放
orginImg = cv2.imread("1.jpg")
size = ((int)(650*1.8), (int)(930*1.8))  # 尽可能将图片弄大一点,下面好处理
img = cv2.resize(orginImg, size)

# 显示图像
def imshow(name, image):
    scale_percent = 50  # 缩放比例
    width = int(image.shape[1] * scale_percent / 100)
    height = int(image.shape[0] * scale_percent / 100)
    dim = (width, height)
    resized_image = cv2.resize(image, dim, interpolation=cv2.INTER_AREA)
    cv2.imshow(name, resized_image)

imshow("1.orgin", img)

# 2.转灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
imshow("2.gray", gray)

# 3.黑帽运算:移除干扰项
cvblackhat = cv2.morphologyEx(
    gray, cv2.MORPH_BLACKHAT, cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)))
imshow("3.black", cvblackhat)

# 4.二值化突出轮廓,自动阈值范围 cv2.THRESH_BINARY|cv2.THRESH_OTSU
thresh = cv2.threshold(
    cvblackhat, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
imshow("4.thresh", thresh)

# 5.提取轮廓,并在图上标记轮廓
cnts, hierarchy = cv2.findContours(
    thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mark = img.copy()
cv2.drawContours(mark, cnts, -1, (0, 0, 255), 2)
imshow("5.contours", mark)

# 6.提取我们感兴趣的部分(这里我们只需要答题部分) img[y:y+h, x:x+w]
roi = None
for (i, c) in enumerate(cnts):
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w/float(h)
    if w > 500 and h > 500 and ar > 0.9 and ar < 1.1:
        roi = img[y:y+h, x:x+w]
        break
imshow("5.roi", roi)

# 7.查找每个选项的中心点坐标
# 思路:
# 通过分析:
# 1.答题区域分为6行,每行4组,第6行只有1组,我们暂不处理第6行,只处理前面5行。
# 2.只要给定每一行第一个选项中心坐标,该行其余选项的中心坐标可以推算出来。
# 3.通过找到每个选项中心点坐标,再加上选项宽高,就可以在答题区域绘出每个选项的范围。
# 4.通过计算每个选项范围图像里非0像素点个数,结合阈值判断该选项是否选中。
# 5.结合题目个数,遍历每个选项,构造出最终答案。
item = [34, 20]  # 每个选项宽度(跟图形缩放有关系)
x_step = 44  # x方向行距(两个选项水平方向距离)
y_step = 28  # y方向行距(两个选项垂直方向距离)
blank = 92  # 每组间距(5个一组)水平方向距离
centers = []  # 每个选项的中心点坐标,用来框选选项
# 答题区域有5行多1组,这里只处理前面5行,最后一组暂不处理
startPonits = [(25, 44), (25, 216), (26, 392), (26, 566), (28, 744)]
for (i, p) in enumerate(startPonits):
    temp = []  # 暂存该组选项坐标
    start = list(p)  # 该行起始点坐标
    for g in range(0, 4, 1):  # 每行有4组
        if len(temp) > 0:
            startx = temp[len(temp)-1][0] + blank  # 最后一个选项的x坐标+每组间距
        else:
            startx = start[0]
        start[0] = startx
        for i in range(start[0], start[0]+5*x_step, x_step):  # 水平5个选项
            for j in range(start[1], start[1]+4*y_step, y_step):  # 垂直4个选项
                temp.append((i, j))
    for (i, c) in enumerate(temp):
        centers.append(c)

# 8.将选项绘制到答题区域
show = roi.copy()
for (i, (x, y)) in enumerate(centers):
    left = x-(int)(item[0]/2)
    top = y-(int)(item[1]/2)
    # 绘选项区域矩形
    cv2.rectangle(show, (left, top), (left +
                  item[0], top + item[1]), (0, 0, 255), -1)
    # 绘制序号标签
    cv2.putText(show, str(i+1), (left, top+10),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
cv2.imshow("5.show", show)

# 9.截取每个答题选项,并计算非0像素点个数
map = roi.copy()
map = cv2.cvtColor(map, cv2.COLOR_BGR2GRAY)
map = cv2.threshold(map, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
points = []
for (i, (x, y)) in enumerate(centers):
    left = x-(int)(item[0]/2)
    top = y-(int)(item[1]/2)

    # 截取每个选项小图
    item_img = map[top:top+item[1], left:left+item[0]]
    count = cv2.countNonZero(item_img)
    points.append(count)
    cv2.imwrite("item/"+str(i+1)+"-"+str(count)+".jpg", item_img)

# 10.整理答案
answer = []  # 二维数组:保存每个题ABCD4个选项值
group = []  # 将点分组,每4个1组,对应每题的4个选项
for i in range(0, len(points), 1):
    group.append(points[i])
    if (i+1) % 4 == 0:
        answer.append(group)
        group = []

def printItem(i, optoins):
    question = {0: "A", 1: "B", 2: "C", 3: "D"}
    index = 0
    if i < 80:
        # 单选题(非0像素点最少的既是答案)
        an = min(optoins)
        index = optoins.index(an)
        print("第"+str(i+1)+"题"+question.get(index))
    else:
        # 多选题(根据阈值来判断是否选中)
        ans = ""
        for (j, p) in enumerate(options):
            if p < 400:
                index = optoins.index(p)
                ans += question.get(index)
        print("第"+str(i+1)+"题"+ans)

# 打印题目答案
for (i, options) in enumerate(answer):
    printItem(i, options)

cv2.waitKey(0)
cv2.destroyAllWindows()

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

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

相关文章

【数据分析:RFM客户价值度模型】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索大数据技术RFM客户价值度模型&#xff0c;本篇文章主要讲述了&#xff1a;RFM客户价值度模型等等。欢迎大家一起探索讨论&#xff01;&#xff01;&#xff01…

大数据-99 Spark 集群 Spark Streaming DStream 文件数据流、Socket、RDD队列流

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

GATK ReadsPathDataSource类介绍

GATK(Genome Analysis Toolkit)是一个广泛使用的基因组分析工具包,它的核心库之一是htsjdk,用于处理高通量测序数据。在GATK中,ReadsPathDataSource类是负责管理和提供读取高通量测序数据文件(如BAM、SAM、CRAM)的类。 常见使用场景 数据加载:在GATK的基因组分析工具链…

MySQL的MRR(Multi-Range Read)优化原理详解

❃博主首页 &#xff1a; 「码到三十五」 &#xff0c;同名公众号 :「码到三十五」&#xff0c;wx号 : 「liwu0213」 ☠博主专栏 &#xff1a; <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 &#xff1a…

LeetCode:反转区间内的链表

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;力扣刷题日记 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 文章目录 反转区间内的链表题目链接方法一&#xff1a;拆开反转…

【TB作品】PIC16F1719单片机,EEPROM,PFM,读写

对于PIC16F1719单片机&#xff0c;没有直接的EEPROM&#xff0c;而是使用高耐久度的程序闪存&#xff08;PFM&#xff09;作为非易失性数据存储区域。这个区域特别适合存储那些需要频繁更新的数据。读写这个内存区域需要操作一些特殊功能寄存器&#xff0c;比如用于地址的PMADR…

2.K8s集群搭建

K8s搭建 搭建方案kubeadm搭建系统初始化操作k8s Master节点初始化将node节点加入集群安装网络插件Calico集群测试 搭建方案 minikube&#xff1a;轻量化的Kubernetes集群&#xff0c;为了能够更好学习和体验k8s功能而推出的&#xff0c;借助个人PC的虚拟化环境就可以实现Kuber…

如何使用ssm实现基于java web的网上书城系统的设计与实现+vue

TOC ssm123基于java web的网上书城系统的设计与实现vue JAVA简介 Java主要采用CORBA技术和安全模型&#xff0c;可以在互联网应用的数据保护。它还提供了对EJB&#xff08;Enterprise JavaBeans&#xff09;的全面支持&#xff0c;java servlet API&#xff0c;JSP&#xff…

【Redis】Redis客户端——Jedis(Java)

Redis Java使用案例 环境配置引入依赖配置端⼝转发连接 Redis Server Java基础代码操作Redisset 和 getexsits 和 del 环境配置 引入依赖 Java 操作 redis 的客⼾端有很多. 其中最知名的是 jedis. 创建 maven 项⽬, 把 jedis 的依赖拷⻉到 pom.xml 中. <!-- https://mvnr…

ssrf--web-ssrfme例题

将web-ssrfme.zip解压缩在Ubuntu下 Docker-compose up -d 更新后的镜像重新启动容器 可以看到已经拉取成功ssrfme镜像 我们使用端口访问文件&#xff0c;可以看到有一个过滤条件&#xff0c;它限制了file&#xff0c;dict协议&#xff0c;127.0.0.1和localhost 也不能用&…

【55-90】结构型模式

目录 一.结构型模式概述 二.代理模式 2.1 概述 2.2 结构 2.3 静态代理 2.4 JDK动态代理 2.5 CGLIB动态代理 2.6 三种代理的对比 2.7 优缺点 三.适配器模式 3.1 概述 3.2 结构 3.3 类适配器模式 3.4 对象适配器模式 3.5 应用场景 四.装饰者模式 4.1 概述 4.2 结…

从并发20到并发120之laravel性能优化

调优成果 遇到问题 单台服务并发20&#xff0c;平均响应时间1124ms&#xff0c;通过htop观察&#xff0c;发现cpu占用率达到100%&#xff08;包括sleep的进程&#xff09;&#xff0c;内存几乎没怎么用。 调优后 单机最大吞吐量达到120 响应时长不超过1000ms 硬件信息 …

数学建模----线性回归分析(引入热力图的绘制方法)

目录 0.直击重点 1.一元线性回归分析 1.1散点图的绘制 1.2相关性的分类 1.3计算相关系数 1.4模型的检验 1.5模型的预测 2.多重线性回归分析&#xff08;上&#xff09; 2.1多重线性的概念 2.2散点图的分类 2.3热力图的绘制 2.4根据结果确定新的变量 3.多重线性…

【开端】 如何判断手机号码属于哪个国家(手机号判断正则)汇总

import org.apache.commons.lang3.StringUtils; /** * 手机号判断正则 */ public enum MobileRegularExp { /** * 国家 正则 */ CN("中国", 86, "^(\\?0?86\\-?)?1[3456789]\\d{9}$"), TW("中国台湾", 886, "…

第七节 循环结构;goto语句

目录 7.1 while循环 7.1.1 if 和 while的对⽐ 7.1.2 while的执行流程 7.1.3 while的练习 7.2 for循环 7.2.1 语法形式 7.2.2 for循环的执⾏流程 7.2.3 for 循环的练习 7.3 while 和 for 循环的对比 7.4 do while 循环 7.4.1 do while 的语法形式 7.4.2 do while循…

Jamba前生今世:1.5开源来袭

AI21服务于企业&#xff0c;为企业构建基础模型和AI系统以加速GenAI在生产中的使用。AI21 成立于2017年&#xff0c;已从NVIDIA、Intel、Google等公司共筹集了3.36亿美元。它是最早将生成式AI推向大众的公司之一&#xff0c;借助AI21平台&#xff0c;企业可以构建自己的生成式A…

菲菲更名宝贝:批量处理,文件命名不再繁琐

你是否有这样的经历&#xff1f;曾几何时&#xff0c;在堆积如山的文件中迷失方向&#xff0c;为了一个个手动重命名文件而加班到深夜&#xff1f;是否渴望有一种魔法&#xff0c;能瞬间让你的文件整理得井井有条&#xff0c;让繁琐的命名工作变得轻松愉快&#xff1f;那么&…

大数据毕业设计开题报告100例

文章目录 &#x1f6a9; 1 前言1.1 选题注意事项1.1.1 难度怎么把控&#xff1f;1.1.2 题目名称怎么取&#xff1f; 1.2 开题选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢&#xff1f; &#x1f6a9;2 选题概览&#x1f6a9; 3 项目概览题目1 : 深度学习社…

前端网站优化-Brotli 压缩

杨绛先生说:“岁不声不响&#xff0c;你且不慌不忙。在凡俗的烟火里&#xff0c;愿以素心&#xff0c;阅来日方长。生活总是一地鸡毛&#xff0c;繁杂琐碎的日常&#xff0c;无力掌控的局面&#xff0c;以及猝不及防的变化&#xff0c;让日子多了几分慌张”。 市井长巷&#xf…

ssrf漏洞复现

环境搭建 zhuifengshaonianhanlu/pikachu: 一个好玩的Web安全-漏洞测试平台 (github.com) 直接将其复制到linux环境下拉取docker就行 我这里已经拉去过了&#xff0c;如果拉去速度慢话&#xff0c;可以在/etc/docker下的daemon.json中配置镜像加速 vim /etc/docker/daemon.js…