dlib人脸识别实战:128D特征提取与欧式距离比对全解析
发布时间:2026/10/4 5:10:42 作者:尧图编辑部 阅读量:1,286

简介这份资源围绕 Python 与 dlib 库实现的人脸识别方案展开核心是欧式距离算法将人脸图像转为 128D 特征向量后通过计算特征间欧式距离判断是否为同一张脸通常距离低于 0.6 即视为匹配。内容面向具备一定 Python 基础、希望入门或复现人脸识别流程的学习者与开发者可用于课程设计、项目实践或算法验证等场景。压缩包共 20 个文件约 27.36MB包含 5 个 py 源码、3 个 pyc 编译文件、若干 jpeg 与 png 测试图片、2 个 dat 模型文件、1 份 docx 设计报告以及 csv 特征数据、txt 依赖说明、md 说明文档和 license 等覆盖从源码到模型再到实验数据的完整链路。已有 666 人学习。读者可据此掌握人脸注册、特征提取、特征保存与比对识别的完整实现思路并借助设计报告与示例数据快速跑通流程、理解欧式距离阈值判定逻辑。1. 从一张 128D 特征向量说起这套 dlib 人脸识别包到底能跑出什么很多人第一次接触人脸识别脑子里想的是「摄像头一照门就开了」但真正落到代码层面核心其实是一串数字。这个资源包做的事情很朴素用 dlib 把一张人脸图片转成一个 128 维的浮点向量再用欧式距离去量两张脸的向量差多少。差值小于 0.6基本可以判定是同一个人。听起来简单但它把「人脸识别」这件玄学的事情变成了一个可以复现、可以调参、可以排查的工程问题。包里带的东西挺全face_128D.py负责把人脸转成 128D 特征face_register.py负责注册人脸并保存特征到 CSVface_recognition.py负责比对识别dlib_tools.py和cv_tools.py是两个工具模块data目录下放了cmp1.png、harden1.jpeg、james.jpeg等测试图片feature 1.csv是已经提取好的特征文件还有两个关键模型文件dlib_face_recognition_resnet_model_v1.dat和shape_predictor_5_face_landmarks.dat。另外附了一份设计报告 Word对写课程设计或者做技术方案的人来说这份文档的价值不比代码低。这套东西适合谁如果你正在做一个人脸识别门禁的原型、想理解 128D 特征到底怎么用、或者需要一个能跑通的 dlib 入门项目它很合适。但如果你期待的是工业级、高并发、戴口罩也能识别的方案那得先看完后面的坑再决定要不要投入时间。2. 环境搭起来dlib 安装、模型文件放置与依赖版本对齐2.1 为什么 dlib 的安装总是翻车dlib 不是一个纯 Python 库它底层是 C 写的安装的时候需要编译。很多人pip install dlib直接报错大概率是三个原因没有 CMake、没有 C 编译器、Python 版本和 dlib 版本对不上。Windows 上最常见的是缺 Visual Studio Build ToolsLinux 上通常是缺cmake和gmacOS 上则是 Xcode Command Line Tools 没装全。我一般会先确认三件事Python 版本、pip 版本、有没有编译工具链。这个包里的requirements.txt应该列了依赖但 dlib 的版本往往需要根据你的 Python 版本来选。Python 3.8 及以下可以用比较老的 dlib 版本Python 3.9 以上建议用 19.22 之后的版本。如果你不想折腾编译conda 安装 dlib 会省事很多因为 conda 有预编译好的包。# 先升级 pip 和 setuptools避免因为打包工具太旧导致安装失败 python -m pip install --upgrade pip setuptools wheel # 安装 CMakedlib 编译需要它来找编译配置 pip install cmake # 安装 dlib如果本机编译环境齐全这一步会直接编译安装 pip install dlib # 如果上面失败试试 conda 渠道前提是你用的是 conda 环境 conda install -c conda-forge dlib逻辑说明第一步升级 pip 和 setuptools 是为了避免旧版打包工具不识别新的 wheel 格式。第二步装 cmake 是因为 dlib 的 setup.py 会调用 cmake 来生成编译配置。第三步才是真正安装 dlib。如果第三步报错看错误信息里有没有CMake Error或者Microsoft Visual C 14.0 is required有的话就对应去补编译工具。conda 那条是备选方案适合不想在本机装编译器的场景。参数说明--upgrade表示强制升级到最新版-c conda-forge指定从 conda-forge 频道拉包这个频道的 dlib 更新比较及时。2.2 模型文件放哪里、怎么加载这个包里有两个.dat文件它们不是可执行文件而是 dlib 的模型权重。shape_predictor_5_face_landmarks.dat负责定位人脸的 5 个关键点两只眼睛、鼻尖、两个嘴角dlib_face_recognition_resnet_model_v1.dat负责把对齐后的人脸转成 128D 向量。这两个文件必须放在代码能读到的路径下否则dlib.get_frontal_face_detector()能跑但一到特征提取就报错。我一般会把模型文件放在项目根目录下的models文件夹里然后在代码里用相对路径加载。这个包的结构看起来是直接放在根目录那加载的时候就要注意工作目录。如果你在src目录下跑脚本而模型在上一级路径就得写成../dlib_face_recognition_resnet_model_v1.dat。import dlib import os # 获取当前脚本所在目录避免因为工作目录不同导致找不到模型 BASE_DIR os.path.dirname(os.path.abspath(__file__)) # 拼接模型文件的绝对路径 shape_predictor_path os.path.join(BASE_DIR, shape_predictor_5_face_landmarks.dat) face_rec_model_path os.path.join(BASE_DIR, dlib_face_recognition_resnet_model_v1.dat) # 加载人脸检测器、关键点预测器和特征提取模型 detector dlib.get_frontal_face_detector() sp dlib.shape_predictor(shape_predictor_path) facerec dlib.face_recognition_model_v1(face_rec_model_path) print(模型加载完成)逻辑说明os.path.abspath(__file__)拿到当前脚本的绝对路径再取目录这样无论你从哪个目录执行脚本模型路径都是对的。get_frontal_face_detector()是 dlib 自带的 HOG 人脸检测器不需要额外模型文件。shape_predictor和face_recognition_model_v1都需要对应的.dat文件。参数说明shape_predictor_5_face_landmarks.dat里的 5 表示只定位 5 个点速度比 68 点版本快适合实时场景。dlib_face_recognition_resnet_model_v1.dat是 ResNet 结构的模型输出 128 维向量这个维度是固定的不能改。2.3 requirements.txt 里没写全的依赖这个包的requirements.txt大概率只列了 dlib、opencv-python、numpy 这几个核心库。但实际跑的时候你可能还需要scikit-learn来做距离计算或者pandas来读写 CSV。feature 1.csv这个文件名里带空格用 pandas 读的时候要注意。# 核心依赖 pip install dlib opencv-python numpy # 如果代码里用了 sklearn 的 euclidean_distances pip install scikit-learn # 如果要处理 CSV 特征文件 pip install pandas逻辑说明dlib 负责人脸检测和特征提取opencv-python 负责图片读取和显示numpy 负责向量运算。scikit-learn 和 pandas 不是必须的但如果你要扩展代码或者做批量处理它们会省很多事。参数说明opencv-python是完整版包含 GUI 功能如果只在服务器上跑可以用opencv-python-headless减小体积。3. 128D 特征提取与欧式距离比对从注册到识别的完整链路3.1 人脸注册把一张脸变成 CSV 里的一行注册的逻辑是读一张图片检测人脸提取 128D 特征然后把特征和对应的名字存到 CSV 里。这个包里的face_register.py应该就是干这个的。feature 1.csv是已经生成好的特征文件你可以直接用它来测试识别也可以自己重新注册。import dlib import cv2 import numpy as np import csv import os detector dlib.get_frontal_face_detector() sp dlib.shape_predictor(shape_predictor_5_face_landmarks.dat) facerec dlib.face_recognition_model_v1(dlib_face_recognition_resnet_model_v1.dat) def register_face(image_path, name, csv_pathfeature 1.csv): # 用 OpenCV 读图片注意 OpenCV 默认是 BGR 格式 img cv2.imread(image_path) if img is None: print(f读不到图片{image_path}) return # 转成 RGB因为 dlib 期望 RGB 输入 rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 检测人脸1 表示对图片放大一倍再检测能提高小脸检出率 faces detector(rgb, 1) if len(faces) 0: print(f没检测到人脸{image_path}) return # 取第一张脸提取 128D 特征 shape sp(rgb, faces[0]) face_descriptor facerec.compute_face_descriptor(rgb, shape) # 转成 list方便写入 CSV vec list(face_descriptor) # 如果 CSV 不存在先写表头 file_exists os.path.isfile(csv_path) with open(csv_path, a, newline) as f: writer csv.writer(f) if not file_exists: header [name] [ff{i} for i in range(128)] writer.writerow(header) writer.writerow([name] vec) print(f已注册{name}特征维度{len(vec)}) # 注册示例 register_face(data/harden1.jpeg, harden) register_face(data/james.jpeg, james)逻辑说明cv2.imread读进来是 BGRdlib 要 RGB所以必须转。detector(rgb, 1)里的 1 是上采样次数对远景或者小脸有用但会增加计算量。compute_face_descriptor返回的是一个 dlib 向量对象转成 list 才能写 CSV。CSV 第一列是名字后面 128 列是特征值。参数说明csv_path默认是feature 1.csv文件名里有空格写路径的时候要加引号或者用转义。newline是为了避免 Windows 下 CSV 出现空行。3.2 欧式距离怎么算、0.6 这个阈值怎么来的欧式距离的公式很简单两个 128 维向量对应维度相减平方求和再开根号。dlib 官方给出的经验阈值是 0.6意思是如果两张脸的 128D 特征欧式距离小于 0.6就认为是同一个人。这个 0.6 不是理论推导出来的而是在大量人脸数据上跑出来的经验值。import numpy as np def euclidean_distance(vec1, vec2): # 转成 numpy 数组方便做向量运算 a np.array(vec1, dtypenp.float64) b np.array(vec2, dtypenp.float64) # 对应维度相减平方求和开根号 diff a - b squared np.square(diff) total np.sum(squared) distance np.sqrt(total) return distance # 假设从 CSV 里读到了两个特征向量 vec_a [0.1] * 128 # 这里只是占位实际要从 CSV 读 vec_b [0.11] * 128 dist euclidean_distance(vec_a, vec_b) print(f欧式距离{dist:.4f}) print(判定结果, 同一人 if dist 0.6 else 不同人)逻辑说明np.array把 list 转成 numpy 数组dtypenp.float64保证精度。np.square对差值逐元素平方np.sum求和np.sqrt开根号。整个过程就是标准欧式距离公式的代码化。参数说明0.6是 dlib 官方推荐阈值但在实际场景里可以调。如果你要求高安全性可以降到 0.5如果光线变化大、角度偏可以放宽到 0.65。但不要低于 0.4否则同一个人不同角度都可能被拒。3.3 识别流程从 CSV 读特征、逐条比对、输出结果识别的时候先把待识别图片转成 128D 特征然后从 CSV 里读出所有已注册的特征逐条算欧式距离取最小值。如果最小值小于 0.6就输出对应的名字否则输出「未知」。import csv def load_features(csv_pathfeature 1.csv): names [] features [] with open(csv_path, r) as f: reader csv.reader(f) header next(reader) # 跳过表头 for row in reader: names.append(row[0]) features.append([float(x) for x in row[1:]]) return names, features def recognize(image_path, csv_pathfeature 1.csv, threshold0.6): img cv2.imread(image_path) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) faces detector(rgb, 1) if len(faces) 0: return 未检测到人脸 shape sp(rgb, faces[0]) descriptor facerec.compute_face_descriptor(rgb, shape) target np.array(list(descriptor), dtypenp.float64) names, features load_features(csv_path) best_name 未知 best_dist float(inf) for name, feat in zip(names, features): dist euclidean_distance(target, feat) if dist best_dist: best_dist dist best_name name if best_dist threshold: return f{best_name}距离{best_dist:.4f} else: return f未知最小距离{best_dist:.4f} # 测试识别 result recognize(data/cmp1.png) print(result)逻辑说明load_features把 CSV 里的名字和特征分别读成两个列表。recognize先提取待识别图片的特征然后遍历所有已注册特征记录最小距离和对应名字。最后用阈值判断。参数说明threshold默认 0.6可以根据场景调整。best_dist初始化为无穷大保证第一次比较一定能更新。4. 避坑与排查dlib 人脸识别里最容易翻车的五个地方4.1 检测不到人脸但图片里明明有人现象跑detector(rgb, 1)返回空列表或者只检测到一部分脸。原因dlib 的 HOG 检测器对正面、光线均匀的人脸效果最好。如果人脸偏转超过 30 度、光线太暗、或者人脸在图片里占比太小就容易漏检。解决先确认图片是不是 RGB 格式OpenCV 读进来是 BGR不转 RGB 直接喂给 dlib 会出问题。然后试着把上采样次数从 1 改成 2能提高小脸检出率但速度会慢。如果还是不行考虑换 CNN 人脸检测器dlib.cnn_face_detection_model_v1需要额外的模型文件但侧脸检出率更高。4.2 欧式距离算出来总是大于 0.6现象同一个人两张照片距离算出来 0.7 甚至 0.8被判定为不同人。原因两张照片的光线、角度、表情差异太大导致 128D 特征本身就不接近。另外如果人脸对齐没做好关键点定位偏了特征也会偏。解决注册的时候尽量用正面、光线均匀的照片。识别的时候如果距离在 0.6 到 0.7 之间可以人工看一眼不要完全依赖阈值。另外检查shape_predictor是不是加载对了5 点模型和 68 点模型输出的对齐结果不一样混用会导致特征偏差。4.3 CSV 读写时中文名字乱码现象注册的时候名字是中文写到 CSV 里变成乱码读出来也对不上。原因Python 的open默认编码跟系统有关Windows 上可能是 GBKLinux 上可能是 UTF-8。CSV 模块本身不处理编码。解决打开文件的时候显式指定encodingutf-8写和读都要加。如果还是乱码试试utf-8-sig这个编码会在文件开头加 BOMExcel 打开不会乱码。with open(csv_path, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) # 写入逻辑4.4 模型文件路径写错报错信息看不懂现象运行的时候报RuntimeError: Unable to open shape_predictor_5_face_landmarks.dat或者类似的文件找不到错误。原因dlib 的模型加载函数不会告诉你它到底在哪个目录找文件只会说打不开。如果你用的是相对路径而工作目录不是脚本所在目录就会找不到。解决统一用os.path.dirname(os.path.abspath(__file__))拼绝对路径。如果模型文件在上一级就用os.path.join(BASE_DIR, .., model.dat)。另外确认文件名大小写Linux 下大小写敏感。4.5 图片格式支持不全读进来是 None现象cv2.imread返回 None后面所有操作都报错。原因OpenCV 对某些格式支持不好比如带透明通道的 PNG、CMYK 模式的 JPEG、或者文件本身损坏。解决先确认文件存在用os.path.isfile检查。然后用 PIL 读一遍看能不能正常打开。如果 PIL 能读就用 PIL 读进来转成 numpy 数组再转 RGB 喂给 dlib。from PIL import Image def read_image_safe(path): try: img Image.open(path).convert(RGB) return np.array(img) except Exception as e: print(f读图失败{e}) return None5. 进阶技巧用 128D 特征做批量比对和阈值调优5.1 批量比对一次算完所有距离矩阵如果你要在一个小规模人脸库里做全量比对逐条循环算欧式距离太慢。可以用 numpy 的广播机制一次性算出距离矩阵。假设你有 N 个已注册特征M 个待识别特征可以构造一个 N×M 的距离矩阵。def distance_matrix(features_a, features_b): # features_a: N x 128, features_b: M x 128 a np.array(features_a, dtypenp.float64) b np.array(features_b, dtypenp.float64) # 利用广播(N,1,128) - (1,M,128) - (N,M,128) diff a[:, np.newaxis, :] - b[np.newaxis, :, :] squared np.square(diff) dist np.sqrt(np.sum(squared, axis2)) return dist # 返回 N x M 矩阵逻辑说明a[:, np.newaxis, :]把 a 变成 (N,1,128)b[np.newaxis, :, :]把 b 变成 (1,M,128)相减的时候 numpy 自动广播成 (N,M,128)。然后对最后一个维度求和再开根号得到 (N,M) 的距离矩阵。参数说明这个方法在 N 和 M 都不大的时候很快但如果 N 超过几千内存会吃紧。那种情况就得分块计算。5.2 阈值调优用 ROC 曲线找最佳切点0.6 是 dlib 的默认值但你的场景不一定适用。如果你有一组标注好的正负样本对同一人的两张照片、不同人的两张照片可以画 ROC 曲线找约登指数最大的点作为阈值。from sklearn.metrics import roc_curve def find_best_threshold(genuine_dists, impostor_dists): # genuine_dists: 同一人的距离列表 # impostor_dists: 不同人的距离列表 y_true [1] * len(genuine_dists) [0] * len(impostor_dists) y_scores genuine_dists impostor_dists # 注意距离越小越像同一人所以用负距离作为分数 fpr, tpr, thresholds roc_curve(y_true, [-s for s in y_scores]) youden tpr - fpr best_idx np.argmax(youden) best_threshold -thresholds[best_idx] return best_threshold逻辑说明roc_curve期望分数越大越可能是正类但欧式距离是越小越像同一人所以取负号。youden tpr - fpr是约登指数最大值对应的阈值就是最佳切点。参数说明genuine_dists和impostor_dists需要你自己构造一般每个至少几十对才有统计意义。如果样本太少调出来的阈值可能过拟合。5.3 特征归一化让距离更稳定dlib 输出的 128D 特征本身已经是归一化的但如果你自己做了某些变换或者从不同来源拿到的特征最好再确认一下 L2 范数。归一化之后欧式距离和余弦距离会有对应关系阈值也更好解释。def l2_normalize(vec): arr np.array(vec, dtypenp.float64) norm np.linalg.norm(arr) if norm 0: return arr return arr / norm逻辑说明np.linalg.norm默认算 L2 范数也就是向量的长度。除以长度之后向量长度变成 1。这样两个向量的欧式距离范围就在 0 到 2 之间0 表示完全一样2 表示完全相反。参数说明dlib 的 128D 特征本身长度接近 1所以这一步通常不需要。但如果你把特征存到 CSV 再读出来浮点数精度损失可能导致长度有微小变化归一化可以消除这个影响。5.4 一个我踩过的坑CSV 里的特征精度feature 1.csv里的特征值如果被 Excel 打开过再保存可能会被截断成几位小数导致距离计算偏差。我一般会先用csv模块读一遍确认每行有 129 列1 列名字 128 列特征并且特征值的小数位数足够。如果发现精度不够就重新跑一遍注册脚本不要用 Excel 编辑特征文件。从那以后我每次拿到新的特征 CSV都会先跑一遍列数和精度检查确认没问题再往下做。希望帮到你。本文还有配套的精品资源点击获取