从在线考试、远程答辩到机房监控,考场中的作弊行为检测一直是个“看起来能做、实际一做就翻车”的任务。不少人一开始会觉得,这不就是拿现成的目标检测模型框一下人、框一下手机吗?等真正部署到浏览器页面、接入摄像头实时画面,才发现漏检、误报、延迟、并发抢占这些问题全堆在一起。这篇文章是我基于 YOLOv5/v8/v11/v12 四个版本、配合 Django + Web 技术栈,完整搭建一套考试作弊检测演示系统的实战记录。整个项目代码量不大,但工程链路很长:数据集标注、模型训练、推理服务、前端实时视频流、后端告警日志,每一环都有自己的坑。希望给正在做类似项目或者准备用 YOLO 系列模型做行为检测的同学一些可以直接参考的经验。
1. 系统设计的第一原则:先想清楚“检测”与“判定”的区别
很多初学者上手就把 YOLO 模型当成“作弊识别器”,试图让模型直接输出“正在作弊”四个字。这是个典型的认知偏差。YOLO 这类目标检测模型做的事非常纯粹:在单帧画面里找到物体边界框,并给出类别概率。它不知道一个人是否在抄小抄、是否在偷看邻座——它只知道“这个框里是一个人”“那个框里是一部手机”。
所以我在搭建这个系统时,第一件事不是选模型,而是把需求拆成了两层:底层用 YOLO 做目标检测,上层用自己的业务规则做行为判定。这样既能把模型的泛化能力发挥到最大,又能在不重新训练模型的情况下调整判定灵敏度。
1.1 考试场景中到底要抓哪些行为
在线监考和机房考试的典型作弊行为其实可以归结为几类:低头看手机、频繁侧头张望、长时间离开监控区域、与他人交头接耳、身体姿态异常(比如侧身遮挡屏幕)。这些行为在单帧画面上呈现出来的视觉特征是不同的:
- 低头看手机:人脸俯角明显,手部靠近桌下或屏幕下方;
- 频繁张望:头部朝向偏转,且偏转状态持续时间短、切换频繁;
- 交头接耳:两个人脸框距离接近,且头部朝向彼此;
- 离开座位:人形检测框在连续多帧内消失或位移超过阈值;
- 打哈欠:嘴部张开程度持续扩大,但打哈欠本身不一定是作弊,需要当作软指标。
我的最终方案是定义了几个检测类别:person、phone、head_down、turn_around、hand_raise,其中 head_down 和 turn_around 这种姿态类标签其实是对 YOLO 的检测能力做了“行为近似”——严格来说姿态识别应该用关键点模型,但在演示场景中,用目标检测区分“低头的人”和“正常坐姿的人”反而更直接,训练成本也低得多。
1.2 两级架构:单帧检测 + 时序规则
系统整体分两层,第一层是 YOLO 模型对摄像头画面的每帧进行目标检测,得到若干边界框和类别。第二层是业务判定模块,把最近 N 帧的检测结果放进来做时间窗口分析,只有满足“持续帧数”和“置信度累加”两个条件才判定为一次作弊事件。
以“低头看手机”为例,单帧里模型可能误判,但如果一个人连续几十帧都被检测到低头姿态,那基本可以确认异常。这个时间窗口的设计非常关键:判断窗口太短会导致误报率飙升,太长又会漏掉真实作弊行为。我实际测试下来,在 25 帧/秒的视频流中,设置 15 帧左右的持续检测窗口是比较合理的折中。
这就是我反复强调的:检测不是判定,判定需要时间维度的证据。这套思路在项目初始阶段就确定下来,后面所有训练和 Web 集成都是围绕这个架构展开的,比起临时改逻辑要省心得多。
2. 数据集与标注:最容易被低估的成败因素
这个项目最花时间的不是写 Django 代码,也不是调模型结构,而是整理数据集。考试作弊检测这个任务没有特别通用的大型公开数据集可以直接拿过来用,绝大多数团队都面临“自己录、自己标、自己洗”的局面。模型的上限取决于数据质量,这句话在作弊检测场景里尤其成立。
2.1 类别设计与样本分布
我开始只设了 person 和 phone 两个类,训练出来的模型只能检测“有人在”“有手机”,对作弊判定毫无意义。后来引入了姿态近似类别:head_down(低头)、turn_around(侧头/回头)、hand_raise(举手示意,主要针对在线考试需要监考老师确认的场景)。这几个类别加起来大概构成一个自采数据集的基础结构。
样本分布上,head_down 是最难收集的。很多录制素材里根本没有足够的低头样本,当时我把数据量配比调成了 person 占 40%,phone 占 25%,head_down 占 20%,turn_around 占 10%,hand_raise 占 5%。之所以让 person 占比最高,是因为 YOLO 对人员漏检会影响后续所有行为判断,person 类准召率必须优先保证。而 hand_raise 样本最少,因为它本身在作弊判定里属于辅助信号,类不均衡对它的影响可以接受。
2.2 标注工具、格式与清洗流程
标注阶段我用的工具支持导出成多种格式,但我最终统一转成了 YOLO 训练用的 .txt 格式。YOLO 格式每条标注是一行文本:类别编号 + 归一化后的中心点 x、y + 归一化后的宽、高。看似简单,但问题出在坐标理解上:归一化坐标必须除以图片原始宽高,很多人转完格式之后发现锚框全乱,十有八九是这里出了问题。
转完格式之后一定要做可视化校验。我当时写了一个快速脚本,把标注框画回原图上,逐一检查是否有框位错乱、类别标反、图片和标签文件名不匹配的问题。一次检查就发现了大约三百多个低质量标注,包括手抖画出的太小框、把投影仪误标成 phone 的框。这个步骤看着笨,但能给后面省下大量返工时间。
还需要注意一点:视频抽帧时相邻帧高度相似,直接抽帧会导致模型过拟合到具体背景上去。正确的做法是每隔 10 到 15 帧抽一帧,并且从不同时段、不同机位、不同学生姿态下分别采集,保证背景和动作的多样性。
2.3 数据增强策略与自采数据补充
这套系统的数据增强策略分为两块:离线增强和在线增强。离线阶段,我用翻转、随机裁剪、色彩抖动等方式把样本量扩充了大约 1.5 倍。在线增强则依赖 YOLO 训练框架自带的 mosaic、mixup 等策略,每次迭代时动态合成训练图,大幅增加模型对遮挡、复杂背景的鲁棒性。作弊检测场景中手机目标普遍偏小,我把 mosaic 的概率调高了一些,让模型能看到更多“远距离小手机”的例子。
关于数据的基础量级,我多说一句:网上那种“几百张图训练 YOLOv8 就能做到 99% 准确率”的说法,在作弊检测任务上基本不可信。因为行为的视觉表现跨度太大,同一个“低头”,有人是看桌子底下,有人是看手机,视觉差异很大。最终我的自采数据集加公开辅助数据合并后大约是两千多张有效图片,这个体量对于单类别行为识别只能说够用,距离商业化部署还差得远。
3. YOLO 版本横评:v5/v8/v11/v12 在本任务上的实际差距
项目标题里提到了四个 YOLO 版本,其实训练时就做了横向对比。我在这里整理一份“不吹嘘、只说真实差距”的反馈:在同一个自建数据集、相同的训练轮次、相同输入尺寸下,四个模型的表现各有偏向。
3.1 架构变化的简单梳理
YOLOv5 是社区接受度最高的版本,资料最多,部署工具链最成熟。它的 CSPDarknet 骨干网络在检测速度上很有优势,但对小目标和遮挡场景的泛化能力相对一般。YOLOv8 引入了解耦检测头,分类和回归分支分开输出,训练收敛更快,对小目标更友好,这也是很多人从 v5 转向 v8 的直接原因。YOLOv11 在 v8 基础上改进了主干和颈部模块,整体结构更干净,推理速度有提升。YOLOv12 则偏向注意力机制与区域特征建模的方向,理论上对大变形姿态的表现更好,但参数量和显存占用也明显上升。
说白了,每代版本的提升都是针对通用检测数据集的,迁移到“作弊检测”这种特定任务,差异不像论文图表里那么明显,真正要关注的是自己在验证集上的 mAP 和延迟数据。
3.2 同一数据集上的对比结果
以我自己的数据为例,训练配置统一为 640x640 输入、S 尺寸模型、150 轮训练,下面是代表性的实测数据:
| 模型 | 输入尺寸 | mAP50 | 平均推理延迟(GPU) | GPU 显存占用 |
|---|---|---|---|---|
| YOLOv5s | 640 | 0.842 | 约 6ms | 约 3.8GB |
| YOLOv8s | 640 | 0.877 | 约 7ms | 约 4.2GB |
| YOLOv11s | 640 | 0.863 | 约 6.5ms | 约 4.6GB |
| YOLOv12s | 640 | 0.871 | 约 12ms | 约 7.1GB |
可以看出,YOLOv8s 在作弊检测这个小数据集上的 mAP50 最高,YOLOv11s 精度略低但延迟更小,YOLOv12s 精度与 v8 接近,但推理延迟几乎翻倍。对于需要实时处理和频繁切换摄像头画面的 Web 演示系统而言,稳定性和低延迟比零点几个百分点的精度更值钱。
3.3 多版本切换的设计思路
我没有把模型选择写死在代码里,而是在 Django 后台的配置文件中维护了一个模型注册表:模型名称、权重路径、模型尺寸、类别映射表这几个字段。切换到另一个版本只需要改配置,不需要改代码逻辑。这个设计在演示现场非常实用——万一某个模型在某台机器上加载出现问题,可以秒切备用模型而不中断演示流程。
模型加载时还有个容易踩坑的点:YOLO 版本不同,类名列表的顺序也得对应上。我一开始直接加载 v8 的权重到 v5 的推理脚本里,类别编号全乱套,检测结果乱七八糟,排查了很久才发现是类别映射和模型结构不匹配。后来我把每个模型配套的 yaml 配置文件和权重放在同一个目录下,加载时严格读取配套配置,这个问题再没出现过。
4. 训练调参:怎样把模型从“能跑”调成“能用”
训练完的模型在验证集上 mAP 很好看,不代表接进系统里就能正常用。我看过太多项目在训练阶段跑通之后,直接部署到 Web 服务里,结果误报率高得离谱。原因很统一:训练阶段没有围绕“真实使用场景”来调参。
4.1 超参数配置与断点续训
我训练的默认设置是 150 轮,batch size 32,SGD 优化器,初始学习率 0.01。开局前 3 轮用 warmup 预热,正常情况下模型在第 80 轮左右收敛到较高水平,后面几十轮主要是在微调边界框的回归精度。
训练时必须开断点续训机制。这个项目的数据集训练时间长,中途断电、显存溢出、服务器重启都可能发生。我每次训练都在命令行里保留上次的权重作为起点,并且把最佳模型与最后一轮权重分开存储。别小看这个细节,演示前夜训练中断导致第二天没有可用模型,是真实发生过的教训。
4.2 误报率控制:阈值、NMS 与类别后处理
即使训练完成,“能用”和“能跑”之间仍隔着误报率控制这道坎。YOLO 模型输出的是每个目标的置信度,系统需要设定一个阈值来决定哪些框进入后续判定。阈值调低了,手机、低头等目标漏检少但误报多;阈值调高了,误报少但关键行为可能漏掉。我实际把检测置信度阈值设为 0.45,行为判定的置信度阈值则提高到了 0.65,通过两级阈值把单帧误报压下来。
NMS 参数也是重点。默认 NMS 阈值在某些版本里对密集人群的检测框抑制不理想,同一部手机可能会输出好几个重叠框,随后被判定模块当成多个目标,导致作弊事件重复计数。我实测把 NMS 的 IoU 阈值调到了 0.55,重叠框明显减少。
另外,类别后处理规则要写在业务层而不是模型层。例如“低头”这个类别只对 person 框内区域生效,如果模型在一个空墙角的纹理上误检出一个 head_down 框,业务层会因为该框没有与任何 person 框重叠而直接丢弃。这个规则简单但极其有效,能砍掉大半环境类误报。
4.3 模型导出:从 PyTorch 到 ONNX 与量化部署
训练得到的 PyTorch 权重体积大、加载速度一般,直接放在 Django 推理服务里运行体验很差。我做了一次部署优化:把最佳权重导出为 ONNX 格式,再用 ONNX Runtime 进行推理。实测导出后的推理速度比直接在 PyTorch 环境下快了不少,而且不需要在推理机器上安装完整的深度学习训练环境,部署依赖大幅简化。
导出时容易忽略的坑是动态轴设置。ONNX 默认固定输入尺寸,如果摄像头采集比例不是 640x640,需要先裁切或填充再送入模型。在导出配置里把输入输出的 batch 维度和图像尺寸维度设为动态轴,可以避免很多运行时尺寸不匹配的错误。
如果还要更极致的推理速度,可以进一步做 FP16 量化。演示系统对精度损失容忍度不高,我最终没有在默认环境开启量化,而是保留了一个可切换的量化配置。在普通 CPU 部署场景下,量化后的模型速度提升很明显,代价是 person 类的小目标召回率下降约两个百分点,可以根据硬件条件自行权衡。
5. Django 集成实录:视频流、推理线程与证据留存
模型和训练只是这个项目的一半流程,另一半是 Web 系统集成。Django 本身是一个成熟的后端框架,但直接用同步方式把 YOLO 塞进视图函数里是行不通的——摄像头画面需要实时推送、推理不能阻塞请求响应、多个考生位需要同时监控。这套集成方案里有很多设计值得记录。
5.1 OpenCV 抓帧与视频流推送方案
摄像头画面的接入我用的 OpenCV 视频捕获,分别打开摄像头索引或读取视频文件。在网页端实时显示检测结果,比较简单的方案是后端把标注好的帧编码成 JPEG,通过 HTTP 流式响应不断推给前端。这种方式实现简单,但并发能力弱,多个页面同时打开时前端容易卡顿。
我在演示环境里改成了 WebSocket 推送方案。后端负责推理和标注,把标注后的帧和检测结果打包成二进制消息,通过 WebSocket 推送到前端画布渲染。实测下来这种方案流畅度更高,而且可以把“作弊事件”这类结构化消息和视频帧分开推,前端可以根据事件类型弹出告警卡片,交互体验比纯 MJPEG 流好很多。
5.2 推理服务的并发模型
Django 默认的 WSGI 服务在处理耗时推理请求时表现很差。我的做法是把模型加载和推理单独放到一个后台线程中运行,模型只加载一次常驻内存,Django 视图收到新帧后放入队列,后台线程消费队列执行推理,再把结果写入共享缓存。前端页面按固定帧率读取最新结果。这个生产者-消费者模型规避了每个请求都重复加载模型的问题,是 Web 演示系统能实时跑起来的关键。
队列长度要控制好。如果摄像头帧率高于模型推理速度,队列会越来越长,画面延迟越来越大。我设置队列最大长度是 8,超过即丢弃最旧的帧,保证画面始终是最新状态。这个“丢帧保实时”的策略在实践中效果很好,用户感知到的延迟反而降下来了。
5.3 作弊日志、截图留证与告警页面
考试作弊检测系统不只是要在监控画面里画框,还必须有证据留存能力。我实现了三级记录:嫌疑日志、告警截图、统计报表。
嫌疑日志记录了每个行为事件的时间点、摄像头通道号、行为类别、置信度。当系统判定疑似作弊时,后端会把当前帧保存为截图文件,并在数据库里建立事件记录。这里有个细节:截图必须保存判定瞬时的那一帧原始画面,如果截的是已标注过的画面,后续审阅时框和原图已经叠加,不利于证据核验。所以我分别保存了原始帧和标注帧两份。
告警页面用表格展示当天所有嫌疑事件,支持按类别筛选。为了演示效果更直观,我还加了实时告警音效和浏览器通知,但实际严肃场景中这些功能需要注意策略,避免误报造成打扰。
6. 演示运行与实测效果:完整启动流程和踩坑记录
讲完训练和集成,最后这一部分记录的是这个系统跑起来之后的环境准备、启动流程和可能出现的问题。很多项目在开发人员手里运行没问题,一到演示阶段就原形毕露,原因往往不是核心算法,而是依赖环境、模型文件和视频源这些小问题。
6.1 快速启动清单
我复盘出一个比较稳妥的启动顺序:
- 检查模型文件:确认权重路径存在,并且多版本模型的类别配置正确;
- 启动数据库:初始化表结构,确认事件记录表和摄像头配置表就绪;
- 配置摄像头通道:离线演示时直接指定视频文件路径,在线演示时确认摄像头索引可用;
- 启动 Django 服务:先起 Djangoo 开发服务或生产模式服务,再打开 WebSocket 服务;
- 打开浏览器访问监控页:确认视频流是否推送、检测框是否正常标注、页面帧率是否稳定。
6.2 演示现场的观察要点与数据
在演示阶段,系统性观察几个指标比看单帧效果更有参考价值。我自己的记录是:在 GPU 环境下,当前画面推理延迟约 15ms,端到端页面延迟约 200ms 到 300ms;在纯 CPU 环境下,推理速度下降到约 80ms 每帧,画面勉强能达到 20 帧每秒,但仍能维持基本检测流程不卡死。
误报率是整个演示环节最需要盯紧的数字。我用一个专门的统计页面实时显示“每小时误报数”和“每小时有效告警数”。对于演示来说,宁可漏报少数边界行为,也要把明显误报控制住。因为现场观众对误报的感知比漏报强烈得多。
6.3 最容易翻车的细节复盘
回看整个项目的演示工程,有几个细节影响巨大且容易被忽略:
- 摄像头权限:浏览器获取摄像头权限时,HTTP 非安全上下文会被拒绝访问。演示环境必须用本地 HTTPS 或合理处理权限流程;
- 硬件加速:WebSocket 推送的帧率过高时,前端频繁重绘可能导致页面卡死。需要做帧率节流,比如前端固定 25 帧渲染;
- 多路视频源:如果现场需要切多个摄像头通道,直接在原始视频流上切换会产生长时间黑屏,需要做预加载和备用流策略;
- 模型加载的偶发状态:即便权重文件放对了,个别环境下第一次推理仍可能因为显存初始化问题卡顿。建议服务启动后主动跑一次预热推理,确保模型状态就绪。
在实际演示中,很多观众关心的问题是“模型能不能从视频里直接识别出考生在抄袭”。这时候需要把两级架构解释清楚:模型负责找出人和物品,时序规则负责判断是否构成作弊。演示系统真正展示的是这条完整链路,而不是某一张图的检测效果。
最后再说一点个人体会:做完这套系统,我最深刻的感受是,目标检测模型在考试作弊检测这类任务中的角色其实更像一个“感知前哨”,真正决定系统可用性的是数据质量、时序判定逻辑和 Web 集成工程的配合。训练出一套高 mAP 模型只是第一步,把模型稳定、流畅、可解释地嵌入一个真实可用的系统里,才是这类项目真正要解决的问题。如果一开始就明确这个定位,后续每一步选型都会清晰很多。