美文网首页
整张答题卡识别程序

整张答题卡识别程序

作者: 大龙10 | 来源:发表于2023-11-18 12:47 被阅读0次

书名:计算机视觉40例从入门到深度学习:OpenCV-Python
作者:李立宗
出版社:电子工业出版社
出版时间:2022-07-01
ISBN:9787121436857


第9章 答题卡识别

9.2 整张答题卡识别原理

9.2.5 处理每一道题目的选项

  • 处理每一道题目的选项是核心步骤,该步骤的处理算法已在9.1节进行了详细介绍。
    整张图像涉及提取答题卡、逐次提取每道题的4个选项、逐次提取每道题的各个选项等步骤,针对此使用了多个循环的嵌套结构。

9.2.6 显示结果

  • 显示结果时,在答题卡内主要显示两部分内容。
    • 打印辅助文字说明:
      具体包含题目总数、答对题目的数目、得分。
    • 可视化输出:
      针对选项答对与否进行标注,具体为如果考生填涂的答案正确,那么在其填涂的正确答案处标注绿色轮廓;如果考生填涂的答案错误,那么在其填涂的错误答案处标注红色轮廓。

9.3 整张答题卡识别程序

  • 通过上述分析可知,在解决问题时,需要先确定总体方向和步骤。对于本例,我们将解决问题划分为如下六步。
    Step 1:图像预处理。
    Step 2:答题卡处理。
    Step 3:筛选出所有选项。
    Step 4:将选项按照题目分组。
    Step 5:处理每一道题目的选项。
    Step 6:显示结果。

  • 将问题划分为具体步骤,一方面可以让思路更清晰,另外一方面能够让我们在处理问题时专注于当前步骤的操作。

  • 在划分好步骤后,在每一个步骤内,只需专注于解决本步骤要解决的具体问题即可。

【例9.10】整张答题卡识别实现程序。

# -*- coding: utf-8 -*-
"""
Created on Fri Nov 17 11:11:01 2023

@author: dalong10
"""

import cv2
import numpy as np
from scipy.spatial import distance as dist  # 用于计算距离

# 自定义透视函数
# step 1: 参数 pts 是要进行倾斜校正的轮廓的逼近多边形(本例中的答题卡)的四个顶点

