☰
dlib人脸识别实战:128D特征提取与欧式距离比对全解析
2026/10/4 5:10:29 网站建设 项目流程

简介:这份资源围绕 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负责注册人脸并保存特征到 CSV,face_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 Tools,Linux 上通常是缺cmake和g++,macOS 上则是 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 # 安装 CMake,dlib 编译需要它来找编译配置 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_path="feature 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"] + [f"f{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读进来是 BGR,dlib 要 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, dtype=np.float64) b = np.array(vec2, dtype=np.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 数组,dtype=np.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_path="feature 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_path="feature 1.csv", threshold=0.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), dtype=np.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 上可能是 GBK,Linux 上可能是 UTF-8。CSV 模块本身不处理编码。

解决:打开文件的时候显式指定encoding="utf-8",写和读都要加。如果还是乱码,试试utf-8-sig,这个编码会在文件开头加 BOM,Excel 打开不会乱码。

with open(csv_path, "a", newline="", encoding="utf-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 None

5. 进阶技巧:用 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, dtype=np.float64) b = np.array(features_b, dtype=np.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, axis=2)) 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, dtype=np.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,都会先跑一遍列数和精度检查,确认没问题再往下做。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询