四、模拟英语四六级答题卡识别阅卷评分

一、思路分析

首先拿到答题卡照片的时候,需要对照片进行一系列预处理操作,通过透视变换将图像摆正方便后续的操作。每一道题五个选项,有五道题,通过字典存放准确答案。没有依次对答题卡进行轮廓检测,这里采用的是正方形,宽高比是1:1,当然也可以是矩形,也可以通过指定其他的筛选进行进行过滤筛选。最后通过掩膜操作,因为用户所选择的答案都是被涂过的,也就是通过判断黑色和白色来进行区分是否是用户选择的答案。一行一行的存储,因为一道题是五个选项,每一行是一道题,这里采用从上到下从左到右分别依次存放1-5题的A-E选项。通过与标准答案字典所存放的索引进行对比,从而给出用户得分。

二、导包及其相关函数

#导入工具包
import numpy as np
import argparse
import imutils
import cv2
# 设置参数
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,help="path to the input image")
args = vars(ap.parse_args())# 正确答案
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}#存放正确答案BEADB
def order_points(pts):# 一共4个坐标点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 rectdef 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")#看个人需求而定,这里将图像左上角规定为(0,0)位置# 计算变换矩阵M = cv2.getPerspectiveTransform(rect, dst)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 b: b[1][i], reverse=reverse))#排完序之后,前五个是第一题的,之后每五个依次为下一题的return cnts, boundingBoxes
def cv_show(name,img):cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()  

三、对答题卡进行预处理及透视变换摆正

# 预处理
image = cv2.imread(args["image"])#读取图像
contours_img = image.copy()#为了不改动原始图像,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)#Canny边缘检测
cv_show('edged',edged)
# 轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]#轮廓检测完之后会得到三个返回值,这里的[1]存放的是轮廓信息
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3) #将图像通过透视变换进行摆正,绘制出答题卡的大致轮廓,拿到的答题卡图像也不一定是正儿八经的矩形
cv_show('contours_img',contours_img)
docCnt = None
# 确保检测到了
if len(cnts) > 0:#因为可能会检测到其他干扰影响,但是答题卡的轮廓是最大的# 根据轮廓大小进行排序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:#多边形顶点有四个也就是矩形,这个就是我们的答题卡轮廓docCnt = approxbreak
# 执行透视变换
warped = four_point_transform(gray, docCnt.reshape(4, 2))
cv_show('warped',warped)

四、对每一道题均进行轮廓检测,遍历筛选

# 自适应阈值处理
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.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]#这里再次进行轮廓检测,之所以不用霍夫圆检测是因为有可能答题卡会被全部涂满甚至越界
cv2.drawContours(thresh_Contours,cnts,-1,(0,0,255),3) 
cv_show('thresh_Contours',thresh_Contours)
questionCnts = []
# 遍历,对所有的轮廓进行筛选
for c in cnts:# 计算比例和大小(x, y, w, h) = cv2.boundingRect(c)ar = w / float(h)#因为是圆形,这里是宽高比,外接矩形差不多宽高比是1:1# 根据实际情况指定标准if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:questionCnts.append(c)
# 按照从上到下进行排序
questionCnts = sort_contours(questionCnts,	method="top-to-bottom")[0]
correct = 0

五、对比答案,评分

# 每排有5个选项
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):#因为每一题都有五个选项,q为第几行# 排序cnts = sort_contours(questionCnts[i:i + 5])[0]#第i题的五个结果bubbled = None# 遍历每一个结果for (j, c) in enumerate(cnts):#j为第i道题的第j个选项# 使用mask来判断结果mask = np.zeros(thresh.shape, dtype="uint8")cv2.drawContours(mask, [c], -1, 255, -1) #-1表示填充cv_show('mask',mask)# 通过计算非零点数量来算是否选择这个答案mask = cv2.bitwise_and(thresh, thresh, mask=mask)total = cv2.countNonZero(mask)#看下框出来的选项中非零的个数有多少个# 通过阈值判断if bubbled is None or total > bubbled[0]:bubbled = (total, j)# 对比正确答案color = (0, 0, 255)k = ANSWER_KEY[q]#第q道题的答案# 判断正确if k == bubbled[1]:color = (0, 255, 0)correct += 1# 绘图cv2.drawContours(warped, [cnts[k]], -1, color, 3)
score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped, "{:.2f}%".format(score), (10, 30),cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped)
cv2.waitKey(0)

六、Pycharm参数设定

设置参数指定图像路径
在这里插入图片描述
找到Edit Configurations
将image参数改成自己测试图像路径--image images\test11.png,其中images\test11.png为答题卡路径
在这里插入图片描述

七、完整代码

