news 2026/9/10 10:08:30

基于FaceNet与PyQt5的人脸识别系统:从原理到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于FaceNet与PyQt5的人脸识别系统:从原理到工程实践

简介:面向毕业设计的人脸身份识别系统,基于 Python 与 PyQt5 开发,内置图形化操作界面,适合计算机相关专业学生用于课程设计、毕业设计或深度学习项目实践;系统以 FaceNet 预训练模型为核心,结合 OpenCV 与 NumPy 完成人脸检测、特征提取和身份比对,识别准确率可达 99.8%;运行时视频流经预处理后提取特征并与数据库比对,最终在 PyQt5 界面显示识别结果,覆盖从数据到界面的完整链路。 压缩包共 67 个文件,约 187.26MB,主要包含 Python 源码及编译产物、PyQt5 界面文件、TensorFlow 模型权重、配置文件、示例图片和演示视频;源码模块涵盖数据集创建、模型加载、人脸识别与界面交互,目录结构清晰,便于按需修改和二次开发;目前已有 2876 人学习下载。 对毕业设计或人脸识别入门者而言,这份资源可直接运行体验,也可作为项目蓝本,在理解 FaceNet 特征比对原理的基础上,扩展识别逻辑、优化界面或接入其他摄像头场景,配合演示视频与说明文档,可有效降低从零搭建的门槛。

1. 为什么这套FaceNet+PyQt5工程值得直接拆开跑一次

大多数教程里的人脸识别都停在展示一张图片或一段命令行输出,而毕业设计通常被要求有“系统界面、实时识别、演示效果”。我拿到这套工程的第一感觉是,它把FaceNet预训练模型、MTCNN人脸检测、PyQt5界面和离线特征库拼接成了一条完整数据链路,解压后不一定需要改一行代码就能看到摄像头前的人脸被识别出来。更难得的是,压缩包里同时给了生成特征库的create_dataset.py、负责识别的face_recognition.py、承载交互的face1.ui,以及20180402-114759的pb和ckpt权重文件。无论你是为了快速搭一个毕业设计框架,还是想研究“从图像到特征向量再到决策”的完整实现,都可以把它当作一个能改、能跑、能讲的起点。

2. 核心原理与数据表示:FaceNet特征向量、欧氏距离与PyQt5界面

2.1 FaceNet把人脸变成特征向量,比对的不是图片而是数学距离

FaceNet的核心思想是让同类人脸在嵌入空间里尽量靠近,不同人脸尽量远离。这个压缩包里提供的20180402-114759模型是Google预训练的FaceNet权重,对应的网络结构是Inception-ResNet-v1,输入尺寸是160x160的RGB图像。原始论文中FaceNet输出128维向量,而davidsandberg提供的这个checkpoint输出的是512维向量——如果你在调试时打印embedding.shape,将看到(1, 512),这个细节容易让人误以为参数写错。

人脸图像进入模型前要经过一次prewhiten预处理:先把像素值转成float,减去均值再除以标准差,能消除光照差异。识别时,系统把当前人脸也转成同一维度的向量,然后计算它与特征库中所有向量的欧氏距离。距离越小越可能是同一个人。工程里通常把阈值设为1.0左右,超过阈值就判为陌生人。

这里有个容易被忽略的点:当特征向量做过L2归一化后,欧氏距离的平方和余弦相似度之间存在单调关系。也就是说,如果代码里先对特征做归一化,那么用欧氏距离或用余弦距离排序的结果一致,只是阈值范围不同。调试时如果发现阈值怎么调都不对,先确认是否做了归一化,不要盲目改距离函数。

下面给出加载模型并提取向量的最小示例,这段代码也是facenet.py的骨架:

import numpy as np import tensorflow as tf def load_model(model_path): # 从pb文件恢复计算图 with tf.gfile.GFile(model_path, "rb") as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) tf.import_graph_def(graph_def, name="") return tf.get_default_graph() def prewhiten(x): # 将图像转为float并标准化,减少亮度干扰 mean = np.mean(x) std = np.std(x) std_adj = np.maximum(std, 1.0 / np.sqrt(x.size)) return (x - mean) / std_adj graph = load_model("model/20180402-114759.pb") images_placeholder = graph.get_tensor_by_name("input:0") embeddings = graph.get_tensor_by_name("embeddings:0") phase_train_placeholder = graph.get_tensor_by_name("phase_train:0") # image是一张已经剪裁到160x160的人脸BGR图 face = cv2.resize(image, (160, 160)) face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB) face = prewhiten(face) emb = sess.run(embeddings, feed_dict={ images_placeholder: np.expand_dims(face, 0), phase_train_placeholder: False })[0]

