简介:面向毕业设计、课程设计和期末大作业场景的Python深度学习多目标人脸识别项目,以完整可运行源码与逐段注释为核心,新手可对照理解模型加载、特征提取和多目标匹配流程,部署后即可用于人脸检测演示。压缩包共121个文件,总大小32.31MB;主要包含31个py源码、37张jpg测试图像、8个txt说明文档、6个npy特征数据与3个csv特征表,还附带2个spec打包配置、avi/mp4演示视频和exe可执行程序。目前已有273人学习下载。除核心识别代码外,还提供测试素材、特征库、打包配置和演示录屏,可直接运行exe查看效果,也能对照源码理解多目标人脸识别的完整实现思路。整个项目以模块化方式组织,从人脸检测、特征提取到识别输出均有对应脚本,便于按需修改与调试;代码注释清晰、目录结构分明,适合毕业设计答辩和课程报告复现,也可作为入门深度学习的练手项目。
1. 多目标人脸识别毕设:这个 .zip 到底解决什么问题
“多目标人脸识别”这个题目在本科毕设选题表里出现频率不低,但多数人交上来的版本只做到“识别单张脸”,摄像头一开就露馅。多目标人脸识别要求的是完整链路:画面里同时出现三五个人的时候,系统要先把每个人框出来,再逐个判断身份,最后在屏幕上标注出名字或“陌生人”。这个 .zip 对应的是用 Python 和深度学习模型实现的整套代码:人脸检测、关键点对齐、特征提取、身份比对,外加预训练权重和可直接运行的脚本,属于拿到手能逐段复现的项目。适合做计算机视觉、人工智能方向的毕设,也适合想快速入门人脸识别的开发者。
2. 检测与识别两条线:选型先于代码,多目标怎么搭
2.1 多目标不是把整张图送进分类器,而是两条流水线
“多目标”三个字决定了系统架构,和单张人脸识别的差别很大。摄像头打开,画面里常常有三五个人前后站着,有人正对镜头,有人侧脸,还有人被挡住半边。如果直接把整张图送进一个分类网络,模型不知道应该对哪个对象负责,输出既不是身份也不是位置。所以从业者的共识是拆成两个独立环节:先由检测模型给出每个人脸的矩形框,再由识别模型对每个框分别提取特征、比对身份。
这个流程在工程上叫二阶段管线,优点是两个环节可以分别调优:检测漏了就补检测数据,识别错了就调阈值或换特征模型。有人会问,现在端到端的人脸识别研究不是也有吗?那种方案通常要求图像质量高、人脸位置相对正,放到真实多人场景会明显退化,毕设阶段没必要冒险。二阶段管线是评测时最容易产出好消息的路线,也方便在答辩时拆开讲原理。
传统做法里用得多的 OpenCV Haar 级联也不是不行,但它基于手工特征,遇到侧脸、遮挡和光照突变性能崩得很快。为什么很多入门教程还在提它?因为它快,可以在没有 GPU 的机器上跑起来做示例。真要面对多人同框、不同尺度的场景,还是得交给基于深度学习的检测模型。下面把整个流程拆开看:检测模型输出 N 个边框,每个边框裁剪后做归一化和关键点对齐,变成固定尺寸的人脸图,再进入特征提取模型得到 512 维向量;最后拿这个向量去注册库里找最近邻。这个流程里的每一步都能被复现和单独验证,也是后面所有代码的组织方式。
2.2 检测模型和识别模型怎么选:从上手成本到效果
人脸检测方面,MTCNN 是出现频率最高的选项。它分成三个阶段:第一阶段生成若干候选框,第二阶段做回归并过滤,第三阶段再精修并输出五个关键点(两眼、鼻尖、嘴角两点)。这个结构简单,依赖轻,普通笔记本 CPU 也能跑实时。RetinaFace 在公开检测榜单上的表现比 MTCNN 好,对遮挡、小脸的召回更强,但安装依赖相对复杂,pip 安装时经常遇到 protobuf 和 CUDA 版本踩坑。
识别特征提取方面,FaceNet 的目标是让同一个人的特征向量距离更近、不同人的距离更远,输出 512 维向量,下游用欧氏距离或余弦距离都能做比对。ArcFace 进一步把角度间隔加入分类损失,公开评测精度更高,但它更适合大规模训练场景。对毕设来说,直接加载官方预训练权重做推理就够了。下表是针对这个场景的选型建议。
| 环节 | 模型 | 上手成本 | 多人场景表现 | 部署体量 | 毕设推荐度 |
|---|---|---|---|---|---|
| 人脸检测 | MTCNN | 低,pip 即装 | 同框 5 人以内稳定,小脸会漏 | CPU 可跑 | 首选 |
| 人脸检测 | RetinaFace | 中,依赖偏多 | 更稳,小脸召回更好 | 需要 GPU 更顺 | 有环境再选 |
| 特征提取 | FaceNet InceptionResNetV1 | 低,有现成权重 | 特征区分度够用 | CPU 推理百毫秒级 | 首选 |
| 特征提取 | ArcFace / InsightFace | 中,API 变动频繁 | 精度更高,但版本锁不住 | 显存占用偏大 | 备选 |
从这个表能看出我的取向:把稳定可复现放在第一位。MTCNN + FaceNet 的组合在 CPU 上处理 640x480 的视频流,每帧大概 100 到 300 毫秒,多人场景会明显降速,但对离线图片识别完全够用。如果毕设题目标明“实时”两个字,可以把检测替换成更轻的 BlazeFace,或者把输入分辨率降半,代价是损失一部分小脸检测率。这里不建议一上来就堆高级模型,模型越重,环境问题越多,答辩前的调试成本越高。
2.3 数据准备:自建注册库比纯下载公开数据集更可控
公开数据集(比如 LFW)最常见的用法是拿来测模型精度,但毕设答辩现场更期待“系统认识我”的效果。建议做两套数据:注册库 gallery 和测试集 probe。gallery 每人放 3 到 5 张正面照,文件名按“姓名_序号.jpg”命名,比如alice_01.jpg;probe 每人放 10 到 20 张,覆盖正面、左右侧脸、戴眼镜、室内外不同光照。
目录结构建议按data/gallery/和data/probe/分开,训练脚本和测试脚本只读各自的目录,避免混用。下载下来的公开数据如果要用于验证,必须花时间清理坏图:模糊的、遮挡过重的、人脸占比小于 10% 的都应该剔除,否则会拖低整体准确率,还容易让答辩老师误以为你的算法有问题。常见做法是先用检测脚本扫一遍数据集,把人脸置信度低于 0.8 的图片自动移到 discard 目录,人工再确认一次。
对齐问题也要在数据阶段想清楚。注册库里的照片若来源混杂,有人用手机前置摄像头拍,有人从网上下载证件照,人脸的空间分布差异很大。后面训练得到的特征和识别阈值都会受影响。所以数据准备阶段的一致性和算法本身同等重要,这一点很多刚做深度学习入门项目的人容易忽略。
2.4 为什么不用从零训练:迁移学习怎么落地
你需要先想清楚,毕设的核心目标是完整实现和可靠演示,而不是复现一篇论文。人脸识别模型的训练通常需要百万级人脸数据,几十轮训练在单卡上要跑一周以上,对一个学生的周期来说太奢侈。常见做法是加载在 VGGFace2 或 CASIA 上预训练的权重,直接做推理;如果一定要体现“训练”环节,可以做迁移学习,拿自己的小规模数据对最后一层分类头做微调。
迁移学习的坑在于类别不平衡。自建库只有十来个人,每人几十张图,微调很容易过拟合到训练集。我一般建议:权重加载后先不动参数,直接把所有注册照片编码成特征向量,用向量比对做识别。这个方案既不用重训,又能把模型精度保在预训练权重水平。答辩被问“为什么没有训练过程”时,回答“采用预训练加特征比对,规避小样本过拟合”是站得住脚的。如果题目硬性要求出现训练代码,可以单独写一个微调脚本,把骨干网络冻结,只训练最后一层全连接,数据增强打开,训练和验证损失分开打印,这样既符合“做了训练”的要求,又不容易翻车。
3. 用 Python 跑通多目标人脸识别:环境、代码块与可调参数
3.1 环境安装:把依赖锁在可复现的版本上
如果机器上还没有 Python,按 python 安装教程装好 3.9 或 3.10,再用 vscode python 环境配置选中解释器。建议用 conda 或 venv 建虚拟环境,不要直接装进系统解释器,否则 torch 和 opencv 的版本很容易互相污染。下面以 conda 为例。
conda create -n face python=3.9 -y conda activate face pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install facenet-pytorch==2.5.3 opencv-python==4.8.1.78 pillow==10.0.0 numpy==1.24.3 scipy==1.10.1 jupyter第一行固定 Python 3.9,是为了避开部分依赖在新版本上的兼容问题。torch 和 torchvision 版本必须配对,否则 import 阶段就会报错;没有 NVIDIA 显卡时,去掉--index-url ...cu118,pip 会安装 CPU 版 torch,后面代码会自动回退到 CPU。facenet-pytorch 这个库的安装包名带连字符,import 时用下划线,别记反。装完后先执行一条验证命令:python -c "import torch; print(torch.__version__, torch.cuda.is_available())"。这一步能避免后面所有报错都堆在一起看不出原因。
3.2 一段代码框出画面里的所有人
下面这段是检测部分,输入一张多人合照,输出每个人脸框和置信度,并把结果画到图上。
import cv2 import torch from PIL import Image from facenet_pytorch import MTCNN # 有 GPU 自动用 GPU,没有就回退 CPU device = 'cuda' if torch.cuda.is_available() else 'cpu' print('using device:', device) # keep_all=True 表示保留所有检测到的人脸,而不是只留最像的一张 mtcnn = MTCNN( keep_all=True, device=device, thresholds=[0.6, 0.7, 0.7], # 三阶段候选框的置信度阈值 min_face_size=40 # 小于40像素的人脸跳过 ).eval() # OpenCV 读出来是 BGR,需要转成 RGB 再交给模型 img_bgr = cv2.imread('group_photo.jpg') img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img_pil = Image.fromarray(img_rgb) boxes, probs = mtcnn.detect(img_pil) if boxes is None: print('没有检测到人脸') else: for (x1, y1, x2, y2), p in zip(boxes, probs): print(f'人脸位置: ({x1:.0f}, {y1:.0f}) -> ({x2:.0f}, {y2:.0f}), 置信度 {p:.2f}') cv2.rectangle(img_bgr, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.imwrite('group_photo_boxes.jpg', img_bgr)这段代码的核心是mtcnn.detect,它只返回框和置信度,不会做人脸对齐。keep_all=True是多人场景的关键,默认值只保留概率最高的一张脸,忘记改这个参数就会出现“三人在场只框一位”的问题。thresholds控制三个阶段的严格程度,全部调低会得到更多候选框,误检也跟着增加;min_face_size影响小脸召回,调小能框到更远的人脸,但计算量会变大。代码里把彩色转换写出来了,因为 OpenCV 默认 BGR,模型训练时用的是 RGB,这一步漏了,检测效果会莫名下降。
3.3 特征提取与身份比对:让系统说出这是谁
拿到框只是前半程,下面这段做识别:注册库里每个身份保存一个特征向量,测试图里的每一张脸都和库里的向量做余弦相似度比较。
import numpy as np from scipy.spatial.distance import cosine from facenet_pytorch import InceptionResnetV1 # 加载预训练模型,权重在 vggface2 上训练得到 resnet = InceptionResnetV1(pretrained='vggface2').eval().to(device) database = {} def register(name, img_path): """把一张注册照片编码成 512 维向量存入 database。""" img = Image.open(img_path).convert('RGB') face = mtcnn(img) # MTCNN 内部会检测并对齐,返回 N,3,160,160 张量 if face is None: print(f'{name} 注册失败:没检测到人脸') return emb = resnet(face.unsqueeze(0)).detach().cpu().numpy()[0] database[name] = emb print(f'{name} 注册成功,embedding 维度 {emb.shape}') register('alice', 'gallery/alice_01.jpg') register('bob', 'gallery/bob_01.jpg') def identify(test_img_path, threshold=0.45): """检测测试图里的每一张脸,返回最可能的身份。""" img = Image.open(test_img_path).convert('RGB') boxes, _ = mtcnn.detect(img) faces = mtcnn(img) if faces is None: return for i, face in enumerate(faces): emb = resnet(face.unsqueeze(0)).detach().cpu().numpy()[0] best_name, best_sim = 'unknown', threshold for name, ref_emb in database.items(): sim = 1 - cosine(emb, ref_emb) # 余弦相似度,越接近1越像 if sim > best_sim: best_sim = sim best_name = name print(f'第{i+1}张脸: {best_name}, 相似度 {best_sim:.3f}, 位置 {boxes[i]}') identify('test_group.jpg', threshold=0.45)这段是示例代码讲解里最标准的写法。register把注册照编码为 512 维向量,identify把测试图里的每张脸逐个比对。face.unsqueeze(0)是把单张脸张量补出批次维度,因为 PyTorch 模型要求四维输入。cosine返回的是距离,值域 0 到 2,所以用1 - cosine转成相似度。阈值 0.45 只是初始值,上一章说过,必须用自己的注册库重标定。
实际项目里每个身份通常不止一张注册照,常见做法是把同一个人的多张 embedding 求平均后入库,或者把所有 embedding 都存进去,识别时取最近距离。平均法更省内存,但对某张特别像的注册照不敏感;多图法更稳,代价是比对时要循环更多向量。毕设演示用平均法就够了,代码里多写一个循环而已。
3.4 三个必调参数:det 阈值、最小人脸、相似度阈值
| 参数 | 所在位置 | 参考值 | 影响 | 调试方向 |
|---|---|---|---|---|
thresholds | MTCNN | [0.6, 0.7, 0.7] | 三阶段候选框置信度 | 多人密集照片漏脸就降,框太多就升 |
min_face_size | MTCNN | 40 | 最小可检测人脸尺寸 | 有远距离小脸就降到 20,CPU 变慢 |
threshold | identify 函数 | 0.45~0.55 | 陌生人判定边界 | 用正负样本重标定,别沿用别人数值 |
调参顺序有讲究:先调检测的两个参数把框调准,再调识别阈值。在检测都漏人的情况下先调识别没意义。见过不少同学为了让指标好看,把识别阈值直接设成 0.1,结果陌生人全被认成熟人,答辩演示当场翻车。另一个常见误用是把thresholds和理解成识别阈值混在一起,两个参数一个在检测阶段、一个在识别阶段,作用完全不同,答辩被追问时容易答错。
4. 多目标人脸识别避坑:5 个典型翻车现场与排查路径
4.1 安装完跑不起来:CUDA 报错和依赖冲突
现象:装完 torch 和 facenet-pytorch,import 时报ImportError,或运行时报RuntimeError: CUDA out of memory,还有torch.cuda.is_available()返回 False。
原因:大部分是 torch 和 torchvision 版本没配对,或者是 CPU 版 torch 被强行指定到 CUDA 设备;也有一部分是 conda 和 pip 混装导致两个同名包互相覆盖。
解决:先执行python -c "import torch; print(torch.__version__, torch.cuda.is_available())"确认基础环境。返回 True 再装其他依赖,返回 False 就卸载 torch 重新按 3.1 的命令装。facenet-pytorch 只依赖 torch、torchvision 和 Pillow,不需要额外装一堆东西。显存不足时,把输入图片缩小到 640x640,或者把mtcnn的device改成cpu,都能绕开。
4.2 预训练权重下载卡死:离线缓存手动补
现象:第一次运行InceptionResnetV1(pretrained='vggface2')时进度条不动,多次重试仍然报连接超时;或者下载一半中断,重跑又从零开始。
原因:权重文件体积大,默认下载通道不稳定;Python 内置下载模块没有断点续传,中断后缓存文件不完整,模型加载时报格式错误。
解决:权重一般缓存在用户目录下的.cache/torch/checkpoints/,文件名后缀可能是.t7或.pt。常见做法是找能连通的环境提前下载完,把文件放进该目录,再按模型代码期望的文件名重命名。如果实在没有现成权重,用pretrained=None也能跑,但识别效果会断崖式下降。所以我一般把权重下载当成环境清单里的前提项,先确认文件存在再开始写代码,省得调试到一半被下载问题打断。
4.3 照片里有三个人,只框出一个人
现象:多目标场景漏检严重,MTCNN 只输出一个框,而且是离镜头最近、最大的一张脸。
原因:最常见的写法是MTCNN()没传keep_all=True,默认只保留置信度最高的一张脸;另一个原因是min_face_size设得过大,远处的小脸被过滤掉。
解决:把keep_all改成True,把min_face_size从默认值降到 20。如果用的是 RetinaFace 之类的检测器,检查有没有设置最大检测数量上限。这个坑几乎是多人识别代码里的常客,不是模型不行,是参数没打开。写完检测后一定要先拿一张三人以上合照验证,输出框的数量对不上就直接排查这两个参数。
4.4 阈值怎么调都不对:识别结果像玄学
现象:同一个人在灯光不同的两张照片里,相似度一会儿 0.7 一会儿 0.3;把阈值调到 0.4,陌生人误识率又明显上升。
原因:人脸识别对输入分布很敏感。注册图是正面大头照,测试图来自摄像头远距离截屏,光照、模糊、透视全部不一致,特征空间里的分布偏移会非常大。这时候阈值再怎么调也拉不平两个来源的差异。
解决:先统一数据来源,别急着调阈值。注册库和测试集用同一台摄像头拍,分辨率不低于 640x480,检测框向外扩大 20% 再裁脸,避免头发和下巴边缘被截掉。如果波动还是大,改用欧氏距离做比对,并发自建库收集正负样本:同一个人不同照算正样本,不同人算负样本,各取 30 组,画两类分布曲线,取交叉处附近的中间值做阈值。这一步本身就可以当答辩里的“模型评估”素材。
4.5 摄像头识别卡顿:多人场景帧率掉到个位数
现象:把本地图片换成摄像头视频流后,每帧处理要一秒钟以上,画面肉眼可见地卡。
原因:MTCNN 在图像金字塔上做三阶段推理,CPU 下处理一张脸就要几十毫秒;多人场景还要二次调用做对齐,叠加视频采集和绘制开销,帧率自然崩。
解决:三个方向。第一,视频帧先缩到 640 宽再进模型;第二,跳帧处理,每 3 帧检测一次,中间帧沿用上一帧的框;第三,把检测和识别拆到不同线程,主线程只做视频采集,检测结果通过队列传给识别线程。最省事的方案是直接把演示切成离线视频,答辩时这是合法取舍,性能页面上写清楚“离线视频,每帧耗时 xxx 毫秒”就行。实时视频看起来流畅但识别乱跳,比干脆利落的离线演示观感差得多。
5. 让答辩更稳的验证法:指标、可视化与演示布局
5.1 用 gallery 和 probe 划分算 top-1 准确率
按人划分数据:每人 3 张注册照进 gallery,另外 10 张做 probe。对每张 probe 做识别,识别出的身份和标注一致记一个正例,最后统计 top-1 准确率、错误接受率和错误拒绝率。把这些正负样本的相似度收集起来,还能画一条 ROC 曲线。
import numpy as np from sklearn.metrics import roc_curve, auc # scores 是正负样本的相似度,labels 是 1/0 fpr, tpr, thr = roc_curve(labels, scores) print('AUC:', auc(fpr, tpr)) for t in [0.3, 0.4, 0.5, 0.6]: fn = sum((np.array(scores) < t) & np.array(labels, dtype=bool)) fp = sum((np.array(scores) >= t) & ~np.array(labels, dtype=bool)) print(f'th={t}: 误识 {fp}, 漏识 {fn}')这段只是验证骨架,花时间的永远是数据收集。正负样本太少,曲线会过度平滑,答辩时经不起追问。至少准备每类 30 组以上,并把相似度分布图画出来,比空口说“准确率 95%”有说服力得多。阈值的选择也要用这组数据解释,而不是拍脑袋。
5.2 演示时留一条人工补救路径
做过现场演示的人都知道,灯光一变、座位一换,模型表现就变,这不是代码写错,是输入分布漂移。我的习惯是给演示程序加两个快捷键:空格键注册当前检测到的人脸,S 键保存当前帧的标注结果。一旦现场展示时熟人识别失败,直接按空格重新注册一张现场照,几秒内就能继续演示。这个小功能不会拉低项目的技术含量,反而说明你对“模型在什么条件下有效”有清楚的认识。代码里维护一段临时注册库,演示结束再清空。
人脸识别类毕设真正容易翻车的地方,往往不是模型精度,而是注册数据和测试数据不一致。先把数据管好,再把阈值标定做扎实,最后用曲线和样例图撑起答辩页面。这套流程做完,至少在这个方向上能站住脚。希望这份解题思路和踩坑记录帮到你,少走一点弯路。
本文还有配套的精品资源,点击获取