1. 项目概述:考试场景下的作弊检测,难的不只是“把模型跑通”
先说结论:这个项目做的事情,是把监考摄像头画面里出现的异常行为自动框出来,并且实时在桌面上弹窗提醒。检测对象包括使用手机、传递纸条、交头接耳、起身离座这类考场典型动作。模型层面同时集成了 YOLOv5/v8/v11/v12 四个版本做横向对比,前端用 PyQt5 做了一个带视频预览、检测结果叠加、告警记录的桌面程序,项目里还附带了一套可继续扩充的训练代码和标注好的数据集,拿到手之后能直接复现,也能换数据重新训练。
我最早接触这个需求,是因为有个做教育信息化的朋友提到,他们那边的考场每年要安排大量人力看监控回放,一场考试两天、视频几十路,全靠人眼去盯“第几分钟有人把手机拿出来”这种细节,疲劳度极高,还容易漏。当时我的第一反应就是:这个场景天然适合目标检测来做。考场机位固定、背景相对稳定、作弊行为的目标类别也很明确,比起自然场景里的通用检测,难度其实要低不少。于是就有了这套系统的雏形。
需要提前说明的是,这项目不是那种只有骨架的“论文型”代码,而是能真正跑起来的演示系统。它既能读本地视频文件,也能接 USB 摄像头,检测结果实时叠加到画面里,同时把异常事件写进告警列表和日志。对正在做课程设计、毕业设计,或者想了解目标检测如何落地到具体行业场景的同学来说,这套流程是一个相当完整的参考。
另外,虽然项目名里有“作弊检测”,但它本质上就是一个通用的行为识别框架。把数据集换掉,它可以变成课堂行为分析、实验室安全监控、老人跌倒检测之类的系统。理解到这一层,你拿这套代码能改出来的东西,会比表面上看到的更多。
2. 技术方案与模型选型
2.1 为什么锚定 YOLO 系列
考试作弊检测首先是个实时任务。监考老师需要的不是“事后翻录像”,而是“这个房间现在有没有异常”。这意味着从摄像头取帧到画面标框,整个过程必须是低延迟、连续的。YOLO 系列作为一阶段检测器的代表,在速度和精度之间拿捏得比较均衡,尤其适合这种需要多路视频并发的场景。
对比两阶段检测器(比如早期的 Faster R-CNN),YOLO 把目标定位和分类统一在一个网络里完成,一次前向传播就能输出所有候选框和类别置信度。考试教室这种画面相对规整、目标尺寸差异不算极端的场景里,两阶段的精度优势并不明显,反而会拖慢推理速度。所以我一开始就把两阶段方案排除掉了。
还有一个很实际的考虑:YOLO 的生态太成熟了。官方仓库有预训练权重,社区有大量现成教程,换版本、换骨干网络都很方便。项目里需要同时兼容 v5/v8/v11/v12 四个版本,只有 YOLO 这种迭代路径清晰、接口风格统一的家族,才能做到“同一套数据和流程,改一行配置就切换模型”。这也是项目能做成横向对比演示的原因。
2.2 四个版本怎么选
四个版本在项目里并不是各做各的,而是共用同一份数据集和同一套评测脚本,方便横向对比。实际跑下来,它们的差异很直观:
| 版本 | 核心特点 | 典型场景 | 实测感受 |
|---|---|---|---|
| YOLOv5 | 经典稳定,资料最多 | 低配置机器、新手入门 | 训练速度偏慢但稳,预测速度尚可 |
| YOLOv8 | Anchor-Free,C2f 模块,生态好 | 通用基线模型 | 性价比最高,默认首选 |
| YOLOv11 | C3k2 模块,推理优化 | 追求帧率 | CPU 上推理也能勉强带动 |
| YOLOv12 | 注意力机制改造,精度上限高 | 追求精度、GPU 条件好 | 小目标和遮挡目标表现更好 |
选型逻辑其实很简单:如果只是为了验收功能、演示效果,用 YOLOv8 就够了,它是最不容易出错的选择,官方文档、社区问答、模型权重都是现成的。如果机器显存不高,YOLOv5 的轻量版本(yolov5s)更合适。想把精度往上推,就切到 v11 或者 v12,但要注意它们对算力的要求也同步上升。
我的建议是别一上来就追最新版。先把 v8 基线跑通,确认数据集没问题、流程没断层,再逐个换模型对比。很多新手一上来就把四个模型都训练一遍,结果发现问题出在数据集格式上,白白浪费大量时间。
2.3 系统整体架构设计
整个系统的数据流是这样走的:
视频文件或摄像头输入 → OpenCV 逐帧解码 → YOLO 模型推理 → NMS 后处理 → 检测结果叠加到原始画面 → PyQt5 界面显示 → 异常事件入表与预警
这里面有几个关键的设计决策值得展开说。
第一,视频解码和模型推理绝不能放在 UI 线程里。PyQt5 的界面刷新是有事件循环的,如果把推理放在主线程,每一帧推理期间界面都会“假死”,拖动窗口都费劲。所以项目里单独做了一个工作线程负责读帧和推理,界面线程只负责画和记录。
第二,后处理部分除了常规的 NMS,还加了一层“帧间确认”逻辑。单帧误检在目标检测里太常见了,比如有人低头捡笔,可能连续两三帧被识别成“看手机”。如果不做帧间过滤,告警列表会瞬间被垃圾事件刷爆,系统就失去了实用价值。这个细节后面会单独讲。
第三,检测结果统一封装成结构化数据,同时喂给画面绘制模块和告警记录模块。这样前端展示、日志导出、统计报表都能用同一份数据,复盘的时候直接导出 CSV 就能分析哪个时间段异常最密集。
界面部分用 PyQt5,而不是 Tkinter 或者 OpenCV 自带的 HighGUI,原因也很直接:PyQt5 的控件丰富度、样式可定制性、多线程支持都更适合做这种给人用的工具。信号槽机制在多线程场景下尤其省心,QLabel 放视频画面、QTableWidget 做告警列表、QPushButton 做控制按钮,组合起来非常顺手。
3. 数据集构建与训练细节
3.1 作弊行为类别怎么定
项目的类别定义是我当时反复调整过的部分。一开始我想得很宽,列了七八类行为,包括翻书、看小抄、打手势、交换答题卡等等。标注到一半就发现问题:类目越多,标注工作量成倍增加,而且很多类是相互重叠的——比如“翻书”和“看小抄”在画面里的姿态区别并不大,模型学起来容易混淆。
最终我收敛到四个核心类别,加上一个辅助类:
- phone:手持或低头看手机,这个最直观,样本也最好找
- pass_note:传递纸条、传递物品
- turn_around:大幅度回头、左顾右盼
- leave_seat:起身离开座位
- normal:正常作答姿态(辅助类,用于抑制误检)
这里有个经验:辅助类 normal 千万别省。训练的时候如果没有正常姿态的负样本,模型会把一切“手离桌面”的动作都判成作弊,误报率高到没法用。加一个 normal 类,相当于给了模型一个“安全出口”,效果立竿见影。
3.2 数据采集与标注实操
数据来源分两块。一块是公开的监控视角视频片段,另一块是我们自己拿手机在教室、会议室模拟拍摄的。实际项目里不可能指望网上有现成的“考场作弊数据集”,绝大部分数据都得自己造。
拍摄时的站位很关键,要模拟真实考场的监控视角:斜上方俯视、能看到桌面和上半身。纯平视的角度参考价值低,因为考场摄像头基本都是俯视的。室内光照条件至少要覆盖三种:白天自然光、晚上灯光、逆光。光照对检测精度的影响比很多人想象的大,后面排查精度问题时我还会提到。
标注工具用的是 LabelImg,支持 YOLO 格式的 txt 输出。每张图里所有目标都要标,哪怕被遮挡了一半,只要人眼能辨认就标上。标注的实际工作量比预想大得多,一个目标大概标一帧需要十几秒,动辄几千张图,两个人标了一个多星期。
标注格式是 YOLO 的标准格式,每行一个目标:
class_id x_center y_center width height所有坐标都是相对于图片宽高的归一化小数。举个例子,一张 1280x720 的图里,一个目标中心在像素坐标 (640, 360),宽高是 320x180,那么它对应的一行就是:
0 0.5 0.5 0.25 0.25标注最容易踩的坑是框的边界不准确。框偏大或者偏小 10% 以内模型能忍,但如果大量框把背景都包进去,训练出来的位置精度会明显变差。我的习惯是宁紧勿松,尽量贴住目标边缘。
3.3 数据增强策略
训练配置里开启了三类增强。第一类是位置增强,包括随机平移、旋转、缩放,让模型对目标的位置和尺寸变化更鲁棒。第二类是颜色增强,包括 HSV 通道的亮度、饱和度、色相扰动,重点加强亮度和对比度扰动,因为考场画面在不同时段的光照差异非常大。第三类是 Mosaic 拼接增强,把四张图拼成一张送入模型,相当于变相增大了 batch 内目标数量,对小目标尤其友好。
这里要特别说一下亮度扰动。我在调试的时候发现,白天阳光从窗户照进来的时候,靠窗座位的画面亮到过曝,阴天则偏暗。单一的亮度设定会让模型在这些边缘环境下频繁漏检。把亮度扰动幅度调大之后,漏检率肉眼可见地下降。
增强是把双刃剑。增强太猛,模型会学到噪声;增强太少,模型对场景变化不鲁棒。我的经验值是:翻转概率 0.5,缩放和位移都在 0.2 上下,亮度扰动 ±25%,饱和度 ±25%。这个组合在绝大多数室内监控场景里都适用。
3.4 训练参数与评价指标
训练脚本基于官方仓库修改,主要参数如下:
# 数据配置 path: ./dataset train: images/train val: images/val names: ['phone', 'pass_note', 'turn_around', 'leave_seat', 'normal']训练命令大致是:
yolo detect train data=exam.yaml model=yolov8n.pt epochs=120 batch=16 imgsz=640 device=0几个参数的选择逻辑说一下。输入分辨率 640 是 YOLO 系列的默认值,在桌面级显卡上能跑出不错的帧率,精度也能接受。batch 大小根据显存动态调,16G 显存跑 v8s 的话 batch 16 没问题,显存紧张就降到 8,优先保证不 OOM。epochs 放在 120 左右,配合早停机制,一般到 100 轮以后 mAP 提升已经非常平缓了。
训练用的是 COCO 预训练权重做迁移学习。千万不要从零开始训,目标检测基础特征(边缘、纹理、形状结构)在 COCO 上已经学得很好,考试场景虽然和自然图像差异大,但前几层特征是可以直接复用的。迁移学习后只需要较少的 epoch 就能收敛,训练时间和数据需求都能大幅下降。
评价指标重点看三块:mAP50、mAP50-95 和各类别的单独 AP。mAP50 是判断“框得准不准”的直观指标,mAP50-95 更严格,对框的精确度要求更高。每类的单独 AP 一定要逐个看,因为数据不平衡时,样本少的类别 AP 会被整体指标掩盖。我当时 pass_note 类只有 phone 类样本量的三分之一,第一轮训练完 pass_note 的 AP 比 phone 低了一大截,加样本、做增强之后才拉回来。
4. PyQt5 界面与推理系统实现
4.1 界面布局设计
界面布局遵循“左侧画面、右侧控制”的经典结构。左侧主区域放 QLabel 控件用于渲染视频帧,画面实时显示检测框和类别标签。右侧从上到下依次是:模型版本选择下拉框、置信度阈值滑块、控制按钮组(开始/暂停/停止/导入视频)、告警列表、运行状态栏。
告警列表用 QTableWidget 实现,每一行记录一条异常事件,包含时间戳、类别、置信度、画面缩略图路径。双击某一行可以跳转到对应时间点的画面,这个功能在回放复盘时非常实用,省去了拖动进度条找画面的麻烦。
底部还有一个日志区,用 QTextEdit 做只读输出,记录模型加载、视频打开、推理帧率、异常事件等运行时消息。调试阶段这些信息帮了大忙,比如能一眼看出某个时间段帧率骤降是因为推理耗时变长还是解码跟不上。
状态栏用 QLabel 实时显示当前 FPS、已处理帧数、累计异常事件数。FPS 这个数字很直观地告诉我当前模型版本在用户机器上跑得够不够流畅,是判断模型选型的依据。
4.2 多线程设计与信号槽机制
PyQt5 界面卡顿是这类项目的头号痛点,解决方式就是强制把耗时操作全部挪到工作线程。项目里核心线程结构如下:
class DetectionThread(QThread): frame_ready = pyqtSignal(QImage) # 发送待显示的帧 event_detected = pyqtSignal(dict) # 发送异常事件 fps_updated = pyqtSignal(float) # 发送实时帧率 def __init__(self, model, source, conf_thres): super().__init__() self.model = model self.source = source self.conf_thres = conf_thres self._running = True self._pause = False def stop(self): self._running = False def run(self): cap = cv2.VideoCapture(self.source) while self._running: if self._pause: self.msleep(50) continue ret, frame = cap.read() if not ret: break results = self.model(frame, conf=self.conf_thres) processed = draw_detections(frame, results, box_color=(0, 0, 255), label_color=(255, 255, 255)) qimg = convert_cv_to_qimage(processed) self.frame_ready.emit(qimg) self.fps_updated.emit(calculate_fps()) for det in extract_events(results): self.event_detected.emit(det) cap.release()这个设计的核心是frame_ready、event_detected两个信号。frame_ready连接界面上的 QLabel 更新函数,每来一帧刷新一次画面;event_detected连接告警列表的添加函数,每检测到异常追加一行记录。信号槽本质是跨线程的队列通信,界面线程只需要接收并绘制,完全不会因为推理耗时而被阻塞。
这里要提醒一个比较隐蔽的问题:如果线程里发送信号的频率高于界面刷新的频率,信号会堆积在队列里,界面越来越卡。解决办法是给画面更新加一个节流标记,上一帧还没画完就丢弃当前帧。显示环节丢几帧没关系,检测结果不受影响,界面却会流畅很多。
4.3 告警策略与帧间确认
告警是这套系统能不能用的关键分水岭。最初的版本是“每检测到一帧异常就记录一条”,结果一个学生低头捡了个东西,两三秒内刷出来十几条告警,复盘的时候根本没法看。
后来加了一个帧间确认机制:同一检测区域连续 N 帧(我默认设 3 帧)都出现同一类别异常,才判定为一次有效事件并记录。这里说的“同一区域”用 IoU 判断,当前帧检测框与上一帧同类别检测框的重叠率大于 50% 就视为同一目标。
这个逻辑的物理意义很直观:真实作弊行为通常持续数秒甚至更久,而误检往往是零星一两帧的噪声。用连续帧投票的方式滤掉单帧噪声,信噪比大幅提升。实测下来,告警数量压缩了将近 70%,而真正漏掉的罕见作弊事件几乎没有增加。
声音告警也做了级别区分。phone 和 leave_seat 这类高置信度、高危行为触发高频提示音,turn_around 这类相对轻微的行为只写入告警列表。避免系统一响就没完没了,导致监考人员习惯性忽略告警——这是现实里很多安防系统最后沦为摆设的原因。
5. 常见问题与排查技巧
5.1 环境配置阶段
项目复现最耗时间往往不是代码而是环境。CUDA、PyTorch、OpenCV 三者的版本匹配是第一个大坑。常见的现象是模型推理报“CUDA out of memory”或者干脆检测不到 GPU。排查思路是先确认显卡驱动支持的最高 CUDA 版本,再选对应的 PyTorch 安装命令,最后用torch.cuda.is_available()验证能否调用成功。这里有个非常实用的检查方法:官方仓库提供的测试脚本里跑一张样例图,如果能正常输出检测框,说明环境基本通了。
OpenCV 的问题集中在安装源上。Linux 环境下pip install opencv-python一般没事,但有些预编译版本对系统库有要求,报 libGL 之类的错误时,多半是缺少图形库,安装 opencv-python-headless 可以绕开,因为它不带 GUI 组件。PyQt5 和 OpenCV 显示组件冲突的问题偶尔也会出现,注意引入顺序,或者在转换 QImage 之前把画面的颜色通道从 BGR 转成 RGB——这个小细节漏掉的话,画面会蓝得一塌糊涂。
5.2 训练阶段
样本量不足是最普遍的问题。我实际体会是,每类至少要有 800 到 1000 个目标实例(注意不是图片数),否则那一类的 AP 很难突破 0.8。如果某类样本实在补不上,可以把这个类别的增强概率单独调大,或者用复制粘贴增强(把该类目标剪切到其他背景图上),能一定程度上缓解数据不足的问题。
训练后期 loss 降不下去,优先检查数据集。打开几幅训练图看看标注是不是错位了,再查一下类别名和配置里的 names 是否一一对应。这类低级错误占比非常高。
还有一个很多人忽略的点:验证集和训练集不要有同源视频的相邻帧。如果从同一个视频里抽帧时秩序排布不当,模型会“记住”画面而不是学到泛化特征,验证集指标会虚高,一到真实视频上立刻现原形。正确做法是按视频文件来划分,整个视频只进训练集或只进验证集。
5.3 GUI 运行阶段
界面卡顿的排查原则很简单:什么东西卡,就用过程计时把它找出来。在推理、绘制、信号发送三处各加一个时间统计,看哪个环节耗时异常。90% 的情况是信号发送频率太高,或者绘图线程没有节流。
视频播放结束自动退出、再导入新视频时界面不刷新,是另一个高频问题。原因是视频流结束后工作线程悄悄退了,没有把状态同步给 UI。解决方式是线程run()结束时发一个finished信号,界面收到后把按钮状态复位、画面清空、状态栏置为“待机”。
还有一个很实际的问题:多路视频支持。项目目标不止单路,但现实是同时开两路推理对内存和 GPU 的压力成倍增加,很容易把普通办公电脑拖垮。我的建议是先在单路场景里把稳定性磨好,多路并发放到服务器上做,桌面演示就保持单路流畅。
5.4 精度提升方向
如果训练完之后发现误报漏报仍然偏高,下面几条经验值得试。
第一,调置信度阈值。0.25 是默认值,但对考场场景来说偏低,会产生大量低置信度的误报。实测调到 0.35 到 0.45 之间效果最好,这个区间能把噪声压下来,又不至于放过真实目标。
第二,检查是否缺光照变化的训练样本。我调试的时候发现一个有趣的现象:模型在下午两点阳光直射时误报率明显高于上午。原因是训练样本里下午的光照占比少,后期补了一批逆光和强光样本,改善很明显。所以采集数据时要有意识地覆盖不同时段。
第三,用小目标增强。手机在俯视画面里经常只有几十个像素大小,属于典型的小目标。Mosaic 增强对这类目标有帮助,训练时也可以把 imgsz 从 640 提到 1024 试试,输入分辨率变大对小目标检测的提升非常直接,代价是训练和推理时间都会增加。
第四,对大姿态变化做细粒度分析。单纯的目标检测只能回答“这个区域里有什么类别的目标”,它分不清一个人是“正在阅读”还是“正在翻书作弊”。如果想要更细的行为判断,可以在检测框的基础上再接一层动作分类分支,或者改用时序模型,把连续几帧的目标状态拼起来分析。这就是后面扩展的方向了。
6. 演示系统的完整使用流程
6.1 快速跑通的步骤
整个复现流程按照下面步骤走,不容易出岔子:
- 创建虚拟环境,安装依赖(requirements.txt 里有固定版本号)
- 下载预训练权重放到 weights 目录
- 把数据集解压到 dataset 目录,确认 images/train 和 images/val 结构正确
- 运行训练脚本生成模型权重
- 运行主程序
python main.py,导入视频或打开摄像头开始检测
关于预训练权重,有一点要说明:如果只是演示,直接下载官方在 COCO 上训练的模型就能跑,但检测效果只能算“能出框”,对“手机”的识别尤其弱,因为 COCO 数据集里对小型手持物件标注本来就少。真正想要考试场景的可用效果,必须用自己的数据集做微调。
数据集这块,项目里附带的是我整理的公开来源数据,数量上可以支撑把流程跑通,但真要上线使用,还得按实际情况继续扩充。这就是为什么整个项目特意保留了完整的训练代码和标注工具配置——给你一条能自我进化的路。
6.2 从演示到落地还差什么
如果这个系统要放到真实考场环境里用,还差几件事。一是多路摄像头的并发接入,这需要后端服务而不是桌面程序;二是检测结果与考生身份信息的对接,这需要结合考试报名数据做联动;三是合规性设计,考场信息涉及个人隐私,系统要有完整的权限管理和数据加密方案。
技术层面的提升方向也很明确。当前方案是纯目标检测,只能框出“某人在看手机”,不能判断“这个动作持续了多久”。如果引入行为时序分析,把单个目标的多帧状态串起来,就能生成“在第 14 分钟,A 考生连续看手机 20 秒”这样更有决策价值的告警。这已经超出了演示系统的范围,但方向是清晰的。
项目里还有一个我很喜欢的细节:所有检测结果都可以导出成带时间戳、类别、置信度、目标坐标的 CSV 文件。这意味着除了实时监控,它还能当作一个考场行为分析工具。考试结束后把整场视频喂进去,自动生成全场的异常行为时间线,监考人员按时间线回看,效率比手动翻录像高了一个数量级。这一点在跟教育单位交流时尤其受认可。
我个人在整个开发过程中最大的体会是:这类检测系统的可用性瓶颈,往往不是模型的 mAP 指标,而是告警策略和交互设计。一个 mAP 0.95 但一秒钟刷五条误报的模型,在实际场景里不如一个 mAP 0.85 但稳定安静的系统。所以在打磨模型精度的同时,把同样的精力放到帧间确认、阈值调节和界面反馈上,收益会超出你的预期。
最后再分享一个小技巧:开发阶段别直接用真实考场视频调试,先准备两三段时长五分钟、包含固定作弊动作的模拟视频,作为统一测试集。每次改完代码都跑一遍这组测试集,记录检出行为和告警数量的变化。这样你能快速确认改动是变好还是变坏,而不是在长视频里靠感觉判断。我自己用这个办法,排查问题的效率比一开始高了好几倍。