写点什么

自考答题卡识别初级解决方案,基于 Python OpenCV

发布于: 2021 年 04 月 10 日
自考答题卡识别初级解决方案,基于 Python OpenCV

橡皮擦,一个逗趣的互联网高级网虫。


文章背景

今天是 2021 年 4 月 10 日,一个非常普通的日子,但是对于参加自考的学生而言,今天是全国统考的日子,而橡皮擦也参与了这次全国统考。

参加考试,就要填涂答题卡,所以今天的博客就应个景,实现一个答题卡识别案例。

目标图片来自网络,如下所示。



需求描述:识别出用户答题卡涂抹的答案,并输出 A,B,C,D。


注意:本案例需要 Python OpenCV 技术栈基础知识,所以对 OpenCV 中 API 函数的基本使用不再进行解释。

实现逻辑

拿到需求之后的第一步,就是对需求进行拆解,得到问题解决的思路。

完成需求的步骤如下:

  1. 读取图片转换成灰度图;

  2. 图像二值化处理;

  3. 腐蚀或者执行形态学操作,突出涂抹区域;

  4. 边缘检测与寻找轮廓(如果存在小轮廓,需要调节第二步与第三步);

  5. 获取外接矩形,从而得到轮廓坐标;

  6. 匹配出用户涂抹答案,并输出。


编码过程

获取目标区域

本案例中采用的解决方案是,基于坐标对图片单题切割,针对上述答题卡测试图片,按题切割为下述内容。


图片中的第一题和第二题


获取图片 ROI 为最基本的图片处理方式,为便于后续测试识别效果,可先硬编码为第一题或第二题。


import cv2 as cvimport numpy as np
def main(): src = cv.imread("img.jpg") # 获取题目 1 roi_1 = src[38:60, 46:190] # 获取题目 2 roi_2 = src[60:85, 46:190]
cv.imshow("roi_1", roi_1) cv.imshow("roi_2", roi_2) cv.waitKey(0)
if __name__ == '__main__': main()
复制代码


对目标图像进行灰度,二值化,腐蚀操作

该步骤的重点是对图像进行形态学处理,为后续轮廓检索降低图像数据的处理量。二值化操作使用的是 OTSU 算法。



上述图片的实现代码如下所示,形态学处理部分,橡皮擦只对图片进行了腐蚀操作就得到了较为满意的效果。针对不同图片该步骤的处理方案不同,需要依据图像进行处理。


import cv2 as cvimport numpy as np
def main(): src = cv.imread("img.jpg") # 获取题目 1 roi_1 = src[38:60, 46:190] cv.imshow("roi_1", roi_1) # 灰度图 gray = cv.cvtColor(roi_1, cv.COLOR_BGR2GRAY) cv.imshow("gray", gray) # 二值化操作 ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) cv.imshow("thresh", thresh) # 腐蚀 kernel = np.ones((5, 5), np.uint8) dst = cv.erode(thresh, kernel=kernel) cv.imshow("dst", dst)
cv.waitKey(0)
if __name__ == '__main__': main()
复制代码


边缘检测,寻找轮廓,绘制轮廓

本步骤在于找到图像的轮廓区域,在获取轮廓函数调用时,注意只获取 外轮廓 即可,参数值为 cv.RETR_EXTERNAL。边缘检测方法采用的是 Canny 算法,可以更换为其它算法,以下代码为本步骤代码,添加到完整代码中即可。


 		# 边缘检测  	edges = cv.Canny(dst, 50, 150)    # 寻找轮廓    contours, hierarchy = cv.findContours(        edges, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)    # 绘制轮廓    cv.drawContours(roi_2, contours, -1, (0, 255, 255), 2)
复制代码


通过以上步骤,已经得到了学生涂抹区域,该区域获取的精准度不需要特别高,因后续我们是依据坐标范围进行判断,存在一定的容差空间。



通过外接矩形 x 坐标判断用户答案

本步骤需要依赖一个答案字典进行用户答案判断,该字典定义如下,其中字典的键值分别是 (选项,选项对应的 x 坐标范围)。该字典中的值依据答题卡的格式与内容进行调整,如测试图片不同,下述值也不同。


USER_ANSWER_X = {    "A": (0, 30),    "B": (30, 60),    "C": (60, 90),    "D": (90, 120)}
复制代码


最终实现的代码如下,对用户答案的判断,直接使用提前设定好的范围进行判断即可。


import cv2 as cvimport numpy as np
USER_ANSWER_X = { "A": (0, 30), "B": (30, 60), "C": (60, 90), "D": (90, 120)}
def answer(x): for item in USER_ANSWER_X.items(): if x > item[1][0] and x < item[1][1]: return f"用户答案是 {item[0]}"
def main(): src = cv.imread("img.jpg") # 获取题目 1 roi_1 = src[38:60, 46:190] cv.imshow("roi_1", roi_1) # 灰度图 gray = cv.cvtColor(roi_1, cv.COLOR_BGR2GRAY) cv.imshow("gray", gray) # 二值化操作 ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) cv.imshow("thresh", thresh) # 腐蚀 kernel = np.ones((5, 5), np.uint8) dst = cv.erode(thresh, kernel=kernel) cv.imshow("dst", dst)
edges = cv.Canny(dst, 50, 150) cv.imshow("edges", edges) # 寻找轮廓 contours, hierarchy = cv.findContours( edges, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # 绘制轮廓 cv.drawContours(roi_1, contours, -1, (0, 255, 255), 2) cv.imshow("roi_1", roi_1)
for cnt in contours: # 外接矩形 x, y, w, h = cv.boundingRect(cnt) print(x, y) ret_answer_str = answer(x) print(ret_answer_str)
cv.waitKey(0)

if __name__ == '__main__': main()
复制代码


此时运行代码,在控制台会得到用户涂抹的答案,第一行为轮廓左上角顶点的 (x,y) 坐标,第二行为判断到的用户答案。


111 9用户答案是 D
复制代码


案例总结

本案例中采用最基本 OpenCV 图像处理技巧,实现了一个初级答题卡识别系统,测试准确性时,你可以切割不同区域的试题,然后对其进行测试。

本案例可扩展的点:

  • 本案例可增加正确选项字典,除涂抹选项外,还可判断学生答案是否正确;

  • 扫描的答题卡如出现倾斜情况,需要对其进行透视变换;

  • 如果不想将答题卡按题切割,可以整体对答题卡进行处理,然后与涂抹正确选项的答题卡进行图像运算操作,直接获取作答结果与得分;

  • 边缘检测算法存在优化空间。


发布于: 2021 年 04 月 10 日阅读数: 118
用户头像

爬虫 100 例作者,蓝桥签约作者,博客专家 2021.02.06 加入

6 年产品经理+教学经验,3 年互联网项目管理经验; 互联网资深爱好者; 沉迷各种技术无法自拔,导致年龄被困在 25 岁; CSDN 爬虫 100 例作者。 个人公众号“梦想橡皮擦”。

评论

发布
暂无评论
自考答题卡识别初级解决方案,基于 Python OpenCV