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

  • 20
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基于Python的主观题自动阅卷系统是一种利用人工智能和自然语言处理技术实现自动化评分工具。该系统旨在减轻教师在批改大量主观题时的负担,提高评分效率和准确性。以下是关于这个系统的简要介绍:功能特点:自动识别题目类型:系统可以识别不同类型的主观题,如填空题、选择题、简答题等,从而为每种类型的题目提供相应的评分标准。智能评分:系统通过深度学习算法对学生的答案进行分析,根据预设的评分规则为每个答案打分。这有助于减少人为评分过程中的主观性和偏见。错误纠正:系统能够识别并纠正学生在作答过程中可能犯的常见错误,如拼写错误、语法错误等,从而提高评分的准确性。反馈与建议:系统会为学生提供详细的评分报告,包括正确答案、得分情况以及可能存在的改进空间。这有助于学生了解自己的优缺点,提高学习效果。应用场景:基于Python的主观题自动阅卷系统适用于各类教育机构,如学校、培训机构等。它可以帮助教师更高效地批改大量主观题,节省时间和精力,同时提高评分质量。此外,该系统还可以应用于在线教育平台,为学生提供实时、准确的评估服务。技术实现:基于Python的主观题自动阅卷系统主要依赖于自然语言处理(NLP)技术和机器学习算法。具体来说,它可能包括以下几个方面的技术实现:文本预处理:对学生的答案进行分词、去停用词、词干提取等操作,以便后续的分析和评分。特征提取:从预处理后的文本中提取有用的特征信息,如关键词、短语等,用于后续的评分计算。模型训练:使用深度学习算法(如神经网络)对提取的特征进行训练,以便生成一个能够自动评分的模型。结果输出:将评分结果以可视化的形式呈现给用户,如表格、图表等。
Python中的OpenCV库可以通过使用计算机视觉技术来实现动态物体检测和跟踪。具体步骤如下: 1.导入必要的库: 首先,导入必要的Python库和OpenCV库。 import cv2 import numpy as np 2.初始化摄像头: 创建VideoCapture对象来读取视频或图像。这可以是连接到计算机的摄像头或加载的视频文件。 cap = cv2.VideoCapture(0) 3.设置背景图像: 通过读取一些帧来设置背景图像,可以考虑使用静态场景的参考。 _, background = cap.read() 4.捕捉动态物体: 在一个while循环中,读取视频的每一帧并对其进行处理。首先,要检测到动态物体,需要计算背景差异。通过对背景图像和当前帧之间的差异进行阈值处理,从而检测图像中的物体。 _, frame = cap.read() diff = cv2.absdiff(background, frame) gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (5, 5), 0) _, thresh = cv2.threshold(blur, 20, 255, cv2.THRESH_BINARY) dilated = cv2.dilate(thresh, None, iterations=3) contours, _ = cv2.findContours(dilated, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 5.绘制轮廓: 找到轮廓后,可以绘制矩形或圆形框来标识动态物体。 for contour in contours: (x, y, w, h) = cv2.boundingRect(contour) cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) 6.显示结果: 通过显示每一帧的结果来实现动态物体的实时检测。 cv2.imshow("Dynamic Object Detection", frame) 7.释放资源: 在完成操作之后,释放资源并关闭窗口。 cap.release() cv2.destroyAllWindows() 通过以上步骤,我们可以利用PythonOpenCV实现动态物体的检测和跟踪。可以根据具体需求进行进一步的优化和改进。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值