OpenCv高阶(8.0)——答题卡识别自动判分

文章目录


前言

一、代码分析及流程讲解

(一)初始化模块

python 复制代码
import numpy as np
import cv2
import os

正确答案映射字典(题目序号: 正确选项索引)

python 复制代码
ANSWER_KEY = {0:1, 1:4, 2:0, 3:3, 4:1}  

图像显示工具函数

python 复制代码
def cv_show(name, value):
    """可视化显示图像,按任意键继续"""
    cv2.imshow(name, value)
    cv2.waitKey(0)

(二)轮廓处理工具模块

轮廓定向排序函数

参数:

cnts: 轮廓列表

method: 排序方向(left-to-right/right-to-left/top-to-bottom/bottom-to-top)

返回值:

排序后的轮廓及边界框

python 复制代码
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

保持宽高比的图像缩放函数

参数:

width: 目标宽度

height: 目标高度

inter: 插值方法

python 复制代码
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
   
    dim = None
    (h, w) = image.shape[:2]
    if width is None and height is None:
        return image
    if width is None:
        r = height / float(h)
        dim = (int(w * r), height)
    else:
        r = width / float(w)
        dim = (width, int(h * r))
    return cv2.resize(image, dim, interpolation=inter)

(三)几何变换核心模块

坐标点规范化排序(左上、右上、右下、左下)

实现方法:

  1. 计算各点坐标和,最小值为左上,最大值为右下

  2. 计算坐标差值,最小值为右上,最大值为左下

python 复制代码
def order_points(pts):
    
    rect = np.zeros((4, 2), dtype='float32')
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]  # 左上点
    rect[2] = pts[np.argmax(s)]  # 右下点
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]  # 右上点
    rect[3] = pts[np.argmax(diff)]  # 左下点
    return rect

透视变换函数

参数:

image: 原始图像

pts: 源图像四个坐标点

处理流程: 1. 坐标点规范化排序。2. 计算变换后图像尺寸。 3. 生成透视变换矩阵。 4. 执行透视变换

python 复制代码
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")

    # 执行透视变换
    M = cv2.getPerspectiveTransform(rect, dst)
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
    return warped

二、主处理流程

图像读取

python 复制代码
image = cv2.imread('../data/images/test_01.png')
contours_img = image.copy()

>>> 阶段1:图像预处理 <<<

1、灰度转换(注意:COLOR_BGRA2GRAY适用于含alpha通道图像,通常使用COLOR_BGR2GRAY)

python 复制代码
gray = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)  

2、高斯滤波(5x5卷积核去噪)

python 复制代码
blurred = cv2.GaussianBlur(gray, (5,5), 0)
cv_show('blurred', blurred)

3、Canny边缘检测(双阈值设置)

python 复制代码
edged = cv2.Canny(blurred, 75, 200)  
cv_show('edged', edged)

>>> 阶段2:答题卡定位 <<<

1、轮廓检测(仅检测最外层轮廓)

python 复制代码
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

2、绘制所有轮廓(红色,3px线宽)

python 复制代码
cv2.drawContours(contours_img, cnts, -1, (0,0,255), 3)  
cv_show('contours_img', contours_img)

3、轮廓筛选(按面积降序排列)

python 复制代码
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:
    # 多边形近似(精度=2%周长)
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02*peri, True)
    if len(approx) == 4:  # 识别四边形轮廓
        doCnt = approx
        break

4、执行透视变换

python 复制代码
warped_t = four_point_transform(image, doCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)

>>> 阶段3:选项识别 <<<

1、灰度转换与二值化

python 复制代码
warped_gray = cv2.cvtColor(warped_t, cv2.COLOR_BGRA2GRAY)

2、自适应阈值处理(反色二值化+OTSU算法)

python 复制代码
thresh = cv2.threshold(warped_gray, 0, 255,
                      cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)

3、选项轮廓检测

python 复制代码
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