#导入工具包
import numpy as np
import argparse
import imutils
import cv2# 设置参数
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,help="path to the input image")
args = vars(ap.parse_args())# 正确答案
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}#存放正确答案BEADBdef order_points(pts):# 一共4个坐标点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 rectdef 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")#看个人需求而定,这里将图像左上角规定为(0,0)位置# 计算变换矩阵M = cv2.getPerspectiveTransform(rect, dst)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 b: b[1][i], reverse=reverse))#排完序之后,前五个是第一题的,之后每五个依次为下一题的return cnts, boundingBoxes
def cv_show(name,img):cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()  # 预处理
image = cv2.imread(args["image"])#读取图像
contours_img = image.copy()#为了不改动原始图像,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)#Canny边缘检测
cv_show('edged',edged)# 轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]#轮廓检测完之后会得到三个返回值,这里的[1]存放的是轮廓信息
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3) #将图像通过透视变换进行摆正,绘制出答题卡的大致轮廓,拿到的答题卡图像也不一定是正儿八经的矩形
cv_show('contours_img',contours_img)
docCnt = None# 确保检测到了
if len(cnts) > 0:#因为可能会检测到其他干扰影响,但是答题卡的轮廓是最大的# 根据轮廓大小进行排序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:#多边形顶点有四个也就是矩形,这个就是我们的答题卡轮廓docCnt = approxbreak# 执行透视变换
warped = four_point_transform(gray, docCnt.reshape(4, 2))
cv_show('warped',warped)# 自适应阈值处理
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.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]#这里再次进行轮廓检测,之所以不用霍夫圆检测是因为有可能答题卡会被全部涂满甚至越界
cv2.drawContours(thresh_Contours,cnts,-1,(0,0,255),3) 
cv_show('thresh_Contours',thresh_Contours)
questionCnts = []# 遍历,对所有的轮廓进行筛选
for c in cnts:# 计算比例和大小(x, y, w, h) = cv2.boundingRect(c)ar = w / float(h)#因为是圆形,这里是宽高比,外接矩形差不多宽高比是1:1# 根据实际情况指定标准if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:questionCnts.append(c)# 按照从上到下进行排序
questionCnts = sort_contours(questionCnts,	method="top-to-bottom")[0]
correct = 0# 每排有5个选项
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):#因为每一题都有五个选项,q为第几行# 排序cnts = sort_contours(questionCnts[i:i + 5])[0]#第i题的五个结果bubbled = None# 遍历每一个结果for (j, c) in enumerate(cnts):#j为第i道题的第j个选项# 使用mask来判断结果mask = np.zeros(thresh.shape, dtype="uint8")cv2.drawContours(mask, [c], -1, 255, -1) #-1表示填充cv_show('mask',mask)# 通过计算非零点数量来算是否选择这个答案mask = cv2.bitwise_and(thresh, thresh, mask=mask)total = cv2.countNonZero(mask)#看下框出来的选项中非零的个数有多少个# 通过阈值判断if bubbled is None or total > bubbled[0]:bubbled = (total, j)# 对比正确答案color = (0, 0, 255)k = ANSWER_KEY[q]#第q道题的答案# 判断正确if k == bubbled[1]:color = (0, 255, 0)correct += 1# 绘图cv2.drawContours(warped, [cnts[k]], -1, color, 3)score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(warped, "{:.2f}%".format(score), (10, 30),cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped)
cv2.waitKey(0)

答题卡原题:
在这里插入图片描述
这里展示的太大,我就截取了其中一小部分进行展示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来就是依次对每道题进行遍历找到掩膜,一共25次,这里就不一一展示了
最后根据设定的字典里面的正确答案给出评分
在这里插入图片描述

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

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

相关文章

leetcode 17. 电话号码的字母组合 思考分析

题目 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 思考与递归程序 解空间树的宽度是输入数字对应的字符的个数&#xff0c;深度是输入的数字的个数…

BHMS的完整形式是什么?

BHMS&#xff1a;顺势疗法医学和外科学士 (BHMS: Bachelor of Homeopathic Medicine and Surgery) BHMS is an abbreviation of Bachelor of Homeopathic Medicine and Surgery. It is a medical degree program for under graduation in Homeopathy; an alternative move towa…

WordPress Event Easy Calendar插件多个跨站请求伪造漏洞

漏洞名称&#xff1a;WordPress Event Easy Calendar插件多个跨站请求伪造漏洞CNNVD编号&#xff1a;CNNVD-201309-083发布时间&#xff1a;2013-09-11更新时间&#xff1a;2013-09-11危害等级&#xff1a; 漏洞类型&#xff1a;跨站请求伪造威胁类型&#xff1a;远程CVE编号&…

XML转txt格式脚本

一、东北大学老师收集的钢材缺陷数据集是XML格式的&#xff0c;但是YOLOv5只允许使用txt文件标签 例如其中一种缺陷图片所对应的标签&#xff1a;crazing_1.xml <annotation><folder>cr</folder><filename>crazing_1.jpg</filename><source&…

python程序生成exe_使用Python程序生成QR代码的Python程序

python程序生成exeQR code is a short form of the quick response code. It is a type of matrix barcode that contains some information like some specific link, important message, email-id, etc. In Python, the qrcode module is used to generate the QR code of so…

leetcode 242. 有效的字母异位词 思考分析

