书名:计算机视觉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()
![](https://img.haomeiwen.com/i17748967/7c05b165d0c43c67.png)
![](https://img.haomeiwen.com/i17748967/74f4b1161d3f34f0.png)
网友评论