ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习的智能监考系统:目标检测与姿态估计实战

基于深度学习的智能监考系统:目标检测与姿态估计实战 简介人工智能与深度学习方向的学生尤其是正在准备毕业设计或课程设计的人群可从中获得一套完整的智能监考系统实现与设计方案聚焦利用图像识别和行为分析辅助考试防作弊。资源共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(backendmtcnn, devicecuda): if backend mtcnn: from facenet_pytorch import MTCNN return MTCNN(keep_allTrue, devicedevice) if backend retinaface: from retinaface import RetinaFace return RetinaFace(qualitymedium) raise ValueError(funknown 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 retinaface3. 用 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, deviceNone, min_face_size40): self.device device or (cuda if torch.cuda.is_available() else cpu) self.mtcnn MTCNN( keep_allTrue, # 返回图像中所有人脸不只最大一张 min_face_sizemin_face_size, factor0.709, # 图像金字塔缩放因子 deviceself.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_allTrue是关键参数监考画面里同时出现多个考生默认的 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), ], dtypenp.float32) img_pts np.array([landmarks_2d[i] for i in IDX], dtypenp.float32) size (640, 480) focal size[1] camera_matrix np.array([ [focal, 0, size[0] / 2], [0, focal, size[1] / 2], [0, 0, 1] ], dtypenp.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, nowNone): 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_limit0.3, contrast_limit0.3, p0.8), # 荧光灯导致色温整体偏移 A.HueSaturationValue(hue_shift_limit8, sat_shift_limit20, val_shift_limit25, p0.5), # 快速掏手机带来的运动模糊 A.MotionBlur(blur_limit5, p0.3), # 模拟前排头部遮挡随机区域涂抹 A.CoarseDropout(max_holes3, max_height60, max_width60, p0.4), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ], bbox_paramsA.BboxParams(formatcoco, label_fields[labels]))CoarseDropout是被很多监考项目忽略但很关键的一层。教室里前排人头的遮挡会让手机目标框被拦腰截断加入随机区域遮挡后检测器不再依赖完整轮廓而是学会利用手机屏幕发光这类局部特征。MotionBlur对应考生快速掏手机时产生的拖影这类帧恰恰是最该抓住的。训练时增强参数不要全部拉到最大值否则会造成增强分布与真实分布偏差过大先按参考值跑看验证集 loss 曲线再回调。4.4 训练参数与业务指标目标检测训练以 YOLOv8 为例训练命令和参数如下。yolo detect train \ dataconf/custom.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.001 \ cos_lrTrue \ patience20 \ cacheTruecos_lrTrue在总轮数不多时能缓解尾部震荡patience20让训练在验证指标连续 20 轮不提升时提前停止省去反复手动看曲线的时间。batch 大小以显卡显存能容纳为准显存吃紧时优先调小 imgsz 而不是 batch分辨率对考场小目标的影响远大于 batch 的影响。评估指标不能只看 mAP监考场景更关心两个业务指标误报率每小时无中生有的报警次数和漏报率漏掉真实作弊行为的比例。| 指标 | 计算方式 | 参考目标 | | mAP0.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_version13, 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 帧」做窗口而摄像头在暗光下自动掉到 10fps3 秒的低头动作要 5 秒才能触发报警整个系统的时间语义全乱。前面状态机按时间戳累计就是为了避开这个坑排查时先看事件时间戳间隔是否均匀。第二个是摄像头安装角度变动后阈值失效。考试前挪过摄像头原来标定的 pitch 阈值直接变成废值重新校准需要让考生做固定动作最快的办法是录一段 1 分钟视频对着视频里的动作角分布重新设阈值而不是现场调参数。第三个是数据处理与隐私边界。预测结束后不要保存全过程视频只保留报警事件前后 10 秒的切片并对人脸区域做裁剪后再落盘输出的 JSON 事件流去掉不必要的面部信息审计需要时再回溯原始整场视频。这个处理顺序反过来决定了存储设计事件切片的目录结构按场次年月日加摄像机编号组织才能在一场考试结束后快速定位到指定座位的事件。本文还有配套的精品资源点击获取
返回列表