目录

一、项目介绍

二、项目优势(为什么推荐你学这个)

三、整体算法详细流程

四、完整可运行代码

五、逐模块超详细原理解析

1、四点排序原理(重点)

2、透视变换原理

3、高斯模糊作用

4、Canny 边缘检测

5、OTSU 二值化

6、轮廓筛选圆原理

7、Mask 掩码判题核心原理

六、运行部署教程(超详细)

七、项目拓展方向(适合毕设加分)

八、总结


一、项目介绍

在计算机视觉学习路径中,答题卡识别是最经典、最适合新手入门、最适合做课程设计的实战项目。

目前网上很多方案都是深度学习训练模型,门槛高、需要数据集、训练耗时。 本项目 不使用任何深度学习、不训练模型、无需 GPU,纯传统机器视觉算法实现:

  • 自动矫正倾斜、透视变形的答题卡
  • 自动检测试卷边框
  • 自动定位所有答题圆圈
  • 自动排序题目顺序
  • 自动识别学生填涂答案
  • 自动对比标准答案、打分、可视化对错

适合:OpenCV 练手、计算机视觉课程设计、Python 期末大作业、毕设入门项目。

二、项目优势(为什么推荐你学这个)

✅ 零模型、零训练、零数据集 ✅ 代码量适中、逻辑清晰、适合新手吃透 CV 基础 ✅ 涵盖 透视变换、轮廓检测、轮廓排序、掩码运算、二值化、边缘检测 六大核心 CV 知识点 ✅ 识别稳定、速度快、可直接部署 ✅ 效果直观,可视化效果满分,作业颜值极高

三、整体算法详细流程

我把每一步给你讲的非常细:

  1. 图像读取:读取本地答题卡图片
  2. 灰度化:彩色图转灰度图,减少计算量
  3. 高斯模糊:去除图像噪点,防止误检测轮廓
  4. Canny 边缘检测:提取答题卡边缘轮廓
  5. 轮廓查找:找出图片中所有轮廓
  6. 筛选最大四边形轮廓:自动找到答题卡外框
  7. 四点透视变换:矫正倾斜试卷,生成俯视图(本项目核心难点
  8. 灰度 + 自适应二值化:将填涂黑色选项转为白色,背景变黑
  9. 轮廓筛选答题圆圈:通过尺寸、圆度筛选有效选项
  10. 轮廓排序:从上到下排题目、从左到右排选项
  11. 掩码 mask 像素统计:判断哪个选项被填涂
  12. 匹配标准答案:自动判对错、打分
  13. 可视化结果:对错红绿标注、输出总分

四、完整可运行代码

python

运行

import numpy as np
import cv2

# 自定义标准答案库:对应5道选择题
# key=题号,value=正确选项(01234对应ABCDE)
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}

def order_points(pts):
    """
    【核心函数1】答题卡四点坐标排序
    任意倾斜四边形,自动排序为固定顺序:左上、右上、右下、左下
    """
    rect = np.zeros((4, 2), dtype="float32")

    # x+y 最小 = 左上,x+y最大 = 右下
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]

    # y-x 最小 = 右上,y-x最大 = 左下
    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):
    """
    【核心函数2】四点透视变换,矫正倾斜试卷
    输入倾斜试卷四点,输出垂直俯视的标准答题卡
    """
    # 排序四点
    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) + ((tl[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'):
    """
    【核心函数3】通用轮廓排序工具
    支持:从左到右、从右到左、从上到下、从下到上
    """
    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 cv_show(name,img):
    """自定义窗口显示,按任意键关闭"""
    cv2.imshow(name,img)
    cv2.waitKey(0)

# ========================= 1.图像预处理 =========================
# 读取原图
image = cv2.imread(r'./images/test_01.png')
contours_img = image.copy()

# 1.灰度化:减少通道,简化计算
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 2.高斯模糊:去除噪点,平滑图像
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)

# 3.Canny边缘检测:提取所有物体边缘
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)

# ========================= 2.定位答题卡外框 =========================
# 查找所有外层轮廓
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)
    # 最大轮廓且是4个顶点=答题卡矩形
    if len(approx) == 4:
        docCnt = approx
        break

# 透视变换矫正整张试卷
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)

# ========================= 3.OTSU自适应二值化 =========================
# THRESH_BINARY_INV 反向二值化:
# 亮的地方变黑、暗的填涂区域变白,方便识别填涂圆圈
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)

# ========================= 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)
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)

print("有效答题圆圈数量:",len(questionCnts))

# ========================= 5.轮廓排序 =========================
# 整体题目从上到下排序
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]
correct = 0

# 每题5个选项,逐题遍历
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    # 单题选项从左到右排序
    cnts = sort_contours(questionCnts[i:i + 5])[0]
    bubbled = None

    # ========================= 6.Mask掩码识别填涂答案 =========================
    for (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)

    # ========================= 7.标准答案匹配打分 =========================
    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('warpeding', warped_new)

# 计算得分百分比
score = (correct / 5.0) * 100
print("[INFO] 最终得分: {:.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 Result", warped_new)
cv2.waitKey(0)

五、逐模块超详细原理解析

1、四点排序原理(重点)

拍摄的答题卡永远是倾斜的,四个点顺序混乱。 通过数学规律强制排序:

  • x+y 最小 → 左上
  • x+y 最大 → 右下
  • y-x 最小 → 右上
  • y-x 最大 → 左下

保证透视变换永远不会错乱。

2、透视变换原理

普通仿射变换只能旋转缩放,透视变换可以矫正拍摄透视变形。 通过计算原图四边形 → 标准矩形的映射矩阵,还原俯视正视图

3、高斯模糊作用

图像拍摄会有椒盐噪点、颗粒噪点,不模糊会导致检测出大量无效小轮廓,干扰答题圈识别。

4、Canny 边缘检测

灰度梯度计算,精准提取物体边缘,只保留轮廓信息,去除纹理信息。

5、OTSU 二值化

自动寻找最佳阈值,不需要手动调参。 反向二值化让:

  • 答题卡白纸 → 黑色
  • 铅笔填涂区域 → 白色 极大方便像素统计判断填涂。

6、轮廓筛选圆原理

圆形外接矩形 宽高比无限接近 1。 通过 0.9~1.1 比例筛选,完美过滤长方形、不规则噪点轮廓。

7、Mask 掩码判题核心原理

这是整段代码最精髓、面试常问的地方:

  1. 单独抠出每一个选项圆圈
  2. 统计圈内白色像素多少
  3. 填涂越重、白色像素越多
  4. 像素最多的就是考生选择答案

抗干扰能力极强!

六、运行部署教程(超详细)

  1. 安装依赖

plaintext

pip install opencv-python numpy
  1. 项目结构

plaintext

project
   ├─ images
   │   └─ test_01.png
   └─ main.py
  1. 替换自己的答题卡图片
  2. 修改 ANSWER_KEY 为你的标准答案
  3. 直接运行即可

七、项目拓展方向(适合毕设加分)

  1. 支持 多题、多选项 自动适配
  2. 增加 去反光、形态学操作
  3. 支持 多选、缺考、空题判断
  4. 批量识别多张答题卡
  5. 输出 Excel 成绩报表

八、总结

本项目完整覆盖 OpenCV 最核心、最常用 的全部知识点: 图像预处理、边缘检测、轮廓操作、透视变换、二值化、掩码运算、轮廓排序。

非常适合:新手入门、课程设计、期末大作业、毕设基础项目

更多推荐