基于深度学习的智能监考系统:目标检测与姿态估计实战
2026/9/11 3:13:20 网站建设 项目流程

简介:人工智能与深度学习方向的学生,尤其是正在准备毕业设计或课程设计的人群,可从中获得一套完整的智能监考系统实现与设计方案,聚焦利用图像识别和行为分析辅助考试防作弊。资源共87个文件,以43个YAML配置、19个Python脚本为主,搭配图片样张、Jupyter Notebook、Shell脚本、npy数据文件、SQL建表文件及演示视频,压缩包仅18.41MB。内容覆盖人脸注册与采集、口罩识别、手机检测、人体姿态估计、异常行为分析等环节,并提供YOLOv5系列模型配置文件、数据预处理与训练脚本和数据库文件,可支撑从数据准备、模型训练到实时预警的完整流程。目前已有205人学习借鉴,适合入门到进阶的AI项目实践。

1. 基于深度学习的智能监考系统要解决的真实问题

一名巡考老师要同时盯着十几路考场画面,注意力撑不过二十分钟,低头看手机、侧身张望这类动作很难被及时捕捉。基于深度学习的智能监考系统,本质是把「看画面的人」换成「逐帧计算的模型」:人脸检测负责找考生位置,头部姿态估计判断头的朝向,目标检测盯住桌面上出现的手机和纸条。这类项目常以 zip 压缩包交付,里面是训练代码、权重文件和目录说明,动手前先理清目录结构,比直接跑训练脚本重要得多。方案适合两类人:做课程设计、毕业设计需要完整系统的学生,以及给考场做智能化改造的工程师。难点不在单模型精度,而在把单帧结果变成可复核、可追溯的事件流,误报率压不下来,模型再准也没人敢用。

2. 监考场景的任务拆解与深度学习模型选型

2.1 先把作弊行为翻译成视觉任务

构建系统的第一步,是给「作弊」下一个视觉上可计算的定义。同一个行为在不同考场会被不同监考老师认定,而模型只能识别画面里可观测的信号。所以常见做法是先把异常行为列成行为清单,逐个映射到对应的深度学习任务上,清单长什么样,直接决定后续数据怎么标、模型怎么选。不要一上来就找模型,先把行为清单敲定,后面每一步都省事。

| 异常行为 | 可观测视觉信号 | 对应任务 | 常用模型 | | 频繁低头 | 头部俯仰角(pitch)持续偏大 | 关键点检测 + 头部姿态回归 | MTCNN + solvePnP | | 侧身张望 | 偏航角(yaw)长时间偏离屏幕 | 头部姿态回归 | MediaPipe FaceMesh | | 盯别人屏幕 | 视线方向指向邻座区域 | 视线估计 | L2CS-Net | | 桌面出现手机 | 手机目标框 | 目标检测 | YOLOv8、RT-DETR | | 夹带纸张 | 手部与纸片目标框 | 目标检测 | YOLOv8 | | 替考换人 | 人脸身份与登记照不一致 | 人脸识别 | ArcFace、FaceNet |

这个映射表不需要一步到位,但必须有两列:视觉信号列决定要不要加一路模型,任务列决定是分类、回归还是检测。实际交付里,视线估计是性价比最低的一块,它对摄像头角度极其敏感,稍微俯拍视线落点就漂移,通常先用头部姿态替代。替考检测则单独走人脸比对,不同入口,不跟行为检测混在同一路模型里。

2.2 人脸检测与关键点模型怎么选

人脸检测是整个链路的前置,后面所有姿态和视线判断都依赖人脸框的可靠性。三个常见选择:MTCNN 体积小、CPU 可跑、自带五点回归,适合早期原型;RetinaFace 在遮挡和小脸场景下漏检少,适合教室内多人密集场景;YOLOv8-face 能与手机检测共用一套推理引擎,适合算力有限的边缘设备。我的建议是先 MTCNN 把链路跑通,等拿到真实考场数据再评估要不要换 RetinaFace,理由是人脸检测换模型只影响前置框,不影响后端的姿态和状态逻辑。

# 选型时统一封装,切换检测器不动后端逻辑 def create_detector(backend='mtcnn', device='cuda'): if backend == 'mtcnn': from facenet_pytorch import MTCNN return MTCNN(keep_all=True, device=device) if backend == 'retinaface': from retinaface import RetinaFace return RetinaFace(quality='medium') raise ValueError(f'unknown backend: {backend}')

