答题卡检测

答题卡识别评分代码完整讲解

1. 答题卡处理流程图

  1. 读取答题卡图像并进行灰度化、模糊处理和边缘检测;

  2. 定位答题卡区域并进行透视变换;

  3. 通过阈值处理和轮廓分析检测填涂的选项泡泡;

  4. 将检测结果与标准答案对比计算得分。系统支持自定义参数调整,包括泡泡最小尺寸、宽高比范围等,能够处理不同形态的答题卡。

  5. 最终输出评分结果并在图像上标记正确/错误选项。

2. Python 代码及详细讲解

导入库

import cv2

import numpy as np

import matplotlib.pyplot as plt

功能讲解:cv2: OpenCV图像处理库,用于图像处理

numpy: 数值计算,处理坐标和矩阵运算

matplotlib.pyplot: 可视化,用于绘制调试图像或流程图

参数设置

image_path = r"F:\project\pytorch_project\CV学习\image.png"

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

MIN_BUBBLE_W, MIN_BUBBLE_H = 10, 10

ASPECT_RATIO_MIN, ASPECT_RATIO_MAX = 0.5, 1.5

功能讲解:image_path: 答题卡图片路径

ANSWER_KEY: 正确答案索引字典

MIN_BUBBLE_W/H: 泡泡最小尺寸,过滤噪点

ASPECT_RATIO_MIN/MAX: 宽高比范围,过滤非圆形轮廓

辅助函数:显示图像

def cv_show(name, img):

cv2.imshow(name, img)

cv2.waitKey(0)

cv2.destroyAllWindows()

功能讲解:用于显示图像窗口,便于调试

辅助函数:轮廓排序

def sort_contours(cnts, method="left-to-right"):

根据指定方向排序轮廓并返回排序后的轮廓列表

...

功能讲解:method可选: 'left-to-right', 'top-to-bottom', 'right-to-left', 'bottom-to-top'

辅助函数:四点排序

def order_points(pts):

将四个角点按顺序排列为 top-left, top-right, bottom-right, bottom-left

...

功能讲解:用于透视变换前整理角点顺序

辅助函数:透视变换

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

功能讲解:将答题卡校正为俯视视角,保证泡泡排列规则

主流程:读取图像并预处理

img = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

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

edged = cv2.Canny(blurred, 75, 150)

功能讲解:读取图像,灰度化,高斯模糊去噪,Canny边缘检测

主流程:检测答题卡轮廓

cnts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

cnts = sorted(cnts, key=cv2.contourArea, reverse=True)

docCnt = None

for c in cnts:

peri = cv2.arcLength(c, True)

approx = cv2.approxPolyDP(c, 0.02*peri, True)

if len(approx) == 4:

docCnt = approx

break

功能讲解:找到最大四边形轮廓,假设为答题卡

主流程:透视变换

warped = four_point_transform(gray, docCnt.reshape(4,2))

功能讲解:得到俯视图,泡泡排列规则

主流程:二值化

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

功能讲解:泡泡为白色,背景黑色

主流程:检测泡泡

cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

questionCnts = []

for c in cnts:

(x, y, w, h) = cv2.boundingRect(c)

ar = w / float(h)

if w >= MIN_BUBBLE_W and h >= MIN_BUBBLE_H and ASPECT_RATIO_MIN <= ar <= ASPECT_RATIO_MAX:

questionCnts.append(c)

功能讲解:根据尺寸和宽高比过滤噪点,得到候选泡泡

主流程:排序泡泡

questionCnts, _ = sort_contours(questionCnts, method="top-to-bottom")

bubbles_per_row = 5

rows = []

for i in range(0, len(questionCnts), bubbles_per_row):

row_cnts = questionCnts[i:i+bubbles_per_row]

row_cnts, _ = sort_contours(row_cnts, method="left-to-right")

rows.append(row_cnts)

功能讲解:先按行排序,再按列排序

主流程:评分

correct = 0

for q, row_cnts in enumerate(rows[:len(ANSWER_KEY)]):

bubbled = None

for j, c in enumerate(row_cnts):

mask = np.zeros(thresh.shape, dtype="uint8")

cv2.drawContours(mask, [c], -1, 255, -1)

mask = cv2.bitwise_and(thresh, thresh, mask=mask)

total = cv2.countNonZero(mask)

if bubbled is None or total > bubbled[0]:

bubbled = (total, j)

k = ANSWER_KEY[q]

color = (0,0,255)

if bubbled and k == bubbled[1]:

color = (0,255,0)

correct += 1

cv2.drawContours(warped, [row_cnts[k]], -1, color, 3)

功能讲解:每行找到涂黑最多泡泡,对比答案并标记正确/错误

主流程:输出分数

score = (correct / len(ANSWER_KEY)) * 100

print(f"Score: {score}%")

cv2.putText(warped, f"Score: {score:.2f}%", (10, 20),

cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2)

功能讲解:计算总分,并在图像上显示

python 复制代码
import cv2
import numpy as np
import matplotlib.pyplot as plt

# ================= 参数设置 =================
image_path = r"F:\project\pytorch_project\CV学习\image.png"

# 正确答案字典,每题的索引对应答案位置
ANSWER_KEY = {0:1, 1:4, 2:0, 3:3, 4:1}

# 自动适配泡泡的最小宽高和宽高比范围
MIN_BUBBLE_W, MIN_BUBBLE_H = 10, 10
ASPECT_RATIO_MIN, ASPECT_RATIO_MAX = 0.5, 1.5  # 支持略长或略扁的泡泡

