当前位置: 首页 > news >正文

《深度学习》【项目】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)


http://www.mrgr.cn/news/48341.html

相关文章:

  • PDFMathTranslate 一个基于AI优秀的PDF论文翻译工具
  • 2.6 网络面试问题
  • RunCam WiFiLink连接手机图传测试
  • Pytorch | 利用BIM/I-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
  • [react]redux异步处理和重写useDispatch
  • 【Linux】进程间通信的秘密通道:IPC机制详解
  • QD1-P4 HTML标题标签(h)水平线标签(hr)
  • dd 工具 是一个在 Linux 系统中用于复制文件和转换文件的工具
  • vue后台管理系统从0到1(2)
  • Basic penetration_1靶机渗透
  • 数据结构——树和森林
  • Bob_ 1.0.1靶机渗透
  • Linux `sort` 命令详解
  • 【Python】Python实现串口通信(Python+Stm32)
  • 1374. 生成每种字符都是奇数个的字符串
  • 18708 最大子段和
  • ARM学习(32)FreeRTOS 调度和timer流程
  • Java->Map和Set
  • Jave常用的类---String类
  • 英语中 ing后缀
  • BUG修复(不断整理想起什么就整理什么)
  • Java中的流:高效处理数据的新方式
  • Vivado工程如何生成TCL文件以及如何利用TCL文件还原工程
  • 2025秋招倒计时---招联金融
  • 阿里云短信接口配置信息利用方式
  • jenkins 插件SSH Pipeline Steps