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

利用OpenCV进行对答题卡上的答案进行识别的案例

 代码实现:

import cv2
import numpy as npANSWER_KEY = {0:1,1:4,2:0,3:3,4:1}def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)def order_points(pts):# 一共有4个坐标点rect = np.zeros((4, 2), dtype="float32")  # 用来存储排序之后的坐标位置# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下s = pts.sum(axis=1)  # 对pts矩阵的每一行进行求和操作,(x+y)rect[0] = pts[np.argmin(s)]rect[2] = pts[np.argmax(s)]diff = np.diff(pts, axis=1)  # 对pts矩阵的每一行进行求差操作,(y-x)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 - 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
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, boundingBoxesimage = cv2.imread('test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
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.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show('contours_img',contours_img)
docCnt = 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:docCnt = approxbreak# 执行透视变换
warped_t = four_point_transform(image,docCnt.reshape(4,2))
warped_new = warped_t.copy()
cv_show('warped',warped_t)
warped = cv2.cvtColor(warped_t,cv2.COLOR_BGR2GRAY)
#闽值处理
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_Contouts',warped_Contours)questionsCnts = []
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:questionsCnts.append(c)
print(len(questionsCnts))questionsCnts = sort_contours(questionsCnts,method='top-to-bottom')[0]
correct = 0
for (q,i) in enumerate(np.arange(0,len(questionsCnts),5)):cnts = sort_contours(questionsCnts[i:i+5])[0]bubbled = Nonefor (j,c) in enumerate(cnts):mask = np.zeros(thresh.shape,dtype='uint8')cv2.drawContours(mask,[c],-1,255,-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('warpeding',warped_new)
score = ( correct / 5.0)*100
print("[INF0]score:{:.2f}%".format(score))
cv2.putText(warped_new,"{:.2f}%".format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)
cv2.imshow("Original",image)
cv2.imshow("Exam",warped_new)
cv2.waitKey(0)

这段代码是一个基于 OpenCV 的选择题自动批改程序,能够识别答题卡上的填涂选项并与标准答案对比,最终计算得分。下面分步骤解析代码的核心功能和实现逻辑:

1. 核心库与参数定义

import cv2  # 图像处理库
import numpy as np  # 数值计算库ANSWER_KEY = {0:1,1:4,2:0,3:3,4:1}  # 标准答案:键为题目序号,值为正确选项索引(0-4对应A-E)

2. 自定义工具函数

这些函数是后续图像处理的核心工具:

(1)图像显示函数 cv_show
def cv_show(name,img):cv2.imshow(name,img)  # 显示图像cv2.waitKey(0)  # 等待按键(0表示无限等待)

作用:简化图像显示操作,方便中间结果查看。

(2)四边形顶点排序 order_points
def order_points(pts):rect = np.zeros((4, 2), dtype="float32")  # 存储排序后的4个顶点s = pts.sum(axis=1)  # 计算每个点的x+y之和rect[0] = pts[np.argmin(s)]  # 左上:x+y最小rect[2] = pts[np.argmax(s)]  # 右下:x+y最大diff = np.diff(pts, axis=1)  # 计算每个点的y-x差值rect[1] = pts[np.argmin(diff)]  # 右上:y-x最小(x-y最大)rect[3] = pts[np.argmax(diff)]  # 左下:y-x最大(x-y最小)return rect

作用:将四边形的 4 个顶点按「左上→右上→右下→左下」顺序排列,为透视变换做准备。

(3)透视变换 four_point_transform
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))# 目标图像的4个顶点(正视图)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

作用:将倾斜的答题卡(四边形)矫正为正视图,方便后续按顺序识别选项。

(4)轮廓排序 sort_contours
def sort_contours(cnts, method='left-to-right'):reverse = False  # 是否反向排序i = 0  # 排序依据的维度(0为x坐标,1为y坐标)if method in ['right-to-left', 'bottom-to-top']:reverse = Trueif method in ['top-to-bottom', 'bottom-to-top']:i = 1  # 按y坐标排序(上下方向)# 按边界框的指定维度排序boundingBoxes = [cv2.boundingRect(c) for c in cnts]  # 每个轮廓的边界框(x,y,w,h)(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),key=lambda b: b[1][i], reverse=reverse))return cnts, boundingBoxes

作用:按指定方向(如从上到下、从左到右)排序轮廓,确保题目和选项按顺序处理。

3. 主流程:答题卡识别与批改

(1)图像预处理
image = cv2.imread('test_01.png')  # 读取原始图像
contours_img = image.copy()  # 复制图像用于绘制轮廓
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 转为灰度图
blurred = cv2.GaussianBlur(gray, (5,5), 0)  # 高斯模糊降噪
edged = cv2.Canny(blurred, 75, 200)  # Canny边缘检测(提取边缘)

目的:简化图像,突出轮廓边缘,为后续轮廓检测做准备。

(2)检测答题卡边界
# 检测所有外轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img, cnts, -1, (0,0,255), 3)  # 绘制轮廓(红色)# 筛选出答题卡的矩形轮廓(面积最大的四边形)
docCnt = 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:  # 若近似为四边形(答题卡通常是矩形)docCnt = approxbreak

目的:从图像中定位答题卡的边界(假设答题卡是最大的四边形)。

(3)透视变换矫正答题卡
# 对答题卡进行透视变换,得到正视图
warped_t = four_point_transform(image, docCnt.reshape(4,2))  # 彩色正视图
warped_new = warped_t.copy()  # 用于后续绘制批改结果
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)  # 转为灰度正视图# 阈值处理:将填涂区域转为白色,背景为黑色(反二进制阈值)
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

