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

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

文章目录

  • 前言
  • 一、代码分析及流程讲解
    • (一)初始化模块
      • 正确答案映射字典(题目序号: 正确选项索引)
      • 图像显示工具函数
    • (二)轮廓处理工具模块
    • (三)几何变换核心模块
  • 二、主处理流程
    • 图像读取
    • >>> 阶段1:图像预处理 <<<
      • 1、灰度转换(注意:COLOR_BGRA2GRAY适用于含alpha通道图像,通常使用COLOR_BGR2GRAY)
      • 2、高斯滤波(5x5卷积核去噪)
      • 3、Canny边缘检测(双阈值设置)
    • >>> 阶段2:答题卡定位 <<<
      • 1、轮廓检测(仅检测最外层轮廓)
      • 2、绘制所有轮廓(红色,3px线宽)
      • 3、轮廓筛选(按面积降序排列)
      • 4、执行透视变换
    • >>> 阶段3:选项识别 <<<
      • 1、灰度转换与二值化
      • 2、自适应阈值处理(反色二值化+OTSU算法)
      • 3、选项轮廓检测
      • 4、绘制绿色轮廓(1px线宽)
      • 5、选项筛选条件(宽高>20px,宽高比0.9-1.1)
      • 6、轮廓排序(从上到下)
    • >>> 阶段4:评分系统 <<<
      • 1、遍历每道题(每5个选项为一题)
      • 2、分数计算与显示
      • 3、在图像左上角添加红色分数文本
      • 4、结果展示
  • 总结


前言

一、代码分析及流程讲解

(一)初始化模块

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)

(二)轮廓处理工具模块

轮廓定向排序函数
参数:
cnts: 轮廓列表
method: 排序方向(left-to-right/right-to-left/top-to-bottom/bottom-to-top)
返回值:
排序后的轮廓及边界框

def 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 = 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: 插值方法

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 imageif 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. 计算坐标差值,最小值为右上,最大值为左下

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. 执行透视变换

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

二、主处理流程

图像读取

image = cv2.imread('../data/images/test_01.png')
contours_img = image.copy()

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

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

gray = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)  

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

blurred = cv2.GaussianBlur(gray, (5,5), 0)
cv_show('blurred', blurred)

在这里插入图片描述

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

edged = cv2.Canny(blurred, 75, 200)  
cv_show('edged', edged)

在这里插入图片描述

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

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

cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

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

cv2.drawContours(contours_img, cnts, -1, (0,0,255), 3)  
cv_show('contours_img', contours_img)

在这里插入图片描述

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

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 = approxbreak

4、执行透视变换

warped_t = four_point_transform(image, doCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)

在这里插入图片描述

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

1、灰度转换与二值化

warped_gray = cv2.cvtColor(warped_t, cv2.COLOR_BGRA2GRAY)

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

thresh = cv2.threshold(warped_gray, 0, 255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)

在这里插入图片描述

3、选项轮廓检测

cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

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

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)

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、轮廓排序(从上到下)

questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]

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

correct = 0

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

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、分数计算与显示

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

在这里插入图片描述

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

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

4、结果展示

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

在这里插入图片描述

总结

完整代码展示

import numpy as np
import cv2
import osANSWER_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=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,boundingBoxesdef 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 imageif 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 resizeddef 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 rectdef 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=approxbreak#执行透视变换
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+=1cv2.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图像处理技术,构建了一个完整的答题卡自动评分系统,展现了计算机视觉在自动化领域的典型应用。其模块化设计、清晰的代码结构和可调参数,为二次开发提供了良好的基础,具备较高的实用价值和扩展潜力。

相关文章:

  • 【LeetCode 热题 100】有效的括号 / 最小栈 / 字符串解码 / 柱状图中最大的矩形
  • Elasticsearch 实战面试题,每个题目都会单独解析
  • 多类型RFID电子标签定制 助力行业精准化管理
  • 在hadoop中实现序列化与反序列化
  • Java EE初阶——定时器和线程池
  • 使用 Navicat 工具管理时,点击某一列,能否查看该列的平均值和最大值等关联信息?
  • 【前端部署】通过 Nginx 让局域网用户访问你的纯前端应用
  • SSH漏洞修复方案
  • GitHub 趋势日报 (2025年05月19日)
  • 机器学习第十九讲:交叉验证 → 用五次模拟考试验证真实水平
  • DataLight(V1.7.12)版本更新发布
  • 进程间通信(IPC):LocalSocket
  • ES(Elasticsearch) 基本概念(一)
  • 开疆智能Profinet转RS485网关连接电磁流量计到西门子PLC配置案例
  • WD5030L CC/CV模式DCDC15A高效同步转换器消费电子工业控制汽车电子优选择
  • Linux X86平台安装ARM64交叉编译器方法
  • LLM大模型工具链
  • MySQL与Redis一致性问题分析
  • 4大AI智能体平台,你更适合哪一个呐?
  • 单端传输通道也会有奇偶模现象喔
  • 来论|以法治之力激发民营经济新动能
  • 破题“省会担当”,南京如何走好自己的路?
  • 技术派|台军首次试射“海马斯”火箭炮,如何压制这种武器?
  • 著名文学评论家、原伊犁师范学院院长吴孝成逝世
  • 小米法务部:犯罪团伙操纵近万账号诋毁小米,该起黑公关案告破
  • 周国辉谈花开岭现象 :年轻的公益人正在用行动点亮希望