代码里phase_train_placeholder必须设置成False,否则网络会启用Dropout和Batch Normalization的训练行为,导致特征不稳定。每次识别前都做一次prewhiten,这一点不能省。模型加载后可以继续沿用tf.Session,但要注意TensorFlow 2.x默认不再支持tf.gfile,这也是后文排错章节的一个重要伏笔。

2.2 特征库为什么用npy和txt,而不是每张图都跑一次模型

一个容易被忽略的问题是:如果每次识别都拿摄像头里的人脸去和数据库里所有原始图片做特征提取,计算开销会成倍增长。所以工程里单独提供了create_dataset.py,它离线把dataset目录下的人脸图像全部转换为特征向量,并保存为emb特征矩阵和dataall.txt文本索引。常见的保存格式是:路径,用户ID,特征值列表。这样做的好处是,识别阶段只需要把当前人脸向量和特征矩阵做距离计算,不需要再经过一次昂贵的神经网络推理。

import numpy as np def load_feature_db(npy_path, txt_path): emb_matrix = np.load(npy_path) # shape (N, 512) meta = [] with open(txt_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split(",") meta.append({"path": parts[0], "label": parts[1]}) return emb_matrix, meta

这种设计对毕业设计很友好,因为你可以随时打开txt,逐行检查每个人的特征向量是不是重复写入、有没有“脏数据”。如果发生误识别,问题往往出在create_dataset阶段——某张背景复杂或人脸框偏移的图片被错误入库,成了一个“幽灵特征”。有人会问为什么不直接用SQLite或pickle,txt的好处是方便人工查看和增量追加,比如新增一个人的特征时,直接往txt里追加一行,再在npy后拼接一行即可。npy则是NumPy二进制格式,加载后就是连续内存矩阵,比从图片实时提取快得多。

2.3 PyQt5界面和识别模块如何协作而不卡成PPT

主程序face1.py使用QTimer定时采集摄像头帧。如果直接在QTimer回调里做完整推理,界面会在模型计算期间冻结,表现为拖拽窗口极卡。常见做法是只抓帧,然后把任务丢给QThread或Python线程,识别完成后再通过信号回传结果到界面线程。下面是一段典型的界面协作骨架,face1.py里的摄像头显示逻辑就是这种模式:

import sys import cv2 from PyQt5.QtCore import QTimer from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow, QLabel class MainWindow(QMainWindow): def __init__(self): super().__init__() self.video_label = QLabel(self) self.setCentralWidget(self.video_label) self.cap = cv2.VideoCapture(0) self.timer = QTimer(self) self.timer.timeout.connect(self.update_frame) self.timer.start(30) # 约33fps def update_frame(self): ret, frame = self.cap.read() if not ret: return rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, c = rgb.shape image = QImage(rgb.data, w, h, c * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(image))

timer.start(30)表示每30毫秒触发一次,即理论最大33帧,实际帧率取决于摄像头读取速度。这套方案没有把识别放在回调里,而是在update_frame里把帧推给识别线程,识别结束后用pyqtSignal带着人名和坐标回来,再在标签上绘制结果框。这样即使单帧识别需要200ms,界面依然能拖得动。

值得一提的是,MTCNN检测出的原始人脸框通常是矩形,但人的头部可能有旋转。常见做法是利用detect_face.py返回的五个关键点(左右眼、鼻尖、左右嘴角)做相似变换,把眼睛对齐到固定水平位置,再送入FaceNet。压缩包中的utils/image_processing.py里通常封装了align_face函数,如果没有,直接用矩形框resize也能跑,但识别率会打折扣。

模块技术选型作用
界面层PyQt5显示视频、控制按钮、展示识别结果
人脸检测MTCNN(det1/det2/det3)从图像中定位人脸边界框
特征提取FaceNet 20180402-114759将人脸转成512维嵌入向量
特征库numpy + txt存储离线特征,支持追加
决策层欧氏距离 + 阈值判断当前人脸与特征库的最小距离

