使用 OpenCV-Python 识别答题卡判卷功能

任务

识别用相机拍下来的答题卡,并判断最终得分(假设正确答案是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.jpg')
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 = approx
            break

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

# 透视变换——提取答题卡主体
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 warped

def 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)]

    # 返回result
    return 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 += 1
        color = (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, b2t
    reverse = False
    i = 0
    if method == "r2l" or method == "b2t":
        reverse = True
    if method == "t2b" or method == "b2t":
        i = 1

    boundingBoxes = [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 np

def 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 warped

def 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)]

    # 返回result
    return res

def sort_contours(contours, method="l2r"):
    # 用于给轮廓排序,l2r, r2l, t2b, b2t
    reverse = False
    i = 0
    if method == "r2l" or method == "b2t":
        reverse = True
    if method == "t2b" or method == "b2t":
        i = 1

    boundingBoxes = [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.jpg')
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 = approx
            break

# 透视变换——提取答题卡主体
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 += 1
        color = (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 识别答题卡判卷的文章就介绍到这了,更多相关OpenCV Python 识别答题卡判卷内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • python利用opencv如何实现答题卡自动判卷

    目录 1.设定答题卡模板 2.读取答题卡图像并对图像进行灰度化处理 3.高斯模糊图像去噪点 4.使用大津法二值分割图像 5.使用开运算去噪点 6.使用canny边缘检测算法 7.筛选答题区域轮廓,透视变换矫正目标区域 使用摄像头实时判卷部分 总结 1.设定答题卡模板 该图像为答题卡的答题区域,黑色边框是为了能够在各种环境中轻易的检测,左部分和上部分的黑色矩形,是为能够定位到答题选项的坐标而设置,同时题目数量为20×3共60道选择题,在进行批改试卷之前,需要手动输入该次考试的正确答案作为模板来对识

  • python OpenCV实现答题卡识别判卷

    本文实例为大家分享了python OpenCV实现答题卡识别判卷的具体代码,供大家参考,具体内容如下 完整代码: #导入工具包 import numpy as np import argparse import imutils import cv2 # 设置参数 ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", default="./images/test_03.png"

  • 使用 OpenCV-Python 识别答题卡判卷功能

    任务 识别用相机拍下来的答题卡,并判断最终得分(假设正确答案是B, E, A, D, B) 主要步骤 轮廓识别--答题卡边缘识别 透视变换--提取答题卡主体 轮廓识别--识别出所有圆形选项,剔除无关轮廓 检测每一行选择的是哪一项,并将结果储存起来,记录正确的个数 计算最终得分并在图中标注 分步实现 轮廓识别--答题卡边缘识别 输入图像 import cv2 as cv import numpy as np # 正确答案 right_key = {0: 1, 1: 4, 2: 0, 3: 3, 4

  • 基于Opencv图像识别实现答题卡识别示例详解

    目录 1. 项目分析 2.项目实验 3.项目结果 总结 在观看唐宇迪老师图像处理的课程中,其中有一个答题卡识别的小项目,在此结合自己理解做一个简单的总结. 1. 项目分析 首先在拿到项目时候,分析项目目的是什么,要达到什么样的目标,有哪些需要注意的事项,同时构思实验的大体流程. 图1. 答题卡测试图像 比如在答题卡识别的项目中,针对测试图片如图1 ,首先应当实现的功能是: 能够捕获答题卡中的每个填涂选项. 将获取的填涂选项与正确选项做对比计算其答题正确率. 2.项目实验 在对测试图像进行形态学操

  • Opencv+Python识别PCB板图片的步骤

    任务要求: 基于模板匹配算法识别PCB板型号 使用工具: Python3.OpenCV 使用模板匹配算法,模板匹配是一种最原始.最基本的模式识别方法,研究某一特定对象物的图案位于图像的什么地方,进而识别对象物,模板匹配具有自身的局限性,主要表现在它只能进行平行移动,即原图像中的匹配目标不能发生旋转或大小变化. 事先准备好待检测PCB与其对应的模板: 子模版: 基本流程如下: 1.在整个图像区域发现与给定子图像匹配的小块区域 2.选取模板图像T(给定的子图像) 3.另外需要一个待检测的图像--源图

  • opencv+python识别七段数码显示器的数字(数字识别)

    目录 一.什么是七段数码显示器 二.创建opencv数字识别器 一.什么是七段数码显示器 七段LCD数码显示器有很多叫法:段码液晶屏.段式液晶屏.黑白笔段屏.段码LCD液晶屏.段式显示器.TN液晶屏.段码液晶显示器.段码屏幕.笔段式液晶屏.段码液晶显示屏.段式LCD.笔段式LCD等. 如下图,每个数字都由一个七段组件组成. 七段显示器总共可以呈现 128 种可能的状态: 我们要识别其中的0-9,如果用深度学习的方式有点小题大做,并且如果要进行应用还有很多前序工作需要进行,比如要确认识别什么设备的

  • opencv python在视屏上截图功能的实现

    OpenCV简介 OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux.Windows.Android和Mac OS操作系统上.它轻量级而且高效--由一系列 C 函数和少量 C++ 类构成,同时提供了Python.Ruby.MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法. OpenCV用C++语言编写,它的主要接口也是C++语言,但是依然保留了大量的C语言接口.该库也有大量的Python.Java and MATLAB/OCTAVE(版本

  • OpenCV+Python识别车牌和字符分割的实现

    本篇文章主要基于python语言和OpenCV库(cv2)进行车牌区域识别和字符分割,开篇之前针对在python中安装opencv的环境这里不做介绍,可以自行安装配置! 车牌号检测需要大致分为四个部分: 1.车辆图像获取 2.车牌定位. 3.车牌字符分割 4.车牌字符识别 具体介绍 车牌定位需要用到的是图片二值化为黑白后进canny边缘检测后多次进行开运算与闭运算用于消除小块的区域,保留大块的区域,后用cv2.rectangle选取矩形框,从而定位车牌位置 车牌字符的分割前需要准备的是只保留车牌

  • Python答题卡识别并给出分数的实现代码

      哈喽大家好,这里是滑稽研究所.看过我们图像处理系列的朋友,应该知道识别答题卡那期文章.其中利用opencv框架,完美的实现了答题卡填涂区域的识别.在后台有小伙伴想要我完善一下判断选项对错并打分的功能,本期我们就来实现一下.   那么我们来复习一下往期的代码原理.我们需要对图片素材进行灰度化处理.透视变换.轮廓检测.腐蚀膨胀处理.区域分割.边框计算.区域计算.实际上我们是通过像素面积的过滤.填涂区域优化和获取选项坐标来完成答题卡的识别的. 素材:   那么在获取到答题卡的填涂区域之后就好办了.

  • opencv实现答题卡识别

    本文实例为大家分享了opencv实现答题卡识别的具体代码,供大家参考,具体内容如下 """ 识别答题卡 """   import cv2 import numpy as np   def showImg(img_name, img): cv2.imshow(img_name, img) cv2.waitKey() cv2.destroyAllWindows()   def get_max_rect(sorted_cnts): for cnt in

随机推荐