答题卡检测

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

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)
相关推荐
火山引擎开发者社区2 小时前
火山方舟 Coding Plan 服务变更公告
人工智能
强风7942 小时前
OpenCV的创建与配置
人工智能·opencv·计算机视觉
qq_452396232 小时前
【Python × AI】Prompt Engineering 深度工程化:打造大模型的“确定性”控制链路
人工智能·python·ai·prompt
nap-joker2 小时前
【表格+图像融合+多模态分类标签不一致问题】TNF:多模态医学数据分类的三分支神经融合
人工智能·分类·数据挖掘·多模态融合·图像+表格模态融合·三分支神经网络
是Dream呀2 小时前
ABFPN:一种面向小目标检测的多尺度特征融合网络
人工智能·目标检测·目标跟踪
weixin_452953322 小时前
openclaw新手部署详细教程——适用于ubuntu22.04
linux·人工智能·ubuntu
火山引擎开发者社区2 小时前
扣子×飞书深度打通!自带浏览器的 OpenClaw,一键领养!
人工智能
挂科边缘2 小时前
Transformer模型理论介绍
人工智能·深度学习·transformer