关键点数量不是一个可以随便拍的参数。五点在绝大多数情况下够用,但如果你要算视线方向,至少需要眼部角点加鼻尖,68 点模型里这些索引是固定的;模型输出 72 点或 106 点在部分标注规范里也常见,落地时第一件事是确认关键点索引表,索引错位会导致姿态角全部算错,而且很难从结果上发现。

2.3 帧级检测加状态机,还是端到端动作识别

监考系统有两种主流架构。第一种是帧级检测加决策状态机:每一帧先做人脸检测、姿态回归、目标检测,然后把连续几秒的判断结果喂给一个有限状态机,由状态机决定是否报警。第二种是直接上视频级动作识别,把十几帧画面一起送给 SlowFast、TSM 或 ST-GCN,让模型自己学时序上的异常模式。

| 方案 | 代表模型 | 标注数据需求 | 可解释性 | 工程复杂度 | | 帧级检测 + 状态机 | YOLOv8 + FSM | 数万张标注帧 | 高,逐帧可查 | 低 | | 视频级动作识别 | SlowFast、TSM、ST-GCN | 数十万片段级标注 | 低,接近黑盒 | 高 |

做第一版原型,选前者基本不会错。帧级方案每一步都有据可查,考试结束复盘时能直接指到某一帧的检测结果;动作识别则把「为什么报警」藏在了网络里,监考老师本来就对系统不信任,再看不到判断依据,系统很难用起来。ST-GCN 只在「传递小抄、两人同时伸手」这类强时序行为上有明显优势,这类行为在真实考场数据里占比极低,不值得为它付出整条数据链路的成本。

2.4 算力决定模型体量

选型时先算摄像头路数。单人机位用 1080p 30fps,一块中端显卡跑 MTCNN 加姿态回归加一个轻量目标检测,单路还有余量;超过 6 路机位就要考虑把输入降到 720p,或者把帧率限制在 15fps。CPU 部署只推荐跑 360p 低帧率,人脸检测换用 OpenCV 的 DNN 模块加载 MobileNet-SSD,姿态估计用 MediaPipe 的 CPU 版本。这个结论反过来也指导模型选型:先决定跑在什么设备上,再决定用多大的 backbone,别一上来就用大模型,后面换模型时数据标注和阈值全要返工。

# 用同一段考场视频压测不同检测器,记录单帧耗时 python infer/bench.py --source data/raw/sample.mp4 --backend mtcnn python infer/bench.py --source data/raw/sample.mp4 --backend retinaface

3. 用 PyTorch 搭出可运行的深度学习监考检测链路

3.1 项目结构规划:zip 压缩包先理目录再动手

标题里的 zip 说明项目按压缩包形式交付。常见做法是解压后先把代码、配置和数据分开,后续训练和推理路径才不混乱。环境配置建议用 Python 3.10 加 PyTorch 2.x,先装好 requirements.txt 里的版本再跑代码,版本错位往往是第一轮报错的根源。

提示:如果压缩包体积超过 2GB,多半是误把数据集或权重打进去了,正规交付会拆成多个分卷或用独立存放方式管理权重。

exam-proctor/ ├── conf/ │ └── behavior.yaml # 行为判定阈值、摄像头配置 ├── data/ │ ├── raw/ # 考场原始视频,按场次分目录 │ ├── labels/ # COCO 格式标注 │ └── processed/ # 增强后的训练集 ├── models/ │ ├── face_detector.py # MTCNN 封装 │ └── head_pose.py # solvePnP 姿态估计 ├── weights/ # 预训练权重 ├── infer/ │ ├── video.py # 视频推理入口 │ └── proctor_fsm.py # 行为状态机 └── train/ └── train_yolo.py # 目标检测训练入口

先把权重单独放。一个推理用权重文件只有几十 MB,但训练中间产生的 checkpoint 动辄几个 GB,混在一起打进同一个 zip,传输和校验都很难受。其次,conf 里的行为阈值要能和代码分离,考场不一样、摄像头装高装低都影响阈值,配置外置能让你在部署现场只改 yaml 不改代码。

