尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python+dlib人脸识别实战:128维向量与欧式距离阈值调优

Python+dlib人脸识别实战:128维向量与欧式距离阈值调优 简介这份资源围绕 Python 与 dlib 库实现的人脸识别项目展开核心是欧式距离算法将人脸图像转为 128D 特征向量后通过计算特征差异判断是否为同一张脸通常差距在 0.6 以下即视为同一人。内容面向图像识别入门者、课程设计或毕业设计需求者帮助理解人脸识别从采集、特征提取、特征保存到比对识别的完整流程。压缩包共 20 个文件约 27.36MB包含 5 个 py 源码、3 个 pyc 缓存、若干 jpeg 与 png 测试图片、2 个 dat 模型文件、1 份 docx 设计报告以及 csv 特征数据、txt 依赖说明和 md 说明文档覆盖代码、数据与报告三类材料。目前已有 665 人学习下载。读者可据此获得可运行的人脸注册与识别脚本、128D 特征提取与欧式距离比对实现、预训练模型与测试样本并借助设计报告梳理算法原理与项目结构适合作为人脸识别入门实践与二次开发的参考。1. 用 pythondlib 的欧式距离做人脸识别为什么 128 维向量比你想的更耐用很多人第一次听到「pythondlib 的欧式距离算法进行人脸识别」脑子里浮现的是摄像头一扫就认出人的画面但真正动手时卡住的地方往往不是模型而是「两张脸到底怎么算相似」。dlib 给出的答案是把一张人脸压成一个 128 维向量再用欧式距离衡量两个向量的远近。这个思路看起来朴素却比早期基于特征点几何比例的方法稳得多因为它不依赖你精确标出眼角嘴角而是让深度模型自己学出对光照、表情、姿态更鲁棒的表征。这套方案适合谁适合想在自己电脑上跑通人脸识别全流程的人用 Python 做胶水dlib 做检测和特征提取欧式距离做判定。它不需要 GPU 也能跑树莓派、门禁机这类边缘设备上也有大量落地案例。代价是它对「注册质量」很敏感——同一个人如果只注册一张侧脸照后面正脸来刷大概率翻车。所以这篇笔记不打算停在概念上而是把环境、检测、对齐、特征、距离阈值、避坑一条条拆开让你能照着复现也能判断它值不值得放进你的项目里。2. 环境与依赖把 dlib 装进 Python 的正确姿势2.1 为什么 dlib 的安装总是第一个坑dlib 不是纯 Python 包它底层是 C 模板库安装时要现场编译。很多人pip install dlib直接报错不是网络问题而是缺 CMake 和 C 编译器。Windows 上最常见的是缺 Visual Studio Build ToolsLinux 上缺build-essential和cmakemacOS 上缺 Xcode Command Line Tools。另一个隐形坑是 Python 版本dlib 对 3.12 的预编译轮子覆盖不全3.8 到 3.11 通常最省事。我一般会先确认三件事Python 版本、CMake 是否在 PATH 里、编译器能不能用。确认完再装能省掉大量「编译到一半失败」的时间。如果你只是想做识别而不是改 dlib 源码优先找带预编译轮子的版本别硬刚源码编译。2.2 最小可复现的安装命令# 先确认 Python 版本建议 3.8 - 3.11 python --version # Linux 下补齐编译工具链 sudo apt update sudo apt install -y build-essential cmake python3-dev # 安装核心依赖顺序不要乱 pip install numpy opencv-python pip install dlib # 验证 dlib 是否可用 python -c import dlib; print(dlib.__version__)逻辑说明build-essential和cmake是 dlib 编译的硬前提python3-dev提供 Python 头文件。numpy和opencv-python后面读图、画框都要用。最后一行验证很关键如果它报ImportError说明 dlib 没装成功不要继续往下写代码。参数说明pip install dlib不加版本号会拉最新版如果你在旧设备上跑可以指定一个已知稳定的版本比如dlib19.24.0。Windows 用户如果编译失败可以先用pip install cmake把 CMake 装进 Python 环境再重试 dlib。提示不要在虚拟环境外直接装 dlib一旦编译失败会污染全局环境。用python -m venv faceenv建一个干净环境再操作。2.3 模型文件从哪来、放哪去dlib 的人脸检测和特征提取依赖两个预训练模型一个用于人脸检测一个用于 68 点关键点定位还有一个用于把脸映射成 128 维向量。常见做法是下载官方提供的.dat文件放到项目目录下的models/文件夹。代码里用相对路径引用别写死绝对路径否则换台机器就找不到。import os import dlib MODEL_DIR os.path.join(os.path.dirname(__file__), models) detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor( os.path.join(MODEL_DIR, shape_predictor_68_face_landmarks.dat) ) face_rec dlib.face_recognition_model_v1( os.path.join(MODEL_DIR, dlib_face_recognition_resnet_model_v1.dat) )逻辑说明get_frontal_face_detector是 dlib 自带的 HOG 检测器不需要额外模型文件。shape_predictor和face_recognition_model_v1必须加载对应的.dat文件路径拼错会直接抛异常。用os.path.dirname(__file__)是为了让路径相对于脚本本身而不是当前工作目录。参数说明HOG 检测器对正脸效果好、速度快但侧脸和遮挡容易漏检。如果你要处理监控画面可以换成 CNN 检测器dlib.cnn_face_detection_model_v1代价是需要 GPU 才跑得动。3. 从一张图到 128 维向量检测、对齐、编码三步走3.1 人脸检测先找到脸再谈识别检测是整个流程的入口。dlib 的 HOG 检测器返回的是矩形框每个框代表一张脸的位置。这里有个容易被忽略的点检测器对图像尺寸敏感。图太大小人脸会被漏掉图太小检测框会抖。我一般会把输入图缩放到宽度 800 左右再检测检测完再把坐标映射回原图。import cv2 import dlib def detect_faces(image_path): img cv2.imread(image_path) if img is None: raise FileNotFoundError(f读不到图片: {image_path}) # 缩放到宽度 800兼顾速度和召回 scale 800 / img.shape[1] resized cv2.resize(img, (800, int(img.shape[0] * scale))) detector dlib.get_frontal_face_detector() # 第二个参数 1 表示上采样一次能提升小人脸召回 faces detector(resized, 1) # 把检测框映射回原图坐标 results [] for f in faces: x1 int(f.left() / scale) y1 int(f.top() / scale) x2 int(f.right() / scale) y2 int(f.bottom() / scale) results.append((x1, y1, x2, y2)) return img, results逻辑说明cv2.resize把图缩到固定宽度保证检测耗时稳定。detector(resized, 1)里的1是上采样次数设成 1 能明显提升小脸召回但耗时翻倍。检测完必须把坐标除以缩放比例否则画框会偏。参数说明宽度 800 是经验值人脸占画面比例大时可以降到 600画面里人多时可以升到 1200。上采样次数一般用 0 或 1用 2 会非常慢除非你确定画面里脸特别小。3.2 关键点对齐让 128 维向量不受姿态干扰检测到脸之后dlib 会用 68 个关键点标出眉毛、眼睛、鼻子、嘴巴、下巴的轮廓。这一步不是为了好看而是为了做人脸对齐。对齐的意思是把脸旋转、缩放、平移到一个标准姿态再送进特征网络。不做对齐同一个人歪头和正脸算出来的向量距离会偏大阈值就不好定。def get_face_chip(img, rect): predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) # dlib 的 rectangle 需要重新构造 dlib_rect dlib.rectangle(rect[0], rect[1], rect[2], rect[3]) shape predictor(img, dlib_rect) # get_face_chip 内部会做对齐输出 150x150 的标准脸 face_chip dlib.get_face_chip(img, shape, size150, padding0.25) return face_chip逻辑说明predictor返回 68 个点的坐标get_face_chip根据这些点做仿射变换把脸摆正并裁成固定大小。size150是 dlib 特征网络期望的输入尺寸padding0.25表示在脸周围多留 25% 边距避免裁得太紧丢掉下巴和额头信息。参数说明padding一般用 0.25脸特别大时可以降到 0.15脸特别小时可以升到 0.35。size不要改改了特征网络输入不匹配识别率会掉。3.3 生成 128 维向量欧式距离的输入从这来对齐后的脸送进face_recognition_model_v1输出就是一个 128 维的浮点向量。这个向量就是这张脸的「数字指纹」。同一个人不同照片的向量欧式距离通常小于 0.6不同人的向量距离通常大于 0.6。这个 0.6 不是绝对标准而是 dlib 官方在 LFW 数据集上给出的经验阈值。import numpy as np def face_encoding(face_chip): face_rec dlib.face_recognition_model_v1( models/dlib_face_recognition_resnet_model_v1.dat ) # compute_face_descriptor 返回 128 维向量 encoding face_rec.compute_face_descriptor(face_chip) return np.array(encoding) def euclidean_distance(a, b): # 欧式距离逐元素差平方和开根号 return np.linalg.norm(a - b)逻辑说明compute_face_descriptor接收的是对齐后的face_chip不是原图。如果你直接把原图裁出来的脸丢进去距离会偏大。np.linalg.norm(a - b)就是欧式距离等价于sqrt(sum((a_i - b_i)^2))。参数说明128 维向量每个元素是浮点数范围大致在 -1 到 1 之间。欧式距离阈值 0.6 是起点实际项目里要根据你的数据调。注册照质量高、场景稳定可以降到 0.5注册照少、光照变化大可以升到 0.65。4. 欧式距离判定阈值怎么定、注册库怎么建4.1 注册库把已知人脸存成向量文件识别的前提是有一批已知人脸。常见做法是给每个人拍几张照片提取 128 维向量存成一个字典或 JSON 文件。不要存原图存向量就够了既省空间又避免隐私问题。注册时每个人至少 3 张不同角度、不同光照各一张这样后面判定时可以用「最小距离」而不是「平均距离」。import json import os def build_database(image_dir, output_path): db {} for person_name in os.listdir(image_dir): person_dir os.path.join(image_dir, person_name) if not os.path.isdir(person_dir): continue encodings [] for img_name in os.listdir(person_dir): img_path os.path.join(person_dir, img_name) img, rects detect_faces(img_path) if len(rects) ! 1: # 注册照必须只有一张脸否则跳过 continue chip get_face_chip(img, rects[0]) encodings.append(face_encoding(chip).tolist()) if encodings: db[person_name] encodings with open(output_path, w) as f: json.dump(db, f) return db逻辑说明遍历每个人一个文件夹每张图检测人脸只保留恰好一张脸的图。多张脸或没脸的图直接跳过避免污染注册库。encodings存成列表JSON 不支持 numpy 数组所以用.tolist()转换。参数说明image_dir下每个子文件夹名就是人名。注册照建议 3 到 5 张太少容易漏判太多没必要。如果某个人一张合格图都没有这个人在库里就不存在后面识别会返回「未知」。4.2 识别最小距离 阈值双条件识别时把待测脸的向量和库里每个人所有向量算欧式距离取最小值。如果最小值小于阈值就判为那个人否则判为未知。这里有个细节不要用平均距离因为注册照里可能有一张质量差平均会把好的也拉偏。最小距离更稳。def recognize(encoding, db, threshold0.6): best_name unknown best_dist float(inf) for name, encodings in db.items(): for ref in encodings: dist euclidean_distance(encoding, np.array(ref)) if dist best_dist: best_dist dist best_name name if best_dist threshold: return unknown, best_dist return best_name, best_dist逻辑说明双重循环遍历库里所有向量记录最小距离和对应人名。最后用阈值卡一道超过阈值就返回 unknown。这样即使库里没有这个人也不会强行认成某个最像的人。参数说明threshold默认 0.6实际项目里建议用一批已知数据画 ROC 曲线来定。简单做法是拿 10 个人的各 5 张图两两算距离看同人距离分布和异人距离分布的交界点在哪取交界点附近的值。4.3 阈值调参0.6 不是万能药0.6 是 dlib 在 LFW 上的推荐值但 LFW 是正脸、光照均匀的学术数据集。你的场景如果是门禁、考勤光照和角度都更差0.6 可能偏严导致本人刷不开。反过来如果阈值放到 0.7陌生人误认的概率会上升。我一般会先跑一批测试数据统计两个指标本人通过率、陌生人误认率然后取一个业务能接受的平衡点。阈值本人通过率陌生人误认率适用场景0.5偏低极低高安全场景0.6中等低通用门禁0.65较高中等考勤打卡0.7高偏高体验优先注意阈值调高带来的误认在门禁场景里可能意味着陌生人能刷开别人的门。安全敏感场景宁可调低让本人多刷一次也不要放陌生人进去。5. 避坑与排查那些让我重跑一整天的细节5.1 检测框偏移缩放后忘了映射回原图现象画出来的框整体偏左上或偏右下人脸明明在中间框却跑到边上。原因检测时把图缩放了但画框时直接用缩放后的坐标。解决检测完把left/top/right/bottom全部除以缩放比例再画框。这个坑我踩过不止一次尤其是代码里同时处理多张不同尺寸的图时缩放比例忘了逐图更新。5.2 距离普遍偏大用了原图裁脸而不是对齐脸现象同一个人两张照片算出来距离 0.8超过阈值被判成未知。原因直接把检测框里的脸裁出来送进特征网络没有做关键点对齐。解决必须先用shape_predictor拿 68 点再用get_face_chip做对齐最后才compute_face_descriptor。对齐这一步省不得省了识别率直接掉一个档次。5.3 注册照多张脸库里混进别人的向量现象某个人识别时总是串到另一个人。原因注册照里有一张是合影检测出多张脸代码取了第一张结果把别人的脸注册进去了。解决注册阶段强制要求len(rects) 1多脸或没脸的图直接跳过并在日志里打出来方便补拍。5.4 内存持续上涨每帧都重新加载模型现象视频流跑几分钟后内存爆掉。原因在循环里反复调用dlib.shape_predictor和face_recognition_model_v1每次都在加载模型文件。解决把 detector、predictor、face_rec 在循环外初始化一次循环里只调用它们的推理方法。这个坑在写 demo 时不容易发现一上视频流就暴露。5.5 侧脸识别失败HOG 检测器本身就不擅长侧脸现象正脸能识别一转头就检测不到。原因HOG 检测器基于梯度方向直方图对正脸敏感侧脸特征分布变化大容易漏检。解决换 CNN 检测器或者要求用户正对摄像头。如果场景里侧脸不可避免可以在注册时多存几张侧脸但检测这一关还是得先过。6. 进阶技巧用多帧投票把误判压下去单帧识别的稳定性有限尤其是视频流里偶尔一帧光照突变距离就会跳。我后来习惯用多帧投票连续取 10 帧每帧算一个识别结果最后取出现次数最多的那个。如果 10 帧里 unknown 超过一半就判为未知。这样单帧的玄学波动会被平滑掉门禁场景下体验提升很明显。from collections import Counter def recognize_video(frame_encodings, db, threshold0.6, vote_window10): results [] for enc in frame_encodings[-vote_window:]: name, dist recognize(enc, db, threshold) results.append(name) # 统计出现次数取最高 counter Counter(results) top_name, top_count counter.most_common(1)[0] # 如果最高票不到一半判为未知 if top_count len(results) / 2: return unknown return top_name逻辑说明frame_encodings是最近若干帧的 128 维向量列表vote_window控制投票窗口大小。Counter统计每个名字出现次数most_common(1)取最高票。如果最高票不过半说明结果分散判为未知更安全。参数说明vote_window一般用 10 到 15太小平滑效果不够太大响应变慢。门禁场景可以设 10考勤场景可以设 15。阈值仍然用 0.6 起步投票只解决单帧抖动不解决阈值本身偏严或偏松的问题。还有一个我常用的技巧注册时给每个人存一个「质心向量」即所有注册向量的平均识别时先和质心比一次如果距离小于 0.5 就直接通过不再遍历所有向量。这样能减少计算量也能避免某张质量差的注册照把结果带偏。质心向量的代码很简单就是np.mean(encodings, axis0)但要注意质心只适合注册照质量均匀的情况如果注册照角度差异大质心反而会落在中间不如逐张比。最后说个血泪经验dlib 的 128 维向量对光照的敏感度比你想的高。同一个人的两张照片一张室内暖光、一张室外冷光距离可能到 0.55接近阈值。解决办法不是调阈值而是在注册阶段就覆盖不同光照或者在前处理里做直方图均衡化。我现在的习惯是注册照至少包含一张顺光、一张逆光、一张侧光这样后面识别时阈值可以稳在 0.6不用反复调。希望帮到你。本文还有配套的精品资源点击获取
返回列表