
1. 选题环节为什么我没有选“看起来更简单”的题目项目实训名单发下来的那天我盯着屏幕看了很久。题目五花八门有做电商后台的有做天气预报小程序的还有做图书管理系统的。说实话这些题目都挺好上手网上模板一抓一大把随便改改就能交差。但我知道如果只是为了混个学分选那种题目就是浪费时间——实训真正的价值在于逼自己碰一遍那些“课本里没写但工作中一定会遇到”的问题。我最后选了“基于人脸识别的课堂考勤系统”。原因很简单它足够新、足够有挑战而且涉及的技术链条长——从图像采集、人脸检测、特征提取到后端数据存储、前端界面展示、出勤统计报表几乎把一门课能学到的东西全串起来了。光是“把一个摄像头拍到的人脸变成一条数据库记录”这个闭环就值得花几周去啃。1.1 候选题目的淘汰逻辑我把几个候选题目放在一起做了个对比标准不是“哪个容易做”而是“哪个能让我在实训结束时真的多会点东西”候选题目上手难度技术含金量为什么淘汰/留下图书管理系统低低淘汰。CRUD写一百遍也是CRUD电商后台中中淘汰。前端框架写完就忘没什么记忆点天气预报小程序中中淘汰。核心逻辑是调API不是自己做人脸识别考勤高高留下。有算法、有硬件、有前后端踩坑空间大实训的目的不是“完成”而是“经历”。选一个稍微超出自己能力边界的题目才有东西可写、有经验可总结。这是我选题时的第一原则。1.2 为什么考勤非得用“人脸”而不是更简单的方案实训小组讨论时有人提出既然只是考勤为什么不直接用校园卡刷卡成本低、技术简单、稳定可靠。这个反对意见其实很有道理但有一个关键问题——代刷。让同学帮忙带刷一张卡技术上完全无法识别。人脸识别的核心价值在于“物理本人必须到场”这是刷卡、扫码都做不到的。当然严格来说照片也能骗过一些简陋的系统但如果配合活体检测或者人工复核安全性会大幅提高。我当时的判断是实训项目选人脸识别看中的不是“识别准确率”本身而是它背后整套问题域——光照、角度、遮挡、模糊——这些都是教科书不会告诉你但真实世界一定会碰到的坑。2. 需求分析与整体架构先画清楚图纸再动手选题定下来之后我犯了一个新手常犯的错——直接打开IDE开始写代码。写了三天界面改了八版数据库重建了四次才意识到问题我根本不清楚这个系统到底要解决谁的什么问题。于是老老实实回到白板前把需求一个个列出来再敲定架构。2.1 用户角色与核心使用流程一个课堂考勤系统表面看只有“点名”和“记录”两个动作但实际上涉及三类角色学生到达教室后站到摄像头前完成人脸识别确认签到。教师发起一次考勤查看实时签到情况导出缺勤名单。管理员我实训中兼任维护学生信息、录入人脸照片、管理系统配置。把角色梳理清楚后核心流程就清晰了教师发起考勤系统进入“识别模式”。学生依次走到摄像头前系统自动检测人脸、提取特征、比对数据库。比对成功界面显示学生姓名并标记“已签到”同时写入数据库。比对失败系统提示“未识别”学生可以重试或改用人工签到。考勤结束后教师可查看统计结果并导出Excel。这个流程还不算完整但至少有了一个可运行的最小闭环。2.2 功能模块拆解哪些必须做哪些可以后补我把功能按优先级分了三层P0第一批必须完成的核心链路人脸检测从摄像头画面中找到人脸的位置。人脸识别检测到的人脸与数据库中的特征比对识别出身份。数据存储学生信息表、考勤记录表的基本增删改查。手动签到识别失败时的兜底方案。P1第二批完善体验实时状态展示界面上显示当前的识别结果和已签到名单。出勤统计按课程、按日期生成缺勤名单。批量导入从Excel导入学生名单。P2第三批锦上添花考勤界面友好化比如签到成功时的头像展示。多次签到判定防止同一人重复签到。跨日考勤数据分析。划分优先级的目的很简单先保证主链路完整跑通再考虑好看和好用。实训时间有限最怕的是主功能没做完倒是在样式上花了几天。2.3 技术选型Flask加OpenCV的组合是怎么定下来的技术选型我考虑了三个方向最后确定下来不是最潮的但一定是最适合个人实训的技术组件我的选择备选方案选择理由后端框架FlaskDjangoFlask轻量、灵活适合单机项目Django太重很多功能用不上前端界面基于浏览器的HTML加JavaScriptPyQt5桌面端浏览器调试方便跨平台也可以直接部署在教室一体机上人脸检测OpenCV的Haar级联检测器MTCNN、YOLO简单快速CPU上跑得动适合入门人脸识别FaceNet预训练模型提取128维特征向量dlib、LBPH特征对比准确率高且模型是现成的不需要自己训练数据库SQLiteMySQL单机应用SQLite零配置够用且省事这里要重点说下为什么没有自己训练模型。人脸识别模型的训练需要海量数据和昂贵的显卡实训周期完全撑不起这个工作量。更合理的做法是借用成熟的预训练模型做特征提取自己只负责“比对逻辑”和“前后端整合”——这恰恰是实训中最值得练的部分。我选的是FaceNet的预训练模型输入一张人脸图片输出128维的特征向量再用余弦相似度判断是否为同一个人。注意实训选型不是科研选型不要追求“从零手写一个神经网络”。工程上能解决问题的方案就是好方案先跑起来再优化。3. 人脸识别模型的选型与实践从“能用”到“好用”的距离模型这块我最初的想法也特别天真以为把FaceNet模型调起来就万事大吉。实际上从“模型能识别”到“系统真的好用”之间隔着至少三座山训练数据怎么准备、特征怎么比对、阈值怎么设。3.1 三种人脸识别方案的横向对比在最终定下FaceNet之前我把市面上常见的几种方案都快速试了一遍OpenCV的LBPH局部二值模式直方图传统方法训练一个模型只需要几十张图片速度极快。但缺点是精度一般偏转角度一大就识别不对光照变化时也很不稳定。dlib的HOG特征SVM比LBPH好一些但本质上还是传统机器学习对复杂场景的鲁棒性有限。FaceNet的深度学习方案用预训练模型提取128维向量然后计算向量间的欧氏距离或余弦相似度。准确率明显更高对角度和光照的容忍度也更好。在一次对比测试中同一个学生站在同样的位置和光线下LBPH识别成功率为70%左右HOG大概85%FaceNet稳定在95%以上。虽然FaceNet模型文件有接近100MB加载相对慢一点但我判断这点延迟可以接受——准确率才是考勤系统的命根子。最后还是选了FaceNet加OpenCV的组合。3.2 数据准备自己拍的三百张照片里藏了多少坑识别模型是现成的但“把谁识别成谁”需要数据支撑。我在实训中给班上30个同学每人拍了10张不同角度的照片正面、左侧、右侧、仰头、低头总共约300张。这个过程比想象中麻烦得多第一个坑是光线。第一次拍摄在实训室的白炽灯下进行照片整体偏暗识别时每个同学都变成了“陌生人”。后来换了靠窗的位置自然光充足识别率明显提高。第二个坑是角度覆盖。一开始偷懒每个人只拍了正面照结果现场识别时同学稍微歪一下头就匹配不上。后来补拍了多角度照片情况才好转。第三个坑是数据增强过头了。我原本担心数据不够用OpenCV对每张照片做了水平翻转、亮度调整、微小旋转生成了一堆“增强数据”。结果发现翻转后的照片和原始照片同时存在于训练集中反而导致比对阈值不稳定。后来我把增强数据全部删掉只用原始照片效果反而更好。数据准备这个环节给我上了实训中印象最深刻的一课工程系统的数据质量远比数据量重要。三百张认真控制光线和角度的照片比一千张随手乱拍的照片有用得多。3.3 特征向量的比对与阈值设定那个被反复调试的0.6FaceNet模型输入一张人脸图片输出一个128维的向量。同一人的不同照片向量在空间中应该距离很近不同人的向量距离应该很远。我用来衡量相似度的指标是余弦相似度取值范围是-1到1越接近1代表越相似。系统逻辑很简单import numpy as np def calculate_similarity(vec1, vec2): 计算两个128维特征向量的余弦相似度 dot_product np.dot(vec1, vec2) norm1 np.linalg.norm(vec1) norm2 np.linalg.norm(vec2) similarity dot_product / (norm1 * norm2) return similarity阈值的选择是一个平衡问题阈值设得太高比如0.9那同一个人只要稍微换个角度就会被拒绝考勤时学生得把脸端端正正对准摄像头体验极差阈值设得太低比如0.5又容易出现“两个人长得像就互相签到成功”的误判。我做了几轮测试统计了30个同学的700多组匹配数据后发现同一人不同照片的相似度一般在0.75到0.92之间不同人的相似度一般在0.3到0.6之间。这个区间有交叉所以无法做到完美区分于是我把阈值定在了0.65——既能容忍一定角度变化又能挡住大多数误识别。经验阈值的“最优值”永远是一个经验值而不是理论值和你的人脸照片质量、数量、场景光线直接相关。不要相信网上的默认参数一定要用自己收集到的数据做一次分布统计再决定。4. 核心功能模块的实现顺序先跑通主链路再去补细节实训过程中我的开发顺序不是从上到下按模块一个个写而是先搭一条“摄像头 → 检测 → 识别 → 数据库写入 → 界面显示”的最短链路。等这条链路完全跑通了再回头细化每个环节。这种“最小可用闭环先行”的做法有效防止了做了半个月还不知道系统到底能不能用的情况。4.1 最关键的第一步把摄像头的画面变成一张张人脸人脸检测我用的是OpenCV的Haar级联检测器。它本质上是一个用AdaBoost训练出来的分类器通过滑动窗口判断图像中某个区域是否是人脸。虽然它不如深度学习检测器如MTCNN、YOLO精准但胜在速度快在普通CPU上也能跑到每秒15帧以上而且集成简单。import cv2 # 加载OpenCV自带的Haar级联人脸检测模型 face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) def detect_faces(frame): 从一帧画面中检测人脸返回包含所有人脸坐标的列表 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 参数解释scaleFactor是每次缩放的比例minNeighbors是每个候选区域周围需要多少个矩形框才保留 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(100, 100) ) return faces改参数时我踩过一个坑minNeighbors设得太大比如10以上会导致小尺寸人脸检测不到学生坐远一点就“隐形”了设得太小又会把背景的杂物误判成人脸。我结合教室实际场景依经验值先设为5现场调试后确认这个参数在2米到4米的识别距离内效果最好。minSize设成(100, 100)也是个关键决定它是为了过滤掉远处那些面积太小、即便检测到也无法有效识别的人脸。4.2 识别模块从检测框到“这个人是某某某”的完整流程人脸检测框拿到之后下一步是把框住的人脸区域裁下来送入FaceNet模型提取特征再和数据库中存储的特征向量逐一比对。def match_identity(face_img, db_features, threshold0.65): 将当前人脸与数据库中的特征比对返回匹配到的学生信息和相似度 # 将裁剪的人脸区域调整为模型输入尺寸(160, 160) face_img cv2.resize(face_img, (160, 160)) face_img cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) face_img face_img.astype(float32) / 255.0 # 这里使用FaceNet模型提取特征向量简化写法 query_vector facenet_model.get_embedding(face_img) best_match None best_score -1 for student_id, stored_vector in db_features.items(): score calculate_similarity(query_vector, stored_vector) if score best_score: best_score score best_match student_id if best_score threshold: return None, best_score return best_match, best_score这段逻辑里有几个细节值得展开说。第一是尺寸归一化FaceNet要求输入固定为160×160像素而摄像头抓到的脸大小不一必须统一缩放否则模型输出的特征向量会不稳定。第二是颜色通道转换OpenCV默认是BGR格式但模型训练时用的是RGB不做转换会直接影响提取结果。第三是比对策略我选择了“全库扫描”逐一遍历所有学生的特征向量因为班级人数只有30人速度完全能接受如果换成全校几千人的场景就必须引入向量索引如FAISS来加速了。数据库表结构也很简单核心就两张表CREATE TABLE students ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, face_vector BLOB NOT NULL ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, course TEXT NOT NULL, checkin_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT present );face_vector存的是128维浮点数组序列化后的二进制数据。识别时全部取出来在内存里做比对避免频繁访问数据库拖慢速度。4.3 出勤状态判定这个逻辑比我想象中复杂十倍原本我以为“识别成功就等于签到成功”真的写完测试才发现事情没这么简单。至少要处理三种情况重复签到同一个学生认完一次又凑上来再“刷脸”系统必须识别出这是同一个人已签到不能重复记录。跨课冲突同一时间学生A在“高数”课签到成功就不能再在“英语”课签到成功。迟到判断如果考勤开始10分钟后才识别成功状态应该标记为“迟到”而不是“已签到”。from datetime import datetime def process_checkin(student_no, course, current_time, attendance_window_minutes15): 处理一次签到请求返回签到状态。 状态枚举present(正常) / late(迟到) / duplicate(重复) / conflict(冲突) # 检查是否已签到 existing db.query( SELECT status FROM attendance WHERE student_no? AND course? AND date(checkin_time)date(?), (student_no, course, current_time) ) if existing: return duplicate # 检查同一时间是否已有其他课程的签到记录 conflict db.query( SELECT course FROM attendance WHERE student_no? AND date(checkin_time)date(?) AND time(checkin_time) time(?, -10 minutes), (student_no, current_time, current_time) ) if conflict: return conflict # 计算迟到 start_time get_course_start_time(course) late_threshold start_time timedelta(minutesattendance_window_minutes) if current_time late_threshold: db.execute(INSERT INTO attendance (student_no, course, status) VALUES (?, ?, late), ...) return late db.execute(INSERT INTO attendance (student_no, course, status) VALUES (?, ?, present), ...) return present实际测试中“重复签到”出现得最频繁上课时学生总喜欢凑过来看热闹结果同一个人的脸反复出现在画面里。后来我在识别成功后设置了一个30秒的冷却时间同一个学号在冷却期内不会被再次处理。这个小改动让签到记录干净了许多。5. 联调阶段五个把我卡住最久的Bug如果说前面写代码的节奏是“一路绿灯”那联调阶段就是“红灯一串”。整个实训中我累计遇到了不下20个问题其中五个耗时最长也最值得拿出来分享。5.1 Bug一摄像头画面卡顿导致识别率暴跌系统刚联调时摄像头画面显示是流畅的但只要一有人脸进入画面帧率立刻从30帧掉到不到10帧识别率随之暴跌。排了好久才发现原因我在每一帧画面上都跑了一次完整的人脸检测和特征提取但特征提取的耗时在200到300毫秒主循环完全被阻塞了。解决方案是两条线并行import threading from queue import Queue # 主线程只负责读取摄像头画面和人脸检测 def camera_loop(): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: continue # 只检测人脸不在这里做识别 faces detect_faces(frame) if len(faces) 0: # 将人脸区域交给工作线程处理 face_queue.put((frame, faces)) cv2.imshow(camera, frame) # 工作线程只负责特征提取和比对 def worker_loop(): while True: frame, faces face_queue.get() for (x, y, w, h) in faces: face_img frame[y:yh, x:xw] student_no, score match_identity(face_img, db_features) if student_no: update_display(student_no, score) # 启动两个线程 face_queue Queue(maxsize5) t1 threading.Thread(targetcamera_loop, daemonTrue) t2 threading.Thread(targetworker_loop, daemonTrue) t1.start() t2.start()人脸检测很轻量保持在主循环里做识别很重丢到单独线程中慢慢算。加了队列缓冲后画面不再卡顿识别结果只是略微延迟几百毫秒完全可接受。5.2 Bug二中文姓名写入CSV全部变成乱码导出考勤Excel是个硬需求。我最初用Python原生的csv模块写中文姓名打开文件后全是“且这样的乱码。原因很简单csv模块默认用ASCII编码写文件中文字符自然无法正常保存。解决方案有两种一是写文件时指定encodingutf-8-sig二是改用Excel兼容性更好的openpyxl库直接生成xlsx格式。import csv with open(attendance_records.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([学号, 姓名, 课程, 签到时间, 状态]) for record in records: writer.writerow(record)这里有个细节utf-8和utf-8-sig的区别在于后者会在文件开头写入一个BOM头Excel在打开UTF-8编码文件时有BOM头才能正确识别为UTF-8而不是按本地默认编码解析。5.3 Bug三两个人同时出现在画面时只能识别出其中一个OpenCV的detectMultiScale是可以检出多张人脸的但我的代码第一次测试时只处理了检测列表中的第一个人导致站在后面的同学永远无法被系统识别。修复方法很直接遍历列表中的所有人脸一一处理。for (x, y, w, h) in faces: # 对每个人的脸独立裁切、独立识别 face_img frame[y:yh, x:xw] student_no, score match_identity(face_img, db_features) if student_no: process_checkin(student_no, current_course, datetime.now())后来我又加了“识别框跟随”的功能每张检测到的人脸周围画一个矩形框识别成功时变绿色并显示姓名识别失败时保持红色。这样一来即便是多人同时出现在镜头中谁签到成功谁没成功一眼就能看清。5.4 Bug四模型加载要花5秒钟每次启动都像死机了一样FaceNet模型文件接近100MB加载时需要把整个模型和权重读入内存。启动程序后界面空白5秒很多使用者会以为是程序崩溃了。解决思路有两个方向一是减少加载时间二是转移等待体验。减少加载时间的做法是换用更轻量的模型比如MobileFaceNet文件大小可以压缩到20MB左右但涉及重新测试精度周期较长。转移等待体验的做法更简单启动时先弹出一个“正在加载模型请稍候…”的页面同时把模型加载放到后台线程加载完成后自动切换主界面。from PyQt5.QtCore import QThread class ModelLoader(QThread): 后台线程加载模型避免主界面卡死 loaded pyqtSignal(bool) def run(self): # 在这里加载FaceNet模型 load_facenet_model() self.loaded.emit(True)实训场景下我选择了第二种方案成本最小体验提升立竿见影。5.5 Bug五摄像头资源被占用导致程序第二次启动失败这个问题出现在测试后期。程序第一次正常运行退出后想再次启动结果摄像头打不开报出的错误信息是Device or resource busy。原因在于程序退出前没有正确释放摄像头资源。解决方法是两步一是在程序退出时显式执行cap.release()二是设置OpenCV的CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT属性来初始化摄像头避免某些驱动下资源未释放干净的问题。cap cv2.VideoCapture(0) # 显式指定采集分辨率避免驱动下的默认参数问题 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 程序退出时释放资源 def close_camera(): cap.release() cv2.destroyAllWindows()经验在Windows和Linux下OpenCV摄像头释放的机制不同Windows下有时不释放会在下一次进程启动时正常但Linux下几乎必然报错。实训期间我用的主要环境是Windows到了答辩演示的机器上变成了Linux才第一次真正重视境外资源释放的问题。6. 部署演示与实训答辩系统能跑起来只是及格线系统在开发机上跑得好好的不代表在演示环境就能顺利运行。实训答辩前的两天我把精力几乎全部放在了“让系统在陌生环境里稳定运行”这件事上。6.1 部署到演示机器时最容易忽略的三件事答辩用的机器是实训室统一配置的台式机硬件和我的开发机不完全相同。第一件容易忽略的事情是摄像头的编号。开发机上cv2.VideoCapture(0)默认打开的是笔记本内置摄像头但台式机的摄像头可能是外接USB设备设备编号不一定是0。我特意写了一个小工具来枚举所有可用摄像头编号打印出支持的分辨率列表避免答辩时打开一个不存在的摄像头。第二件是依赖包的版本一致性。我的开发环境是Python 3.9演示机上是Python 3.8。TensorFlow、OpenCV、NumPy这些大依赖在不同版本下的行为差异足以让程序崩溃。我在部署前用pip freeze requirements.txt把依赖固定了下来然后在演示机上新装了一个虚拟环境用同一份requirements安装。第三件是模型文件的路径问题。我在开发机上用的是绝对路径加载模型换机器后必然失效。纠正方案是改成相对路径并强制要求模型文件与程序文件放在同一目录下。6.2 演示脚本的设计逻辑把可能翻车的地方提前预演实训答辩时很多项目组是现场随机演示我见过识别失败后学生手足无措、教师脸色铁青的情况。为了降低风险我花了半天设计了一套演示脚本演示流程固定先展示数据库中的学生名单再启动识别界面按顺序让3位同学逐一到摄像头前签到。这3位同学就是我自己和组员提前反复演练过光线的位置和站姿。准备一个备用方案如果识别连续失败超过3次手动切换到“人工签到”模式在界面上手动录入学号保证演示流程不会中断。设置好合理的考勤窗口演示时课程开始时间设成当前时间的前15分钟这样无论学生什么时候签到都是“正常”不会出现“迟到”的尴尬状态。这套脚本没有改变系统的任何核心逻辑只是让演示在有限时间内呈现出了最优效果。实训答辩评分的核心还是看功能完整度但“稳定跑完演示”本身就是功能完整度的直接体现。6.3 答辩时被问得最多的五个问题答辩环节老师提的问题五花八门但统计下来最频繁的还是这几个“你的识别准确率到底是多少怎么测出来的”——这个问题老师一定会问。我的回答是在30人规模、固定光照的教室场景下500次识别测试的准确率为94.2%失败主要集中在低光照和大幅侧面角度的场景。老师接着会问“为什么不是100%”——这时候如实回答“受限于光线和角度真实场景做不到100%”比瞎吹更能得高分。“如果两个人长得特别像系统会不会认错”——我的回答是系统依赖的是FaceNet在大量人脸数据上学到的特征表达能力双胞胎在特征空间中距离会比普通人近很多但通常会低于0.65的阈值。如果确实存在极端情况还有人工签到作为兜底。“考勤数据能不能被篡改”——当时我的系统没有做任何防篡改设计我如实承认了这一点并补充说“如果更进一步可以增加管理员权限校验、数据库加密存储、操作日志审计等功能”。这种“我知道目前的不足并思考过改进方向”的态度效果远好于硬撑。“为什么选用SQLite而不是MySQL”——我的回答是项目规模小、单机运行、无并发写入压力SQLite完全够用。如果要部署到教室服务器上多人同时使用会换成PostgreSQL或MySQL。老师的追问往往是“你觉得什么时候该换”——只要回答了并有理有据答辩就有分。“你的模型是哪里来的”——这是一个送分问题。我如实说FaceNet的预训练权重来自开源社区模型结构公开自己只做特征提取和比对逻辑没有从零训练。诚实交代模型的来源和使用范围比假装“自己的模型”稳妥得多。7. 复盘如果再做一次我会在哪些地方完全不同实训结束交完报告后我花了一个晚上把整个过程重新过了一遍。不是为了交差而是想弄清楚一件更重要的事如果时间能倒回实训第一天我会在哪些地方做完全不同的选择7.1 做得对的地方先跑通最小闭环这个策略确实有效我特别庆幸自己没有一开始就扎进“老师点名”界面的美化工作。从摄像头画面直接到数据库记录这条主链路只花了两天就完全打通。过程中用到的都是最朴素的技术——OpenCV读帧、Haar检测、FaceNet提特征、SQLite写库——但每一步都能看到明确的进展这种“每写一段代码系统就变好一点点”的节奏感是整个实训期间保持动力的关键。7.2 做得不够好的地方数据收集的时机和策略太随意如果重新来一次我会把数据收集放到实训第一周就启动而不是等人脸识别的代码写完才开始。原因很简单收集数据依赖同学配合而同学的时间是不可控的。我在实训第三周才集中拍照片导致有些同学只来了一次角度只拍了正面后期识别时这些同学的准确率明显偏低。正确做法是第一天就用手机把每个同学的多角度照片拍了存储下来随时可以用于测试。另外数据集的划分也做得不好。我只是把所有照片存到一个文件夹里没有区分“用于注册的照片”和“用于验证的照片”。这导致后续评估识别准确率时我只能从头再拍一批验证照片浪费了大量时间。正确做法是从一开始就建立清晰的数据集目录结构比如data/ ├── enroll/ # 用于注册入库的照片 │ ├── 20210001/ │ │ ├── 01_front.jpg │ │ ├── 02_left.jpg │ │ └── 03_right.jpg │ └── 20210002/ │ └── ... └── validate/ # 用于测试识别准确率的照片 ├── 20210001/ │ └── test_1.jpg └── ...7.3 后续可以继续扩展的方向实训虽然结束但这个系统还有很多可以深入的地方。举个例子如果学生的照片采集成本能大幅降低系统就可以覆盖更多课程和更多学生。这个方向的手段包括用手机小程序自助上传照片、结合校园卡照片库批量导入或者把特征提取接上更高效的轻量模型来降低部署门槛。另外考勤数据目前只是简单记录了“来没来”再往前走一步可以是“来了之后干了什么”——比如与教室门禁联动或者与课后的在线学习行为数据做关联分析。当然这些都是实训之外的延展了短期内我大概率不会再改代码但把这些问题留在脑子里等之后遇到类似项目时起步会快很多。最后再分享一个小技巧如果你也在做类似的项目实训务必把每天遇到的问题记到一个“Bug日志”文件里哪怕只是两三行。写报告的时候这个文件就是你最宝贵的素材来源。我当时记录了三十多条问题最后报告里的“联调与排错”章节几乎全部来自这些随手记的笔记——那是网上任何教程都找不到的、属于你自己的实战积累。