3.2 人脸检测封装与人脸框过滤

# models/face_detector.py import cv2 import torch from facenet_pytorch import MTCNN class FaceDetector: def __init__(self, device=None, min_face_size=40): self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu') self.mtcnn = MTCNN( keep_all=True, # 返回图像中所有人脸,不只最大一张 min_face_size=min_face_size, factor=0.709, # 图像金字塔缩放因子 device=self.device ) def __call__(self, frame_bgr): rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) boxes, probs = self.mtcnn.detect(rgb) if boxes is None: return [], [] keep = probs > 0.9 # 只保留置信度高于阈值的框 return boxes[keep], probs[keep]

keep_all=True是关键参数,监考画面里同时出现多个考生,默认的 MTCNN 只回传最大人脸,会漏掉后排。min_face_size控制最小可检人脸尺寸,摄像头离考生 5 到 8 米时这个值要调到 40 甚至 50,太小会引入大量背景误检。置信度阈值给 0.9 是保守取值,漏检比误检代价更低,因为状态机能容忍单帧漏检,却很难容忍一个假框触发后续姿态计算。

3.3 头部姿态估计代码与参数

头部姿态估计的常见做法是用人脸关键点和一个标准 3D 人脸模型做透视求解。OpenCV 的 solvePnP 输入是 2D 关键点和对应的 3D 点,输出旋转向量,转成欧拉角后就是俯仰角(pitch)、偏航角(yaw)和滚转角(roll)。

# models/head_pose.py import cv2 import numpy as np IDX = [30, 8, 36, 45, 48, 54] def estimate_head_pose(landmarks_2d): # 68点模型中鼻尖、下巴、左眼右角、右眼左角、嘴左角、嘴右角 obj_pts = np.array([ (0.0, 0.0, 0.0), (0.0, -63.6, -12.5), (28.8, 28.9, -24.1), (-28.8, 28.9, -24.1), (-28.9, -28.9, -24.1), (28.9, -28.9, -24.1), ], dtype=np.float32) img_pts = np.array([landmarks_2d[i] for i in IDX], dtype=np.float32) size = (640, 480) focal = size[1] camera_matrix = np.array([ [focal, 0, size[0] / 2], [0, focal, size[1] / 2], [0, 0, 1] ], dtype=np.float32) dist_coeffs = np.zeros((4, 1)) _, rvec, _ = cv2.solvePnP(obj_pts, img_pts, camera_matrix, dist_coeffs) rmat, _ = cv2.Rodrigues(rvec) pitch = np.degrees(np.arcsin(-rmat[1, 2])) yaw = np.degrees(np.arctan2(rmat[0, 2], rmat[2, 2])) roll = np.degrees(np.arctan2(rmat[1, 0], rmat[1, 1])) return pitch, yaw, roll

有两个参数容易被忽略。第一是 3D 模型坐标,示例用的是通用人脸的毫米级近似,不同论文给的标准脸坐标有出入,求出来的绝对角度偏差在 5 度以内,用于监考判断足够;如果要跟视线估计融合,就得换成同源的 3D 模型。第二是相机内参,示例里用焦距近似画幅宽度做了简化,没有标定相机时这样够用,标定后把内参换成真实值,姿态角的波动会明显减小。

3.4 行为判定的阈值参数表

姿态角算出来只是原始信号,还需要一套阈值把角度翻译成「低头、侧看、异常起身」这类业务事件。阈值不是一个定值,它跟摄像头的安装高度和俯仰角强相关,下面这组参数是常见部署场景的初始值。

| 行为 | 判定条件 | 建议初始值 | | 低头看桌 | pitch 小于 -15° 且持续 ≥ 3 秒 | 摄像头 2.5 米高、俯角 10° 时 | | 左/右张望 | yaw 绝对值大于 30° 且持续 ≥ 2 秒 | 考生面部正对镜头时 | | 面部遮挡 | 连续 5 秒检测不到人脸关键点 | 配合人脸框追踪判定 | | 桌面异常 | 手机类置信度高于 0.6 持续 ≥ 1 秒 | 按考场光照调整 |

注意:pitch 的符号方向和阈值大小,在顶部俯拍和正面平摄两种机位下符号会反转。部署时录 10 分钟校准视频,让考生做三个固定动作——低头看桌面、左右转头 45 度、举手,用采集到的角度分布来回推阈值,而不是照抄别人的值。