# ================= 辅助函数 =================

def cv_show(name, img):
    """显示图像"""
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def sort_contours(cnts, method="left-to-right"):
    """
    对轮廓进行排序
    method: 'left-to-right', 'top-to-bottom', 'right-to-left', 'bottom-to-top'
    """
    if len(cnts) == 0:
        return [], []

    reverse = False
    i = 0  # 0: x, 1: y
    if method in ["right-to-left", "bottom-to-top"]:
        reverse = True
    if method in ["top-to-bottom", "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 list(cnts), list(boundingBoxes)

def order_points(pts):
    """
    将四个点按顺序排列:
    [top-left, top-right, bottom-right, bottom-left]
    """
    rect = np.zeros((4,2), dtype="float32")
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]  # top-left
    rect[2] = pts[np.argmax(s)]  # bottom-right

    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]  # top-right
    rect[3] = pts[np.argmax(diff)]  # bottom-left
    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")

    # 计算透视变换矩阵
    M = cv2.getPerspectiveTransform(rect, dst)
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
    return warped

# ================= 主流程 =================

# 1. 读取原图
img = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR)
if img is None:
    raise FileNotFoundError(f"无法读取输入图像: {image_path}")
cv_show("Original", img)

# 2. 灰度化 + 高斯模糊 + 边缘检测
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5,5), 0)  # 去噪
cv_show("Blurred", blurred)
edged = cv2.Canny(blurred, 75, 150)        # 边缘检测
cv_show("Edged", edged)

# 3. 找最大四边形轮廓作为答题卡,做透视变换
cnts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
docCnt = None
for c in cnts:
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02*peri, True)
    if len(approx) == 4:
        docCnt = approx
        break
if docCnt is None:
    raise ValueError("未找到答题卡四边形轮廓!")

# 透视变换,得到俯视图
warped = four_point_transform(gray, docCnt.reshape(4,2))
cv_show("Warped", warped)

# 4. 二值化(反转+OTSU)
thresh = cv2.threshold(warped, 0, 255,
                       cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show("Threshold", thresh)

# 5. 找轮廓
cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 6. 筛选可能的泡泡轮廓
questionCnts = []
for c in cnts:
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    if w >= MIN_BUBBLE_W and h >= MIN_BUBBLE_H and ASPECT_RATIO_MIN <= ar <= ASPECT_RATIO_MAX:
        questionCnts.append(c)

# 7. 全局排序:先 top-to-bottom
questionCnts, _ = sort_contours(questionCnts, method="top-to-bottom")
if len(questionCnts) == 0:
    raise ValueError("未检测到有效 bubbles!请检查图像质量或阈值")

# 每行泡泡数量(可修改)
bubbles_per_row = 5
rows = []
for i in range(0, len(questionCnts), bubbles_per_row):
    row_cnts = questionCnts[i:i+bubbles_per_row]
    row_cnts, _ = sort_contours(row_cnts, method="left-to-right")
    rows.append(row_cnts)

# 8. 评分
correct = 0
for q, row_cnts in enumerate(rows[:len(ANSWER_KEY)]):
    bubbled = None
    for j, c in enumerate(row_cnts):
        # 生成泡泡掩膜
        mask = np.zeros(thresh.shape, dtype="uint8")
        cv2.drawContours(mask, [c], -1, 255, -1)
        # 与二值化图像结合
        mask = cv2.bitwise_and(thresh, thresh, mask=mask)
        total = cv2.countNonZero(mask)  # 统计白色像素数量
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)
    # 对比答案
    k = ANSWER_KEY[q]
    color = (0,0,255)  # 红色默认错误
    if bubbled and k == bubbled[1]:
        color = (0,255,0)  # 绿色表示正确
        correct += 1
    # 绘制标记
    cv2.drawContours(warped, [row_cnts[k]], -1, color, 3)

# 9. 计算总分
score = (correct / len(ANSWER_KEY)) * 100
print(f"Score: {score}%")
cv2.putText(warped, f"Score: {score:.2f}%", (10, 20),
            cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2)
cv_show("Graded", warped)
相关推荐
小程故事多_803 分钟前
Harness实战指南,在Java Spring Boot项目中规范落地OpenSpec+Claude Code
java·人工智能·spring boot·架构·aigc·ai编程
Anastasiozzzz5 小时前
深入研究RAG: 在线阶段-查询&问答
数据库·人工智能·ai·embedding
tq10865 小时前
资本主义的时间贴现危机:AI时代的结构性淘汰机制
人工智能
砍材农夫5 小时前
spring-ai 第四多模态API
java·人工智能·spring
土豆12507 小时前
LangGraph TypeScript 版入门与实践
人工智能·llm
土豆12508 小时前
OpenSpec:让 AI 编码助手从"乱猜"到"照单执行"
人工智能·llm
Thomas.Sir8 小时前
第二章:LlamaIndex 的基本概念
人工智能·python·ai·llama·llamaindex
m0_694845578 小时前
Dify部署教程:从AI原型到生产系统的一站式方案
服务器·人工智能·python·数据分析·开源
LS_learner8 小时前
VS Code 终端默认配置从 PowerShell 改为 CMD
人工智能
小毅&Nora9 小时前
【人工智能】【大模型】大模型“全家桶”到“精兵简政”:企业AI落地的理性进化之路
人工智能·大模型·平安科技