news 2026/10/21 10:14:27

基于OpenCV与dlib的智能监考系统:从人脸检测到行为告警的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于OpenCV与dlib的智能监考系统:从人脸检测到行为告警的完整实现

简介:面向计算机相关专业毕业生与项目实战学习者,这份基于Python+OpenCV的智能监考系统源码,可用作毕业设计、课程设计或期末大作业。项目来自高分毕设,评审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_times:

face_detector = dlib.get_frontal_face_detector() faces = face_detector(gray, 1)

参数1表示人脸检测前对图像做一次金字塔上采样,能有效提升较远距离、较小人脸的召回,代价是单帧检测耗时增加约 10%~20%。现场摄像头离考生常有两三米,这个参数填 0 会导致后排人脸经常检测不到,这是新手最容易翻车的地方。

2.2 注意力判定的三个关键特征:人脸数、EAR 和欧拉角

我把“注意力”定义成三个可计算特征:

第一个是画面中的人脸数。dlib 返回的人脸框数量就是交头接耳和离座的判定来源。但要注意人脸检测有漏检,所以不能单帧判断,要配合时间窗口。

第二个是眼睛纵横比 EAR(Eye 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 左右;闭眼时上下眼睑距离趋近于 0,EAR 会掉到 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

如果系统没有安装 dlib,Windows 下建议直接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)], dtype=np.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], # 嘴右角 ], dtype="double") 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), ], dtype="double") camera_matrix = np.array([ [frame_width, 0, frame_width / 2], [0, frame_width, frame_height / 2], [0, 0, 1] ], dtype="double") 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(fps=30) 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_ok=True) ts = time.strftime("%Y%m%d_%H%M%S") path = f"alerts/{ts}_{event_name}.jpg" cv2.imwrite(path, frame) with open("exam_alert.csv", "a", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow([time.strftime("%Y-%m-%d %H:%M:%S"), event_name, len(frame)])

这里唯一的“坑”是 Windows 下打开 CSV 时如果没加encoding="utf-8",中文事件名会乱码。encoding="utf-8"写在日志模块里,比在 Excel 里反复转换编码要省事得多。

声音提醒可以用系统蜂鸣,Windows 下用winsound:

import 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(clipLimit=2.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_seconds=5.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 同学按要求演两遍违规动作。这个习惯让我在答辩前少翻了很多次车。系统不用做得多花哨,关键是每个告警都能讲清“为什么触发、依据什么参数、怎么调阈值”。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 21:02:19

最长公共英文单词序列:从分词陷阱到动态规划实战

1. 从一道字符串题说起&#xff1a;为什么"最长公共英文单词"值得单独拎出来讲很多人第一次看到"最长公共英文单词"这个标题&#xff0c;会下意识觉得这不就是最长公共子序列&#xff08;LCS&#xff09;的变体吗&#xff1f;把字符换成单词&#xff0c;套…

作者头像 李华
网站建设 2026/10/22 5:48:03

微信小程序医院挂号预约系统全流程开发复盘

基于微信小程序的移动医院挂号预约系统&#xff1a;从0到上线的完整实操复盘接到“基于微信小程序的移动医院挂号预约系统”这个项目时&#xff0c;我第一反应是&#xff1a;这不只是做一个“能点按钮的页面”&#xff0c;而是一个涉及用户身份、号源库存、订单状态管理和支付环…

作者头像 李华
网站建设 2026/10/19 13:00:26

PLC温度控制实战:PID参数整定与自整定技巧详解

1. 从一个温度控制项目说起&#xff1a;为什么PID是逃不开的坎去年接了个小项目&#xff0c;给一家食品加工厂做一套烘干房的温度控制系统。需求听起来特别简单&#xff1a;把烘干房温度稳定在85摄氏度&#xff0c;波动不超过正负1度&#xff0c;升温阶段不能超调太多&#xff…

作者头像 李华
网站建设 2026/10/19 10:45:30

手装Tomcat:Java Web底层容器配置与生产部署指南

1. 为什么现在还要亲手装Tomcat&#xff1f;——一个被低估的Java Web基础能力很多人看到“Tomcat下载安装配置”这个标题&#xff0c;第一反应是&#xff1a;“都2024年了&#xff0c;还手装Tomcat&#xff1f;用IDE一键启动不香吗&#xff1f;”我完全理解这种想法——某次给…

作者头像 李华
网站建设 2026/10/19 22:44:37

U盘启动盘制作工具哪个好用?PE系统纯净无捆绑选型指南

U盘启动盘这东西&#xff0c;平时躺在抽屉里吃灰&#xff0c;真到系统崩了、新机装系统、老电脑救援的时候&#xff0c;它就是那根救命稻草。但问题也恰恰出在这里——市面上叫得出名字的PE工具箱少说十几款&#xff0c;有的干净得像白纸&#xff0c;有的装完系统桌面多出七八个…

作者头像 李华