目的:将倾斜的答题卡矫正为平整的正视图,并用阈值处理突出填涂区域(填涂部分为白色)。

(4)识别选项轮廓(圆圈)
# 检测阈值图像中的轮廓(候选选项)
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0,255,0), 1)  # 绘制选项轮廓(绿色)# 筛选出符合条件的选项轮廓(大小接近正方形的圆圈)
questionsCnts = []
for c in cnts:(x,y,w,h) = cv2.boundingRect(c)  # 轮廓的边界框ar = w / float(h)  # 长宽比# 筛选:宽度和高度≥20像素,且长宽比接近1(正方形)if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:questionsCnts.append(c)

目的:从图像中筛选出选项的圆圈轮廓(排除无关的小轮廓或干扰区域)。

(5)识别填涂选项并批改
# 按从上到下排序选项轮廓(对应题目顺序)
questionsCnts = sort_contours(questionsCnts, method='top-to-bottom')[0]
correct = 0  # 正确题数# 遍历每个题目(假设每题5个选项,按顺序每5个轮廓为一组)
for (q, i) in enumerate(np.arange(0, len(questionsCnts), 5)):# 对当前题目的5个选项按从左到右排序(A→E)cnts = sort_contours(questionsCnts[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)  # 将当前选项轮廓内填白# 计算掩码与阈值图像的交集(即填涂区域)thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)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)

核心逻辑:通过掩码计算每个选项的填涂面积,面积最大的即为选中项;与标准答案对比,正确则标记绿色,错误标记红色。

(6)计算并显示分数
# 计算得分(正确率×100)
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)# 显示最终结果
cv2.imshow("Original", image)  # 原始图像
cv2.imshow("Exam", warped_new)  # 批改后的答题卡
cv2.waitKey(0)

最终效果图:

总结

整个流程从「图像预处理→答题卡定位→透视矫正→选项识别→填涂判断→分数计算」,通过 OpenCV 的轮廓检测、透视变换等技术,实现了选择题的自动批改。核心是通过轮廓排序和填涂面积分析,准确识别选中的选项,并与标准答案对比。


文章转载自:

http://uJlBosi8.nmbbt.cn
http://eHXVrU8n.nmbbt.cn
http://9ZtXzIBp.nmbbt.cn
http://0itKbWMZ.nmbbt.cn
http://FGJ1TzqW.nmbbt.cn
http://awv9Ab9n.nmbbt.cn
http://aoSRDHdB.nmbbt.cn
http://sGyWKbuo.nmbbt.cn
http://LNaRS7ST.nmbbt.cn
http://1jJj8J59.nmbbt.cn
http://pxiz5LTu.nmbbt.cn
http://4xIEoHTC.nmbbt.cn
http://XC7irv5p.nmbbt.cn
http://USnoVSZi.nmbbt.cn
http://oOCQIuKJ.nmbbt.cn
http://ujUeGLdE.nmbbt.cn
http://bsz0bYoj.nmbbt.cn
http://ituOUc6c.nmbbt.cn
http://kLxmAGWV.nmbbt.cn
http://MbnepnR8.nmbbt.cn
http://iUTTWlmb.nmbbt.cn
http://YLhOVaw6.nmbbt.cn
http://0NEERPFl.nmbbt.cn
http://T4FjXpxI.nmbbt.cn
http://ySt2P93I.nmbbt.cn
http://6WUGxBmK.nmbbt.cn
http://uncC8ANU.nmbbt.cn
http://tPkZnUqw.nmbbt.cn
http://FiZoCjM6.nmbbt.cn
http://GcKk7aNa.nmbbt.cn
http://www.dtcms.com/a/383406.html

相关文章:

  • 如何用 Rust 实现的基础屏幕录制程序?
  • 认知语义学隐喻理论对人工智能自然语言处理中深层语义分析的赋能与挑战
  • 常见索引失效场景及原因分析(含示例)
  • 嵌入式Linux常用命令
  • xtuoj Rectangle
  • C++内存管理:new与delete的深层解析
  • Nginx 实战系列(十)—— 搭建LNMP环境与部署Discuz!社区论坛指南
  • 计算机视觉案例分享之答题卡识别
  • 端口打开与服务可用
  • 如何解决 pip install 安装报错 ModuleNotFoundError: No module named ‘requests’ 问题
  • 使用Docker和虚拟IP在一台服务器上灵活部署多个Neo4j实例
  • Web前端面试题(2)
  • 硬件开发_基于物联网的仓鼠饲养监测系统
  • 资产负债表、利润表、经营现金流、统计指标计算程序
  • JWT简介
  • Week1:类,类与类之间的关系,继承,封装,多态
  • PostgreSQL 上的向量搜索实践
  • 金融科技:讓銀行服務更簡單便捷,推動數碼化轉型和提升客戶體驗
  • Games101 第七章 几何
  • 四、Scala深入面向对象:类、对象与伴生关系
  • quick_sort【快速排序】
  • Python 入门教学
  • 从零到顶会:NLP科研实战手册
  • C++(new和malloc)
  • JAVA算法练习题day11
  • 嵌入式固件升级要点总结
  • HarmonyOS 应用开发深度实践:驾驭 Stage 模型与 ArkTS 声明式 UI
  • MySQL的性能优化。
  • [硬件电路-208]:电阻的本质是按需消耗电势,并把电势能转化成热能
  • 智能推荐新纪元:快手生成式技术对系统边界的消融与重建