
简介一个面向人脸关键点检测的轻量级模型包源自insightface项目基于MXNet实现可对二维图像中的人脸定位106个关键点覆盖眼睛、眉毛、鼻子、嘴巴、脸颊等部位适合面部识别、表情分析、姿态估计及AR/VR面部追踪等场景。压缩包共2个文件模型结构文件json描述神经网络各层与连接方式权重参数文件params存储训练后的参数二者配合可在MXNet环境中完成加载与推理。整个资源仅4.43MB结构简洁便于快速集成到现有视觉流程中。开发者可通过此资源了解轻量级关键点检测模型的架构组织并直接用于人脸对齐、美颜特效等应用开发省去从零训练的繁琐过程。目前已有1522人学习下载适合计算机视觉入门者及需要高精度人脸关键点输出的工程师作为参考。 很多搞人脸方向的朋友应该都有过这种经历好不容易在网上找到一个看起来挺靠谱的模型包下载下来是一个2d106det.zip双击一解压要么报invalid zip archive: could not find eocd要么解出来一堆奇奇怪怪的文件夹兴致直接被浇灭一半。我最近在做实时人脸特效项目需要106点人脸关键点正好就是从这样一个压缩包开始的。这篇文章就聊聊我从这个zip包到真正跑通检测流程的全过程包括那些很多人不会写进文档里的坑。先说结论2d106det.zip这类文件通常不是单纯的“解压就能用”的权重包它里面往往混合了模型结构定义、预训练权重、示例脚本甚至是对应框架的版本依赖信息。能不能顺利跑起来一半看解压环节运气另一半看你对运行环境的理解。下面我会把整个过程拆开一步一步说清楚。1. 这个压缩包的真实身份2D106点人脸关键点检测模型1.1 为什么是106个点人脸关键点检测目前常见的有五个点、68点、81点、106点、240点、468点等不同方案。106点的体系在国内很多实时美颜、贴纸、人脸特效项目里用得非常多因为它既保持了关键点的稠密度又不像468点那样需要极其庞大的模型才能维持实时性。106点主要覆盖了眉毛、眼睛、鼻子、嘴巴、脸部轮廓等区域对于常规特效贴纸、表情识别、视线估计来说已经足够了。从命名上拆解2d106det可以理解为“2D 106-point detection”也就是说这个模型输出的是二维坐标下的106个关键点而不是带深度的3D关键点。你拿到这个zip包本质上是拿到了一种“从人脸区域的图像到106个坐标”的映射能力。1.2 包内通常包含什么我之前在网上找到的这个包解压后大致结构是这样的2d106det/ ├── models/ │ ├── 2d106det.onnx │ ├── 2d106det.pth │ └── face_detector/ ├── utils/ │ ├── align.py │ └── draw.py ├── demo.py ├── requirements.txt └── README.md不同来源的包结构可能差异很大但核心东西一般逃不出这几类模型权重文件常见格式有.onnx、.pth、.caffemodel、.tflite、.rknn等对应不同的推理框架。人脸检测器因为106点模型本身通常不负责找脸它需要先有人脸框再对框内区域做关键点回归。所以包里往往还会带一个轻量级的人脸检测模型比如 RFB、SCRFD、RetinaFace 的轻量版本。对齐和绘制工具用于把模型输出的坐标映射回原图以及可视化。示例代码和依赖清单这个是重点很多人在这一步翻车。1.3 与其他关键点方案的对比我拿常见的68点和468点做个对比方便你判断这个包是否适合自己项目方案点位数典型用途模型体积实时性难度68点68传统表情识别、基础贴纸较小优低106点106实时美颜、特效贴纸、视线估计中等优中468点468高精度人脸重建、AR面具较大中高106点最大的优势是处于“精度和性能平衡点”。468点的拓扑复杂度高在高通骁龙或者边缘设备上跑实时推理要费不少劲68点又太稀疏做眼睛周围这种细粒度贴纸容易穿模。2. 解压安装一次典型的zip文件问题排查2.1 解压报“could not find EOCD”的真相我一开始解压这个文件时直接在系统自带解压工具里双击结果弹出一个很让人崩溃的提示Archive: 2d106det.zip error: invalid zip archive: could not find EOCD这句话的意思是zip文件末尾需要一个End of Central Directory结构也就是EOCD记录它相当于整份zip文件的目录索引和结尾标志。如果解压工具找不到这个标志就会认为文件不完整或者根本不是合法的zip。出现这个问题的原因通常有两个文件下载不完整服务器传输中断或者浏览器断点续传出错导致文件尾部缺失。你可以先看一下下载下来的文件大小和页面上标注的字节数是否一致。文件本身被包装过有些资源站的zip不是标准zip而是在尾部追加了文件尾巴或者做了自定义加密壳。这种情况用常规解压工具解析不到标准EOCD就容易误报。我当时把文件下载了三遍才排查出是下载源的问题。后来我发现一个更稳妥的办法先检查文件完整性再解压。# macOS/Linux shasum -a 256 2d106det.zip # Windows PowerShell Get-FileHash .\2d106det.zip -Algorithm SHA256拿到哈希之后和下载页面上的哈希对比。如果网站没给哈希至少用右键“压缩文件预览”看能不能看到条目。如果工具能预览到目录却无法解压大概率是文件尾部问题可以尝试用 7-Zip 的“打开压缩包”强制读取然后逐个文件拖出来。2.2 分卷压缩包和文件名乱码的处理我见过不少用户跑来问“解压提示必须有下列压缩分卷 z01”这其实说明你拿到的不是完整zip而是分卷压缩的某个分卷如2d106det.z01、2d106det.zip。你需要把同一套分卷全部放在同一个目录并且不能改名然后再对.zip那个文件执行解压。更隐蔽的一个坑是文件名乱码。比如你是Windows用户压缩包是用macOS或者Linux打包的里面中文文件名很可能在你的系统上显示为日文或乱码。尤其某些包里封装了韩文、日文命名的人脸样本图解压后文件名直接变成Ѫ.jpg这种。这时候不需要去找“乱码修复工具”用支持编码识别的解压工具就好。我自己的做法是如果只是临时查看用 7-Zip 打开选项里设置“列表编码”为 UTF-8。如果是要长期使用先把整个zip解压到临时目录然后在终端里用python的zipfile模块手动解压并显式指定文件名编码。import zipfile import os zip_path 2d106det.zip target_dir 2d106det_extracted os.makedirs(target_dir, exist_okTrue) with zipfile.ZipFile(zip_path) as zf: for info in zf.infolist(): # 尝试把文件名强制解析为 UTF-8失败则使用 cp437 try: raw info.filename.encode(cp437) filename raw.decode(utf-8) except Exception: filename info.filename target_path os.path.join(target_dir, filename) os.makedirs(os.path.dirname(target_path), exist_okTrue) with zf.open(info) as src, open(target_path, wb) as dst: dst.write(src.read())这样能规避大多数因编码标记缺失导致的乱码问题。2.3 目录结构整理与直接可用性评估解压出来之后别急着运行demo。先做三件事确认requirements.txt是否存在存在就打开看一眼里面框定的版本范围。确认模型权重文件是否存在且大小不为0。有些网盘分享的包里模型文件被删除或替换成了下载链接文档。确认demo.py或其他示例脚本的入口参数。如果包里的模型文件是.pth这种PyTorch权重而你的项目里用的是TensorFlow那这个包就只能提供“参考价值”不能直接用。遇到这种情况我会去搜一下这个源码仓库是否提供了ONNX或者TensorRT的导出版本。实在没有就得自己在本地搭PyTorch环境把权重转换成目标格式。3. 部署环境与依赖关系比解压更值得注意的事3.1 Python和推理框架版本匹配模型能用不代表代码一定能跑。很多开源人脸模型是老项目它的依赖可能还停留在torch1.7.0、opencv-python4.5.*这种版本。你要是直接装最新版PyTorch有大概率会在加载模型时遇到KeyError: unexpected key in state_dict: ...或者RuntimeError: Error(s) in loading state_dict for Module:这是因为权重是在旧版本模块定义下训练产生的旧模型的state_dict里的键名和最新库的模块定义对不上。解决方案不是硬解而是在项目里用精确版本锁定环境conda create -n face106 python3.8 conda activate face106 pip install -r requirements.txt如果requirements.txt缺失或者没写版本建议去看压缩包里的模型结构代码。若模型是.pth且结构由torch.nn.Module定义那么相同的模块定义必须和训练时完全一致哪怕改变了一个层的命名都会加载失败。3.2 模型权重文件的格式识别models目录下如果有一个.onnx文件和多个.pth文件优先用.onnx。原因很简单ONNX是计算图的一种通用交换格式不依赖PyTorch或者TensorFlow这类特定训练框架只要你有一个ONNX Runtime就能推理。而且ONNX的部署路径通常更干净不会遇到模型代码版本不一致的问题。如果只有一个.pth文件也先不要太担心。用下面的代码可以快速看这个权重文件是否包含优化器状态即训练中断后保存的checkpoint含额外状态需要特殊处理import torch ckpt torch.load(2d106det/models/2d106det.pth, map_locationcpu) print(type(ckpt)) if isinstance(ckpt, dict): print(ckpt.keys())如果打印出来的是{state_dict: ..., optimizer: ...}这样的字典说明它不是一个可直接推理的模型而是训练时保存的checkpoint。你需要提取state_dict然后配合模型定义才能使用。如果打印出来的是collections.OrderedDict那它就是裸的state_dict直接用模型加载即可。3.3 一个容易忽略的opencv版本问题在装依赖时opencv-python的版本坑非常隐蔽。人脸关键点绘制的示例代码里经常用到cv2.putText、cv2.circle这类基础函数这些在新旧版本里都差不多。真正容易出问题的是cv2.dnn模块如果用OpenCV的DNN模块加载人脸检测模型旧版本支持的层格式可能到了新版本就不兼容了。比如我在某个版本的OpenCV里加载一个旧的Caffe人脸检测模型会报OpenCV(4.9.0) Error: Unspecified error in function ReadProtoFromBinaryFile这种问题大多出在OpenCV版本升级后对某些op层不再兼容。建议先固定一个成熟版本我在实际项目中用的是opencv-python4.8.1.78这个版本对常见的人脸检测模型兼容性都不错。4. 跑通检测流水线从图片到106个关键点4.1 计算人脸检测框106点关键点模型很少会自己直接从全图回归出所有点它通常依赖一个人脸框。如果你随便丢一张包含多人的图片给106点模型模型很有可能会把多人脸混合到一个点集里结果就是点在几个脸之间乱飘。使用包内自带的人脸检测器时输出通常是一个边界框[x, y, w, h]或者[x1, y1, x2, y2]。注意很多模型的坐标系原点在图片左上角y轴向下。拿到框之后需要做一次轻微的margin扩展把眼睛、下巴的边缘留出空间。我通常按比例扩展def expand_bbox(x1, y1, x2, y2, margin_ratio0.1): w x2 - x1 h y2 - y1 x1 int(max(0, x1 - margin_ratio * w)) y1 int(max(0, y1 - margin_ratio * h)) x2 int(x2 margin_ratio * w) y2 int(y2 margin_ratio * h) return x1, y1, x2, y2为什么要扩因为关键点回归模型在训练时其输入图像通常是人脸区域加上一定背景余量。只给一个特别紧的框模型可能没法正确感知脸在框中的相对位置。4.2 关键点回归模型的输入输出解析这个模型通常接收一个(1, 3, 192, 192)或者(1, 3, 112, 112)大小的输入。在把裁剪出来的人脸图像送入模型之前要按训练时的预处理规范做标准化。常见的标准化有两种像素值除以255然后归一化到[0, 1]。减均值除以标准差比如均值[0.5, 0.5, 0.5]标准差[0.5, 0.5, 0.5]。如果预处理做错了模型的输出坐标会明显偏离原图常见现象是“点都朝一个方向偏移”。我踩过一次就是在某个包里demo代码用的是(img - 127.5) / 128而我自己图省事直接img / 255导致输出点整体上移到头皮上。输出层一般有两种形式直接输出一个(1, 106, 2)的坐标数组坐标是在输入图像分辨率下的。输出一个(1, 106, 3)或(1, 106, 2)的置信度和坐标组合后一位是置信度或visibility。拿到输出后最关键的一步是坐标缩放# 假设模型输入尺寸是 input_size # 人脸框在原图上是 bbox # 模型输出 points 是相对输入图像的坐标以输入图像的左上角为原点 scale_x (bbox[2] - bbox[0]) / input_size scale_y (bbox[3] - bbox[1]) / input_size for point in points: original_x point[0] * scale_x bbox[0] original_y point[1] * scale_y bbox[1]如果你跳过这一步直接拿模型输出的坐标画图关键点会全部位于人脸框左上角附近的小区域里。4.3 可视化与坐标变换的坑绘制关键点时有一个容易忽略的问题OpenCV的cv2.circle接受的坐标是(int, int)如果坐标是浮点数直接强转会引入偏移。特别是在做视频流时人脸抖动本身就有几个像素的噪声强转会让点看起来一跳一跳的。更平滑的做法是保留浮点数坐标只在绘制前做一次round而不是int。round是四舍五入int是截断在视觉上差别不大但如果后续你要做特征点对齐这两三个像素的偏差会影响后续贴纸或美颜效果。如果要把关键点用于贴纸、眼镜这类效果还需要把人脸关键点的坐标系映射到贴纸素材坐标系这一步通常用相似变换scale rotation translate实现。106点模型提供了足够多的点用于估计人脸姿态你可以用左右眼坐标来计算旋转角用两眼距离来计算缩放倍率。5. 实测性能与进阶调优从能用到好用5.1 1080P图片上的帧率实测我用一个普通的i5-12400 CPU加载ONNX格式的106点模型配合基于ONNX Runtime的人脸检测在1080P图片上单次推理时间大约是阶段耗时人脸检测12ms ~ 20ms关键点回归2ms ~ 5ms预处理后处理3ms ~ 8ms也就是说纯CPU单帧全流程大概20到30ms勉强能到30fps左右但这是在输入图片没有拉伸到超大尺寸的前提下。如果视频流是4K预处理耗时会被明显拉长建议先把图像缩小到宽度不超过1280再做检测。5.2 调整输入尺寸和模型精度的取舍多数106点模型的官方输入是192x192或者112x112。如果你想要更高的精度可以尝试把输入放大到256x256关键点回归的稳定性会好一些尤其是眼睛嘴角这种细节区域但推理耗时也会翻倍。反之如果做移动端部署输入调到80x80也不是不能跑只是大角度侧脸时的点会明显抖动。实际上我发现一个更划算的优化手段在人脸检测阶段把检测框稍微变大一点让关键点回归模型有更多上下文。因为人脸关键点模型不只需要脸内部的纹理还需要一点背景边缘来帮助判断脸的朝向。5.3 常见异常检测不到人脸、点漂移和多人脸场景检测不到人脸先检查输入的图像通道顺序。如果你用OpenCV读取图片图像是BGR顺序但很多模型的预处理要求是RGB。直接用BGR输入模型在人脸检测阶段可能也能检测到因为人脸检测器对颜色通道顺序不敏感但关键点回归模型通常是RGB训练直接用BGR会导致眼睛、嘴角等局部纹理的响应发生变化输出点会漂。点漂移最典型的场景是眨眼和说话时眼睛和嘴巴附近的点会跟着抖动。这不是模型坏了而是模型对动态模糊的鲁棒性一般。解决方法是在时间序列上做平滑最简单的是一阶低通滤波smoothed prev_smoothed * 0.8 current * 0.2在视频流里这个平滑系数搭配30fps的效果还不错。再高级一点就可以用卡尔曼滤波或者One Euro Filter但那是后话。多人脸场景下建议把每一帧的人脸检测框先做人脸跟踪匹配保证每个ID对应的关键点序列是连续的否则两个脸一旦发生交叉关键点标签就乱了。简单做法是使用IoU去匹配相邻帧的人脸框IoU大于0.3就算同一个ID。这一招对单人场景同样有效因为能避免人脸框偶尔跳变导致的关键点坐标抖动。最后再分享一个小技巧如果你对拿到手的2d106det.zip里的模型效果不满意不要急着删先看一眼它检测的人脸框具体在哪很多时候不是模型精度不够而是人脸检测框给得太偏或者太大。换一个更强的人脸检测器关键点精度会有立竿见影的提升。我自己后来就把包里的轻量检测器换成了SCRFD-500M106点模型无需任何改动整体效果就明显上了一个档次。本文还有配套的精品资源点击获取