3.5 有限状态机:把单帧结果变成可审计事件

单帧检测结果不能直接报警。深度学习模型天然存在偶发误检,一帧误检就报警,系统每天会刷几千条垃圾事件。检测层上面需要加一个按时间累计的状态机。

# infer/proctor_fsm.py import time class ProctorFSM: LOW_HEAD_SECONDS = 3.0 YAW_SECONDS = 2.0 PHONE_SECONDS = 1.0 def __init__(self): self.low_head_start = None self.yaw_start = None self.phone_start = None def update(self, pitch, yaw, phone_ok, now=None): now = now or time.time() events = [] if pitch < -15: self.low_head_start = self.low_head_start or now if now - self.low_head_start >= self.LOW_HEAD_SECONDS: events.append('LOW_HEAD') else: self.low_head_start = None if abs(yaw) > 30: self.yaw_start = self.yaw_start or now if now - self.yaw_start >= self.YAW_SECONDS: events.append('LOOK_SIDE') else: self.yaw_start = None if phone_ok: self.phone_start = self.phone_start or now if now - self.phone_start >= self.PHONE_SECONDS: events.append('PHONE') else: self.phone_start = None return events

这个状态机用时间戳而不是帧数累计。摄像头在低光照下会自动降帧,帧数窗口会随时间漂移,时间戳方式不受帧率波动影响,这是在实际考场踩过的坑。now参数可以在离线回放时注入视频时间轴,保证在线实时和离线复盘的判定完全一致。事件返回后,统一写入事件日志,保存报警前后各 10 秒的原始画面切片,供人工复核。

4. 数据工程与深度学习模型训练的关键细节

4.1 数据来源与数量底线

监考行为数据基本没有现成的大规模公开数据集,通用的人脸检测数据只能用来做前置人脸模型,低头、侧看、用手机这类行为数据必须自建。自建的第一原则是「找真实考场空间,不要只在工位录」。教室里桌椅、窗光、后排同学的存在,会极大改变模型见到的背景分布,单一背景训练出来的检测器,换考场后误报率会成倍上升。数据量按类别看,手机目标检测每类不低于 5000 个实例,头姿回归不低于 3 万帧;课程设计可以把实例数压到 1500、帧数压到 8000,后续再增量补数据。

4.2 标注格式与工具

目标检测推荐 COCO 格式,用 labelImg 或 labelme 标注,类别列表尽量短:phone、paper、face、person 四类足够,类别越多类间误检越高。头部姿态不建议人工标注角度,代价大且主观偏差严重。更省力的做法是录制「刻意动作」数据:让志愿者在镜头前做低头、左看、右看、正常答题四类动作,把动作类别当弱标签绑定到姿态角区间。标注规范值得写成一页文档:纸张类只标 A4 大小及以上的纸片,手机只标屏幕可见的机型,出现争议时按文档判定,标注一致性比标注精度更重要。

4.3 针对监考场景的数据增强

考场环境有两个显著特点:大面积荧光灯下色温偏冷且容易忽变,人脸长期被口罩或手部遮挡。通用增强(随机翻转、随机裁剪)不够用,需要针对这两个特点做增强。

# train/setup_augment.py import albumentations as A behavior_aug = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.8), # 荧光灯导致色温整体偏移 A.HueSaturationValue(hue_shift_limit=8, sat_shift_limit=20, val_shift_limit=25, p=0.5), # 快速掏手机带来的运动模糊 A.MotionBlur(blur_limit=5, p=0.3), # 模拟前排头部遮挡,随机区域涂抹 A.CoarseDropout(max_holes=3, max_height=60, max_width=60, p=0.4), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ], bbox_params=A.BboxParams(format='coco', label_fields=['labels']))

CoarseDropout是被很多监考项目忽略但很关键的一层。教室里前排人头的遮挡会让手机目标框被拦腰截断,加入随机区域遮挡后,检测器不再依赖完整轮廓,而是学会利用手机屏幕发光这类局部特征。MotionBlur对应考生快速掏手机时产生的拖影,这类帧恰恰是最该抓住的。训练时增强参数不要全部拉到最大值,否则会造成增强分布与真实分布偏差过大,先按参考值跑,看验证集 loss 曲线再回调。