3. 工程结构与完整识别流程:从create_dataset到face_recognition的模块拆解

3.1 压缩包内的文件角色:先弄清哪些是入口,哪些是核心

解压后文件很多,第一眼容易发懵。按职责可以分成四类:界面入口(face1.py、face2.py、main.py)、算法核心(facenet.py、face_recognition.py、detect_face.py、faceStorage.py)、工具脚本(create_dataset.py、debug.py、utils目录下的image_processing.py、file_processing.py)和预训练权重(20180402-114759.pb、.meta以及ckpt文件)。下表是我整理的角色说明,可以直接贴在项目文档里当目录结构说明:

文件类型功能说明
face1.py入口主界面程序,连接摄像头和识别模块
face2.py入口/参考另一套界面,常用于测试不同布局
main.py入口控制台版调用链,方便调试
create_dataset.py工具批量提取dataset下图片的特征并写入dataall.txt
face_recognition.py核心提供识别函数:检测+嵌入+比较
facenet.py核心加载模型,封装embedding计算
detect_face.py核心MTCNN检测,返回人脸框和关键点
faceStorage.py数据层读写特征库文件
utils/image_processing.py工具图像缩放、BGR/RGB转换、prewhiten等
1.mp4素材本地视频流测试文件
1.jpg素材静态图片测试
20180402-114759.*模型预训练FaceNet权重
face1.ui界面对应face1.py的布局文件
face2.ui界面对应face2.py的布局文件
.pyc文件缓存可忽略,不影响运行

face1.pyface2.py可能存在两套界面实现,建议先跑face1.py,它对应的是face1.ui,编译后的界面逻辑更直观。如果运行出现AttributeError,通常是Qt界面与业务类的连接装配问题,可以在main.py里找到替代入口。

3.2 第一次启动:依赖安装、特征库生成、界面运行

从零环境启动这套系统,一般按三步走。首先是安装依赖,我建议固定TensorFlow大版本,因为这个工程里的facenet.py是基于TensorFlow 1.x的API写的,如果直接装最新的TensorFlow 2.x会报模块不存在的错误。常见的依赖安装命令:

pip install pyqt5==5.15.7 opencv-python==4.5.5.64 numpy==1.19.5 pip install tensorflow-gpu==1.14.0

如果机器没有NVIDIA GPU,就把tensorflow-gpu换成tensorflow==1.14.0,CPU也能跑,只是每帧识别延迟会高一些。numpy不要装太新版本,因为TensorFlow 1.14和numpy 1.20以上可能存在兼容性冲突。安装完成后先准备特征库:把同一个人的多张正脸照片放到dataset目录下,子目录或文件名用于区分身份,然后执行:

python create_dataset.py

脚本会遍历dataset下所有图片,用MTCNN检测人脸,截取160x160区域并交给FaceNet,最后把特征向量追加到dataall.txt和矩阵文件中。成功后可以用face_recognition.py里的某个测试函数验证单张图片,再启动主界面:

python face1.py

参数说明:create_dataset.py若是读取某一路径图片,需要手动改脚本里的路径;如果图片较暗或人脸较小,可先调整detect_face.py中minsize参数,默认一般设为20像素,低于20像素的人脸会直接忽略。项目还包含1.mp4本地视频文件,如果不想开摄像头调试,可以把face1.py中的cv2.VideoCapture(0)改成cv2.VideoCapture("1.mp4"),这对没有摄像头的电脑非常有用。

3.3 调用链:一个摄像头帧是如何变成“张三”两个字的

从摄像头到界面显示,核心调用链可以浓缩为三步。第一步,QTimer抓到一帧;第二步,detect_face.py过滤出所有人脸框,按坐标剪裁并resize到160x160;第三步,face_recognition.py将预处理后的图像送入FaceNet网络得到512维向量,与dataall.txt里的每一行求欧氏距离,取最小值及对应身份。face_recognition.py中对应的核心段一般长这样:

import numpy as np from scipy.spatial.distance import euclidean def recognize(emb, emb_db, labels, threshold=1.0): # emb: 当前人脸的512维向量 # emb_db: 特征库矩阵 (N, 512) # labels: 与矩阵每行对应的身份列表 distances = [euclidean(emb, row) for row in emb_db] min_idx = np.argmin(distances) min_dist = distances[min_idx] if min_dist < threshold: return labels[min_idx], min_dist return "unknown", min_dist

