十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

基于FaceNet与PyQt5的人脸识别系统:从原理到工程实践 简介面向毕业设计的人脸身份识别系统基于 Python 与 PyQt5 开发内置图形化操作界面适合计算机相关专业学生用于课程设计、毕业设计或深度学习项目实践系统以 FaceNet 预训练模型为核心结合 OpenCV 与 NumPy 完成人脸检测、特征提取和身份比对识别准确率可达 99.8%运行时视频流经预处理后提取特征并与数据库比对最终在 PyQt5 界面显示识别结果覆盖从数据到界面的完整链路。 压缩包共 67 个文件约 187.26MB主要包含 Python 源码及编译产物、PyQt5 界面文件、TensorFlow 模型权重、配置文件、示例图片和演示视频源码模块涵盖数据集创建、模型加载、人脸识别与界面交互目录结构清晰便于按需修改和二次开发目前已有 2876 人学习下载。 对毕业设计或人脸识别入门者而言这份资源可直接运行体验也可作为项目蓝本在理解 FaceNet 特征比对原理的基础上扩展识别逻辑、优化界面或接入其他摄像头场景配合演示视频与说明文档可有效降低从零搭建的门槛。1. 为什么这套FaceNetPyQt5工程值得直接拆开跑一次大多数教程里的人脸识别都停在展示一张图片或一段命令行输出而毕业设计通常被要求有“系统界面、实时识别、演示效果”。我拿到这套工程的第一感觉是它把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, encodingutf-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或pickletxt的好处是方便人工查看和增量追加比如新增一个人的特征时直接往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显示视频、控制按钮、展示识别结果人脸检测MTCNNdet1/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.txtface_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.py和face2.py可能存在两套界面实现建议先跑face1.py它对应的是face1.ui编译后的界面逻辑更直观。如果运行出现AttributeError通常是Qt界面与业务类的连接装配问题可以在main.py里找到替代入口。3.2 第一次启动依赖安装、特征库生成、界面运行从零环境启动这套系统一般按三步走。首先是安装依赖我建议固定TensorFlow大版本因为这个工程里的facenet.py是基于TensorFlow 1.x的API写的如果直接装最新的TensorFlow 2.x会报模块不存在的错误。常见的依赖安装命令pip install pyqt55.15.7 opencv-python4.5.5.64 numpy1.19.5 pip install tensorflow-gpu1.14.0如果机器没有NVIDIA GPU就把tensorflow-gpu换成tensorflow1.14.0CPU也能跑只是每帧识别延迟会高一些。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, threshold1.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_disteuclidean来自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/ckptpb文件适合推理部署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.Session、tf.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张正脸统计这些向量两两之间的距离再收集35个不同人员的向量距离。把阈值放在“类内距离最大值”和“类间距离最小值”中间再上下浮动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, encodingutf-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观察控制台日志确认覆盖“加载模型→打开摄像头→提取特征→比对”的每一步这样答辩现场即使出错也能顺着日志快速定位问题。本文还有配套的精品资源点击获取
返回列表