4、绘制绿色轮廓(1px线宽)

python 复制代码
warped_contours = cv2.drawContours(warped_t.copy(), cnts, -1, (0,255,0), 1)
cv_show('warped_contours', warped_contours)

5、选项筛选条件(宽高>20px,宽高比0.9-1.1)

python 复制代码
questionCnts = []
for c in cnts:
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:
        questionCnts.append(c)

6、轮廓排序(从上到下)

python 复制代码
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]

>>> 阶段4:评分系统 <<<

python 复制代码
correct = 0

1、遍历每道题(每5个选项为一题)

python 复制代码
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    # 当前题目选项排序(左到右)
    cnts = sort_contours(questionCnts[i:i+5])[0]
    bubbled = None

    # 遍历每个选项
    for (j, c) in enumerate(cnts):
        # 创建选项掩膜
        mask = np.zeros(thresh.shape, dtype="uint8")
        cv_show('mask',mask)
        cv2.drawContours(mask, [c], -1, 255, -1)  # 填充式绘制
        
        # 应用掩膜统计像素
        thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
        cv_show('thresh_mask_and',thresh_mask_and)
        total = cv2.countNonZero(thresh_mask)
        
        # 记录最大填涂区域
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)

    # 答案比对
    color = (0, 0, 255)  # 默认红色(错误)
    k = ANSWER_KEY[q]
    if k == bubbled[1]:
        color = (0, 255, 0)  # 绿色(正确)
        correct += 1
    
    # 绘制结果轮廓
    cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)

通过掩膜的方法依次遍历每个选项。

2、分数计算与显示

python 复制代码
score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))

3、在图像左上角添加红色分数文本

python 复制代码
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 20),
           cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)

4、结果展示

python 复制代码
cv_show('Original', image)        # 显示原始图像
cv_show("Exas", warped_new)     # 显示评分结果
cv2.waitKey(0)                    # 等待退出

总结

完整代码展示

python 复制代码
import numpy as np
import cv2
import os

ANSWER_KEY={0:1,1:4,2:0,3:3,4:1}

def cv_show(name,value):
    cv2.imshow(name,value)
    cv2.waitKey(0)

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


def resize(image,width=None,height=None,inter=cv2.INTER_AREA):

    dim=None
    (h,w)=image.shape[:2]
    if width is None and height is None:
        return image
    if width is None:
        r=height/float(h)
        dim=(int(w*r),height)
    else:
        r=width/float(w)
        dim=(width,int(h*r))
    resized=cv2.resize(image,dim,interpolation=inter)
    return resized


def order_points(pts):
    #一共四个坐标点
    rect=np.zeros((4,2),dtype='float32')
    #按顺序找到对应的坐标0123,分别是左上右上右下、左下
    s=pts.sum(axis=1)   #对矩阵的每一行进行求和操作
    rect[0]=pts[np.argmin(s)]
    rect[2]=pts[np.argmax(s)]
    diff=np.diff(pts,axis=1)
    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

    #计算输入的w和h值
    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,0],[maxwidth,maxheight],[0,maxheight]],dtype='float32')

    M=cv2.getPerspectiveTransform(rect,dst)
    warped=cv2.warpPerspective(image,M,(maxwidth,maxheight))
    return warped

#预处理
image=cv2.imread('../data/images/test_01.png')
contours_img=image.copy()

