赞
踩
在我们的日常生活中有很多自动化阅卷的需求,比如我们的期中考试、期末考试、中考、高考、四级、六级等等。总而言之,现实场景中我们有很多的考试,但是每一次考完试后,焦虑的不仅仅是学生,还有老师!老师们需要夜以继日的去阅卷,尽快得出学生的考试成绩,这是一个费人费力但又是刚需的一个任务。庆幸的是当前这个任务基本上已经被自动化啦,本文的任务就是来理解这其中的奥秘!
百度的定义-网上阅卷全称是网上阅卷系统,是指以计算机网络技术和电子扫描技术为依托,实现客观题自动阅卷,主观题网上评卷的一种现代计算机系统。
这里面有几点需要进行说明:
正如文章开头所讲,现实的场景中我们有各种各样的考试,老师们需要进行大量的阅卷工作,因而解决这个问题具有重大的意义。
步骤1-检测图片中的答题卡;
步骤2-采取透视变换取得答题卡自上而下的俯视图.;
步骤3-从第二步结果中获取泡泡集(例如可能的答案选项);
步骤4-按行进行排序处理;
步骤5-确认每行用户填写的答案
步骤6-查找正确的答案,判断用户选择是否正确
步骤7-遍历所有的问题,重复以上的步骤。
# coding=utf-8 # 导入一些python包 from imutils.perspective import four_point_transform from imutils import contours 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} # 读取输入图片 image = cv2.imread(args["image"]) # 输入图片灰度化 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 进行高斯滤波 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 进行边缘检测处理 edged = cv2.Canny(blurred, 75, 200) # 在边缘图像中发现轮廓 cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) 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) # 如果我们在近似轮廓中发现4个点,表明我们发现了答题卡 if len(approx) == 4: docCnt = approx break # 将变换关系应用到原始的输入图像和灰度图像中,从而获得一个答题卡的鸟瞰图 paper = four_point_transform(image, docCnt.reshape(4, 2)) warped = four_point_transform(gray, docCnt.reshape(4, 2)) # 在warped图像上面使用Otsu方法进行阈值分割 thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # 在二值图像中寻找轮廓 cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) questionCnts = [] # 遍历每一个轮廓 for c in cnts: # 计算轮廓的BB,并根据BB获得横纵比 (x, y, w, h) = cv2.boundingRect(c) ar = w / float(h) # 为了将轮廓标记为问题,区域应足够宽、足够高,且长宽比约等于1 # 即选择满足条件的结果 if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1: questionCnts.append(c) # 对问题轮廓进行排序 questionCnts = contours.sort_contours(questionCnts, method="top-to-bottom")[0] correct = 0 # 对于每一个问题而言,有5个可能的答案,遍历每一个可能的答案 for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)): # 对每一个问题的5个答案进行排序 cnts = contours.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) # 将mask应用到阈值图像中,然后计算气泡区域中的非零像素数 mask = cv2.bitwise_and(thresh, thresh, mask=mask) total = cv2.countNonZero(mask) # 如果它大于该非0像素,我们认为它是答案 if bubbled is None or total > bubbled[0]: bubbled = (total, j) # 初始化轮廓颜色和正确答案的索引 color = (0, 0, 255) k = ANSWER_KEY[q] # 将用户的答案和标准答案进行对比,进行统计 if k == bubbled[1]: color = (0, 255, 0) correct += 1 # 在图像中绘制结果 cv2.drawContours(paper, [cnts[k]], -1, color, 3) # 计算正确率并打印得分 score = (correct / 5.0) * 100 print("[INFO] score: {:.2f}%".format(score)) # 显示原始图片和测试后的结果并显示得分 cv2.putText(paper, "{:.2f}%".format(score), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) cv2.imshow("Original", image) cv2.imshow("Exam", paper) cv2.waitKey(0)
上图展示了整个算法运行的一些中间输出结果,其中第1行第1列表示输入的原图,对应于程序中的image;第1行第2列表示灰度化的结果,对应于程序中的gray;第1行第3列表示边缘检测的结果,对应于程序中的edged;第1行第4列是对原图中所有检测到的轮廓的可视化,我们可以观察到基本上所有的答题区域都可以检测到;第2行第1列表示的使用坐标变换后的原图,即从原图中裁剪出来的答题卡图片,对应于程序中的paper;第2行第2列表示的灰度化的图片的变换结果,对应于程序中的warped;第2行第3列表示的在paper中检测到的所有轮廓,我们需要的答题区域都可以检测出来,答过的结果和没有答过的结果差距较大;第2行第2列表示最终的输出结果,可以看出该答题卡的答案和正确的答案完全符合,因此得100分。
上图展示了整个mask的生成过程,使用.gif动态图进行展示,我们可以观察到整个mask从上到下,从左到右遍历所有的答题位置,并判断当前的结果是否和标准答案相同。
# 添加中间结果可视化 # coding=utf-8 # 导入一些python包 from imutils.perspective import four_point_transform from imutils import contours import numpy as np import argparse import imageio 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} # 读取输入图片 image = cv2.imread(args["image"]) image1 = cv2.imread(args["image"]) # 输入图片灰度化 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) cv2.imwrite("vis\gray.png", gray) # 进行高斯滤波 blurred = cv2.GaussianBlur(gray, (5, 5), 0) cv2.imwrite("vis\blurred.png", blurred) # 进行边缘检测处理 edged = cv2.Canny(blurred, 75, 200) cv2.imwrite("vis\edged.png", edged) ret,binary = cv2.threshold(gray.copy(), 127, 255, cv2.THRESH_BINARY) contour, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(image1,contour,-1,(0,255,0),3) cv2.imwrite("vis\Contours1.png", image1) # 在边缘图像中发现轮廓 cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) 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) # 如果我们在近似轮廓中发现4个点,表明我们发现了答题卡 if len(approx) == 4: docCnt = approx break # 将变换关系应用到原始的输入图像和灰度图像中,从而获得一个答题卡的鸟瞰图 paper = four_point_transform(image, docCnt.reshape(4, 2)) paper1 = four_point_transform(image, docCnt.reshape(4, 2)) cv2.imwrite("vis\paper1.png", paper1) warped = four_point_transform(gray, docCnt.reshape(4, 2)) cv2.imwrite("vis\warped.png", warped) # 在warped图像上面使用Otsu方法进行阈值分割 thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # 在二值图像中寻找轮廓 cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) questionCnts = [] ret1,binary1 = cv2.threshold(warped.copy(), 127, 255, cv2.THRESH_BINARY) contour, hierarchy = cv2.findContours(binary1, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(paper1,contour,-1,(255,0,0),3) cv2.imwrite("vis\Contours2.png", paper1) # 遍历每一个轮廓 for c in cnts: # 计算轮廓的BB,并根据BB获得横纵比 (x, y, w, h) = cv2.boundingRect(c) ar = w / float(h) # 为了将轮廓标记为问题,区域应足够宽、足够高,且长宽比约等于1 # 即选择满足条件的结果 if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1: questionCnts.append(c) # 对问题轮廓进行排序 questionCnts = contours.sort_contours(questionCnts, method="top-to-bottom")[0] correct = 0 # 对于每一个问题而言,有5个可能的答案,遍历每一个可能的答案 img_mask = [] for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)): # 对每一个问题的5个答案进行排序 cnts = contours.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) # 将mask应用到阈值图像中,然后计算气泡区域中的非零像素数 mask = cv2.bitwise_and(thresh, thresh, mask=mask) total = cv2.countNonZero(mask) img_mask.append(mask) # 如果它大于该非0像素,我们认为它是答案 if bubbled is None or total > bubbled[0]: bubbled = (total, j) # 初始化轮廓颜色和正确答案的索引 color = (0, 0, 255) k = ANSWER_KEY[q] # 将用户的答案和标准答案进行对比,进行统计 if k == bubbled[1]: color = (0, 255, 0) correct += 1 # 在图像中绘制结果 cv2.drawContours(paper, [cnts[k]], -1, color, 3) # 计算正确率并打印得分 score = (correct / 5.0) * 100 print("[INFO] score: {:.2f}%".format(score)) # 显示原始图片和测试后的结果并显示得分 cv2.putText(paper, "{:.2f}%".format(score), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) cv2.imshow("Original", image) cv2.imwrite("vis\paper.png", paper) cv2.imshow("Exam", paper) imageio.mimsave("vis\mask.gif", img_mask) cv2.waitKey(0)
上面几幅图展示了整个算法的实际测试效果,整体来看,整个算法的准确率高,基本可以满足实时场景的需要。
尽管从上面的测试结果来看该算法比较鲁棒、可靠,但是在现实场景中会遇到一些其它的问题,比如
1、空白答题卡
对于这种情况而言,当计算cv2.countNonZero时,我们可以设置一个最小的阈值(108行),如果这个值足够大,我们就可以将泡泡标记为"已填涂".相反,如果total太小,我们就跳过那个泡泡.如果直到行结尾,没有泡泡具有足够大的阈值计数,就将该问题标记为测试者"跳过".
2、每个问题填写了多个答案
对于问题2而言,我们采用类似的方法,我们追踪一个问题具有total的泡泡数量是否超过预定义值,我们将其标记为无效问题或者是错误答案。
3、对于复杂的答题卡该如何处理呢?
上图仅仅是答题卡的一种,对于答题卡而言,基本每一个考试会有一种类型,对于这些类型我们不可能分别设计一个算法吧,这是基于深度学习的方法就具有独特的优势啦。具体的效果大家可以去实现并测试一些欧。
1、参考博客
[1] 如果您对AI、自动驾驶、AR、ChatGPT等技术感兴趣,欢迎关注我的微信公众号“AI产品汇”,有问题可以在公众号中私聊我!
[2] 该博客是本人原创博客,如果您对该博客感兴趣,想要转载该博客,请与我联系(qq邮箱:1575262785@qq.com),我会在第一时间回复大家,谢谢大家的关注。
[3] 由于个人能力有限,该博客可能存在很多的问题,希望大家能够提出改进意见。
[4] 如果您在阅读本博客时遇到不理解的地方,希望您可以联系我,我会及时的回复您,和您交流想法和意见,谢谢。
[5] 本文测试的图片可以通过关注微信公众号AI产品汇之后找我索取!
[6] 本人业余时间承接各种本科毕设设计和各种小项目,包括图像处理(数据挖掘、机器学习、深度学习等)、matlab仿真、python算法及仿真等,有需要的请加QQ:1575262785详聊!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。