尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
人脸关键点检测必备:PFLD轻量模型源码+权重+ONNX部署全攻略
简介基于PFLD算法的人脸关键点检测完整工程提供Python源码、预训练模型与ONNX导出文件适合算法学习者和有检测落地需求的研究者。压缩包共74个文件大小约89.88MB涵盖py源码、pth模型、onnx模型、jpg样例、txt说明、npy数据等类型其中源码包含多种骨干网络变体并配有数据预处理、训练、测试与ONNX转换脚本。预训练模型提供lite、v2、v3等多个版本可直接加载推理也可借助模型转换工具完成移动端部署适配。工程还集成了MTCNN检测器相关文件便于构建完整的人脸检测→关键点定位流程。目前已有123人学习该资源适合需要参考完整实现、对比不同主干网络效果或准备相关课程设计的开发者使用。1. 基于PFLD的人脸关键点检测轻量模型配齐源码、权重与ONNX基于PFLD算法的人脸关键点检测是移动端和嵌入式场景里绕不开的一个方案。这套Python资源把源码、预训练权重、ONNX模型一次配齐解压之后不需要重新训练就能在桌面端把106点人脸关键点检测跑通。PFLD这类轻量模型的主干网络只有百万级参数量速度优势明显再加上包内已经内置了MTCNN检测器和转换好的ONNX文件课程设计、毕业设计选人脸方向用它起步非常顺。新手可以从onnxrt_inference.py这个入口直接读流程老手则可以直接拿tools目录里的pytorch2onnx.py和onnx2ncnn走一遍模型迁移部署的完整链路。这套资源适合所有需要快速验证人脸关键点方案的人。2. 源码与模型结构盘点四种PFLD变体和三套权重怎么对应拿到zip先别急着跑把目录过一遍再动手能省不少时间。整个包的结构是models模型定义、checkpoint预训练权重、output转好的ONNX、tools转换工具、mtcnn人脸检测器、data和dataset训练数据相关、train.py和test.py训练与测试脚本外加一个onnxrt_inference.py作为ONNX Runtime推理入口。这个结构对应的是一个完整的训练部署闭环不是那种只有推理脚本的半成品。2.1 目录职责先分清哪个文件在什么时候用下面的表格把关键目录和文件的作用列出来后面章节涉及到的文件都能在这里对上号。目录/文件作用什么时候用models/pfld.py基础PFLD主干网络定义加载pfld.onnx做PyTorch推理、二次开发models/ghost_pfld.pyGhost模块轻量化版本移动端低算力部署models/mobilev3_pfld.pyMobileNetV3主干变体需要更高精度时models/lite.pyLite变体定义对应lite.pth/lite.onnxcheckpoint/lite.pth、v2.pth、v3.pth三套权重训练微调、转ONNX的起点output/pfld.onnx、v2.onnx、v3.onnx、lite.onnx直接用ONNX Runtime推理tools/pytorch2onnx.py、onnx2ncnn、ncnnoptimizePyTorch转ONNX、再转NCNNmtcnn/detector.py pnet.npy/rnet.npy/onet.npy人脸检测关键点推理的前置步骤data/106人脸标注规则.jpg、prepare.py制作训练数据这个资源里最值得注意的一点是它不是只给了训练脚本而是把权重和ONNX都直接放进来了。也就是说训练、转换、部署三段链路各自都有可以直接跑的入口这在课程设计和工程验证里非常省时间。实际使用顺序一般是先跑onnxrt_inference.py验证ONNX格式没问题再去用train.py做迁移训练最后用tools里的转换脚本导出自己训好的模型。2.2 四种变体怎么选主干网络差异与选型边界PFLD变体的差异主要在主干网络。原始PFLD基于类MobileNet结构用深度可分离卷积把计算量压下来配合全连接层直接回归关键点坐标。Ghost-PFLD把普通卷积替换成Ghost module先用小计算量生成固有特征再用廉价线性操作生成冗余特征整体计算量进一步下降。MobileV3-PFLD引入了MobileNetV3的h-swish激活和SE注意力模块精度上限更高但在ONNX转换时对算子支持要求也更高。Lite变体是在前三者基础上继续压缩通道数和层数适合树莓派、Android端这类资源紧张的环境。变体主干思路建议使用场景PFLDpfld.py原始轻量卷积块精度优先、做基准对比Ghost-PFLDGhost module中低算力移动端MobileV3-PFLDMobileNetV3算子算力充足、精度敏感Lite极致压缩实时摄像头、嵌入式选择上我一般会优先试v2、v3对应的权重确认精度满足需求后才考虑换Lite压缩体积。不要一上来就追求最轻量的模型如果设备算力够v2/v3的鲁棒性通常比Lite好不少尤其是侧脸和大角度姿态下轻量模型的点会明显飘。另外要提一句checkpoint目录下有三套pth但具体哪一套对应哪一种变体打开模型定义对比state_dict的key就知道了。常见做法是print(checkpoint.keys())如果有对应预训练模型的字段就取出来再load没有就直接load整个字典。2.3 106点标注规则知道每个点在说什么这个项目用的是106点标注不是更常见的68点。多出来的点集中在眼睑轮廓、眉毛细节和唇部周边对人脸美颜、表情分析和眼部状态检测更友好。data/106人脸标注规则.jpg就是用来对照点序的训练和推理都必须按这个规则里的点序来。训练数据文件是bbox_landmark.txt每一行对应一张人脸图片数据格式一般是图片路径、人脸框x1/y1/x2/y2、置信度、然后是106个点的坐标也就是212个数。datasets.py在读取时会把这个文本解析成训练样本同时完成resize到112x112和归一化。prepare.py的用途则是把原始数据集整理成上述格式。如果你想用WFLW或300W这些公开数据需要先写脚本把坐标映射到106点规则上映射错了后面全盘都是错的。顺便再说一个容易忽略的点归一化方式一定要和datasets.py里保持一致。如果datasets.py把坐标除以112做归一化那推理端就照做如果它除以的是图像宽高推理端也要换成对应方式。这一步是后面所有踩坑源头的大头后面第5章会展开讲。3. 跑通一个完整推理流程从MTCNN框人脸到106点坐标输出推理链路分为两步MTCNN负责框出人脸PFLD负责在框出来的人脸上回归106点。关键点模型本身不负责找脸输入是一张已经裁剪好的人脸。整个流程看起来简单但预处理、坐标映射、模型输入格式这几处细节没对齐输出的点就会乱飞。3.1 MTCNN三级级联检测器为什么是PFLD的好搭档MTCNN全称是Multi-task Cascaded Convolutional Network由PNet、RNet、ONet三个网络级联。PNet在图像金字塔上快速扫出大量候选框RNet对候选框做精排过滤ONet输出最终的边界框和5个人脸关键点两个眼睛、鼻尖、两个嘴角。mtcnn目录下的detector.py加载的是pnet.npy、rnet.npy、onet.npy这三个npy就是MTCNN的权重说明前向推理可以直接用numpy计算不依赖额外的大框架。我一般会在调用前看一眼detector.py暴露的接口比如detect方法返回的是boxes和landmark数组以代码为准。通常接口形式是detect(img)返回两个值boxes形如Nx5每行为x1/y1/x2/y2/scorelandmark则是Nx10保存5个点坐标。MTCNN返回的5点还可以用来做旋转矫正如果输入人脸是倾斜的先根据两只眼睛的角度把人脸转正再送进PFLD关键点检测的精度会明显提升。项目里如果没带这个矫正你在自己的封装里加上即可。3.2 ONNX Runtime推理从图像到106点的完整代码下面这段代码是onnxrt_inference.py的典型流程也是读完这个资源后最应该先抄作业的地方。import cv2 import numpy as np import onnxruntime as ort from mtcnn.detector import Detector IMG_SIZE 112 # 初始化检测器和关键点模型 detector Detector() session ort.InferenceSession( output/v2.onnx, providers[CPUExecutionProvider] ) # 读图并转RGBMTCNN内部一般约定RGB输入 img_bgr cv2.imread(1.png) img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 检测人脸框 boxes, points detector.detect(img_rgb) if len(boxes) 0: print(no face detected) exit(-1) x1, y1, x2, y2, score boxes[0].astype(np.int32) # 裁剪人脸并resize到112x112 face img_rgb[y1:y2, x1:x2] face cv2.resize(face, (IMG_SIZE, IMG_SIZE)) # 归一化到[0,1]并转为NCHW input_tensor face.astype(np.float32) / 255.0 input_tensor np.transpose(input_tensor, (2, 0, 1))[np.newaxis, ...] # 推理输出形状为1x212 output session.run(None, {input: input_tensor})[0] landmarks output.reshape(-1, 2)逻辑说明前两步建立检测关键点的组合链路显式指定CPUExecutionProvider避免在无GPU环境下来回探测resize到112x112是PFLD系列的约定尺寸训练阶段也用的这个尺寸。归一化这里用了除以255的标准做法但最终以datasets.py里写的变换为准这一点在第5.1节会详细说。注意如果要在GPU上跑把providers改成[CUDAExecutionProvider, CPUExecutionProvider]没有CUDA的环境里onnxruntime会自动落到CPU。input_name以导出时input_names参数为准导出脚本里定义成input就用input。3.3 坐标映射与可视化验证确保点画在正确位置模型输出的106个点坐标是相对于112x112输入图的归一化坐标范围大致在0到1之间。要画回原图需要把坐标乘回人脸框的宽高再加上框左上角的偏移。face_w x2 - x1 face_h y2 - y1 abs_pts landmarks.copy() abs_pts[:, 0] landmarks[:, 0] * face_w x1 abs_pts[:, 1] landmarks[:, 1] * face_h y1 # 可视化 for (px, py) in abs_pts: cv2.circle(img_bgr, (int(px), int(py)), 1, (0, 255, 0), -1) cv2.imwrite(result_check.jpg, img_bgr)参数说明face_w和face_h是裁剪前的人脸框宽高用做缩放而不是直接用112是为了把归一化坐标映射回原图尺度。加x1/y1是因为裁剪时移除了框左上角的偏移量画回原图时需要补回来。如果训练数据里坐标是按112像素直接存的即没有除以112那这里的landmarks就不是0到1而是0到112映射公式要对应改成landmarks[:, 0] / 112.0 * face_w x1。快速校验方法把项目output目录里自带的result_1.jpg、result_2.jpg和三张模型结果图对比用同一张图跑一遍如果关键点位置和项目给出的结果图基本重合说明映射公式和预处理流程是对的。这个步骤建议在改任何代码前先跑一遍把基线锁住。4. PyTorch转ONNX导出脚本参数、动态轴与NCNN移植项目里已经自带转换好的ONNX为什么还要自己转一遍因为换数据集、换模型变体、改输入尺寸时原带的ONNX就没法直接用了。train.py训练完自己的权重后pytorch2onnx.py就是衔接训练和部署的那道桥。4.1 torch.onnx.export的每个参数都意味着什么pytorch2onnx.py的核心是用torch.onnx.export完成模型转换。下面是一段符合这个项目场景的导出代码模型定义以models目录为准。import torch from models.pfld import PFLD # 初始化模型并加载权重 model PFLD(num_points106) # 以模型定义实际参数为准 state torch.load(checkpoint/lite.pth, map_locationcpu) if state_dict in state: state state[state_dict] model.load_state_dict(state) model.eval() # 固定输入尺寸 dummy_input torch.zeros(1, 3, 112, 112) torch.onnx.export( model, dummy_input, output/pfld.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11, do_constant_foldingTrue, )参数说明dummy_input的H和W必须固定为112PFLD里如果有interpolate或resize层动态宽高在导出时很容易变成能导出但结果异常的图。dynamic_axes里只把batch维度设成动态W、H保持静态这样同一份ONNX既能单张跑也能批量跑同时避免动态shape带来的算子兼容问题。opset_version选择11向后兼容性比较好NCNN工具链对opset 11的支持也最成熟改到13或更高前先确认onnx2ncnn版本跟不跟得上。do_constant_foldingTrue会把常量折叠进权重图减少运行时的计算节点对含BatchNorm的模型来说这个选项会把BN层融合进卷积对推理速度和精度稳定性都有利。4.2 导出后的数值一致性检查转换本身不算完成必须用同一输入把PyTorch模型和ONNX模型各跑一次对比输出差异。这一步我每次都会做因为能最快发现权重加载错误、算子转换异常这类问题。import numpy as np import onnxruntime as ort import torch # 假设model已加载且eval x torch.randn(1, 3, 112, 112) with torch.no_grad(): ref model(x).numpy() sess ort.InferenceSession(output/pfld.onnx, providers[CPUExecutionProvider]) out sess.run(None, {input: x.numpy()})[0] max_diff np.max(np.abs(ref - out)) mean_diff np.mean(np.abs(ref - out)) print(max diff:, max_diff, mean diff:, mean_diff)判断标准max diff在1e-4量级说明转换可信到1e-2说明某个算子被替换成了近似实现建议用onnx-simplifier简化后再对比再大的差异基本就是哪里配置错了不要继续往下走。输入建议用随机张量不要用全0张量全0输入在某些模型里会走固定的捷径分支掩盖数值偏差。数值一致性对比只看计算路径不需要真实人脸。另一个容易被忽略的点是预处理是否封装进ONNX。我一般会建议把除以255、NCHW转置这类操作留在模型外部原因有两个一是预处理留在调用端便于在PyTorch和ONNX推理之间做一致性对比两边输入格式可以完全一致二是端侧框架对输入layout各有约定预处理留在外部反而更容易适配。4.3 ONNX到NCNNonnx2ncnn和ncnnoptimize的配合方式为了跑在Android或Linux ARM设备上通常还要把ONNX再转成NCNN格式tools目录里的两个工具正好覆盖这个需求。标准命令如下./tools/onnx2ncnn output/pfld.onnx output/pfld.param output/pfld.bin ./tools/ncnnoptimize output/pfld.param output/pfld.bin output/pfld_opt.param output/pfld_opt.bin 0第一行做格式转换产出param描述网络结构、bin存放权重第二行做结构优化把算子重排、算子融合自动完成。ncnnoptimize最后那个数字是存储精度标志0表示fp321表示fp162表示bf16。移动端部署时我会先用0确认逻辑没问题再用1换fp16压缩一半权重体积。注意tools目录下的onnx2ncnn和ncnnoptimize是编译好的二进制工具如果和你当前的Linux环境不匹配需要去NCNN仓库拉源码自己编译一份。转换时最常见的问题是某些高版本算子不支持比如GridSample、部分动态Resize。NCNN转换时最常遇到的三个问题hardswish算子版本不匹配、Resize在动态shape下产生多余算子、权重文件过大。hardswish在onnx2ncnn里可以展开成clipscale的组合动态Resize则需要在导出时固定输入尺寸来规避。兜底思路是把这类层在PyTorch里先改写为等价的基础卷积和采样操作再走导出流程。5. 常见问题排查五个翻车现场与对应解法这部分内容来自实际跑这类项目时的高频踩坑每一条都按现象、原因、解决三步展开照着排查能省下不少时间。前两条聚焦推理链路中间两条聚焦模型加载和转换最后一条是数据标注规则的坑。5.1 关键点画出来不在五官上现象106个点全部能输出但画到图上后点不在五官对应位置甚至全部挤在图像一角。原因推理时的预处理和训练时不一致。最常见的是归一化方式错位——datasets.py训练时把坐标除以112做归一化推理端却用了除以255的像素归一化或者反过来。这个差异会让模型拿到分布完全不同的输入输出自然全乱。解决打开dataset/datasets.py读一遍transform逻辑严格复刻。如果训练时用坐标除以112那推理时把112x112图像直接转float32后除以255给模型输出坐标直接就是0~112范围无需再乘缩放系数如果训练时是除以宽高那推理端就用除以255再回乘人脸框宽高的方式。不要凭经验猜以源码为准。5.2 MTCNN检测慢或漏检大图和远距离人脸的两个极端现象1920x1080的整图检测要跑好几秒或者画面里人脸比较小完全检测不出来。原因MTCNN的minsize参数控制最小人脸尺寸。开太小PNet要在很多层金字塔上跑候选框数量暴增拖慢整体速度开太大远处的小脸直接在第一层就被过滤掉了。解决桌面端我常用minsize40、thresholds[0.6, 0.7, 0.7]的组合兼顾速度和漏检率。摄像头近景场景可以降到20左右。确认一下detector.py里有没有暴露这些参数没有就自己加一层配置。另外图像较大时可以先把图缩小到短边720再做检测检测框按比例映射回原图速度提升非常明显。5.3 pth权重加载报错state_dict的key对不上现象load_state_dict时报missing keys或unexpected keys模型能初始化但推理结果明显不对。原因checkpoint里存的key和模型定义不一致。常见两种情况一是pth里存的不只是model.state_dict()而是包含了optimizer、epoch等字段的完整checkpoint需要取state_dict子项二是v2.pth或v3.pth实际是另一个变体训练出来的权重和当前模型定义不匹配。解决print(checkpoint.keys())看顶层结构有state_dict字段就取它没有就直接尝试。如果还报缺失用strictFalse加载输出missing keys列表对照models目录下的定义确认当前模型变体是不是选对了。换一个对应模型的构造函数再load通常一两分钟就能定位。5.4 PyTorch转ONNX后数值偏差过大现象同样的输入ONNX输出和PyTorch输出最大差值到1e-2甚至更大画出来的点明显偏移。原因有两个高频来源。一是opset版本差异某些算子在不同opset下行为不同二是do_constant_folding没有开启或BN折叠异常导致推理路径和原模型不一致。解决先固定opset_version11开启do_constant_foldingTrue导出后用第4.2节的方法查最大误差。仍然偏大时用onnx-simplifier对导出的图做一次化简化简后再对比一次。如果模型里用了自定义算子比如NMS这类检测专用算子不要把它包进关键点模型的导出范围留在外部单独实现。5.5 按点序连线时线条乱飞现象单点位置都对但按顺序把106个点连起来时线条乱飞轮廓完全不是人脸形状。原因106点标注规则里定义了严格的点序输出时要按标注规则图的编号顺序排列。如果训练数据来自WFLW等公开数据集98点或iBUG68点坐标顺序和本项目106点的定义完全不同直接套用自然乱线。解决对照data/106人脸标注规则.jpg确认点序训练数据导入时先做坐标到106点规则的重映射不要直接拿别的标注格式硬塞进网络。推理时输出顺序按训练时使用的点序保存可视化时按同一顺序连线。6. 快速验证用坐标MSE对比脚本确认三个ONNX模型的部署一致性换模型变体后最怕的是预处理不一致导致结果悄悄变差这里有一个我常用的快速验证脚本让v2、v3、lite同时吃同一张输入统计两两之间的坐标MSE先看数值再谈效果。import onnxruntime as ort import numpy as np paths { v2: output/v2.onnx, v3: output/v3.onnx, lite: output/lite.onnx, } # 随机输入即可重点是数值一致性 img np.random.rand(1, 3, 112, 112).astype(np.float32) outs {} for name, path in paths.items(): sess ort.InferenceSession(path, providers[CPUExecutionProvider]) outs[name] sess.run(None, {input: img})[0].reshape(-1, 2) print(v2 vs v3 MSE:, np.mean((outs[v2] - outs[v3]) ** 2)) print(v2 vs lite MSE:, np.mean((outs[v2] - outs[lite]) ** 2)) print(v3 vs lite MSE:, np.mean((outs[v3] - outs[lite]) ** 2))运行后看MSE量级两两在1e-3以下说明三个模型对输入响应基本一致部署端可以随意切换验证如果某个模型和其他两个差出几个数量级它很可能是转换时出了配置问题或者是不同变体在算子实现上有较大差异要回到checkpoint和导出参数上去查。验证完数值后再把三组点画在同一张原图上叠起来看。与我预期一致时重合度很高如果某一组明显偏出结合MSE数值基本就能定位问题出在转换环节还是预处理环节。从那以后我每次换一个新的人脸关键点资源都会先跑一遍这个MSE脚本再进入可视化环节省掉了不少被异常结果误导的排查时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Python身份证识别系统源码拆解:从照片到结构化字段的精度优化

