简介:这是一份面向计算机相关专业毕业生与课程设计学习者的毕业论文文档,围绕基于Python的课堂人脸签到系统展开,针对传统点名效率低、代课替课难监管等问题,给出从需求分析到系统测试的完整实现路线。文档以单个docx文件交付,约1.58MB,按论文规范组织,依次涵盖概述、相关技术介绍、系统调研与可行性分析、系统设计、系统实现、系统测试及结论等章节,可直接用作选题参考与写作模板。技术部分介绍了OpenCV与Dlib在人脸检测识别中的应用、Flask框架配合jinja2模板引擎搭建动态页面,以及RESTful API完成前后端的数据交互;功能模块覆盖选课、人脸识别签到、考勤查询、开放与编辑课程、拍照权限设置、修改学生考勤等。目前已有458人学习下载,适合需要完整毕设思路、模块划分方式与论文框架的读者借鉴。
1. 课堂人脸签到系统到底解决什么问题
课堂点名从纸质签到表换成二维码,代签只是多走两步;换成刷脸,问题会立刻变得具体:后排光线太暗、学生低头看手机、教室只装了一个 720P 摄像头、帧率卡在 15fps,以及“到底把相似度阈值卡在多少才不漏签也不误签”。基于 Python 设计的课堂人脸签到系统,要做的是把摄像头取流、人脸检测、对齐、特征比对、签到状态落库、导出考勤表这几步串成一条能连续跑一整节课的链路,而不是调一次cv2就结束的演示脚本。它适合两类人:正在做课程设计或本科毕设、需要把“系统设计与实现”写出代码量和技术细节的同学;以及已经在用 Python 做视觉项目、需要认真处理误识率、并发写库和部署边界的工程师。接下来按模型选型、最小可跑代码、业务表结构、实测排错和论文材料逐层拆开。
2. 人脸签到链路的选型:检测、对齐与特征比对的参数账
2.1 检测器与识别模型怎么配
课堂场景的第一个约束是人数:一间教室 40 到 120 人,摄像头可能只拍到前几排。检测器要在 1080P 画面上稳定找到 20 到 50 张脸,且单帧耗时不能超过 60ms,否则 15fps 的摄像头都喂不满。常见做法是把“检测”和“识别”拆成两个模型:检测负责框出人脸并给出 5 个关键点,识别负责把对齐后的人脸映射成特征向量,最后用余弦相似度或欧氏距离做 1:N 比对。
Haar 级联在小场景够快,但侧脸和遮挡下误检明显,不建议作为毕设主方案;MTCNN 精度好,CPU 上单帧约 80 到 150ms,适合人数少的签到终端;RetinaFace 或 SCRFD 在 640×640 输入下能到 30fps 以上,配合 ONNX Runtime 在 CPU 上也可用;YOLOv8-face 则是另一种思路,把人脸框当目标检测任务,适合需要同时检测人形和脸的场景。
识别模型上,face_recognition用的是 dlib 的 ResNet-29,输出 128 维特征,优点是 API 极简、论文里引用方便;缺点是 dlib 编译麻烦、1:N 规模大时精度下降。InsightFace 的buffalo_l包输出 512 维 ArcFace 特征,1:N 场景下同阈值下的误识率明显更低,代价是依赖onnxruntime和多一个 5 点对齐步骤。下面这张表是我在选型时会对比的几组搭配。
| 方案 | 检测耗时(CPU,1080P) | 特征维度 | 1:N 表现 | 主要坑 |
|---|---|---|---|---|
| Haar + LBPH | 20ms | 直方图 | 差 | 光照一变就失效 |
| MTCNN + FaceNet | 100ms | 128/512 | 中 | 小脸漏检 |
| dlib HOG + dlib ResNet | 60ms | 128 | 中 | 侧脸阈值难调 |
| SCRFD + ArcFace | 35ms | 512 | 好 | 需要 onnxruntime |
| YOLOv8-face + ArcFace | 25ms(需 GPU) | 512 | 好 | 训练/导出成本高 |
选型结论很直接:毕设演示阶段用face_recognition能把代码量压到最低,但论文里最好写清楚它属于 dlib 的 HOG 检测加 ResNet 特征;如果要在真实教室跑,换 SCRFD + ArcFace,特征维度从 128 提到 512,阈值也要重新标定,不能把tolerance=0.6直接搬过去。
2.2 环境搭建与依赖锁定:Python、OpenCV、dlib 的安装坑
Python 安装教程和 VSCode 配置 Python 环境的文章很多,但人脸签到系统真正的坑在 dlib 和 OpenCV 的版本配合。我一般先建虚拟环境,再锁版本,避免pip install face_recognition时自动拉一个需要现场编译的 dlib。
# 1. 建虚拟环境,Python 建议 3.9 到 3.11 python -m venv .venv source .venv/bin/activate # Windows 是 .venv\Scripts\activate # 2. 先装 numpy 和 opencv,避免依赖顺序导致的 ABI 冲突 pip install "numpy==1.26.4" "opencv-python==4.9.0.80" # 3. dlib 在 Windows 下没有官方 wheel,优先用 conda 或预编译包 # Linux/macOS 可尝试直接编译,需要 cmake 和 g++ pip install cmake pip install "dlib==19.24.2" pip install "face_recognition==1.3.0" # 4. 想避开 dlib 编译,用 InsightFace + onnxruntime pip install insightface onnxruntime参数说明:numpy==1.26.4是很多 OpenCV 4.9 轮子编译时的 ABI 基准,升到 2.x 后部分旧版opencv-python会报_ARRAY_API not found;opencv-python不带 GUI 功能,如果要用cv2.imshow需要装opencv-python而不是opencv-python-headless;dlib 编译失败时看第一行报错,如果是CMake not found就补 cmake,如果是Visual Studio not found就在 Windows 上装 Build Tools 并勾选 C++ 桌面开发。VSCode 里按Ctrl+Shift+P选Python: Select Interpreter,指到.venv/bin/python或.venv\Scripts\python.exe,再在.vscode/settings.json里写"python.analysis.extraPaths": ["./src"],否则跨目录导入会标红但实际能跑。
提示:dlib 编译一次可能超过 10 分钟,建议在自己的机器上编译好后把 wheel 缓存下来,换机器时直接
pip install dlib-19.24.2-*.whl,不要在答辩现场重装。
2.3 最小可跑的人脸注册与比对代码
注册阶段每个学生录 3 到 5 张正脸,分别提取特征后取均值,比单张模板稳。比对时不要把注册特征全部展开成矩阵反复算,直接算欧氏距离取最小即可。
import face_recognition import numpy as np import pickle import os def enroll(student_id, img_paths): """注册:返回该学生的平均特征向量""" encodings = [] for p in img_paths: img = face_recognition.load_image_file(p) # model="hog" 快但小脸漏检;model="cnn" 准但需要 GPU locations = face_recognition.face_locations(img, model="hog") if len(locations) != 1: print(f"{p} 检测到 {len(locations)} 张脸,跳过") continue encodings.append(face_recognition.face_encodings(img, locations)[0]) if not encodings: return None return np.mean(encodings, axis=0) # 多张取均值,降低单张噪声 def load_db(path="templates.pkl"): with open(path, "rb") as f: return pickle.load(f) # {student_id: np.ndarray(128,)} def match(probe_enc, db, tolerance=0.45): """1:N 比对,返回 (student_id, distance)""" ids = list(db.keys()) matrix = np.array([db[i] for i in ids]) dists = np.linalg.norm(matrix - probe_enc, axis=1) idx = int(np.argmin(dists)) best_id, best_dist = ids[idx], float(dists[idx]) return (best_id, best_dist) if best_dist <= tolerance else (None, best_dist) if __name__ == "__main__": db = load_db() probe = face_recognition.face_encodings( face_recognition.load_image_file("probe.jpg") )[0] sid, dist = match(probe, db, tolerance=0.45) print(sid, dist)逻辑说明:face_locations返回的是(top, right, bottom, left)元组列表,如果一张图里检测到多张脸,注册阶段直接跳过,避免把别人的脸算进模板。face_encodings返回的是 128 维向量,np.mean按列求均值后仍是一个 128 维向量,可以直接参与距离计算。np.linalg.norm(matrix - probe_enc, axis=1)是逐行算欧氏距离,矩阵大小是N×128,N 是学生数,几百人以内毫秒级完成。
参数说明:tolerance是判定阈值,face_recognition默认 0.6,那是 1:1 验证场景的值;课堂签到是 1:N,阈值要收到 0.42 到 0.48,具体看下面那张表。model="hog"与model="cnn"的区别在检测器,HOG 在 CPU 上快,CNN 需要 CUDA 才划算。如果换 InsightFace,match里的欧氏距离要改成余弦相似度,阈值方向相反,通常是相似度大于 0.35 到 0.45 判为同一人。
2.4 阈值、误识率与课堂场景的容错设计
阈值不是拍脑袋的。准备一组注册库和一组探测图,让同一个人不同角度、不同光照的图做正样本,让库外的人做负样本,扫一遍阈值,画 FAR/FRR 曲线。下面是我在一间 60 人教室、1080P 摄像头、MTCNN + FaceNet 组合下实测的粗略口径,供调参时参考。
| 阈值 | 误识率 FAR | 拒识率 FRR | 课堂体验 |
|---|---|---|---|
| 0.60 | 明显偏高 | 很低 | 陌生人可能被认成某学生 |
| 0.50 | 中等 | 低 | 侧脸容易签不上 |
| 0.45 | 低 | 中等 | 多数毕设演示的平衡点 |
| 0.40 | 很低 | 偏高 | 需要学生正对摄像头 |
| 0.35 | 接近零 | 高 | 后排基本签不上 |
课堂签到和门禁不一样:门禁误识一次是安全问题,课堂签到误识一次是考勤纠纷,拒识一次是学生举手补签。所以工程上更常见的做法是把阈值设在 0.45 附近,同时对拒识样本走一次“二次确认”——让学生靠近摄像头再拍一帧,或者弹出一个挑战动作,而不是把阈值一路降到 0.55。
注意:同一份代码换摄像头、换镜头焦距后,特征分布会变,阈值必须重新标定。别把实验室调好的 0.45 直接搬到另一间教室。
3. 签到业务层实现:数据库、状态机与课堂日期课程表
3.1 SQLite 表结构与签到状态字段设计
人脸比对只是链路的一半,另一半是“谁、在哪节课、什么时间、以什么状态签到”。表设计如果只留一张attendance,后面统计迟到、请假、补签时会很难受。我一般拆成学生、模板、课程、课次、签到流水五张表,SQLite 足够撑住一个院系的课程设计,并发写库用 WAL 模式。
PRAGMA journal_mode = WAL; -- 允许读的同时有一个写者 PRAGMA busy_timeout = 5000; -- 写锁等待 5 秒,避免直接报 database is locked CREATE TABLE students ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT NOT NULL ); CREATE TABLE face_templates ( student_id TEXT NOT NULL REFERENCES students(student_id), model_name TEXT NOT NULL, -- 例如 dlib_resnet_128 / arcface_512 vector BLOB NOT NULL, -- numpy 数组 tobytes() updated_at TEXT NOT NULL, PRIMARY KEY (student_id, model_name) ); CREATE TABLE courses ( course_id TEXT PRIMARY KEY, course_name TEXT NOT NULL, teacher TEXT NOT NULL, room TEXT ); CREATE TABLE sessions ( -- 一次课 session_id INTEGER PRIMARY KEY AUTOINCREMENT, course_id TEXT NOT NULL REFERENCES courses(course_id), start_time TEXT NOT NULL, -- ISO8601 end_time TEXT NOT NULL, late_after TEXT NOT NULL -- 超过这个时间算迟到 ); CREATE TABLE attendance_logs ( -- 签到结果,一人一课次一条 session_id INTEGER NOT NULL REFERENCES sessions(session_id), student_id TEXT NOT NULL REFERENCES students(student_id), status TEXT NOT NULL, -- present / late / absent / leave confidence REAL, -- 最小距离或余弦相似度 checked_at TEXT NOT NULL, source TEXT NOT NULL, -- face / manual PRIMARY KEY (session_id, student_id) );参数说明:PRAGMA journal_mode = WAL是 SQLite 在“一个写者 + 多个读者”场景下的关键设置,不开 WAL 时摄像头线程一边写日志、导出线程一边读表会直接锁死;busy_timeout设成 5000 表示遇到写锁时等 5 秒再报错,实测比默认的 0 好用很多。face_templates里存BLOB而不是把 JSON 字符串写进 TEXT,是因为numpy.tobytes()和np.frombuffer()的往返没有精度损失,512 维 float32 只要 2KB。attendance_logs用(session_id, student_id)做联合主键,天然防止同一次课重复插入。
3.2 用 FastAPI 暴露签到接口和活体挑战
如果系统只有一台摄像头接在讲台上,可以直接在桌面程序里调match。但如果要做成前后端分离、让助教用手机看签到结果,就把比对封装成一个 HTTP 接口。FastAPI 的好处是上传文件、返回 JSON、自动生成文档都在一个文件里完成。
from fastapi import FastAPI, UploadFile, File, HTTPException from pydantic import BaseModel import numpy as np, face_recognition, pickle, io from PIL import Image app = FastAPI() db = pickle.load(open("templates.pkl", "rb")) class CheckinResp(BaseModel): student_id: str | None distance: float status: str @app.post("/api/checkin", response_model=CheckinResp) async def checkin(session_id: int, file: UploadFile = File(...)): raw = await file.read() img = np.array(Image.open(io.BytesIO(raw)).convert("RGB")) locs = face_recognition.face_locations(img, model="hog") if len(locs) != 1: raise HTTPException(400, f"expect 1 face, got {len(locs)}") enc = face_recognition.face_encodings(img, locs)[0] ids = list(db.keys()) mat = np.array([db[i] for i in ids]) dists = np.linalg.norm(mat - enc, axis=1) idx = int(np.argmin(dists)) best_id, best_dist = ids[idx], float(dists[idx]) if best_dist > 0.45: return CheckinResp(student_id=None, distance=best_dist, status="unknown") return CheckinResp(student_id=best_id, distance=best_dist, status="present")逻辑说明:UploadFile接收前端FormData传来的图片,Image.open读成 RGB 数组后交给face_locations。如果检测到的人脸数不是 1,直接返回 400,因为签到接口一次只处理一个人。距离小于阈值就返回学号,大于阈值返回unknown,让前端提示重拍。真正的写库动作不在这个函数里做,而是交给一个单线程写入队列,避免多个请求同时打 SQLite。
提示:接口层不要直接传原始特征向量给前端,特征是可以被重放攻击的。如果需要更强的防代签,加一步随机动作挑战,例如“请眨眼”或“请向左转头”,由前端录 1 秒视频,后端抽 3 帧比对。
3.3 批量签到与并发写入:队列与事务边界
教室里的摄像头是连续取流的,每 500ms 抽一帧,检测到脸就比对。如果每比对成功一次就INSERT,SQLite 在高频写入下会频繁触发 checkpoint,导出 Excel 时容易卡。更稳的做法是:推理线程只把(session_id, student_id, distance, ts)推进queue.Queue,写库线程每积累 10 条或每 1 秒批量executemany一次。
import queue, threading, sqlite3, time write_q = queue.Queue(maxsize=1000) def writer_loop(db_path: str): conn = sqlite3.connect(db_path, isolation_level=None) conn.execute("PRAGMA busy_timeout=5000") batch = [] while True: try: item = write_q.get(timeout=1.0) batch.append(item) except queue.Empty: if batch: flush(conn, batch); batch.clear() continue if len(batch) >= 10: flush(conn, batch); batch.clear() def flush(conn, batch): conn.execute("BEGIN IMMEDIATE") # 立刻拿写锁,避免和其他写者交错 conn.executemany( "INSERT OR IGNORE INTO attendance_logs" "(session_id, student_id, status, confidence, checked_at, source)" " VALUES (?,?,?,?,?,?)", batch, ) conn.execute("COMMIT") threading.Thread(target=writer_loop, args=("checkin.db",), daemon=True).start()逻辑说明:q.get(timeout=1.0)在队列空时最多等 1 秒,超时后 flush 残留批次,保证下课铃响时数据不丢。BEGIN IMMEDIATE比默认的延迟事务更可控,它在事务一开始就请求写锁,避免两个写线程同时进入后又互相等。INSERT OR IGNORE配合联合主键,同一学生同一次课重复签到不会报错,也不会覆盖第一次的checked_at。
参数说明:maxsize=1000是防止推理线程比写库线程快太多时内存膨胀,满了就丢弃最旧数据或者阻塞推理。如果一台机器上同时跑多个摄像头、多个班级,不要用多进程各自开 SQLite 写,把写入收敛到一个进程,推理可以用multiprocessing开多个 worker,worker 通过Queue把结果发回主进程写库。
3.4 导出签到表到 Excel 与论文里可用的统计口径
导出不是简单SELECT *。考勤表要能直接交给教务,列一般是:学号、姓名、班级、课程、课次时间、签到状态、签到方式、相似度。统计口径要先定义清楚:present是checked_at <= late_after的签到,late是超过late_after但仍在end_time前,absent是整节课没有记录,leave是人工标记的请假。下面这段用 pandas 把三张表拼起来,注意 SQL 里已经把时间比较下推给了数据库,避免把全表拉到内存里再算。
import pandas as pd, sqlite3 conn = sqlite3.connect("checkin.db") sql = """ SELECT s.student_id, s.name, s.class_name, c.course_name, se.start_time, se.late_after, COALESCE(a.status, 'absent') AS status, COALESCE(a.source, '-') AS source, a.confidence, a.checked_at FROM students s CROSS JOIN sessions se JOIN courses c ON c.course_id = se.course_id LEFT JOIN attendance_logs a ON a.student_id = s.student_id AND a.session_id = se.session_id WHERE se.session_id = ? """ df = pd.read_sql(sql, conn, params=(1,)) df["status"] = df.apply( lambda r: "late" if r["status"] == "present" and r["checked_at"] and r["checked_at"] > r["late_after"] else r["status"], axis=1) df.to_excel("attendance_session_1.xlsx", index=False) print(df.groupby("status").size())逻辑说明:CROSS JOIN sessions的作用是让每个学生都出现在每节课里,再用LEFT JOIN补签到记录,没记录的统一显示为absent。COALESCE把 NULL 替换成可读值,避免 Excel 里出现空单元格。时间比较放在 Python 里做是因为late_after的格式可能带时区,实际项目里建议在 SQL 里用datetime()函数比较。
参数说明:params=(1,)绑定session_id,不要用 f-string 拼 SQL。导出前先print(df.groupby("status").size())看一眼比例,如果某节课absent占 80%,先检查是不是阈值太高或者摄像头没开,而不是直接交给老师。论文里如果要放统计图,用 matplotlib 画应到、实到、迟到的柱状图,横轴课次,纵轴人数,图注里写清楚统计口径。
4. 课堂实测里的误报、代签与光照问题怎么排
4.1 阈值调参:从 FAR/FRR 到实际教室
阈值在实验室里调好,搬到教室往往会偏高或偏低。原因有三个:教室顶部灯光造成的高光、投影仪打在脸上的色偏、以及学生坐姿导致的俯视角度。调参时不要只看一张图,按下面的步骤做一轮标定,通常能把误识率压下来同时保持可接受的拒识率。
第一步,准备数据集。每个学生录 5 张注册图,再在教室不同位置、不同时间拍 3 张探测图,库外再找 5 个不注册的人拍 10 张负样本。第二步,写一个扫描脚本,把阈值从 0.30 到 0.60 按 0.01 步长跑一遍,统计每个阈值下的正样本通过率和负样本误通过率。第三步,画曲线,选 FAR 接近 0 且 FRR 还能接受的拐点。第四步,把选定的阈值写进配置文件,不要在代码里写死。
import numpy as np def sweep(probe_pos, probe_neg, db, lows=0.30, highs=0.60, step=0.01): """probe_pos/probe_neg 是已提取的特征列表,db 是注册库""" ids = list(db.keys()) mat = np.array([db[i] for i in ids]) rows = [] for t in np.arange(lows, highs + 1e-9, step): def accept(enc): d = np.linalg.norm(mat - enc, axis=1).min() return d <= t # 正样本:比对结果必须是本人;负样本:任何匹配都算误识 pass_rate = sum(accept(e) for e in probe_pos) / len(probe_pos) far = sum(accept(e) for e in probe_neg) / len(probe_neg) rows.append((round(float(t), 2), pass_rate, far)) return rows逻辑说明:probe_pos是同一个人的不同角度图提取出的特征,probe_neg是库外人员的特征。pass_rate相当于 1 减 FRR,far是误识率。实际跑的时候要把probe_pos和对应学号绑定,accept里还要判断“最近邻是不是本人”,上面为了简洁只展示了距离判断。输出的rows可以直接写进论文的实验表。
参数说明:步长 0.01 足够,再细没有意义,因为特征分布本身有噪声。扫描范围不要只扫 0.40 到 0.50,先放宽到 0.30 到 0.60 看全貌。如果某组数据在 0.40 到 0.50 之间曲线很陡,说明特征区分度不够,换 ArcFace 或增加注册图数量比继续调阈值更有效。
4.2 常见报错与排查
课堂现场最怕临场报错,下面这几个是我遇到频率最高的,按现象、原因、处理列出来,答辩前逐条过一遍。
| 现象 | 可能原因 | 处理 |
|---|---|---|
dlib安装到一半报 CMake 错误 | 缺 cmake 或 C++ 编译器 | 装 cmake 和 Build Tools,或改用 conda 装 dlib |
cv2.imshow报not implemented | 装的是 headless 版 | pip uninstall opencv-python-headless后重装opencv-python |
cv2.VideoCapture(0)读到全是黑帧 | 摄像头被其他程序占用或索引不对 | 换1、2试,关掉会议软件,Linux 下ls /dev/video* |
database is locked | 多个连接同时写 SQLite | 开 WAL,设busy_timeout,写入收敛到单线程 |
识别结果全是unknown | 注册特征用了 RGB,探测用了 BGR | 统一用face_recognition.load_image_file或统一cv2.cvtColor |
| 同一张脸距离忽大忽小 | 没有做对齐,或检测框抖动 | 用 5 点对齐,检测框外扩 10 像素再裁剪 |
| CPU 占用 400% 但帧率只有 5 | 每个进程加载了独立模型 | 推理放一个进程,多摄像头队列分发 |
numpy报_ARRAY_API not found | numpy 2.x 与旧版 opencv 不兼容 | 降到 1.26.x |
最后一条特别值得说:很多“人脸识别突然变慢”的问题不是模型问题,而是cv2.VideoCapture的缓冲。默认情况下 OpenCV 会缓存几帧,处理慢的时候读到的是旧画面,看起来像延迟。可以在读帧循环里每轮grab()两次再retrieve(),或者把分辨率从 1080P 降到 720P,帧率立刻上来。
注意:答辩现场的插座、USB 延长线、投影仪 HDMI 都可能干扰摄像头。提前用
v4l2-ctl --list-devices(Linux)或设备管理器确认摄像头工作正常,不要等到演示才开始调。
4.3 防代签的几种工程手段
课堂签到的作弊成本很低:举手机照片、戴别人的口罩、找长得像的同学。纯 1:N 比对挡不住照片攻击,因为打印照片也能提取出人脸特征。下面几种手段可以叠加使用,按实现成本从低到高排列。
| 手段 | 实现方式 | 挡住什么 | 代价 |
|---|---|---|---|
| 双帧比对 | 连续两帧特征距离小于阈值 | 静态照片 | 几乎为零 |
| 随机动作挑战 | 弹“眨眼/转头”,抽 3 帧看关键点变化 | 照片、视频重放 | 需要关键点模型 |
| 深度摄像头 | 用 RGB-D 相机的深度通道判断立体 | 照片 | 硬件成本 |
| 签到时间窗 | 开始后 5 分钟内才有效,过期要补签 | 帮别人远程签 | 业务规则 |
| 位置校验 | 手机端签到时校验蓝牙或二维码 | 远程代签 | 需要移动端 |
| 人工复核 | 对confidence接近阈值的记录标黄 | 边界误识 | 助教工作量 |
实际落地的组合通常是:双帧比对 + 随机动作挑战 + 签到时间窗。双帧比对负责挡静态照片,动作挑战负责挡视频重放,时间窗负责把“课后帮签”变成需要人工介入的补签。代码上,双帧比对就是在match之前加一个 3 帧滑动窗口,要求连续两帧的student_id相同且距离差小于 0.05;动作挑战则需要dlib的 68 点或 MediaPipe FaceMesh,计算眼睛纵横比或鼻尖偏移。
有一个容易被忽略的点:注册模板的更新。学生换发型、戴眼镜后,半年前的特征可能匹配不上。可以在每次成功签到且distance < 0.35时,把当前特征以 0.1 的权重混入模板,让模板缓慢适应。但混入前必须确认这次签到不是误识,所以只在高置信度下做,避免模板被污染。
5. 论文与代码同步收尾:把系统设计与实现写成可答辩的材料
5.1 论文结构与人脸签到模块的对应关系
“基于 Python 的课堂人脸签到系统设计与实现”这个题目,论文里最容易写空的是“设计与实现”那一章。一个可操作的做法是让论文的每一节都能指到具体的文件和函数:需求分析对应签到流程和角色用例;总体设计对应模块划分图,至少画清楚采集、检测、识别、业务、存储五层;详细设计对应enroll、match、writer_loop、export四个核心函数,每个函数放一段伪代码或真实代码,配参数说明;数据库设计对应第 3 章那几张表,把 ER 图和建表 SQL 放一起;测试章对应阈值扫描表和 FAR/FRR 曲线,不要只写“测试通过”。
5.2 实验数据表与截图规范
实验部分建议放三张表加两组图。表一是不同模型的检测耗时和内存占用,用同一段视频跑 ten times 取平均;表二是阈值从 0.30 到 0.60 的 FAR/FRR 对照;表三是某次真实课堂的考勤结果,应到、实到、迟到、缺勤的人数。两组图分别是 ROC 曲线和签到界面截图。截图要打码或虚构学号,不要用真实学生信息。所有时间戳统一 ISO8601,分辨率统一写清楚,否则复现不了。
5.3 答辩现场一次跑通的打包命令
演示前不要手忙脚乱地装依赖。把虚拟环境、模板文件、数据库、模型权重放在同一个目录,写一个setup_check.py做自检:读一次摄像头、加载一次模板、往临时数据库插一条再删掉、调用一次比对。演示当天先跑自检,再跑主程序。
# 打包前确认版本 pip freeze > requirements.txt # 自检:摄像头、模板、数据库、比对链路 python setup_check.py --camera 0 --db ./data/checkin.db --templates ./data/templates.pkl # 启动签到主程序(示例:一分钟一节课的演示模式) python main.py --course CS101 --session-demo --duration 60 --threshold 0.45参数说明:--camera 0是默认摄像头索引,如果用了外接 USB 摄像头改成 1;--session-demo让程序在 60 秒后自动结束并导出,适合答辩时控制节奏;--threshold 0.45从命令行传入,避免现场改代码。演示前把摄像头索引、数据库路径、阈值写进config.yaml,跑一次setup_check.py确认全绿,比临场调参数稳得多。
本文还有配套的精品资源,点击获取