def myWarpPerspective(image,pts):    
    #确定四个顶点分别对应左上、右上、右下、左下四个顶点中的哪个顶点
    
    # step 1.1:根据轴坐标值对四个顶点进行排序
    xSorted = pts[np.argsort(pts[:,0]), :]
    
    # step 1.2:将四个顶点划分为左侧两个、右侧两个
    left  = xSorted[:2, :]
    right = xSorted[2:, :]
    
    # step 1.3:在左侧寻找左上顶点、左下顶点
    # 根据 y 轴坐标值排序
    left = left[np.argsort(left[:, 1]),:]

    # 排在前面的是左上角顶点 (tl:top-left)、排在后面的是左下角顶点 (bl:bottom-left)
    (tl,bl) = left
    
    #step 1.4: 根据右侧两个顶点与左上角顶点的距离判断右侧两个顶点的位置
    # 计算右侧两个顶点距离左上角顶点的距离
    D = dist.cdist(tl[np.newaxis], right,"euclidean")[0]
    # 形状大致如下
    # 左上角顶点(t1)            右上角顶点(tr)
    #                 页面中心
    # 左下角顶点(bl)            右下角顶点(br)
    # 右侧两个顶点中,距离左上角顶点远的点是右下角顶点 (br),近的点是右上角顶点(tr)
    # br:bottom-right/tr:top-right
    (br, tr) = right[np.argsort(D)[::-1], :]
    
    # step 1.5:确定 pts 的四个顶点分别属于左上、左下、右上、右下顶点中的哪个
    # src 是根据左上、左下、右上、右下顶点对 pts 的四个顶点进行排序的结果
    src = np.array([tl, tr, br, bl], dtype="float32")

    #========以下5行是测试语句,显示计算的顶点是否正确=========
    # srcx = np.array([tl, tr, br, bl], dtype="int32")
    # print("看看各个顶点在哪: \n",src)               # 测试语句,查看顶点   
    # test=image.copy()                             # 复制 image 图像,处理用
    # cv2.polylines(test,[srcx],True,(255,0,0),8)  # 在 test 内绘制得到的点
    # cv2.imshow("Test",test)  #显示绘制线条结果

    # ======= Step2:根据 pts的四个顶点,计算校正后图像的宽度和高度==    
    # 校正后图像的大小计算比较随意,根据需要选用合适值即可
    # 这里选用较长的宽度和高度作为最终宽度和高度
    # 计算方式:由于图像是倾斜的,所以将算得的X轴方向、Y轴方向的差值的平方根作为实际长度

    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
    maxWidth = max(int(widthA), int(widthB))

    # 根据 (左上,左下)和 (石上,石下)的最大值,获取高度
    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
    maxHeight = max(int (heightA), int(heightB))

    #根据宽度、高度,构造新图像 dst 对应的四个顶点
    dst = np.array([[0,0], [maxWidth - 1,0], [maxWidth - 1,maxHeight - 1], [0,maxHeight - 1]], dtype="float32")
    # print("看看目标如何:\n", dst)  # 测试语句
    # 构造从 src到dst的变换矩阵
    M = cv2.getPerspectiveTransform(src, dst)
    # 完成从 src到dst的透视变换
    warped = cv2.warpPerspective(image, M, (maxWidth,maxHeight)) # 返回透视变换的结果
    return warped

# ======主程序======

# 标准答案
ANSWER ={0:1,1:2,2:0,3:2,4:3}
# 答案用到的字典
answerDICT ={0:"A",1:"B",2:"C", 3:"D"}
# 读取原始图像(考卷)
img =  cv2.imread('d:\\OpenCVpic\\TestPaper.jpg')  
cv2.imshow('img', img)
# 图像预处理:色彩空间变换
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 
# 图像预处理:高斯滤波
gaussian = cv2.GaussianBlur(gray,(5,5), 0)
# 图像预处理:边缘检测
edged =cv2.Canny(gaussian, 50, 200)

# 查找轮廓
cts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  
cv2.drawContours(img,cts,-1,(0,0,255),3)

# 轮廓排序
list=sorted(cts,key=cv2.contourArea,reverse=True)
print("寻找轮廓的个数:",len(cts))
# cv2.imshow("draw contours",img)
rightSum = 0
# 可能只能找到一个轮廓,该轮廓就是答题卡的轮廓
# 由于噪声等影响,也可能找到很多轮廓
# 使用 for 循环,遍历每一个轮廓,找到答题卡的轮廓
# 对答题卡进行倾斜校正处理

