《深度学习》【项目】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)
相关推荐
碣石潇湘无限路13 分钟前
【AI篇】当Transformer模型开始学习《孙子兵法》
人工智能·学习
看到我,请让我去学习24 分钟前
OpenCV开发-初始概念
人工智能·opencv·计算机视觉
汀沿河24 分钟前
8.1 prefix Tunning与Prompt Tunning模型微调方法
linux·运维·服务器·人工智能
陈敬雷-充电了么-CEO兼CTO33 分钟前
大模型技术原理 - 基于Transformer的预训练语言模型
人工智能·深度学习·语言模型·自然语言处理·chatgpt·aigc·transformer
学术 学术 Fun40 分钟前
✨ OpenAudio S1:影视级文本转语音与语音克隆Mac整合包
人工智能·语音识别
风铃喵游1 小时前
让大模型调用MCP服务变得超级简单
前端·人工智能
旷世奇才李先生1 小时前
Pillow 安装使用教程
深度学习·microsoft·pillow
booooooty2 小时前
基于Spring AI Alibaba的多智能体RAG应用
java·人工智能·spring·多智能体·rag·spring ai·ai alibaba
PyAIExplorer2 小时前
基于 OpenCV 的图像 ROI 切割实现
人工智能·opencv·计算机视觉
风口猪炒股指标2 小时前
技术分析、超短线打板模式与情绪周期理论,在市场共识的形成、分歧、瓦解过程中缘起性空的理解
人工智能·博弈论·群体博弈·人生哲学·自我引导觉醒