4.4 训练参数与业务指标

目标检测训练以 YOLOv8 为例,训练命令和参数如下。

yolo detect train \ data=conf/custom.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.001 \ cos_lr=True \ patience=20 \ cache=True

cos_lr=True在总轮数不多时能缓解尾部震荡,patience=20让训练在验证指标连续 20 轮不提升时提前停止,省去反复手动看曲线的时间。batch 大小以显卡显存能容纳为准,显存吃紧时优先调小 imgsz 而不是 batch,分辨率对考场小目标的影响远大于 batch 的影响。评估指标不能只看 mAP,监考场景更关心两个业务指标:误报率(每小时无中生有的报警次数)和漏报率(漏掉真实作弊行为的比例)。

| 指标 | 计算方式 | 参考目标 | | mAP@0.5 | 手机、纸张类平均精度 | ≥ 0.85 | | 误报率 | 报警事件中人工复核为假的比例 | ≤ 2 次/小时/路 | | 漏报率 | 人工标注行为未被检出比例 | ≤ 5% |

mAP 高不代表现场可用。一张误报截图给监考老师带来的信任损失,比十次漏报还大,因为误报直接消耗人肉复核的时间。训练结束后要从验证集里把误报样本收集出来,单独建一个小数据集做硬负样本挖掘,重训一轮。激活函数方面,YOLOv8 默认的 SiLU 在深层网络里表现均衡,不必为监考场景换成别的;如果要自己写分类头,建议在最后一个卷积后面接 LeakyReLU,避免负区间信息直接归零。

5. 部署、排错与轻量化落地的几个技巧

5.1 先离线回放,再上实时流

线上出问题的时候,问题往往不在模型精度,而在「线上实时推理和线下离线验证不一致」。常见做法是先做影子模式:把系统挂在考场录像的离线流上,推演一整场考试,输出事件时间线,再与人工复核结果比对。这一步能暴露绝大多数阈值失配、时间轴对不齐和内存泄漏问题。

python infer/video.py \ --source data/raw/exam_2024_03.mp4 \ --config conf/behavior.yaml \ --out events/exam_2024_03.json

--config把姿态阈值、持续秒数和目标类别都放在 yaml 里,--out输出 JSON 事件流,每条事件带上视频时间戳和关键帧的裁剪图路径。影子模式跑过两场不同教室的录像,再切到实时 RTSP 流,风险会小很多。

5.2 导出 ONNX 降低推理延迟

PyTorch 动态图直接部署会带来额外的解释开销,导出 ONNX 后用推理引擎跑,单帧延迟一般能下降两到三成,多路并发时影响明显。

# export_onnx.py import torch model = torch.load('weights/yolov8n_custom.pt') model.eval() dummy = torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy, 'weights/yolov8n_custom.onnx', opset_version=13, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}} )

opset_version用 13 保证大多数推理引擎能识别,dynamic_axes允许推理时动态调整 batch,多路机位可以合成一个 batch 去跑,比逐张推理节约显存。更进一步可以做 FP16 量化,权重体积减半、显存占用减半,对嵌入式设备收益明显。

5.3 最容易翻车的三个点

第一个是时间尺度写错。状态机里如果用「连续 30 帧」做窗口,而摄像头在暗光下自动掉到 10fps,3 秒的低头动作要 5 秒才能触发报警,整个系统的时间语义全乱。前面状态机按时间戳累计就是为了避开这个坑,排查时先看事件时间戳间隔是否均匀。

第二个是摄像头安装角度变动后阈值失效。考试前挪过摄像头,原来标定的 pitch 阈值直接变成废值,重新校准需要让考生做固定动作,最快的办法是录一段 1 分钟视频,对着视频里的动作角分布重新设阈值,而不是现场调参数。

第三个是数据处理与隐私边界。预测结束后,不要保存全过程视频,只保留报警事件前后 10 秒的切片,并对人脸区域做裁剪后再落盘;输出的 JSON 事件流去掉不必要的面部信息,审计需要时再回溯原始整场视频。这个处理顺序反过来决定了存储设计:事件切片的目录结构按场次年月日加摄像机编号组织,才能在一场考试结束后快速定位到指定座位的事件。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询