题目 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 我们先考虑低阶版本&#xff0c;认为字符只有26种可能&#xff0c;然后将a ~ z的字符映射到数组的索引0 ~ 25&#xff0c;数组中存放的则是该索引出现的频次。 记录下s的频次和t的频次…

js \n直接显示字符串_显示N个字符的最短时间

js \n直接显示字符串Problem statement: 问题陈述&#xff1a; You need to display N similar characters on a screen. You are allowed to do three types of operation each time. 您需要在屏幕上显示N个相似的字符。 每次允许您执行三种类型的操作。 You can insert a c…

三、标签准备

所有操作均在anaconda中的自己配置的环境下进行 一、安装labelimg 因为YOLO模型所需要的样本标签必须是txt类型&#xff0c;本人使用labelimg软件进行对图像进行打标签操作。 pip install pycocotools-windows pip install pyqt5 pip install labelimg 通过labelimg命令打…

leetcode 39. 组合总和 思考分析

目录1、题目2、思考分析3、未经优化代码4、剪枝优化1、题目 给定一个无重复元素的数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。 2、思考分析 解空间树宽度部分即数…

一、机器学习概念

一、何为机器学习(Mechine Learning)&#xff1f; 答&#xff1a;利用已有数据(经验)&#xff0c;来训练某种模型&#xff0c;利用此模型来预测未来。机器学习是人工智能的核心Mechine Learning。 例如&#xff1a;你和狗蛋儿7点在老槐树下集合&#xff0c;如何一块约去开黑&a…

Java线程新特征——Java并发库

一、线程池 Sun在Java5中&#xff0c;对Java线程的类库做了大量的扩展&#xff0c;其中线程池就是Java5的新特征之一&#xff0c;除了线程池之外&#xff0c;还有很多多线程相关的内容&#xff0c;为多线程的编程带来了极大便利。为了编写高效稳定可靠的多线程程序&#xff0c;…

leetcode 40. 组合总和 II 思考分析

题目 给定一个数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 思考以及代码 如果我们直接套用39题的思路&#xff0c;那么就会出现重复的组合。 重复组合的…

二、线性回归

一、回归 可以拿正态分布为例&#xff0c;比如身高&#xff0c;若平均身高为1.78m&#xff0c;绝大多数人都是1.78m左右&#xff0c;超过2m的很少&#xff0c;低于1m的也不多。 很多事情都会回归到一定的区间之内&#xff0c;即回归到平均值。 机器学习没有完美解&#xff0c…

【转】HMM学习最佳范例五:前向算法1 .

五、前向算法&#xff08;Forward Algorithm&#xff09; 计算观察序列的概率&#xff08;Finding the probability of an observed sequence&#xff09; 1.穷举搜索&#xff08; Exhaustive search for solution&#xff09;  给定隐马尔科夫模型&#xff0c;也就是在模型参…

leetcode 349. 两个数组的交集 思考分析

题目 给定两个数组&#xff0c;编写一个函数来计算它们的交集。 1、暴力双for循环 class Solution { public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {vector<int> result;vector<int> res;if(nums1.siz…

三、梯度下降法求解最优θ值

一、梯度下降法(GD&#xff0c;Gradient Descent) Ⅰ、得到目标函数J(θ)&#xff0c;求解使得J(θ)最小时的θ值 当然&#xff0c;这里只是取了俩特征而已&#xff0c;实际上会有m个特征维度 通过最小二乘法求目标函数最小值 令偏导为0即可求解出最小的θ值&#xff0c;即…

leetcode 131. 分割回文串 思考分析

题目 给定一个字符串 s&#xff0c;将 s 分割成一些子串&#xff0c;使每个子串都是回文串。 返回 s 所有可能的分割方案。 思考 问题可以分为两个子问题&#xff1a;1、判断回文串2、分割数组 判断回文串 bool isPalindrome_string(string s,int startindex,int endinde…

android淡入淡出动画_在Android中淡入动画示例

android淡入淡出动画1) XML File: activity_main 1)XML文件&#xff1a;activity_main <?xml version"1.0" encoding"utf-8"?><android.support.constraint.ConstraintLayout xmlns:android"http://schemas.android.com/apk/res/android&…

[慢查优化]联表查询注意谁是驱动表 你搞不清楚谁join谁更好时请放手让mysql自行判定...

写在前面的话&#xff1a; 不要求每个人一定理解 联表查询(join/left join/inner join等)时的mysql运算过程&#xff1b; 不要求每个人一定知道线上&#xff08;现在或未来&#xff09;哪张表数据量大&#xff0c;哪张表数据量小&#xff1b; 但把mysql客户端&#xff08;如SQL…

四、梯度下降归一化操作

一、归一化 Ⅰ什么是归一化&#xff1f; 答&#xff1a;其实就是把数据归一到0-1之间&#xff0c;也就是缩放。 常用的归一化操作是最大最小值归一化&#xff0c;公式如下&#xff1a; 例如&#xff1a;1&#xff0c;3&#xff0c;5&#xff0c;7&#xff0c;9&#xff0c;10…