Python身份证识别系统源码拆解:从照片到结构化字段的精度优化

简介:这是一份基于Python的身份证识别系统源码,面向希望学习OCR与深度学习落地应用的开发者,尤其适合具备一定图像处理基础、想研究高精度证件识别方案的中高级学习者。资源包共2个文件,包含1个py源码与1个md说明文档,…

📅 2026/10/12 14:19:08
AI一周事件(2025年8月20日-8月26日):TaoToken 统一 Key 接入周报

AI一周事件(2025年8月20日-8月26日):TaoToken 统一 Key 接入周报

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/12 14:19:08
AI周报怎么做?智能客服团队技术选型与工程落地实操指南

AI周报怎么做?智能客服团队技术选型与工程落地实操指南

1. 为什么我要做一份“AI 周报” 先说说我做这件事的起因。我在一家做智能客服系统的小团队里负责技术选型和工程落地,每周都要花大量时间刷各种模型发布页、开发者社区、论文预印本和工具更新日志。信息源太散了,光是筛选“这周到底哪些东西值得关注”就…

📅 2026/10/12 14:19:08
MORE NEWS

更多资讯

📰

大模型 Tool Use 手写实战:三套实现方案全拆解

面试现场碰到这种题,估计不少人跟我第一反应一样:Tool Use 不就是 Function Calling 吗,直接调接口就行。可真到了“不借助任何框架,不调 SDK,白板角落写一个运行时”的时候,才会发现里面全是细节。 手写一…

