《深度学习》【项目】OpenCV 答题卡识别 项目流程详解

目录

一、项目上半部分

1、定义展示图像函数

2、预处理

运行结果:

3、轮廓检测并绘制

运行结果:

4、排序轮廓

5、定义排序点函数

6、透视变换

1)定义透视变换处理函数

2)执行透视变换

运行结果:

7、二值化处理

运行结果:

8、绘制圆圈的轮廓

运行结果:

二、项目下半部分

1、筛选选项的圆圈并排序

2、标记正误

运行结果:

3、打印正确率并批注

运行结果:

三、完整代码


一、项目上半部分

1、定义展示图像函数

python 复制代码
def cv_show(name,img):
    cv2.imshow(name,img)
    cv2.waitKey(0)

2、预处理

python 复制代码
# 预处理
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、轮廓检测并绘制

python 复制代码
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、排序轮廓

python 复制代码
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、定义排序点函数

输入轮廓外接四边形的四个顶点,对其进行排序,返回排序后的点坐标

python 复制代码
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)定义透视变换处理函数
python 复制代码
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)  # 根据原始点和变换后的点计算透视变换矩阵 M
    warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))  # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片
    # 返回变换后的结果
    return warped
2)执行透视变换
python 复制代码
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、二值化处理

python 复制代码
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、绘制圆圈的轮廓

python 复制代码
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、筛选选项的圆圈并排序

python 复制代码
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、标记正误

python 复制代码
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):  # 遍历排序后的一行轮廓圆的索引和值
        # 创建掩膜mask
        mask = 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为上述遍历出来的每一个选项的索引,筛选出来对应答案k

    if 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、打印正确率并批注

python 复制代码
# 此处代码无缩进
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)
运行结果:

三、完整代码

python 复制代码
import numpy as np
import cv2

ANSWER_KEY = {0:1,1:4,2:0,3:3,4:1}   # 定义选项的正确答案,键0表示选项A,1表示B、、、E

def 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)  # 根据原始点和变换后的点计算透视变换矩阵 M
    warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))  # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片
    # 返回变换后的结果
    return warped

def sort_contours(cnts, method='left-to-right'):
    reverse = False
    i = 0
    if method == "right-to-left" or method == 'bottom-to-top':
        reverse = True
    if method == 'top-to-bottom' or method == 'bottom-to-top':
        i = 1
    boundingBoxes = [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):  # 遍历排序后的一行轮廓圆的索引和值
        # 创建掩膜mask
        mask = 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为上述遍历出来的每一个选项的索引,筛选出来对应答案k

    if 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)
相关推荐
Jeremy_lf2 分钟前
【生成模型之三】ControlNet & Latent Diffusion Models论文详解
人工智能·深度学习·stable diffusion·aigc·扩散模型
桃花键神40 分钟前
AI可信论坛亮点:合合信息分享视觉内容安全技术前沿
人工智能
野蛮的大西瓜1 小时前
开源呼叫中心中,如何将ASR与IVR菜单结合,实现动态的IVR交互
人工智能·机器人·自动化·音视频·信息与通信
CountingStars6191 小时前
目标检测常用评估指标(metrics)
人工智能·目标检测·目标跟踪
tangjunjun-owen2 小时前
第四节:GLM-4v-9b模型的tokenizer源码解读
人工智能·glm-4v-9b·多模态大模型教程
冰蓝蓝2 小时前
深度学习中的注意力机制:解锁智能模型的新视角
人工智能·深度学习
橙子小哥的代码世界2 小时前
【计算机视觉基础CV-图像分类】01- 从历史源头到深度时代:一文读懂计算机视觉的进化脉络、核心任务与产业蓝图
人工智能·计算机视觉
新加坡内哥谈技术2 小时前
苏黎世联邦理工学院与加州大学伯克利分校推出MaxInfoRL:平衡内在与外在探索的全新强化学习框架
大数据·人工智能·语言模型
小陈phd3 小时前
OpenCV学习——图像融合
opencv·计算机视觉·cv
fanstuck3 小时前
Prompt提示工程上手指南(七)Prompt编写实战-基于智能客服问答系统下的Prompt编写
人工智能·数据挖掘·openai