euclidean来自scipy.spatial.distance,也可用np.linalg.norm(emb - row)计算,结果一致。阈值的单位是欧氏距离,而不是余弦相似度,所以前面提到阈值在1.0附近,实际项目里要根据同一个人多张照片间距和不同人的间距来校准。

这里要特别提醒:如果数据库里只有每个人一张照片,阈值往往很难调。create_dataset.py通常会给每个人准备多张照片,但最终保存进矩阵时,会平均同一人的多个向量,以增强稳定性。你可以检查dataall.txt的生成逻辑,确认是否做了平均;如果没有,可以在生成特征时手动计算同一人特征向量的均值。这个动作对识别率提升非常明显,也是运行这套工程时最值得先改的一行代码。

4. 运行与排错:模型加载、摄像头输入和阈值设置的高频坑

4.1 模型加载失败:pb文件、ckpt文件和TensorFlow版本

工程里同时提供20180402-114759.pb和同名的meta/ckpt,pb文件适合推理部署,ckpt适合重新训练。实际加载时,facenet.py一般优先读取pb文件:

import tensorflow as tf def load_face_model(pb_path): with tf.gfile.GFile(pb_path, "rb") as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) with tf.Graph().as_default() as graph: tf.import_graph_def(graph_def, name="") return graph

如果出现AttributeError: module 'tensorflow' has no attribute 'gfile',说明当前环境是TensorFlow 2.x,最简单的办法是卸载后安装TensorFlow 1.14。如果不想降版本,可以把tf.gfile.GFile换成tf.io.gfile.GFile,但后续的tf.Sessiontf.GraphDef也需要对应改成兼容写法,改动面较大,不推荐在毕业设计阶段硬扛。

加载ckpt的另一个坑是:模型用20180402-114759.pb时,输入张量名是input:0,输出是embeddings:0;如果误读了.meta,还需要额外操作saver.restore(sess, tf.train.latest_checkpoint(...)),而且图中可能多了phase_train变量,这两者混合很容易导致张量名对不上。建议直接走pb加载路线,不要碰ckpt,除非你要做微调训练。

4.2 摄像头打不开、黑屏、按钮无响应

运行face1.py后最常出现的现象是黑屏或无响应。先区分是摄像头索引错误还是界面主循环被阻塞。摄像头索引一般从0开始,但某些设备可能占用1,可以用以下脚本测试:

import cv2 for idx in range(3): cap = cv2.VideoCapture(idx) if cap.isOpened(): print("camera index:", idx) ret, frame = cap.read() if ret: print("frame size:", frame.shape) cap.release()

如果所有索引都打不开,大概率是摄像头被其它应用占用,检查Windows相机隐私策略或VMware中摄像头是否被映射进虚拟机。另一种常见情况是PyQt5的窗口能显示但界面拖动很卡,原因就是前文说的把识别计算放在了主线程,需要改成信号槽与线程组合,而不是继续往update_frame里硬塞代码。可以参考以下最小线程封装:

from PyQt5.QtCore import QThread, pyqtSignal class RecognizerThread(QThread): result_ready = pyqtSignal(str, float) def __init__(self, recognizer): super().__init__() self.recognizer = recognizer self.frame = None def set_frame(self, frame): self.frame = frame def run(self): name, score = self.recognizer.recognize_frame(self.frame) self.result_ready.emit(name, score)

set_frame由界面线程调用,线程在run里执行完整推理,通过result_ready信号把结果传回主线程更新UI。注意set_frame在Qt中要加锁,否则可能会出现界面线程写入一半、识别线程读取一半的数据竞争。一个简单的做法是引入QMutex或使用Python的threading.Lock保护帧拷贝。

4.3 阈值与准确率:怎么在误识和漏识之间找平衡

99.8%准确率来自模型和训练数据,具体到你的现场环境,更多要调整的是决策阈值。下面以欧氏距离阈值给出一个常用参考:

阈值行为适用场景
0.5极少误识但大量漏识高安全等级门禁机
0.8平衡点,适合教室考勤日常演示、毕业设计
1.1识别率高但可能串人样本质量差、光线不稳定
1.5极易把陌生人识别成库里的人不建议实际使用

