任务

识别用相机拍下来的答题卡,并判断最终得分(假设正确答案是B, E, A, D, B)

主要步骤

  1. 轮廓识别——答题卡边缘识别
  2. 透视变换——提取答题卡主体
  3. 轮廓识别——识别出所有圆形选项,剔除无关轮廓
  4. 检测每一行选择的是哪一项,并将结果储存起来,记录正确的个数
  5. 计算最终得分并在图中标注

分步实现

轮廓识别——答题卡边缘识别

输入图像

import cv2 as cv
import numpy as np# 正确答案
right_key = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}# 输入图像
img = cv.imread('./images/test_01.png')
img_copy = img.copy()
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cvshow('img-gray', img_gray)

图像预处理

# 图像预处理
# 高斯降噪
img_gaussian = cv.GaussianBlur(img_gray, (5, 5), 1)
cvshow('gaussianblur', img_gaussian)
# canny边缘检测
img_canny = cv.Canny(img_gaussian, 80, 150)
cvshow('canny', img_canny)

轮廓识别——答题卡边缘识别

# 轮廓识别——答题卡边缘识别
cnts, hierarchy = cv.findContours(img_canny, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
cv.drawContours(img_copy, cnts, -1, (0, 0, 255), 3)
cvshow('contours-show', img_copy)

透视变换——提取答题卡主体

对每个轮廓进行拟合,将多边形轮廓变为四边形

docCnt = None# 确保检测到了
if len(cnts) > 0:# 根据轮廓大小进行排序cnts = sorted(cnts, key=cv.contourArea, reverse=True)# 遍历每一个轮廓for c in cnts:# 近似peri = cv.arcLength(c, True)# arclength 计算一段曲线的长度或者闭合曲线的周长;# 第一个参数输入一个二维向量,第二个参数表示计算曲线是否闭合approx = cv.approxPolyDP(c, 0.02 * peri, True)# 用一条顶点较少的曲线/多边形来近似曲线/多边形,以使它们之间的距离<=指定的精度;# c是需要近似的曲线,0.02*peri是精度的最大值,True表示曲线是闭合的# 准备做透视变换if len(approx) == 4:docCnt = approxbreak

透视变换——提取答题卡主体

# 透视变换——提取答题卡主体
docCnt = docCnt.reshape(4, 2)
warped = four_point_transform(img_gray, docCnt)
cvshow('warped', warped)
def four_point_transform(img, four_points):rect = order_points(four_points)(tl, tr, br, bl) = rect# 计算输入的w和h的值widthA = np.sqrt((tr[0] - tl[0]) ** 2 + (tr[1] - tl[1]) ** 2)widthB = np.sqrt((br[0] - bl[0]) ** 2 + (br[1] - bl[1]) ** 2)maxWidth = max(int(widthA), int(widthB))heightA = np.sqrt((tl[0] - bl[0]) ** 2 + (tl[1] - bl[1]) ** 2)heightB = np.sqrt((tr[0] - br[0]) ** 2 + (tr[1] - br[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')# 最主要的函数就是 cv2.getPerspectiveTransform(rect, dst) 和 cv2.warpPerspective(image, M, (maxWidth, maxHeight))M = cv.getPerspectiveTransform(rect, dst)warped = cv.warpPerspective(img, M, (maxWidth, maxHeight))return warpeddef order_points(points):res = np.zeros((4, 2), dtype='float32')# 按照从前往后0,1,2,3分别表示左上、右上、右下、左下的顺序将points中的数填入res中# 将四个坐标x与y相加,和最大的那个是右下角的坐标,最小的那个是左上角的坐标sum_hang = points.sum(axis=1)res[0] = points[np.argmin(sum_hang)]res[2] = points[np.argmax(sum_hang)]# 计算坐标x与y的离散插值np.diff()diff = np.diff(points, axis=1)res[1] = points[np.argmin(diff)]res[3] = points[np.argmax(diff)]# 返回resultreturn res

轮廓识别——识别出选项

# 轮廓识别——识别出选项
thresh = cv.threshold(warped, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)[1]
cvshow('thresh', thresh)
thresh_cnts, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
w_copy = warped.copy()
cv.drawContours(w_copy, thresh_cnts, -1, (0, 0, 255), 2)
cvshow('warped_contours', w_copy)questionCnts = []
# 遍历,挑出选项的cnts
for c in thresh_cnts:(x, y, w, h) = cv.boundingRect(c)ar = w / float(h)# 根据实际情况指定标准if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:questionCnts.append(c)# 检查是否挑出了选项
w_copy2 = warped.copy()
cv.drawContours(w_copy2, questionCnts, -1, (0, 0, 255), 2)
cvshow('questionCnts', w_copy2)

成功将无关轮廓剔除

检测每一行选择的是哪一项,并将结果储存起来,记录正确的个数

# 检测每一行选择的是哪一项,并将结果储存在元组bubble中,记录正确的个数correct
# 按照从上到下t2b对轮廓进行排序
questionCnts = sort_contours(questionCnts, method="t2b")[0]
correct = 0
# 每行有5个选项
for (i, q) in enumerate(np.arange(0, len(questionCnts), 5)):# 排序cnts = sort_contours(questionCnts[q:q+5])[0]bubble = None# 得到每一个选项的mask并填充,与正确答案进行按位与操作获得重合点数for (j, c) in enumerate(cnts):mask = np.zeros(thresh.shape, dtype='uint8')cv.drawContours(mask, [c], -1, 255, -1)# cvshow('mask', mask)# 通过按位与操作得到thresh与mask重合部分的像素数量bitand = cv.bitwise_and(thresh, thresh, mask=mask)totalPixel = cv.countNonZero(bitand)if bubble is None or bubble[0] < totalPixel:bubble = (totalPixel, j)k = bubble[1]color = (0, 0, 255)if k == right_key[i]:correct += 1color = (0, 255, 0)# 绘图cv.drawContours(warped, [cnts[right_key[i]]], -1, color, 3)cvshow('final', warped)
def sort_contours(contours, method="l2r"):# 用于给轮廓排序,l2r, r2l, t2b, b2treverse = Falsei = 0if method == "r2l" or method == "b2t":reverse = Trueif method == "t2b" or method == "b2t":i = 1boundingBoxes = [cv.boundingRect(c) for c in contours](contours, boundingBoxes) = zip(*sorted(zip(contours, boundingBoxes), key=lambda a: a[1][i], reverse=reverse))return contours, boundingBoxes

用透过mask的像素的个数来判断考生选择的是哪个选项

计算最终得分并在图中标注

# 计算最终得分并在图中标注
score = (correct / 5.0) * 100
print(f"Score: {score}%")
cv.putText(warped, f"Score: {score}%", (10, 30), cv.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv.imshow("Original", img)
cv.imshow("Exam", warped)
cv.waitKey(0)

完整代码

import cv2 as cv
import numpy as npdef cvshow(name, img):cv.imshow(name, img)cv.waitKey(0)cv.destroyAllWindows()def four_point_transform(img, four_points):rect = order_points(four_points)(tl, tr, br, bl) = rect# 计算输入的w和h的值widthA = np.sqrt((tr[0] - tl[0]) ** 2 + (tr[1] - tl[1]) ** 2)widthB = np.sqrt((br[0] - bl[0]) ** 2 + (br[1] - bl[1]) ** 2)maxWidth = max(int(widthA), int(widthB))heightA = np.sqrt((tl[0] - bl[0]) ** 2 + (tl[1] - bl[1]) ** 2)heightB = np.sqrt((tr[0] - br[0]) ** 2 + (tr[1] - br[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')# 最主要的函数就是 cv2.getPerspectiveTransform(rect, dst) 和 cv2.warpPerspective(image, M, (maxWidth, maxHeight))M = cv.getPerspectiveTransform(rect, dst)warped = cv.warpPerspective(img, M, (maxWidth, maxHeight))return warpeddef order_points(points):res = np.zeros((4, 2), dtype='float32')# 按照从前往后0,1,2,3分别表示左上、右上、右下、左下的顺序将points中的数填入res中# 将四个坐标x与y相加,和最大的那个是右下角的坐标,最小的那个是左上角的坐标sum_hang = points.sum(axis=1)res[0] = points[np.argmin(sum_hang)]res[2] = points[np.argmax(sum_hang)]# 计算坐标x与y的离散插值np.diff()diff = np.diff(points, axis=1)res[1] = points[np.argmin(diff)]res[3] = points[np.argmax(diff)]# 返回resultreturn resdef sort_contours(contours, method="l2r"):# 用于给轮廓排序,l2r, r2l, t2b, b2treverse = Falsei = 0if method == "r2l" or method == "b2t":reverse = Trueif method == "t2b" or method == "b2t":i = 1boundingBoxes = [cv.boundingRect(c) for c in contours](contours, boundingBoxes) = zip(*sorted(zip(contours, boundingBoxes), key=lambda a: a[1][i], reverse=reverse))return contours, boundingBoxes# 正确答案
right_key = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}# 输入图像
img = cv.imread('./images/test_01.png')
img_copy = img.copy()
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cvshow('img-gray', img_gray)# 图像预处理
# 高斯降噪
img_gaussian = cv.GaussianBlur(img_gray, (5, 5), 1)
cvshow('gaussianblur', img_gaussian)
# canny边缘检测
img_canny = cv.Canny(img_gaussian, 80, 150)
cvshow('canny', img_canny)# 轮廓识别——答题卡边缘识别
cnts, hierarchy = cv.findContours(img_canny, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
cv.drawContours(img_copy, cnts, -1, (0, 0, 255), 3)
cvshow('contours-show', img_copy)docCnt = None# 确保检测到了
if len(cnts) > 0:# 根据轮廓大小进行排序cnts = sorted(cnts, key=cv.contourArea, reverse=True)# 遍历每一个轮廓for c in cnts:# 近似peri = cv.arcLength(c, True)  # arclength 计算一段曲线的长度或者闭合曲线的周长;# 第一个参数输入一个二维向量,第二个参数表示计算曲线是否闭合approx = cv.approxPolyDP(c, 0.02 * peri, True)# 用一条顶点较少的曲线/多边形来近似曲线/多边形,以使它们之间的距离<=指定的精度;# c是需要近似的曲线,0.02*peri是精度的最大值,True表示曲线是闭合的# 准备做透视变换if len(approx) == 4:docCnt = approxbreak# 透视变换——提取答题卡主体
docCnt = docCnt.reshape(4, 2)
warped = four_point_transform(img_gray, docCnt)
cvshow('warped', warped)# 轮廓识别——识别出选项
thresh = cv.threshold(warped, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)[1]
cvshow('thresh', thresh)
thresh_cnts, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
w_copy = warped.copy()
cv.drawContours(w_copy, thresh_cnts, -1, (0, 0, 255), 2)
cvshow('warped_contours', w_copy)questionCnts = []
# 遍历,挑出选项的cnts
for c in thresh_cnts:(x, y, w, h) = cv.boundingRect(c)ar = w / float(h)# 根据实际情况指定标准if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:questionCnts.append(c)# 检查是否挑出了选项
w_copy2 = warped.copy()
cv.drawContours(w_copy2, questionCnts, -1, (0, 0, 255), 2)
cvshow('questionCnts', w_copy2)# 检测每一行选择的是哪一项,并将结果储存在元组bubble中,记录正确的个数correct
# 按照从上到下t2b对轮廓进行排序
questionCnts = sort_contours(questionCnts, method="t2b")[0]
correct = 0
# 每行有5个选项
for (i, q) in enumerate(np.arange(0, len(questionCnts), 5)):# 排序cnts = sort_contours(questionCnts[q:q+5])[0]bubble = None# 得到每一个选项的mask并填充,与正确答案进行按位与操作获得重合点数for (j, c) in enumerate(cnts):mask = np.zeros(thresh.shape, dtype='uint8')cv.drawContours(mask, [c], -1, 255, -1)cvshow('mask', mask)# 通过按位与操作得到thresh与mask重合部分的像素数量bitand = cv.bitwise_and(thresh, thresh, mask=mask)totalPixel = cv.countNonZero(bitand)if bubble is None or bubble[0] < totalPixel:bubble = (totalPixel, j)k = bubble[1]color = (0, 0, 255)if k == right_key[i]:correct += 1color = (0, 255, 0)# 绘图cv.drawContours(warped, [cnts[right_key[i]]], -1, color, 3)cvshow('final', warped)# 计算最终得分并在图中标注
score = (correct / 5.0) * 100
print(f"Score: {score}%")
cv.putText(warped, f"Score: {score}%", (10, 30), cv.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv.imshow("Original", img)
cv.imshow("Exam", warped)
cv.waitKey(0)

使用 OpenCV-Python 识别答题卡判卷相关推荐

  1. 几步搞定!用Python实现答题卡识别!

    来源丨https://blog.csdn.net/qq_44805233?type=blog 大家好,我是辰哥~ 今天用Python对答题卡进行识别,答题卡素材图片如下: 思路 读入图片,做一些预处理 ...

  2. usb摄像头识别答题卡系统

    最近使用QT+Mysql+OpenCv写了一个USB摄像头识别答题卡系统,可以把数据保存到数据库中并支持导出Excel文件和输出学生答题情况的日志,话不多说,下面介绍一下大概的流程,使用一个线程作为图 ...

  3. OpenCV实战(二)——答题卡识别判卷

    代码见 https://github.com/skyerhxx/Answer-card-recognition-and-judgment 答题卡识别判卷 识别出考生选择的答案并能自动判分 Python ...

  4. 用 Python 实现答题卡识别!

    作者 | 棒子胡豆 来源丨CSDN博客 答题卡素材图片: 思路 读入图片,做一些预处理工作. 进行轮廓检测,然后找到该图片最大的轮廓,就是答题卡部分. 进行透视变换,以去除除答题卡外的多余部分,并且可 ...

  5. opencv+python机读卡识别(四)百度API进行数字识别

    2019独角兽企业重金招聘Python工程师标准>>> 第一部分预处理:https://my.oschina.net/u/3268732/blog/1236298 第二部分图像切割: ...

  6. 基于 SpringMvc + OpenCV 实现的答题卡识别系统(附源码)

    点击关注公众号,实用技术文章及时了解 java_opencv 项目介绍 OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,它提供了一系列图像处理和计算机视觉方面很多通用算法.是研究图像 ...

  7. 基于 SpringMvc+OpenCV 实现的答题卡识别系统(附源码)

    java_opencv 项目介绍 OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,它提供了一系列图像处理和计算机视觉方面很多通用算法.是研究图像处理技术的一个很不错的工具.最初开始接 ...

  8. Opencv识别答题卡

    转自:http://blog.csdn.net/cp562090732/article/details/47804003#comments OpenCV处理答题卡的软件,用于统计答题卡中选择题的得分. ...

  9. opencv python 识别视频水印_opencv+python实现视频实时质心读取

    利用opencv+python实现以下功能: 1)获取实时视频,分解帧频: 2)将视频做二值化处理: 3) 将视频做滤波处理(去除噪点,获取准确轮廓个数): 4)识别图像轮廓: 5)计算质心: 6)描 ...

