《深度学习》【项目】OpenCV 答题卡识别 项目流程详解
目录
一、项目上半部分
1、定义展示图像函数
2、预处理
运行结果:
3、轮廓检测并绘制
运行结果:
4、排序轮廓
5、定义排序点函数
6、透视变换
1)定义透视变换处理函数
2)执行透视变换
运行结果:
7、二值化处理
运行结果:
8、绘制圆圈的轮廓
运行结果:
二、项目下半部分
1、筛选选项的圆圈并排序
2、标记正误
运行结果:
3、打印正确率并批注
运行结果:
三、完整代码
一、项目上半部分
1、定义展示图像函数
def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)
2、预处理
# 预处理
image = cv2.imread(r'./images/test_01.png') # 读取待识别答题卡图片
contours_img = image.copy() # 生成图片副本
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) # 灰度图
blurred = cv2.GaussianBlur(gray,(5,5),0) # 使用高斯滤波对灰度图进行处理,减小图像噪声和细节,高斯核大小为5*5,0表示自动计算标准差
cv_show('blurred',blurred) # 展示
edged = cv2.Canny(blurred,75,200) # 对处理完的图像进行边缘检测,灰度低于75的将其更改为0,高于200的设置为255
cv_show('edged',edged)
运行结果:
3、轮廓检测并绘制
cnts = cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1] # 对副本图像进行轮廓检测,cv2.RETR_EXTERNAL只检测最外层,cv2.CHAIN_APPROX_SIMPLE只保留轮廓端点
# cv2.findContours返回3个值,原图、轮廓信息、层次结构,其受OpenCV版本影响
cv2.drawContours(contours_img,cnts,-1,(0,0,255),4) # 在原图的副本上绘制轮廓cnts,-1表示绘制所有轮廓
cv_show('contours',contours_img) # 展示
运行结果:
4、排序轮廓
docCnt = None
cnts = sorted(cnts,key = cv2.contourArea,reverse=True) # 对检测到的轮廓进行排序,排序方式为轮廓面积,降序形式
for c in cnts: # 遍历排序后的每一个轮廓peri = cv2.arcLength(c,True) # 计算每一个轮廓的周长,True表示闭合approx = cv2.approxPolyDP(c,0.002*peri,True) # 多边形逼近,逼近的精度为周长的0.2%,返回逼近后的多边形轮廓if len(approx)==4: # 判断多边形是否是4边的docCnt = approx # 如果是将其传入docCnt,然后中断循环,因为精度很高,所以认定这个多边形为检测到的轮廓外接多边形break
5、定义排序点函数
输入轮廓外接四边形的四个顶点,对其进行排序,返回排序后的点坐标
def order_points(pts): # 对输入的四个点按照左上、右上、右下、左下进行排序rect = np.zeros((4,2),dtype='float32') # 创建一个4*2的数组,用来存储排序之后的坐标位置# 按顺序找到对应坐标0123分别是左上、右上、右下、左下s = pts.sum(axis=1) # 对pts矩阵的每个点的x y相加rect[0] = pts[np.argmin(s)] # np.argmin(s)表示数组s中最小值的索引,表示左上的点的坐标rect[2] = pts[np.argmax(s)] # 返回最大值索引,即右下角的点坐标diff = np.diff(pts,axis=1) # 对pts矩阵的每一行的点求差值rect[1] = pts[np.argmin(diff)] # 差值最小的点为右上角点rect[3] = pts[np.argmax(diff)] # 差值最大表示左下角点return rect # 返回排序好的四个点的坐标
6、透视变换
1)定义透视变换处理函数
def four_point_transform(image,pts): # 对图像进行透视变换,pts为轮廓外界四边形端点# 获取输入坐标点rect = order_points(pts) # 为上述排序的四个点(tl,tr,br,bl) = rect # 分别返回给四个值,分别表示为左上、右上、右下、左下# 计算四边形的宽高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 = np.array([[0,0], # 定义四个点,表示变换后的矩阵的角点[maxWidth-1,0],[maxWidth-1,maxHeight-1],[0,maxHeight-1]],dtype='float32')# 图像透视变换 cv2.getPerspectiveTransform(src,dst[,solveMethod])→ M获得转换之间的关系# cv2.warpPerspective(src, Mp, dsizel, dstl, flagsl, borderModel, borderValue]]1])- dst# #参数说明:# src:变换前图像四边形顶点坐标/第2个是原图# MP:透视变换矩阵,3行3列# dsize:输出图像的大小,二元元组(width,heiqht)M = cv2.getPerspectiveTransform(rect,dst) # 根据原始点和变换后的点计算透视变换矩阵 Mwarped = cv2.warpPerspective(image,M,(maxWidth,maxHeight)) # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片# 返回变换后的结果return warped
2)执行透视变换
warped_t = four_point_transform(image,docCnt.reshape(4,2)) # 传入原图,docCnt.reshape(4,2)表示将轮廓的4个点排成一列,进行透视变换,返回透视变换后的图片
warped_new = warped_t.copy()
cv_show('wraped',warped_t) # 展示透视变换后的图片
运行结果:
7、二值化处理
warped = cv2.cvtColor(warped_t,cv2.COLOR_BGR2GRAY) # 将经过透视变换的图像转换为灰度图
# 阈值处理
thresh = cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1] # 对灰度图做二值化,THRESH_BINARY_INV反二值化处理,较亮的区域变为黑,较暗的区域变为白,THRESH_OTSU自动计算阈值,返回两个值,一个是阈值,一个是处理后的图像,
cv_show('thresh',thresh) # 展示二值化处理后的图像
运行结果:
8、绘制圆圈的轮廓
thresh_Contours = thresh.copy()
# 找到每一个圆圈轮廓
cnts = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1] # 对二值化处理后的图像进行轮廓检测,检测最外层轮廓,只返回轮廓端点信息,返回值有三个见上文
warped_Contours = cv2.drawContours(warped_t,cnts,-1,(0,255,0),3) # 绘制轮廓
cv_show('wraped_Contours',warped_Contours)
运行结果:
二、项目下半部分
1、筛选选项的圆圈并排序
questionCnts=[]
for c in cnts: # 遍历绘制的每一个轮廓(x,y,w,h) = cv2.boundingRect(c) # 计算轮廓矩形,返回边界矩形的坐标x,y,宽高ar = w / float(h) # 计算宽高比# 根据实际情况指定标准if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1: # 筛选矩形轮廓的宽、高、宽高比questionCnts.append(c) # 将满足条件的轮廓信息增加到列表
# 将圆圈轮廓按照从上到下进行排序
questionCnts = sort_contours(questionCnts,method='top-to-bottom')[0]
2、标记正误
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)): # 生成一个0-25不包括25,步长为5的数组,即0,4,8,16,24五个值,enumerate生成可迭代对象,返回索引和值cnts = sort_contours(questionCnts[i:i+5])[0] # 遍历每五个轮廓传入函数进行从左往右排序,索引0返回排序后的端点信息bubbled = None# 遍历每一个结果for (j,c) in enumerate(cnts): # 遍历排序后的一行轮廓圆的索引和值# 创建掩膜maskmask = np.zeros(thresh.shape,dtype='uint8') # 创建和thresh一样布局的0矩阵,thresh是二值化处理完的图像cv2.drawContours(mask,[c],-1,255,-1) # 绘制掩膜,[c]为单个轮廓的列表,第一个-1表示绘制轮廓列表中的所有轮廓,轮廓为单通道,所以255表示白色,第二个-1表示全部填充cv_show('mask',mask) # 绘制掩膜# 通过计算非零点数量来算是否选择这个答案# 利用掩膜(mask)进行“与”操作,只保留mask位置中的内容thresh_mask_and = cv2.bitwise_and(thresh,thresh,mask=mask) # 按位与操作,按位与两张图片相同的地方,这里是thresh图像本身,返回结果为掩膜中为白色的区域cv_show('thresh_mask_and',thresh_mask_and) # 展示保留掩膜白色区域的thresh图total = cv2.countNonZero(thresh_mask_and) # 计算非0像素的数量,因为遍历的是一排选项轮廓的每一个轮廓,所以计算出非0像素值数量后再判断对应的选项if bubbled is None or total > bubbled[0]:bubbled = (total,j) # 将轮廓的值和索引以元组的形式保存的bubbled,通过遍历不停更新筛选# 对比正确答案color = (0,0,255) # 初始化颜色为红色k = ANSWER_KEY[q] # q为上述遍历出来的每一个选项的索引,筛选出来对应答案kif k == bubbled[1]: # 判断正确color = (0,255,0) # 如果正确则为绿色correct += 1 # 正确就+1,总数为5,最后求正确率cv2.drawContours(warped_new,[cnts[k]],-1,color,3) # 在warped_new为原图副本上绘制轮廓,[cnts[k]]为轮廓列表,-1表示绘制所有轮廓cv_show('warpeding',warped_new)
运行结果:
3、打印正确率并批注
# 此处代码无缩进
score = (correct/5.0)*100 # 计算正确率百分比
print("[INFO] score :{:.2f}%".format(score)) # 输出正确率
cv2.putText(warped_new,'{:.2f}%'.format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2) # 在原图副本上绘制文本,文本内容为正确率百分比,绘制的位置坐标为(10,30),FONT_HERSHEY_SIMPLEX为文本字体类型,0.9为缩放因子
cv2.imshow('Original',image) # 展示原图
cv2.imshow('Exam',warped_new) # 展示绘制完的图
cv2.waitKey(0)
运行结果:
三、完整代码
import numpy as np
import cv2ANSWER_KEY = {0:1,1:4,2:0,3:3,4:1} # 定义选项的正确答案,键0表示选项A,1表示B、、、Edef cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)
def order_points(pts): # 对输入的四个点按照左上、右上、右下、左下进行排序rect = np.zeros((4,2),dtype='float32') # 创建一个4*2的数组,用来存储排序之后的坐标位置# 按顺序找到对应坐标0123分别是左上、右上、右下、左下s = pts.sum(axis=1) # 对pts矩阵的每个点的x y相加rect[0] = pts[np.argmin(s)] # np.argmin(s)表示数组s中最小值的索引,表示左上的点的坐标rect[2] = pts[np.argmax(s)] # 返回最大值索引,即右下角的点坐标diff = np.diff(pts,axis=1) # 对pts矩阵的每一行的点求差值rect[1] = pts[np.argmin(diff)] # 差值最小的点为右上角点rect[3] = pts[np.argmax(diff)] # 差值最大表示左下角点return rect # 返回排序好的四个点的坐标# 将透视扭曲的矩形变换成一个规则的矩阵
def four_point_transform(image,pts):# 获取输入坐标点rect = order_points(pts) # 为上述排序的四个点(tl,tr,br,bl) = rect # 分别返回给四个值,分别表示为左上、右上、右下、左下# 计算四边形的宽高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 = np.array([[0,0], # 定义四个点,表示变换后的矩阵的角点[maxWidth-1,0],[maxWidth-1,maxHeight-1],[0,maxHeight-1]],dtype='float32')# 图像透视变换 cv2.getPerspectiveTransform(src,dst[,solveMethod])→ M获得转换之间的关系# cv2.warpPerspective(src, Mp, dsizel, dstl, flagsl, borderModel, borderValue]]1])- dst# #参数说明:# src:变换前图像四边形顶点坐标/第2个是原图# MP:透视变换矩阵,3行3列# dsize:输出图像的大小,二元元组(width,heiqht)M = cv2.getPerspectiveTransform(rect,dst) # 根据原始点和变换后的点计算透视变换矩阵 Mwarped = cv2.warpPerspective(image,M,(maxWidth,maxHeight)) # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片# 返回变换后的结果return warpeddef sort_contours(cnts, method='left-to-right'):reverse = Falsei = 0if method == "right-to-left" or method == 'bottom-to-top':reverse = Trueif method == 'top-to-bottom' or method == 'bottom-to-top':i = 1boundingBoxes = [cv2.boundingRect(c) for c in cnts] # 遍历每一个轮廓,计算轮廓矩形,将轮廓矩形的坐标、宽高存入列表(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), # 排序轮廓再打包key=lambda b: b[1][i],reverse=reverse))return cnts, boundingBoxes# 预处理
image = cv2.imread(r'./images/test_01.png') # 读取待识别答题卡图片
contours_img = image.copy() # 生成图片副本
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) # 灰度图
blurred = cv2.GaussianBlur(gray,(5,5),0) # 使用高斯滤波对灰度图进行处理,减小图像噪声和细节,高斯核大小为5*5,0表示自动计算标准差
cv_show('blurred',blurred) # 展示
edged = cv2.Canny(blurred,75,200) # 对处理完的图像进行边缘检测,灰度低于75的将其更改为0,高于200的设置为255
cv_show('edged',edged)# 轮廓检剽
cnts = cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1] # 对副本图像进行轮廓检测,cv2.RETR_EXTERNAL只检测最外层,cv2.CHAIN_APPROX_SIMPLE只保留轮廓端点
# cv2.findContours返回3个值,原图、轮廓信息、层次结构,其受OpenCV版本影响
cv2.drawContours(contours_img,cnts,-1,(0,0,255),4) # 在原图的副本上绘制轮廓cnts,-1表示绘制所有轮廓
cv_show('contours',contours_img) # 展示docCnt = None
cnts = sorted(cnts,key = cv2.contourArea,reverse=True) # 对检测到的轮廓进行排序,排序方式为轮廓面积,降序形式
for c in cnts: # 遍历排序后的每一个轮廓peri = cv2.arcLength(c,True) # 计算每一个轮廓的周长,True表示闭合approx = cv2.approxPolyDP(c,0.002*peri,True) # 多边形逼近,逼近的精度为周长的0.2%,返回逼近后的多边形轮廓if len(approx)==4: # 判断多边形是否是4边的docCnt = approx # 如果是将其传入docCnt,然后中断循环,因为精度很高,所以认定这个多边形为检测到的轮廓外接多边形break# 执行透视变換
warped_t = four_point_transform(image,docCnt.reshape(4,2)) # 传入原图,docCnt.reshape(4,2)表示将轮廓的4个点排成一列,进行透视变换,返回透视变换后的图片
warped_new = warped_t.copy()
cv_show('wraped',warped_t) # 展示透视变换后的图片warped = cv2.cvtColor(warped_t,cv2.COLOR_BGR2GRAY) # 将经过透视变换的图像转换为灰度图
# 阈值处理
thresh = cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1] # 对灰度图做二值化,THRESH_BINARY_INV反二值化处理,较亮的区域变为黑,较暗的区域变为白,THRESH_OTSU自动计算阈值,返回两个值,一个是阈值,一个是处理后的图像,
cv_show('thresh',thresh) # 展示二值化处理后的图像thresh_Contours = thresh.copy()
# 找到每一个圆圈轮廓
cnts = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1] # 对二值化处理后的图像进行轮廓检测,检测最外层轮廓,只返回轮廓端点信息,返回值有三个见上文
warped_Contours = cv2.drawContours(warped_t,cnts,-1,(0,255,0),3) # 绘制轮廓
cv_show('wraped_Contours',warped_Contours)questionCnts=[]
for c in cnts: # 遍历绘制的每一个轮廓(x,y,w,h) = cv2.boundingRect(c) # 计算轮廓矩形,返回边界矩形的坐标x,y,宽高ar = w / float(h) # 计算宽高比# 根据实际情况指定标准if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1: # 筛选矩形轮廓的宽、高、宽高比questionCnts.append(c) # 将满足条件的轮廓信息增加到列表
# 将圆圈轮廓按照从上到下进行排序
questionCnts = sort_contours(questionCnts,method='top-to-bottom')[0]
correct = 0
# 每排有5个选项
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)): # 生成一个0-25不包括25,步长为5的数组,即0,4,8,16,24五个值,enumerate生成可迭代对象,返回索引和值cnts = sort_contours(questionCnts[i:i+5])[0] # 遍历每五个轮廓传入函数进行从左往右排序,索引0返回排序后的端点信息bubbled = None# 遍历每一个结果for (j,c) in enumerate(cnts): # 遍历排序后的一行轮廓圆的索引和值# 创建掩膜maskmask = np.zeros(thresh.shape,dtype='uint8') # 创建和thresh一样布局的0矩阵,thresh是二值化处理完的图像cv2.drawContours(mask,[c],-1,255,-1) # 绘制掩膜,[c]为单个轮廓的列表,第一个-1表示绘制轮廓列表中的所有轮廓,轮廓为单通道,所以255表示白色,第二个-1表示全部填充cv_show('mask',mask) # 绘制掩膜# 通过计算非零点数量来算是否选择这个答案# 利用掩膜(mask)进行“与”操作,只保留mask位置中的内容thresh_mask_and = cv2.bitwise_and(thresh,thresh,mask=mask) # 按位与操作,按位与两张图片相同的地方,这里是thresh图像本身,返回结果为掩膜中为白色的区域cv_show('thresh_mask_and',thresh_mask_and) # 展示保留掩膜白色区域的thresh图total = cv2.countNonZero(thresh_mask_and) # 计算非0像素的数量,因为遍历的是一排选项轮廓的每一个轮廓,所以计算出非0像素值数量后再判断对应的选项if bubbled is None or total > bubbled[0]:bubbled = (total,j) # 将轮廓的值和索引以元组的形式保存的bubbled,通过遍历不停更新筛选# 对比正确答案color = (0,0,255) # 初始化颜色为红色k = ANSWER_KEY[q] # q为上述遍历出来的每一个选项的索引,筛选出来对应答案kif k == bubbled[1]: # 判断正确color = (0,255,0) # 如果正确则为绿色correct += 1 # 正确就+1,总数为5,最后求正确率cv2.drawContours(warped_new,[cnts[k]],-1,color,3) # 在warped_new为原图副本上绘制轮廓,[cnts[k]]为轮廓列表,-1表示绘制所有轮廓cv_show('warpeding',warped_new)score = (correct/5.0)*100 # 计算正确率百分比
print("[INFO] score :{:.2f}%".format(score)) # 输出正确率
cv2.putText(warped_new,'{:.2f}%'.format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2) # 在原图副本上绘制文本,文本内容为正确率百分比,绘制的位置坐标为(10,30),FONT_HERSHEY_SIMPLEX为文本字体类型,0.9为缩放因子
cv2.imshow('Original',image) # 展示原图
cv2.imshow('Exam',warped_new) # 展示绘制完的图
cv2.waitKey(0)