当前位置:   article > 正文

Python+Opencv实现自动化阅卷_网上阅卷通过程序自动化处理

网上阅卷通过程序自动化处理

  在我们的日常生活中有很多自动化阅卷的需求,比如我们的期中考试、期末考试、中考、高考、四级、六级等等。总而言之,现实场景中我们有很多的考试,但是每一次考完试后,焦虑的不仅仅是学生,还有老师!老师们需要夜以继日的去阅卷,尽快得出学生的考试成绩,这是一个费人费力但又是刚需的一个任务。庆幸的是当前这个任务基本上已经被自动化啦,本文的任务就是来理解这其中的奥秘!

一、什么是自动化阅卷/网上阅卷?

百度的定义-网上阅卷全称是网上阅卷系统,是指以计算机网络技术和电子扫描技术为依托,实现客观题自动阅卷,主观题网上评卷的一种现代计算机系统。
这里面有几点需要进行说明:

  • 当前的网上阅卷系统还是部分自动化的,即智能实现客观题目的自动阅卷,最典型的就是选择题!而主观题还是需要进行网上阅卷的。
  • 当前的网上阅卷系统主要依托于两个仪器,计算机+电子扫描仪,电子扫描仪的任务是将学生的答题卡录入到电脑中去,充当了摄像机的功能。

二、自动化阅卷有哪些优势?

  正如文章开头所讲,现实的场景中我们有各种各样的考试,老师们需要进行大量的阅卷工作,因而解决这个问题具有重大的意义。

  1. 高效-相比于传统的教师阅卷而言,自动化阅卷系统的速度更快,这是毋庸置疑的,因为人不可能一下子记住所有的选择题答案,需要不断的查看标准答案,这很费时;
  2. 鲁棒-相比于传统的教师阅卷而言,自动化阅卷系统更加理智,不会夹杂一些情感信息,同时不会受到外界条件的干扰,不会因为劳累等原因给出错误的判断,即使是在复杂的干扰环境下,仍然能得到正确的结果;
  3. 准确-相比于传统的教师阅卷而言,自动化阅卷系统能够获得更加准确的结果,只要设备正常,答题卡正常,就能准确的得出答案;
  4. 省时省力-自动化阅卷系统可以节省大量的人力和物力,这是毋庸置疑的!

三、 自动化阅卷的实现流程

步骤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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120

五、自动化阅卷可视化及分析

在这里插入图片描述
  上图展示了整个算法运行的一些中间输出结果,其中第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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144

六、自动化阅卷效果展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  上面几幅图展示了整个算法的实际测试效果,整体来看,整个算法的准确率高,基本可以满足实时场景的需要。

七、问题探讨

  尽管从上面的测试结果来看该算法比较鲁棒、可靠,但是在现实场景中会遇到一些其它的问题,比如

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详聊!!!

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/730132
推荐阅读
相关标签
  

闽ICP备14008679号