"灰度处理、做高斯滤波、边缘检测"
gray = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)
blurred = cv2.GaussianBlur(gray,(5,5),0)
cv_show('blurred',blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged',edged)

#轮廓检测
cnts=cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show('contours_img',contours_img)
doCnt=None

#根据轮廓大小进行排序,准备透视变换
cnts=sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:
    peri=cv2.arcLength(c,True)
    approx=cv2.approxPolyDP(c,0.02*peri,True)

    if len(approx)==4:
        doCnt=approx
        break

#执行透视变换
warped_t=four_point_transform(image,doCnt.reshape(4,2))
warped_new=warped_t.copy()
cv_show('warped',warped_t)
warped=cv2.cvtColor(warped_t,cv2.COLOR_BGRA2GRAY)

#阈值处理
thresh=cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
thresh_contours=thresh.copy()

cnts=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_contours=cv2.drawContours(warped_t,cnts,-1,(0,255,0),1)
cv_show('warped_contours',warped_contours)

questionCnts=[]
for c in cnts:
    (x,y,w,h)=cv2.boundingRect(c)
    ar=w/float(h)

    #根据实际情况指定标准
    if w>20 and h >20 and 0.9<ar<=1.1:
        questionCnts.append(c)



#按照从上到下的顺序排序
questionCnts=sort_contours(questionCnts,method="top-to-bottom")[0]
correct=0   #计算正确率

#依次取出每行的数据
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)):
    cnts=sort_contours(questionCnts[i:i+5])[0]
    bubbled=None

    #遍历每一个结果
    for (j,c) in enumerate(cnts):
        mask=np.zeros(thresh.shape,dtype='uint8')
        cv2.drawContours(mask,[c],-1,255,-1)#-1代表填充
        cv_show('mask',mask)

        thresh_mask_and=cv2.bitwise_and(thresh,thresh,mask=mask)
        cv_show('thresh_mask_and',thresh_mask_and)
        total=cv2.countNonZero(thresh_mask_and)
        if bubbled is None or total>bubbled[0]:
            bubbled=(total,j)

    color=(0,0,255)
    k=ANSWER_KEY[q]

    if k==bubbled[1]:
        color=(0,255,0)
        correct+=1

    cv2.drawContours(warped_new,[cnts[k]],-1,color,3)
    cv_show('warped',warped_new)

score=(correct/5.0)*100
print("[INFO] score:{:.2f}%".format(score))
cv2.putText(warped_new,"{:.2f}%".format(score),(10,20),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)

cv_show('Oringinal',image)
cv_show("Exas",warped_new)
cv2.waitKey(0)

该代码通过经典的OpenCV图像处理技术,构建了一个完整的答题卡自动评分系统,展现了计算机视觉在自动化领域的典型应用。其模块化设计、清晰的代码结构和可调参数,为二次开发提供了良好的基础,具备较高的实用价值和扩展潜力。

相关推荐
BOB-wangbaohai1 小时前
LangChain4j入门AI(六)整合提示词(Prompt)
人工智能·prompt·springboot3.x·langchain4j
灬0灬灬0灬2 小时前
pytorch训练可视化工具---TensorBoard
人工智能·pytorch·深度学习
文火冰糖的硅基工坊3 小时前
[创业之路-369]:企业战略管理案例分析-9-战略制定-差距分析的案例之华为
人工智能·华为·架构·系统架构·跨学科·跨学科融合
平和男人杨争争3 小时前
山东大学计算机图形学期末复习15——CG15
人工智能·算法·计算机视觉·图形渲染
lucky_lyovo3 小时前
OpenCV图像边缘检测
人工智能·opencv·计算机视觉
集和诚JHCTECH3 小时前
NODE-I916 & I721模块化电脑发布,AI算力与超低功耗的完美平衡
大数据·人工智能·电脑
恩喜玛生物4 小时前
深度学习实战 04:卷积神经网络之 VGG16 复现三(训练)
人工智能·深度学习·cnn
那雨倾城5 小时前
使用 OpenCV 实现万花筒效果
图像处理·人工智能·opencv·计算机视觉
小胡说人工智能5 小时前
深度剖析:Dify+Sanic+Vue+ECharts 搭建 Text2SQL 项目 sanic-web 的 Debug 实战
人工智能·python·llm·text2sql·dify·vllm·ollama
MorleyOlsen5 小时前
【数字图像处理】半开卷复习提纲
图像处理·计算机视觉