for c in list:
    peri=0.01*cv2.arcLength(c,True)
    approx=cv2.approxPolyDP(c,peri,True)
    print("顶点个数:",len(approx))
    # 四个顶点的轮廓是矩形(或者是扫描造成的矩形失真为梯形)
    if len(approx)==4:
        # 对外轮廓进行倾斜校正,将其构造成一个矩形
        # 处理后,只保留答题卡部分,答题卡外面的边界被删除#原始图像的倾斜校正用于后续标注
        # print(approx)
        print(approx.reshape(4,2))
        paper = myWarpPerspective(img,approx.reshape(4,2))
        # cv2.imshow("imgpaper", paper)
        #对原始图像的灰度图像进行倾斜校正,用于后续计算
        paperGray = myWarpPerspective(gray, approx.reshape(4,2))
        # 注意,paperGray 与 paper 在外观上无差异
        # 但是 paper 是色彩空间图像,可以在上面绘制彩色标注信息
        # paperGray 是灰度空间图像
        # cv2.imshow("paper", paper)
        # cv2.imshow("paperGray",paperGray)
        # cv2.imwrite("paperGray.jpg",paperGray)
        # 反二值化阀值处理,将选项处理为白色,将答题卡整体背景处理黑色
        ret, thresh = cv2.threshold(paperGray, 0,255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
        # cv2.imshow("thresh",  thresh)
        # cv2.imwrite("thresh.jpg",thresh)
        # 在答题卡内寻找所有轮廓,此时会找到所有轮廓
        # 既包含各个选项的轮廓,又包含答题卡内的说明文字等信息的轮廓
        cnts, hierarchy = cv2.findContours(thresh.copy(),cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        print("找到轮廓个数:",len(cnts))
        
        # 用options 来保存每一个选项(填涂和末填的选项都放进去)
        options =[]
        # 遍历每一个轮廓 cnts,将选项放入 options
        # 依据条件
        # 条件 1:轮廓如果宽度、高度都大于 24 像素
        # 条件 2:纵横比介于[0.7,1.4]
        # 若轮廓同时满足上述两个条件,则判定其为选项:否则,判定其为噪声(说明文字等其他信息)
        for ci in cnts:
            #获取轮廓的矩形包围框
            x,y, w, h = cv2.boundingRect(ci)
            #计算纵横比
            ar = w / float(h)
            # 将满足长度、宽度大于 24 像素且纵横比介于[0.7,1.3]的轮廓加入 options 
            if w >= 24 and h>= 24 and ar >= 0.7 and ar <= 1.4:
                options.append(ci)
        # print(len(options)) 
        # 查看得到多少个选项的轮廓
        # 得到了很多选项的轮廓,但是它们在 options 中是无规则存放的
        # 将轮廓按位置关系自上向下存放
        boundingBoxes = [cv2.boundingRect(c) for c in options]
        (options, boundingBoxes) = zip(*sorted(zip(options,boundingBoxes),key=lambda b:b[1][1],reverse=False))
        # 轮廓在 options 内是自上向下存放的
        # 因此,在 options 内索引为 0、1、2、3 的轮廓是第 1题的选项轮廓
        # 索引为 4、5、6、7的轮廓是第 2 道题的选项轮廓,以此类推
        # 简而言之,options 内轮廓以步长为 4 划分,分别对应着不同题目的 4 个轮廓
        # 从 options 内每次取出 4 个轮廓,分别处理各个题目的各个选项轮廓
        # 使用 for 循环,从 options 内每次取出 4 个轮廓,处理每一道题的 4 个选项的轮廓
        # for 循环使用 tn 表示题目序号 topic number,i表示轮廓序号 (从0开始,步长为4)
        for (tn,i) in enumerate(np.arange(0,len(options), 4)):
        # 需要注意的是,取出的 4 个轮廓,对应某道题目的 4 个选项
        # 这4个选项的存放是无序的
        # 将轮廓按照坐标值实现自左向右顺次存放
        # 将选项 A、选项 B、选项 C、选项 D 按照坐标值顺次存放
            boundingBoxes =[cv2.boundingRect(c) for c in options[i:i + 4]]
            (cnts,boundingBoxes)= zip(*sorted(zip(options[i:i + 4], boundingBoxes),key=lambda x:x[1][0], reverse=False))     

            # 构建列表 ioptions,用来存储当前题目的每个选项(像素值非 0的轮廓的个数,序号)
            ioptions=[]
            # 使用for循环,提取出4个轮廓的每一个轮廓c(contour)及其序号ci(contours index)        
            for (ci, c) in enumerate(cnts):
                # 构造一个和答题卡同尺寸的掩模 mask,灰度图像,黑色(像素值均为 0)
                mask = np.zeros(paperGray.shape, dtype="uint8")
                # 在mask 内,绘制当前遍历的选项轮廓
                cv2.drawContours(mask, [c],-1,255,-1)
                # 使用按位与运算的掩模模式,提取当前遍历的选项
                mask = cv2.bitwise_and(thresh, thresh, mask=mask)
                # cv2.imshow("c"+ str(i)+","+str(ci), mask)
                # 计算当前遍历选项内像素值非 0的轮廓个数
                total = cv2.countNonZero(mask)
                # 将选项像素值非 0的轮廓的个数、选项序号放入列表 options 内
                ioptions.append((total,ci))  
                
            # 将每道题的 4个选项按照像素值非0的轮廓的个数降序排序
            ioptions=sorted(ioptions,key=lambda x: x[0],reverse=True)
            # 获取包含最多白色像素点的选项索引 (序号)
            choiceNum=ioptions[0][1]
            # 根据索引确定选项
            choice=answerDICT.get(choiceNum)                
            print("该生的选项:",choice)
            # 设定标注的颜色类型
            if ANSWER.get(tn) == choiceNum:
                #正确时,颜色为绿色
                color = (0,255,0)
                #答对数量加 1
                rightSum +=1
            else:
                # 错误时,颜色为红色
                color = (0,0,255)                
            cv2.drawContours(paper, cnts[choiceNum],-1, color,3)                
        # cv2.imshow("result",paper)
        s1 ="total:"+ str(len(ANSWER))+" "
        s2 ="right:" + str(rightSum)
        s3="score:" + str(rightSum*1.0/len(ANSWER)*100)+""
        font = cv2.FONT_HERSHEY_SIMPLEX
        cv2.putText(paper,s1 +" "+s2+" "+s3,(10,30),font,0.5,(0,0,255),2)
        cv2.imshow("score", paper)
        #找到第一个具有 4 个顶点的轮廓就是答题卡,用 break 语句跳出循环
        break

cv2.waitKey()
cv2.destroyAllWindows()

运行结果
输出数据

相关文章

  • 1226 - 搞定 iText 连续识别

    先上图: 主要要解决的是此类问题:比如,有时会需要识别 PDF 这种排版复杂的「图片」,如果直接把整张图拿去识别,...

  • 〔两行哥〕OpenCV4Android教程之安卓答题卡识别

    这是一个基于OpenCV的Android答题卡识别Demo,源码已经上传至码云:AnswerSheetScanDe...

  • 弟弟笑话

    弟弟高三考试,发生了些搞笑的事情。 考试时,老师发下答题卡,弟弟拿了两张答题卡,答题卡前后两面黏在一起。后面的...

  • 微信小程序身份证ocr识别

    关键词:身份证识别 身份证ocr识别 微信小程序身份证识别 微信小程序ocr识别 身份证ocr识别api (网图,...

  • 基于OpenCV的答题卡识别

    OpenCV是一款通用的图形处理方面的类库,对图片的处理提供了各种各样的操作。考试里面的选择题非常多,用人工去判卷...

  • 答题卡识别增强项目

    代码地址:https://github.com/SimonLliu/SheetIdentification 请觉得...

  • 用Python实现答题卡识别!

    答题卡素材图片: 1.读入图片,做一些预处理工作。 2.进行轮廓检测,然后找到该图片最大的轮廓,就是答题卡部分。 ...

  • 用Python实现答题卡识别!

    答题卡素材图片: 思路 1.读入图片,做一些预处理工作。 2.进行轮廓检测,然后找到该图片最大的轮廓,就是答题卡部...

  • 2022-05-14

    人生如答题卡 静寂的考场传来“请发答题卡,把条形码粘贴在相应位置”指示,我和莉莉把十张一沓的答题卡发送到每一列的第...

  • 易100:对于考试答题卡,你知道多少?

    考试标配品之一就是答题卡,看似一张简单的纸但实际一点都不简单。能够作为一张答题卡在版面上,印刷清晰方便学生作答;在...

网友评论

      本文标题:整张答题卡识别程序

      本文链接:https://www.haomeiwen.com/subject/vgpjwdtx.html