一般调参时,先收集同一个人不同时段、不同光线下的10张正脸,统计这些向量两两之间的距离;再收集3~5个不同人员的向量距离。把阈值放在“类内距离最大值”和“类间距离最小值”中间,再上下浮动0.1。可以在create_dataset.py里加一段统计,把每个人特征的均值、标准差打印出来,这样调阈值时不需要反复试错。还有一个高效做法:用np.linalg.norm批量计算当前人脸向量与整个特征库的距离,而不是用Python循环,矩阵操作能把耗时从几十毫秒压到几毫秒。

5. 把这套工程改造成完整毕业设计:四个可以加分的改动方向

5.1 增加用户注册模块,让“录入”也变成界面操作

原始的create_dataset.py是离线脚本,答辩时不够直观。可以新增一个“人员注册”按钮:点击后自动截取摄像头当前帧,检测人脸并提取特征,然后把特征写入dataall.txt和npy文件,再保存一张原始照片到dataset目录。核心逻辑是把create_dataset.py里的提取函数抽出来复用,界面里只需调用。代码示意:

def register_user(self, user_id): frame = self.current_frame emb = self.extract_face_embedding(frame) with open("dataall.txt", "a", encoding="utf-8") as f: f.write(f"{user_id},{','.join(str(v) for v in emb)}\n") np.save("emb.npy", np.vstack([self.emb_matrix, emb]))

注意npy文件每次追加都要整体重写,数据量不大时没问题;如果特征数超过几千条,建议在内存中维护矩阵,最后统一保存,避免频繁磁盘IO。

5.2 用SQLite记录识别日志,导出成报表

实际演示时,识别结果的留存很重要。可以增加一张recognition_log表,记录时间、摄像头编号、识别人名、欧氏距离。每次识别完成后插入一条记录,答辩时输出“今天识别了多少次”的统计信息。SQLite是单文件数据库,比MySQL省事,也符合毕业设计“小而全”的定位。可顺手用pandas把记录导出成Excel或CSV,老师问起数据量时直接展示表格。

5.3 增加性能统计,把“准确率”变成可视化图表

可以添加一个统计页面,实时显示最近500帧的识别平均耗时、帧率,以及每个身份的识别次数。用pyqtgraph或matplotlib嵌入到PyQt5中,评审老师能看到检测耗时曲线,比单纯一个识别框更能体现工程能力。这里需要额外记录每帧的起始和结束时间戳,注意单位要用毫秒,否则画图时数字跳动过大。

5.4 界面层换皮与多线程完善

当前face1.ui整体比较朴素。可以把按钮换成QSS样式,增加“开始识别”“停止识别”“清空日志”等业务状态,配合前文提到的RecognizerThread,让摄像头显示、识别计算、结果渲染分别在不同线程/线程内分工。注意线程安全:跨线程更新Qt控件时一定要用信号,不要直接操作QLabel,否则程序可能随机闪退。最后可以运行main.py观察控制台日志,确认覆盖“加载模型→打开摄像头→提取特征→比对”的每一步,这样答辩现场即使出错,也能顺着日志快速定位问题。

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

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

cann/ge Graph Engine API构造函数与析构函数

构造函数和析构函数 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、Tensor…

作者头像 李华
网站建设 2026/9/10 10:05:27

STM32 RS485半双工通信时序控制与帧解析实战

简介&#xff1a;本资源是一套基于STM32F103的RS485通信与MODBUS-RTU协议实现的完整嵌入式开发工程&#xff0c;面向嵌入式初学者及工业通信入门开发者&#xff0c;解决多点工业总线通信中硬件驱动配置、协议帧解析与主从交互等核心问题。压缩包含75个文件&#xff0c;以34个C源…

作者头像 李华
网站建设 2026/9/10 10:03:31

【单片机课程设计/毕业设计】基于 STM32 的多传感器融合智能办公座椅系统设计 基于 STM32 的人体感知座椅灯光联动控制系统设计(018407)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/10 10:02:39

Semgrep快速指南:用30+语言模式匹配找出安全漏洞

Semgrep快速指南&#xff1a;用30语言模式匹配找出安全漏洞 【免费下载链接】semgrep Lightweight static analysis for many languages. Find bug variants with patterns that look like source code. 项目地址: https://gitcode.com/GitHub_Trending/se/semgrep Semg…

作者头像 李华