
简介这是一份基于Python与深度学习的人脸识别系统毕业设计项目源码以PyQt5搭建交互界面集成摄像头实时人脸检测与表情识别功能适合计算机相关专业正在准备毕业设计或课程设计的学生也适合需要项目实战练习的开发者。整套资源共22个文件包含3个Python程序文件主窗口、摄像头线程、主流程、1个PyQt5界面文件.ui、1个OpenCV人脸检测级联文件、1个训练好的模型权重.h5以及12个用于表情分类的png素材图片和项目说明文档压缩包仅13.36MB。项目作者评审得分99分代码结构清晰、运行步骤简单可直接启动体验也可在此基础上更换数据集、调整网络结构或改写界面逻辑二次开发门槛较低。目前已有381人学习下载是完成毕设论文、期末大作业或作为深度学习入门实战参考的高分项目。1. 毕业设计里“基于Python的人脸识别系统”要解决的不是模型而是全链路人脸识别系统在毕业设计中常常被理解成“训练一个CNN模型”但真拿到的照片在复杂环境下识别率骤降时问题往往不在网络结构而在人脸检测、对齐、特征空间距离度量以及阈值选择。这里要搭建的是一个从摄像头/图片输入到“这个人是谁”输出的完整流水线核心是深度学习特征提取但工程上必须同时面对误检、姿态变化和光照问题。本文面向准备做计算机毕业设计、或者需要快速落地一个人脸识别Demo的开发者。内容是直接把一套可运行的Python源码拆开讲从人脸检测器选型、数据集整理、ArcFace损失训练、特征向量检索到最终封装成HTTP接口。整体设计遵循“先检测对齐、再特征提取、最后向量比对”的主线这套思路也适配人脸识别门禁机、课堂签到到照片检索等实际场景。读完之后能动手复现也能回答答辩里常见的“为什么用这个阈值”“模型对侧脸是否鲁棒”这类问题。2. 人脸检测与数据预处理用MTCNN还是OpenCV决定了后续精度的上限2.1 二阶段与一阶段检测器在源码里的选型逻辑人脸识别系统的第一步不是分类而是“把脸找出来并统一成标准尺寸”。代码里常见选择是OpenCV的Haar Cascade和MTCNN。Haar Cascade速度极快适合嵌入式设备或视频流首帧定位但召回率在角度偏大时明显下降MTCNN是二阶段检测网络输出人脸边框、五个关键点双眼、鼻尖、左右嘴角这个输出可以直接用来做仿射对齐。我在毕业设计项目源码里通常保留两者用开关切换。开发调试时用MTCNN最终部署到树莓派或局域网服务器时切回OpenCV。原因是源码要同时满足“效果演示”和“资源占用可控”两个答辩关注点。import cv2 import numpy as np from mtcnn import MTCNN detector MTCNN() img cv2.cvtColor(cv2.imread(test.jpg), cv2.COLOR_BGR2RGB) result detector.detect_faces(img) for face in result: x, y, w, h face[box] keypoints face[keypoints] cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) for kp in keypoints.values(): cv2.circle(img, kp, 2, (0, 0, 255), -1)detect_faces返回的box是原始像素坐标keypoints里存储的是左右眼、鼻子和嘴巴的坐标。这里不做像素值归一化因为MTCNN内部已经处理过。需要留意的是mtcnn库基于TensorFlow 1.x的兼容层运行Python 3.9以上可能遇到protobuf版本冲突解决办法是固定protobuf3.20.3。如果不想处理这部分依赖可以用OpenCV自带的cv2.FaceDetectorYN替换。2.2 关键点对齐用仿射变换把关键点标准化MTCNN给出的五个关键点不能直接用于训练必须把人脸变换到一个标准位置。常见做法是以两眼距离和鼻尖位置为基准计算仿射变换矩阵。人脸对齐是很多毕业设计源码里最容易被跳过、但对最终准确率影响最大的一步。没有对齐的特征提取模型对同一人的不同姿态会输出距离很大的特征向量导致阈值无法设定。def align_face(image, keypoints, size112): left_eye keypoints[left_eye] right_eye keypoints[right_eye] dx right_eye[0] - left_eye[0] dy right_eye[1] - left_eye[1] angle np.degrees(np.arctan2(dy, dx)) center ((left_eye[0] right_eye[0]) / 2, (left_eye[1] right_eye[1]) / 2) M cv2.getRotationMatrix2D(center, angle, scale1.0) rotated cv2.warpAffine(image, M, (image.shape[1], image.shape[0])) # 以眼距的一半作为目标尺寸 eye_dist np.sqrt(dx ** 2 dy ** 2) scale size / (eye_dist * 1.4) M2 cv2.getRotationMatrix2D(center, 0, scale) aligned cv2.warpAffine(rotated, M2, (size, size)) return aligned代码先做旋转再做缩放。之所以分两步是为了保持画面不变形。1.4表示缩放系数的分母控制人脸在112x112图中的占比。实际调试时如果发现人脸过大或过小就调整这个经验值。对齐后的人脸送入特征提取网络网络最后的输出是一个固定长度的特征向量常见的长度是512或128。2.3 训练数据目录结构与标签编码深度学习的训练数据不建议用中文路径也不建议把所有图片塞进一个目录。我一般这样组织dataset/ person_001/ img_0001.jpg img_0002.jpg person_002/ ...用torchvision.datasets.ImageFolder读入时每个子目录名会被当作类别标签。这样在答辩时可以直接展示数据增强、批次采样和标签映射表。from torch.utils.data import Dataset from PIL import Image import os class FaceDataset(Dataset): def __init__(self, root_dir, transformNone): self.paths [] self.labels [] self.classes sorted(os.listdir(root_dir)) for label, cls in enumerate(self.classes): cls_dir os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((jpg, jpeg, png)): self.paths.append(os.path.join(cls_dir, fname)) self.labels.append(label) def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) label self.labels[idx] return img, label这个类返回PIL图像和整数标签。transform中要做随机水平翻转、轻度旋转和色彩抖动用于缓解实际摄像头抓拍时的姿态分布不均衡。注意不要对图像做随机裁剪因为局部裁剪会破坏关键点对齐结果反而引入噪声。3. 特征提取网络设计ResNet骨架搭配ArcFace损失函数3.1 为什么不用纯分类网络输出作为人脸特征人脸识别的核心不是预测“这个人是谁”而是输出一个能代表脸的特征向量。如果直接训练一个分类网络最后一层全连接输出的类别概率在遇到训练集以外的新人脸时会完全失效。因此需要把倒数第二层的特征向量拿出来用度量学习来优化它——让同一人的特征距离近不同人的特征距离远。常用的骨架是SE-ResNet和ResNet-101。SE模块加入了通道注意力在FaceNet和CosFace等论文中被反复验证有效。InsightFace官方的训练工程里以ResNet-50为主干输出512维特征。在毕业设计源码中我一般用ResNet-50因为训练时间可控也能在单张GPU上跑通。3.2 ArcFace损失函数的实现要点ArcFace是在Softmax基础上增加角度间隔约束。它的输入是特征向量与权重向量的夹角余弦通过角度边距扩大类间分离度。import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceLoss(nn.Module): def __init__(self, in_features, out_features, s64.0, m0.5): super().__init__() self.s s self.m m self.weight nn.Parameter(torch.FloatTensor(out_features, in_features)) nn.init.xavier_normal_(self.weight) def forward(self, features, labels): w F.normalize(self.weight, dim1) x F.normalize(features, dim1) cos_theta torch.mm(x, w.t()) theta torch.acos(cos_theta.clamp(-1 1e-7, 1 - 1e-7)) target_logits torch.cos(theta self.m) one_hot F.one_hot(labels, num_classescos_theta.shape[1]).float() output cos_theta * (1 - one_hot) target_logits * one_hot output * self.s return F.cross_entropy(output, labels)s是特征缩放系数论文中常取64用于解决Softmax Loss值过小导致梯度消失的问题。m是角度间隔0.5在多数公开数据集上表现稳定。注意不能直接把theta self.m放入余弦函数而不做clamp否则输入到torch.acos的余弦值如果超过1或小于-1会得到NaN。这里用clamp限制了范围。3.3 训练循环里的数据增强与学习率策略深度学习人脸识别与普通分类任务的一个区别是batch size相对较大因为ArcFace依赖批次内样本的多样性。实际代码中batch size设为64或128学习率使用余弦退火或阶梯式下降。一个较稳妥的启动配置如下参数推荐值说明输入尺寸112x112对齐后的标准尺寸BackboneResNet-50参数量适中Embedding维度512减少维数会牺牲区分度初始学习率0.1SGD配合余弦退火权重衰减5e-4避免过拟合每批次张数64显存不够时降到32训练轮数80~120数据集越大轮数越多数据增强建议包括水平翻转概率0.5、随机亮度/对比度调整、±10度旋转。不要在测试时使用随机增强验证时只需要中心裁剪。optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100, eta_min1e-4) for epoch in range(epochs): for images, labels in train_loader: features model(images) loss criterion(features, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(fEpoch {epoch}: loss{loss.item():.4f}, lr{optimizer.param_groups[0][lr]:.5f})训练完成后不要保留最后一层全连接层而是保存model.backbone部分。预测时不再走分类头的输出。4. 人脸比对与识别流程向量距离、阈值与注册/识别分离4.1 注册阶段构建本地人脸特征库系统分为注册和识别两个阶段。注册阶段把已知身份的人脸图片提取特征存入本地特征库识别阶段实时提取当前帧的人脸特征与库里的特征比较返回距离最小的身份。我这里选用numpy数组作为特征库存储格式原因是代码依赖少、便于答辩演示。大规模场景下可以替换为faiss或milvus但原理不变。import numpy as np import pickle class FaceDatabase: def __init__(self, feature_dim512): self.names [] self.features [] def add(self, name, feature): self.names.append(name) self.features.append(feature) def save(self, path): with open(path, wb) as f: pickle.dump({names: self.names, features: self.features}, f) def load(self, path): with open(path, rb) as f: data pickle.load(f) self.names data[names] self.features data[features]特征向量在保存前必须做归一化也就是用feature / np.linalg.norm(feature)处理。否则余弦相似度计算会受向量模长干扰。4.2 识别阶段余弦距离与欧氏距离的阈值差异人脸识别源码里最常见的错误是分不清余弦相似度和欧氏距离。余弦相似度越高代表越像范围在-1到1之间欧氏距离越低越像。在基于ArcFace的模型里推荐用特征向量的余弦距离。许多开源模型如InsightFace直接在输出时已经做了归一化。对于阈值设置没有固定值需要根据实际场景用负样本对重新标定。比如在教室点名系统里若所有学生都有登记误识别的代价高阈值就要调得更严格。一般以余弦相似度0.4到0.6作为区分区间。def compute_similarity(feature, db_feature): return np.dot(feature, db_feature.T) def recognize(feature, db, threshold0.5): scores [np.dot(feature, np.array(db_feature)) for db_feature in db.features] max_idx int(np.argmax(scores)) if scores[max_idx] threshold: return db.names[max_idx], scores[max_idx] return unknown, scores[max_idx]几个需要现场演示验证的边界情况同一个人在不同光线条件下得分可能在0.5附近波动不同人长得特别像的时候得分也可能超过0.6。若一道简单阈值解决不了常见做法是引入top-2距离比也就是“得分最高的两人差值是否足够大”。4.3 视频流实时识别的批处理优化在实时视频流中不能每帧都对全图做检测更不要拿到检测框后直接送入模型。我一般每隔一帧进行检测检测到的人脸用跟踪器继续跟踪。这样做的理由是人脸识别模型对单张图片的处理时间虽然只有几十毫秒但视频帧率为25帧时连续处理会造成CPU或GPU占用过高也会导致卡顿。cap cv2.VideoCapture(0) skip_frames 2 frame_count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 if frame_count % skip_frames ! 0: continue bboxes detector.detect_faces(frame) for box in bboxes: aligned align_face(frame, box[keypoints]) feature model_extract(aligned) name, score recognize(feature, db, threshold0.5) cv2.rectangle(frame, (box[x], box[y]), (box[x]box[w], box[y]box[h]), (0, 255, 0), 2) cv2.putText(frame, f{name} {score:.2f}, (box[x], box[y]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)这里的detector.detect_faces若使用MTCNNCPU上每帧大约耗时50~100毫秒间隔帧处理能显著降低延迟。如果要进一步提速可以先把帧尺寸缩放到640x480再检测。5. 把源码封装成可答辩的完整系统Flask API与模型验证技巧5.1 基于Flask的注册/识别服务很多毕业设计源码只提供命令行脚本运行起来不够直观。我建议加一层轻量HTTP服务用Flask分别暴露/register和/recognize接口前端通过H5或Uniapp上传照片就能把“毕业设计”演示成一套可用系统。from flask import Flask, request, jsonify import base64 import cv2 import numpy as np app Flask(__name__) db FaceDatabase() db.load(face_db.pkl) model load_model(resnet50_arcface.pt) app.route(/register, methods[POST]) def register(): data request.json name data[name] img_data base64.b64decode(data[image]) nparr np.frombuffer(img_data, np.uint8) img cv2.imdecode(nparr, cv2.IMREAD_COLOR) bboxes detector.detect_faces(img) if len(bboxes) 0: return jsonify({error: no face detected}), 400 aligned align_face(img, bboxes[0][keypoints]) feature get_feature(model, aligned) db.add(name, feature) db.save(face_db.pkl) return jsonify({success: True}) if __name__ __main__: app.run(host0.0.0.0, port5000)这段代码把图像以base64字符串传入后端解码后走完整的“检测—对齐—提取”流程。需要注意Flask默认的JSON大小限制是1MB用于人脸识别的照片最好在请求端先压缩到640x640以内。5.2 三个提升识别率的实用技巧第一针对自己采集的照片集做阈值烘焙。不要直接使用开源库默认的0.5而是收集每个人的2~3张正样本和10张干扰照片画出ROC曲线后选择误识率低于1%对应的阈值。第二做质量过滤。检测到人脸后计算清晰度常用方式是拉普拉斯方差如果小于设定值则丢弃该帧的特征因为模糊人脸特征会发生严重漂移。第三对同一身份的多帧结果做滑动窗口投票。连续五帧中同一身份出现三帧以上才判定为识别成功能有效减少因低头、遮挡造成的瞬时误判。5.3 答辩时如何验证模型不是过拟合答辩老师经常会问“这个模型除了看到的人脸之外还能不能认出别人”。最直接的办法是划分一份完全未参与训练的人脸集在测试时跑一次跨人识别。也可以用公开的LFW数据集进行zero-shot验证直接拿模型提取的LFW图片特征两两比较不需要微调查看准确率是否超过95%。这是深度学习人脸识别项目源码中最能体现“泛化能力”的证据也比展示训练loss曲线更有说服力。如果时间有限至少准备一张与自己人脸差异很大的外网公开名人照片在接口中识别为“unknown”验证阈值与负样本处理的有效性。本文还有配套的精品资源点击获取