最新文章

  1. 树莓派练习程序(火焰检测)
  2. Charles抓取微信小程序数据 以及 其它应用网站数据
  3. 在图片中如何生成带有文字边缘空心字体?
  4. Android 三角形控件
  5. 十几年后我才知道,嫁了一个硬核老公
  6. vue 使用sass 和less
  7. on 和where条件的放置详解
  8. Linux7的ftp日志怎么看,centos7打开sftp操作日志
  9. 程序员数学基础【二、时间复杂度】(Python版本)
  10. 柳传志给年轻人的建议:比起过日子,更要奔日子
  11. php pdo 缓冲,PDO支持数据缓存_PHP教程
  12. 2019级软件1班安卓实训总结
  13. 智能会议系统(25)---linphone代码分析
  14. python 向MySQL里插入中文数据
  15. 【编程题目】12 个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,...
  16. OpenMV 从入手到跑TensorFlow Lite神经网络进行垃圾分类
  17. 路由器+虚拟服务器+ssh,如何实现用SSH方式登陆路由器管理
  18. 2017AI最成功案例
  19. QuestaSim 仿真常用命令
  20. 7-3 降价提醒机器人 (10 分)小 T 想买一个玩具很久了,但价格有些高,他打算等便宜些再买。但天天盯着购物网站很麻烦,请你帮小 T 写一个降价提醒机器人,当玩具的当前价格比他设定的价格便宜时发

热门文章

  1. Qt制作漂亮个性化的界面
  2. 中心极限定理的形象理解
  3. js构造函数(原型链)及Es6的class类
  4. VTK实现电影级渲染效果(CVR)
  5. Linux计划任务、周期性任务执行
  6. SpringCloud 基本使用
  7. 富士胶片消毒喷雾及湿巾证实可抑新冠感染;巴厘岛实施旅行健康安全新准则 | 美通企业日报...
  8. Linux学习 高级网络配置
  9. Test,Evaluate_gpu 修改,自动跑完你要的epoch
  10. 从沟通的一般模型想到互联网,再想到数字媒体,最后想到信息世界