基于OpenCV与dlib的智能监考系统:从人脸检测到行为告警的完整实现
简介面向计算机相关专业毕业生与项目实战学习者这份基于PythonOpenCV的智能监考系统源码可用作毕业设计、课程设计或期末大作业。项目来自高分毕设评审99分代码完整、可直接运行覆盖人脸检测、识别及监考辅助等核心环节能有效解决选题难、复现难等常见问题。资源包共353个文件以253个Python源码文件为核心配合pyd动态库、exe工具、mp4演示视频、jpg/png样例图片以及dlib人脸关键点模型与数据库文件整体约110.78MB。目前已有123人学习下载适合需要参考高分项目、快速跑通完整系统流程的读者。资源还附带环境配置脚本、依赖模型、运行数据库等辅助内容有助于理解OpenCV与dlib在真实监考场景中的结合方式学习模块划分与调用思路并以此为基础二次开发和拓展。1. 智能监考系统为什么先盯“异常行为”而不是盯“人”毕设里能一眼看出工作量、又能完整体现技术链路的题目不多Python OpenCV 的智能监考系统是其中一个。它把摄像头画面连续解析成人脸数、闭眼状态、头部旋转角度和是否出现第二人并把长时间异常累积成告警事件。这个题目的反直觉点在于它不试图“识别作弊行为本身”而是用可量化的注意力特征去覆盖考场中最高频的作弊动作——转头、低头、长时间不看卷面、离座、交头接耳。我建议用 OpenCV 做画面处理用 dlib 提供 68 点面部关键点让这条链路在普通笔记本上就能接近实时运行适合计算机视觉、图像处理方向的本课毕设也适合想往真实产品方向扩展的课程设计。论文要的是能解释、能演示、能复现智能监考系统恰好满足这三条。2. 方案选型与检测原理为什么先做“人脸存在性”再做视线和姿态智能监考系统看起来是一个模型问题实际是一个多任务融合问题。只靠一个人脸检测器无法判断“这个人是不是在看桌下的手机”更无法区分“低头答题”和“低头偷看”。因此第一件事不是堆模型而是先拆任务人脸在哪、脸朝哪个方向、眼睛有没有闭、画面里是否出现第二张脸。只有把这些基础特征算出来后面的告警规则才有依据。常见的一个误区是直接去找“作弊检测开源模型”。且不说公开数据集很难覆盖真实考场遮挡单是“作弊”这个标签本身就定义不清。毕设项目最怕黑匣子式的黑箱模型评委一问“为什么这样判断”就答不上来。所以我建议把系统拆成可解释的特征提取模块再用规则做时间维度上的累积判断。2.1 检测链路选型Haar、dlib 和 YOLO 怎么分工选择方案时我习惯把检测任务按“对象的尺度”和“需要输出什么”分开看人脸存在性和人脸框用 dlib 的 HOG 人脸检测器比 OpenCV Haar 对轻微侧脸更稳CPU 上也能跑。眼睛区域和头部姿态用 dlib 的 68 点面部关键点模型输出 68 个点能覆盖眼睛、鼻子、嘴巴、轮廓。手机等小目标检测可选 YOLOv8n 或 YOLOv5s导出 ONNX 后用 OpenCV 的 DNN 模块推理作为加分项。抬头低头角用 OpenCV 的solvePnP做 3D-2D 对应点求解不需要训练新模型。检测目标首选方案备选方案选择理由人脸dlib HOG detectorOpenCV Haar cascade多人、轻微侧脸场景漏检少眼睛眼睑关键点dlib 68 点模型MediaPipe Face MeshEAR 需要稳定的眼睑上下点头部姿态solvePnPMediaPipe Face Mesh只需 6 组对应点概念清晰手机/书本YOLOv8n ONNXSSD MobileNet小目标召回率更高导出方便为什么强调“先做人脸存在性”因为监考逻辑全部建立在人脸数量上人脸数为 0可能是离座人脸数大于 1可能是交头接耳人脸数为 1才继续判断闭眼和低头。如果人脸检测器本身不稳定后面所有特征都会跟着抖所以不要把精力一开始就投入到视线追踪上。用 dlib 做检测的命令很小真正影响效果的是参数upsample_num_timesface_detector dlib.get_frontal_face_detector() faces face_detector(gray, 1)参数1表示人脸检测前对图像做一次金字塔上采样能有效提升较远距离、较小人脸的召回代价是单帧检测耗时增加约 10%~20%。现场摄像头离考生常有两三米这个参数填 0 会导致后排人脸经常检测不到这是新手最容易翻车的地方。2.2 注意力判定的三个关键特征人脸数、EAR 和欧拉角我把“注意力”定义成三个可计算特征第一个是画面中的人脸数。dlib 返回的人脸框数量就是交头接耳和离座的判定来源。但要注意人脸检测有漏检所以不能单帧判断要配合时间窗口。第二个是眼睛纵横比 EAREye Aspect Ratio。dlib 68 点中右眼是索引 36 到 41左眼是索引 42 到 47。EAR 的计算公式是上下眼睑距离之和除以眼睑宽度def eye_aspect_ratio(eye): # eye 是 6 个关键点按 dlib 坐标顺序传入 A np.linalg.norm(eye[1] - eye[5]) B np.linalg.norm(eye[2] - eye[4]) C np.linalg.norm(eye[0] - eye[3]) return (A B) / (2.0 * C)EAR 的物理含义很好解释人睁眼时上下眼睑距离大EAR 在 0.3 左右闭眼时上下眼睑距离趋近于 0EAR 会掉到 0.15 以下。常见的默认阈值是 0.22但这是正脸、正常光照下的经验值。戴眼镜、低头、侧脸都会让 EAR 变小所以更可靠的做法是先录 30 秒正常状态取正常 EAR 的均值再向下偏移 0.03 到 0.05作为当前场景阈值。第三个是头部姿态欧拉角。用 6 组 3D 人脸模型点和对应图像点做cv2.solvePnP得到旋转向量再转换成 pitch低头、yaw左右转头、roll歪头三个角度。对监考场景来说最有价值的是低头角度因为很多作弊动作都伴随头部明显下压。特征来源默认阈值判定含义EAR68 点中眼睑点计算0.22低于阈值视为闭眼pitchsolvePnP 输出30 度低头幅度过大yawsolvePnP 输出30 度左右转头幅度过大人脸数dlib detector大于 1出现第二人为什么不直接做真正的视线追踪因为严格的视线追踪需要相机标定、屏幕坐标和眼球模型现场监控摄像头的视角、距离一变结果就不可复现。用“头部朝向 眼部闭合”作为注意力的代理特征虽然不是百分之百精确但足够支撑毕设答辩也方便扩展。2.3 异常事件的定义与置信度别用单帧下结论我在第一次实现时犯过这样一个错误某一帧 EAR 小于阈值就立刻弹窗告警。结果是系统因为考生正常眨眼而刷屏。后来我把告警规则改成“事件必须持续一段时间才触发”误报立刻降下来。监考事件不是单帧二分类问题而是时间序列上的持续状态判断。我一般会定义四类事件人脸消失持续 2 秒以上判定为离座。人脸数大于 1 持续 1 秒以上判定为交头接耳或他人靠近。双眼 EAR 持续低于阈值 2 秒以上判定为长时间闭眼。低头或转头角度持续超过阈值 3 秒以上判定为疑似非正常观看。持续时间的设定取决于你想要“敏感”还是“保守”。敏感模式适合演示动作出现 1 秒就提示保守模式适合答辩时展示低误报阈值调到 2 到 3 秒。这里的核心思想是置信度而不是单帧条件。后面第 4 章会给出实际代码用计数器代替复杂的滑窗既能解释原理又不会把代码写到让人看不完。3. 把系统跑起来环境搭建与核心模块实现章节目标只有一个让你在本地把最小可运行版本跑通。不要一开始就追求完整功能先实现“读摄像头 → 检测人脸 → 算 EAR 和头部姿态 → 打印结果”确认每一步没问题再往上加告警和日志。这个顺序能帮你迅速定位问题而不是等到所有代码写完再debug。3.1 环境依赖与摄像头采集封装依赖以轻量为准。需求文件里至少包含下面几样opencv-python dlib numpy如果系统没有安装 dlibWindows 下建议直接pip install dlib新版通常有预编译 wheel。如果编译报错说明缺少 C/C 构建工具优先检查 Visual Studio Build Tools 是否安装而不是强行换 Python 版本。项目目录建议这样组织smart_proctor/ ├── config.py ├── detector.py ├── monitor.py ├── logger.py ├── main.py └── shape_predictor_68_face_landmarks.dat配置项单独放一个文件方便答辩时快速改阈值。我的习惯是把所有“可调参数”都放进 config.py而不是散落在业务代码里# config.py CAMERA_ID 0 FRAME_WIDTH 640 FRAME_HEIGHT 480 # 注意力阈值 CLOSED_THRESH 0.22 HEAD_POSE_THRESH 30.0 WINDOW_SECONDS 3.0 MULTI_FACE_SECONDS 1.0 NO_FACE_SECONDS 2.0 ALERT_RATIO 0.6摄像头采集封装成生成器是比较灵活的写法我通常会这样写 main.py 的基础循环import cv2 import config cap cv2.VideoCapture(config.CAMERA_ID) cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.FRAME_WIDTH) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.FRAME_HEIGHT) if not cap.isOpened(): raise RuntimeError(摄像头打开失败请检查设备索引号) while True: ok, frame cap.read() if not ok: break # 后续检测代码放在这里 cv2.imshow(Smart Proctor, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里把摄像头分辨率固定为 640x480 是有意的。很多笔记本摄像头的 1080p 画面看起来清楚但人脸检测时间随分辨率上升监控场景下 640x480 已经足够还能保证 25 帧以上的处理速度。如果你在答辩现场需要用外接摄像头把CAMERA_ID改成 1 或 2 再试几次即可。3.2 人脸检测与 EAR 计算我把人脸检测和关键点定位写进同一个模块返回人脸框和对应的 68 点坐标方便后面统一使用# detector.py import cv2 import dlib import numpy as np face_detector dlib.get_frontal_face_detector() landmark_predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def detect_faces(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) return face_detector(gray, 1) def get_landmarks(frame, face_rect): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) shape landmark_predictor(gray, face_rect) return np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)], dtypenp.float32) def eye_aspect_ratio(eye_points): A np.linalg.norm(eye_points[1] - eye_points[5]) B np.linalg.norm(eye_points[2] - eye_points[4]) C np.linalg.norm(eye_points[0] - eye_points[3]) return (A B) / (2.0 * C)dlib.shape_predictor返回的是各关键点的像素坐标顺序是固定的。我在这里把 68 个点一次性转成 numpy 数组后面取眼睛区域、画轮廓、算头部姿态都基于同一个数据结构不容易出现索引错位。EAR 函数里的eye_points[0]和eye_points[3]分别对应眼睑外侧点和内侧点[1]、[2]是上眼睑点[4]、[5]是下眼睑点。传入时顺序不能乱否则算出的值没有任何意义。建议在 debug 阶段用 OpenCV 把 36 到 47 号点画出来确认点和眼睛位置是对应的再继续往下写告警逻辑。3.3 头部姿态估计solvePnP 与三个欧拉角头部姿态估计的原理是用“已知 3D 人脸模型点”和“当前图像 2D 关键点”求相机外参。3D 模型点不需要真实人脸尺寸只要相对坐标合理即可。我用的是 6 组标准点鼻尖、下巴、左眼外角、右眼外角、嘴左角、嘴右角。def get_head_pose(landmarks, frame_width, frame_height): image_points np.array([ landmarks[30], # 鼻尖 landmarks[8], # 下巴 landmarks[36], # 左眼外角 landmarks[45], # 右眼外角 landmarks[48], # 嘴左角 landmarks[54], # 嘴右角 ], dtypedouble) model_points np.array([ (0.0, 0.0, 0.0), (0.0, -63.6, -12.5), (-43.3, 32.7, -26.0), (43.3, 32.7, -26.0), (-28.4, -63.6, -24.1), (28.4, -63.6, -24.1), ], dtypedouble) camera_matrix np.array([ [frame_width, 0, frame_width / 2], [0, frame_width, frame_height / 2], [0, 0, 1] ], dtypedouble) dist_coeffs np.zeros((4, 1)) _, rvec, _ cv2.solvePnP(model_points, image_points, camera_matrix, dist_coeffs) rmat, _ cv2.Rodrigues(rvec) rmat rmat.T pitch, yaw, roll cv2.RQDecomp3x3(rmat)[0] return pitch, yaw, roll代码里的camera_matrix是近似内参焦距直接取画面宽度主点取画面中心。这样算出的角度不是精确相机坐标系下的角度但作为低头、转头判定足够。这里有两个经验点一是rmat需要转置一次否则角度方向和手头动作会相反二是不同 OpenCV 版本的RQDecomp3x3输出顺序可能有差异实际调试时打印三个角自己左右摇头看哪一路在变化最明显就是 yaw。我在实际项目里会避免只依赖pitch 阈值而是读绝对值后容忍一定方向误差。例如低头时 pitch 是负值抬头时是正值但正常写题的轻微低头也可能到 20 度所以阈值不能设太低。4. 判定逻辑与考场告警如何把特征变成“作弊”事件特征算出来后真正决定系统体验的是告警逻辑。我的建议是先确认每帧特征是否稳定再写规则。一个常见错误是直接在 while 循环里堆 if 分支最后代码混乱、阈值难调。这里我把逻辑分成两层一层负责把单帧特征打包另一层负责按时间累计出事件。4.1 把单帧特征汇总成时间窗口时间窗口的朴素实现是存一个最近 N 帧的特征历史但更工程化的做法是用计数器。计数器在逻辑上等价于“连续满足条件多久”实现起来更直观参数也更好解释。我用一个统一函数处理所有事件的累计# monitor.py import time class EventCounter: def __init__(self, fps): self.fps fps self.counters {} def tick(self, event_name, condition, trigger_seconds): key event_name if key not in self.counters: self.counters[key] 0 if condition: self.counters[key] min(self.counters[key] 3, int(self.fps * trigger_seconds) 10) else: self.counters[key] max(0, self.counters[key] - 3) if self.counters[key] self.fps * trigger_seconds * config.ALERT_RATIO: self.counters[key] 0 return True return False说三点第一条件满足时计数器加 3 而不是加 1是为了让短暂抖动不易触发告警同时让告警速度更快第二不满足时减 3 而不是清零是为了避免偶发漏检一帧就重启整个计时第三触发条件乘以 0.6 的ALERT_RATIO表示窗口内超过 60% 的帧异常才告警这就是“置信度”的直观实现。在主循环里先把当前帧所有特征算出来face_rects detector.detect_faces(frame) feature { face_count: len(face_rects), ear_avg: 0.0, pitch: 0.0, yaw: 0.0, } if len(face_rects) 1: landmarks detector.get_landmarks(frame, face_rects[0]) left_ear detector.eye_aspect_ratio(landmarks[42:48]) right_ear detector.eye_aspect_ratio(landmarks[36:42]) feature[ear_avg] (left_ear right_ear) / 2.0 pitch, yaw, _ detector.get_head_pose(landmarks, config.FRAME_WIDTH, config.FRAME_HEIGHT) feature[pitch] abs(pitch) feature[yaw] abs(yaw)为什么要单独取abs()因为低头可能是正偏差也可能是负偏差取决于相机的安装高度。取绝对值后阈值设定更通用答辩时解释也更简单“我们只关注角度偏离不关注偏向哪一侧”。4.2 基于连续帧投票的告警规则基于前面打包好的 feature事件定义就非常清晰counter EventCounter(fps30) alert_no_face counter.tick( no_face, feature[face_count] 0, config.NO_FACE_SECONDS ) alert_multi counter.tick( multi_face, feature[face_count] 1, config.MULTI_FACE_SECONDS ) alert_closed counter.tick( closed_eye, feature[face_count] 1 and feature[ear_avg] config.CLOSED_THRESH, config.WINDOW_SECONDS ) alert_head counter.tick( head_down, feature[face_count] 1 and max(feature[pitch], feature[yaw]) config.HEAD_POSE_THRESH, config.WINDOW_SECONDS )这套规则的好处是每个事件对应一个函数调用参数都是可读的英文名答辩时你可以直接对着这四行讲“系统是怎么判断离座、交头接耳、闭眼和低头”的。max(pitch, yaw)表示只要低头或转头任意一个方向偏离超过阈值就认为头部姿态异常而不是要求两个角度都超。如果你想把系统调得更容易在演示时出效果可以把WINDOW_SECONDS从 3.0 改成 1.5这样低头动作出现 1 秒半就会告警。但要提醒一句调低之后正常整理卷子等动作也可能触发误报至少要留 1 秒以上的持续时间否则系统看起来像是在乱报。4.3 告警联动截图、CSV 日志和声音提醒告警触发后只弹窗是不够的毕设项目至少要有“留证”能力。我一般会做两个动作保存当前帧截图并把告警信息写入 CSV 日志。这样评委看演示时能直接看到历史告警列表而不是只看到实时画面。# logger.py import csv import time import cv2 import os def save_alert(frame, event_name): os.makedirs(alerts, exist_okTrue) ts time.strftime(%Y%m%d_%H%M%S) path falerts/{ts}_{event_name}.jpg cv2.imwrite(path, frame) with open(exam_alert.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([time.strftime(%Y-%m-%d %H:%M:%S), event_name, len(frame)])这里唯一的“坑”是 Windows 下打开 CSV 时如果没加encodingutf-8中文事件名会乱码。encodingutf-8写在日志模块里比在 Excel 里反复转换编码要省事得多。声音提醒可以用系统蜂鸣Windows 下用winsoundimport sys import winsound def beep(): if sys.platform win32: winsound.Beep(880, 300)如果你在 Linux 或 macOS 上做答辩演示winsound会报错所以写成sys.platform win32判断一下避免现场翻车。完整告警时只beep()一次就好了不要每帧都响原因看下一章。5. 智能监考系统避坑指南最常见的 5 个刁钻问题这里写的每一条都是实际调系统时容易卡住的地方按“现象 → 原因 → 解决”的格式记录。你至少会遇到其中 3 条。5.1 戴眼镜和刘海让 EAR 频繁误报现象有同学戴着黑框眼镜坐在镜头前什么都没做系统每隔几秒就弹一次“长时间闭眼”。还有留长刘海的考生低头时眼睛被遮住也触发闭眼告警。原因EAR 依赖眼睑上下 6 个点的距离。黑框眼镜会降低关键点检测精度让上眼睑点被外推到镜框边缘刘海遮住眼睛时关键点直接被拉到头发上EAR 值骤减。解决先在灰度图上做一次对比度增强保留关键点的视觉纹理gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray)然后把左右眼 EAR 取平均而不是要求“左眼和右眼都低于阈值”。单眼被遮挡时另一只眼还是正常值均值不会瞬间掉到阈值以下。最后一定用本机摄像头先录 30 秒正常状态算一个本地阈值不要直接用网上常见的 0.22。5.2 侧脸和大幅度抬头时关键点结构性丢失现象考生低头写题时画面里人脸框断断续续系统把人脸消失误判成“离座”直接告警。或者考生抬头看屏幕时68 点中的下巴点已经超出图像区域头部姿态角抖得很厉害。原因dlib HOG 人脸检测器在正脸和大角度侧脸之间召回率波动很大。68 点模型对超出训练分布的大角度姿态会“外推”外推出的坐标不是真实位置。解决给系统加一个“上一帧位置优先”的检测策略。如果当前帧检测不到新的人脸就在上一个人脸框周围扩大 20% 的区域重新找一次并且记录持续丢失的帧数不要让告警逻辑立刻认为人脸消失了。如果你觉得代码写起来太绕可以引入 OpenCV 内置跟踪器tracker cv2.TrackerKCF_create() tracker.init(frame, face_rects[0]) ok, box tracker.update(frame)跟踪器的作用不是支持长时间跟踪而是在两帧检测之间维持人脸框的连续性。当检测器连续多帧丢帧时用跟踪结果顶替避免离座误报。真正离座之后跟踪器也会在几十帧内逐渐丢失所以告警逻辑仍然要等到“连续无脸超过 2 秒”才触发。5.3 逆光和暗光场景下检测概率呈现玄学现象靠窗的座位逆光画面白茫茫或者考场灯没开全人脸黑成一片。同一个 dlib 模型换一个光线位置检测结果可能从 100 帧全中变成一半丢帧完全没有任何规律可循像玄学一样。原因光线问题本质上是图像纹理丢失。人脸检测依赖局部梯度特征过曝或欠曝会让皮肤区域几乎没有梯度HOG 描述子失效。解决在 feeding 之前加 CLAHE限制对比度自适应直方图均衡化clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))clipLimit控制在 2.0 到 3.0 之间太小等于没做太大会放大噪声。建议保留这个模块并把它作为“预处理参数”写进毕业论文里是很受评委认可的工程细节。5.4 CPU 占用高、画面掉帧到个位数现象用笔记本自带摄像头测分辨率 1280x720人脸检测加 68 点加 solvePnP 全跑下来fps 只有 8 到 10把打印日志加上后直接卡顿到 3 帧。原因dlib 的人脸检测器和关键点模型都是 CPU 密集计算每帧全图检测会让 CPU 跑满。很多人没有控制摄像头分辨率或者没有合理分配检测频率。解决第一把采集分辨率统一降到 640x480这是性价比最高的做法第二每两帧只做一次完整检测中间一帧用上一帧结果第三如果画面里人数不超过 3 人不需要每帧都对全图重新检测可以用跟踪器维持人脸框。if frame_id % 2 0: face_rects detector.detect_faces(frame) else: face_rects last_face_rects这个改动逻辑简单不容易引入 bug却能明显降低 CPU 占用。答辩时你还可以补一句“我们为了实时性使用了隔帧检测与跟踪补帧的策略”这就是一个加分表述。5.5 多人同时入镜产生“交头接耳”重复告警现象监考老师从摄像机前走过系统开始连续弹“交头接耳”1 秒内弹了 10 次考生旁边有人交卷路过日志里瞬间多出几十条记录。原因告警触发后没有冷却机制。只要当前帧仍满足多人脸条件条件每秒被判定多次事件就会不断重新触发。系统没有区分“同一事件的持续状态”和“新发生的事件”。解决给每个事件增加冷却时间。告警触发后在设定的冷却时间内不再重复触发last_alert_time {} def need_cooldown(event_name, cooldown_seconds5.0): now time.time() if now - last_alert_time.get(event_name, 0) cooldown_seconds: return False last_alert_time[event_name] now return True对于“交头接耳”事件冷却 5 秒比较合适既有留存证据又不会刷屏。对“离座”事件我通常也设置 5 秒冷却因为离座告警的本身目的是提醒监考老师而不是记录每一次瞬间离开。6. 让毕设从“能跑”到“能答辩”验证方法、演示脚本和两个进阶方向最后这一步是最容易拉开分数的地方。很多同学习惯把摄像头打开对着镜头比划两下就算演示完了但评委更看重的是“你怎么证明系统有效”。最可行的方法是准备一个固定回放视频用代码控制和复现检测过程。6.1 用录制视频建可复现测试集把摄像头输入改成视频文件只需要一行改动cap cv2.VideoCapture(exam_sim.avi)这样答辩时不受现场光线和设备影响。我习惯录三段素材正常答题 60 秒、低头看桌下 30 秒、离座 30 秒。跑完之后统计每一段的告警次数。正常段应接近 0 次告警低头段应至少有 3 次告警离座段应至少有 1 次。把这些数字写进论文的实验部分比贴十张检测图更有说服力。6.2 面向评委的演示脚本演示不要直接进主界面先给一条清晰的操作路径第一步运行程序展示摄像头实时画面框出人脸并显示 EAR 和头部角度数值。第二步请一位同学正常坐在镜头前系统保持绿色状态。第三步让同学低头保持 3 秒系统弹出手写“低头告警”并出现截图文件。第四步打开日志 CSV显示刚才的告警记录和截图文件名。这四步全程不到 2 分钟却覆盖了采集、特征提取、规则判断、存储留证四个环节。答辩时不用解释全部代码重点讲 EAR 的公式含义、solvePnP 的输入输出、ALERT_RATIO 是什么基本就能把技术逻辑讲完。6.3 可选进阶优化YOLO 手机检测与行为热力图如果评委员追问“为什么不管手机”那就说明你还有扩展空间。我建议增加一条可选模块用 YOLOv8n 检测手机。导出 ONNX 后用 OpenCV DNN 模块推理每帧只检测画面中的小目标置信度大于 0.4 且类别 ID 为“cell phone”时叠加一条“手机检测”告警。另一个能体现工作量的是“考场行为热力图”把画面划分成座位网格统计每个网格的低头发言次数、离座次数按时间生成热力图。这个功能不需要新模型只需在日志里记录人脸框中心点对应的网格位置评审时非常直观。我现在的习惯是每次改完阈值都用固定的三分钟回放视频跑一遍记录正常段误报数再让 A 同学按要求演两遍违规动作。这个习惯让我在答辩前少翻了很多次车。系统不用做得多花哨关键是每个告警都能讲清“为什么触发、依据什么参数、怎么调阈值”。希望帮到你。本文还有配套的精品资源点击获取