📰

动动嘴就能建模?Blender 接入 TaoToken 跑通 AI 建模插件全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

LoRA自训实战:从Llama到ChatGLM的消费级显卡微调指南

简介:面向AI研发工程师与算法学习者,这份以LoRA微调实战为核心的学习资料包,聚焦Llama(Alpaca LoRA)与ChatGLM(ChatGLM Tuning)两大主流模型的低秩适配训练。包内围绕用户故事生成、测试代码生成…

📰

EL表达式从Cookie取中文乱码?根源、解码方案与排查清单

EL 表达式从 Cookie 里取中文值出现乱码,是 JSP 老项目里特别经典的问题:页面上一会儿是%E5%BC%A0%E4%B8%89,一会儿是??,偶尔还是一排诡异字符。这个问题我前前后后遇过不下五次,每次复现原因都不完全一样&#xff0…

📰

Python模拟登录教务系统全流程:从抓包到成绩查询

搞自动化的人都知道,学校里的教务系统是最让人又爱又恨的东西。爱的是所有课程、成绩、选课都在上面,恨的是它几乎没有一个开放API,所有操作都得在浏览器里手动点,就连查个平时分也要输入学号、密码、验证码,再点三次确…

📰

Claude-Mem项目解析:本地对话记忆持久化技术探析

项目标题是“claude-mem”,但当前输入中未提供任何有效正文、关键词列表或摘要描述——整段输入为空,仅有一个孤立的标题字符串和空的代码块。根据我的角色设定与创作原则,我必须严格忠于原料:所有内容必须源于用户提供的【项目标…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