ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

face-aip.js自定义检测模型替换实战:从SSD到YOLO

face-aip.js自定义检测模型替换实战:从SSD到YOLO 简介face-api.js 的官方预训练检测模型合集适合需要在浏览器或 Node.js 中实现人脸检测、特征点定位、表情识别、年龄性别估计等功能的 JavaScript 开发者。压缩包内共 63 个文件总大小约 346.5MB包含 face_landmark_68、face_expression、age_gender、face_recognition、ssd_mobilenetv1、tiny_yolov2、mtcnn 等模型的 shard 权重文件与对应 weights_manifest.json 清单并附 README.md 说明目录结构按模型类型分组便于按需加载。这些模型经过优化可直接配合 face-api.js 使用省去自行训练与转换的时间成本适合做实时人脸追踪、表情驱动动画、智能美颜、身份验证等应用。资源包已有 255 人学习下载是前端机器学习实战中便捷的离线模型储备。 face-aip.js 这个名字相信不少折腾过前端 AI 的人都不陌生GitHub 上跑 star 的人脸检测、人脸识别库不少但真正能让你在浏览器里直接调用摄像头、完成实时人脸检测并拿到关键点坐标的face-aip.js 算是最顺手的一类。不过也正因为这类库封装得太友好很多人用了大半年还是停留在“加载官方模型、跑 demo”的阶段一旦遇到产品需求变化——比如要检宠物、要识别人体关键点、要换更高精度的模型——就不知道怎么下手了。这篇文章就把我自己在 face-aip.js 里替换、调试、部署检测模型的全过程摊开讲重点解决“怎么把自定义检测模型塞进前端跑起来”这个核心问题以及换模型之后那些坑都在哪。先交代下我自己的背景主业是前端业余时间折腾了两年多边缘端 AI从 TensorFlow.js 一路玩到 ONNX Runtime Web踩过的坑基本都在“模型格式转换”和“浏览器推理性能”这两块。这篇文章适合的人你已经跑通过 face-aip.js 的基础 demo想搞清楚它背后的模型加载机制或者你手头正好有一个训练好的检测模型不管是 YOLO、SSD 还是自研网络想移植到web 端做人脸或其他目标检测。我不会浪费你时间去讲人脸识别的历史上来直接讲方案选型和能落地的代码。1. 为什么要折腾 face-aip.js 的检测模型从默认模型到定制化的痛点1.1 默认模型的“够用”与“不够用”face-aip.js 默认带的人脸检测模型其实是基于 SSDSingle Shot MultiBox Detector框架 MobileNetV1 作为骨干网络的轻量级模型输出 200 个候选框然后通过非极大值抑制NMS筛选出最终的人脸框和 68 个关键点。这个组合的好处是快在普通笔记本的浏览器里能做到实时移动端也不至于卡到没法用所以官方文档一直推荐直接用默认模型。但问题也出在这套配置上。MobileNetV1 是 2017 年的 backbone在正面大脸、光线充足的场景下表现尚可一旦出现侧脸、遮挡、小尺寸人脸比如教室后排的学生、逆光等情形漏检和误检的概率会明显上升。我自己做过一个课堂考勤的 demo摄像头放在教室前方第一排学生的人脸倒是检测得很好但中后排学生的人脸在画面里只有几十像素默认模型基本是放弃状态。这时候你就得考虑换更合适的检测模型了——要么换一个更大的 SSD/MobileNetV2 变体要么直接上 YOLO 系列。1.2 各家人脸检测方案的横向对比在决定“换模型”之前我整理过市面上主流的前端人脸检测方案。这里放一张简表方便你看看自己到底需要哪种程度的“折腾”。方案模型格式骨干网络实时性普通PC定制难度典型场景face-aip.js 默认JSON/权重分片MobileNetV1很高低官方封装快速原型、课程设计TensorFlow.js 加载自定义模型TFJS Graph/JSON任意需转模型高中需要与 TF 体系深度打通ONNX Runtime Web ONNX 模型ONNX任意中高中高工业项目、需要 PyTorch 导出的模型MediaPipe Face Detection专用 pipelineBlazeFace很高低但封闭移动端实时、相机滤镜OpenCV.js Haar CascadeXMLHaar低低人脸检测教学、离线环境结论很明确如果你只是嫌默认模型精度不够但还想继续用 face-aip.js 的 API 便利你需要的是“换掉模型文件本身”也就是找到另一个结构兼容的 SSD 模型或者把训练好的模型转成 face-aip.js 能读的格式。如果你追求极致的精度、想上 YOLOv8n 这种现代目标检测网络那就别死磕 face-aip.js 的容器了直接用 ONNX Runtime Web 更合理——face-aip.js 的外部封装正好可以用作人脸框的后处理参考。2. 模型准备从训练到 ONNX 导出的完整链路2.1 选型轻量化骨干网络MobileNet / YOLOv8n预训练模型从哪来开源社区其实已经帮你准备好了大量人脸检测模型。我自己常用的路线有两种。第一种继续走 SSD 路线。PyTorch 或 TensorFlow 里都有现成的 SSD 实现骨干网络可以换成 MobileNetV2、MobileNetV3 甚至 EfficientNet-Lite在精度和推理速度之间取一个平衡点。face-aip.js 默认模型直接加载的位置是weights/ssd_mobilenetv1_model这里放的是 JSON 格式的模型描述和分片权重本质上就是 TensorFlow.js 的格式所以只要你用 TensorFlow 训练或者把 PyTorch 模型转成 TFJS 格式就还有机会把模型塞回 face-aip.js 的原始加载逻辑里。第二种从 YOLO 系入手。这里的典型代表是 YOLOv8nnano 版本模型大小也就 6 MB 左右FP32在 COCO 上 mAP 虽然不算顶尖但在人头、人脸这类目标上表现相当不错尤其是小目标召回率远高于 MobileNetV1-SSD。正巧最近社区很火的“yolov8n nano 版模型 人形检测 onnx 模型下载”也是这个路线直接拿 ONNX 格式的 YOLOv8n 来做人形或人脸检测然后通过 ONNX Runtime Web 在浏览器里推理。我给一个选型的建议如果你要识别的目标就是“人脸”且希望尽量沿用 face-aip.js 的代码结构优先尝试方法一因为你可以复用它的先验框解码逻辑只需要替换权重参数。如果你要识别的目标是猫、狗、车辆等非人脸目标就别拧巴了直接走 ONNX Runtime Web 通用检测模型的路线。热词里提到的“宠物检测 AI 模型——嵌入式设备上的猫狗实时识别”本质就是同一类工作流只是目标类别不同。2.2 PyTorch 导出 ONNX 的坑与技巧大部分开源检测模型的训练框架是 PyTorch所以第一步是把.pt权重转成.onnx。这个过程看着简单一行torch.onnx.export但实际会遇到几个坑。第一个坑是动态轴。默认导出时输入尺寸是固定的比如[1, 3, 640, 640]。如果你的图像在预处理时会 resize 到固定尺寸那固定轴也没关系但如果你想保持原始宽高比检测最好把dynamic_axes参数加上把height和width两个维度设为动态。不过动态轴的 ONNX 模型在部分浏览器推理引擎里会慢一些除非必要我还是建议固定输入尺寸常见的是 640x640 或 416x416。第二个坑是 NMS 层。PyTorch 的检测模型 forward 过程中通常会在后处理阶段使用非极大值抑制但 ONNX Runtime Web 对 NMS 算子的支持有好有坏尤其早期版本经常报Unsupported operator。稳妥的做法是导出时只导出模型的 backbone head 部分也就是直接输出原始预测张量通常是[1, 25200, 85]这种形状把 NMS 逻辑放到 JavaScript 里自己写。这样做还有一个好处JavaScript 端可以对阈值、NMS 参数做灵活调整不用重新导出模型。第三个坑是算子兼容性。模型里某些层比如直接用了torchvision.ops.nms、自定义的F.grid_sample等在 ONNX 里可能没有对应的算子或者即使有转换了Web 端推理引擎也不支持。遇到这类问题建议拆解模型结构把特殊层放到模型外处理。实际操作中我大部分情况下只需要最纯粹的卷积池化连接层所以只要避开 NMS、自定义采样层转换基本能顺利通过。import torch import torch.onnx from models.yolo import DetectionModel model DetectionModel(cfgyolov8n.yaml, ch3, nc80) ckpt torch.load(yolov8n.pt, map_locationcpu)[model] model.load_state_dict(ckpt.state_dict()) model.eval() dummy_input torch.Tensor(1, 3, 640, 640) torch.onnx.export( model, dummy_input, yolov8n.onnx, opset_version11, input_names[input], output_names[output], dynamic_axesNone, # 固定输入尺寸 )这段代码是固定输入尺寸的 YOLOv8n 导出如果你后续发现每次推理都要 resize 造成精度损失再考虑加dynamic_axes{input: {2: height, 3: width}, output: {2: height, 3: width}}。2.3 用 onnx-simplifier 和量化给模型瘦身导出之后的模型通常还有不少冗余结构有些是在训练时保留的固定参数有些是 PyTorch 的自动求导信息残留。建议跑一遍onnx-simplifier它能合并常量算子、删除不必要的节点、化简 shape 计算我试过最夸张的一个模型从 90 MB 剪到 52 MB速度也快了不少。pip install onnx-simplifier python -m onnxsim yolov8n.onnx yolov8n-sim.onnx再进一步可以考虑 INT8 量化。浏览器端的 WebGL/WebGPU 推理对 FP32 和 FP16 的支持比较完善但 INT8 的支持因引擎而异有的环境不支持会直接回退到 FP32。而 FP16 量化相对安全ONNX Runtime Web 可以接受 FP16 的模型权重。实际操作中我一般先用 FP32 验证流程最后再用 FP16 版本的模型做部署既能减少带宽占用模型小一半又能提升一定加载速度精度损失在验证集上通常不超过 0.5%。3. face-aip.js 集成让浏览器跑起自定义模型3.1 引入 ONNX Runtime Web 的两种方式真正写代码之前先明确一件事face-aip.js 默认用 TensorFlow.js 做推理如果你想换成一个全新的 YOLO 模型最干净的方式是跳过 face-aip.js 的模型加载层直接在新模型推理完成后再复用 face-aip.js 的展示和交互逻辑。引入 ONNX Runtime Web 有两种方式。第一种是 CDN适合快速验证script srchttps://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js/script第二种是 npm 安装适合正式项目npm install onnxruntime-web然后在代码里创建 sessionimport * as ort from onnxruntime-web; let session; async function initModel(onnxPath) { session await ort.InferenceSession.create(onnxPath, { executionProviders: [webgl, wasm], graphOptimizationLevel: all, }); }注意executionProviders的顺序优先考虑 WebGL因为它能利用 GPU 并行加速卷积计算WebAssembly 是 CPU 兜底。但 WebGL 在部分低端安卓机上有纹理内存限制模型太大时容易崩溃所以我加了wasm作为回退。3.2 把检测结果映射回 640x640 坐标系YOLO 模型输出的坐标是相对于输入尺寸比如 640x640的归一化或像素坐标而视频画面的分辨率一般不是 640x640所以你一共要做三次坐标变换。第一次是预处理时的等比缩放。你从摄像头拿到的帧是 1280x720要把这个画面 resize 成 640x640不能直接拉伸否则人脸会被压扁影响检测精度。标准的做法是 Letterbox计算缩放比例scale min(640 / w, 640 / h)然后把画面等比缩放到 640xhscale或wscalex640剩余部分用灰色填充。第二次变换是模型输出坐标映射回原始帧。YOLO 输出的 box 坐标是相对输入图像的要还原到原始画面需要减去 letterbox 填充的偏移量再除以缩放比例function letterboxResize(srcWidth, srcHeight, targetSize) { const scale Math.min(targetSize / srcWidth, targetSize / srcHeight); const newWidth Math.round(srcWidth * scale); const newHeight Math.round(srcHeight * scale); const padX Math.floor((targetSize - newWidth) / 2); const padY Math.floor((targetSize - newHeight) / 2); return { scale, padX, padY, newWidth, newHeight }; } function decodeBoxes(rawBoxes, srcWidth, srcHeight, targetSize) { const { scale, padX, padY } letterboxResize(srcWidth, srcHeight, targetSize); return rawBoxes.map(box { const x1 (box[0] - padX) / scale; const y1 (box[1] - padY) / scale; const x2 (box[2] - padX) / scale; const y2 (box[3] - padY) / scale; return [x1, y1, x2, y2]; }); }第三次变换是如果画面本身有 CSS 缩放即显示区域小于或大于原始帧尺寸还要把检测框映射到屏幕坐标。这一步在 canvas 或 video 元素上画框时特别容易出错常见 bug 是框和画面错位。在 face-aip.js 里如果用原来的detectAllFaces接口工具函数会帮你处理坐标但换了模型之后你就得自己写这一段坐标变换。我的建议是封装一个drawBoxes(canvas, boxes, labels)工具函数所有画框逻辑都走这个函数避免在业务代码里散落各种 magic number。4. Web 端推理性能优化的四个方向4.1 WebGL 后端与 worker 线程浏览器推理最大的敌人就是主线程阻塞。如果你在摄像头的requestVideoFrameCallback或requestAnimationFrame里直接做模型推理即使在桌面端也会有明显掉帧。我实测过一个 YOLOv8n 模型在主线程推理每一帧耗时 80~100msUI 明显卡顿人脸框在画面上像幻灯片一样跳。正确的做法是把推理操作移到 Web Worker 中。我在 worker 里创建 ONNX session收到的视频帧经过createImageBitmap转成ImageData后传给 workerworker 内部完成预处理、推理、后处理再把检测结果以对象形式 postMessage 回主线程。这样主线程只需要负责绘制检测框UI 的流畅度会大幅提升。代码结构大致如下// worker.js import * as ort from onnxruntime-web; self.onmessage async (e) { if (e.data.type load) { session await ort.InferenceSession.create(e.data.modelPath); postMessage({ type: ready }); } if (e.data.type detect) { const tensor preprocess(e.data.imageData, 640); const results await session.run({ input: tensor }); const boxes postprocess(results.output); postMessage({ type: result, boxes }); } };注意onnxruntime-web在 worker 环境下默认会用 wasm 后端wasm 文件加载路径需要用ort.env.wasm.wasmPaths配置。这个坑我踩过好几次忘了配置的话会一直报Cannot find module wasm之类的错误。4.2 输入尺寸与预处理很多人误以为输入尺寸越大精度越高其实在浏览器里这是个性价比问题。YOLOv8n 在 640x640 时的 mAP 确实比 416x416 高但推理耗时几乎是翻倍的。我的经验是如果是人脸检测416x416 已经够用尤其是摄像头画面里人脸通常只占不到 1/4 区域如果是人体检测或小目标检测再考虑 640x640。预处理部分要注意归一化方式。PyTorch 训练时通常用normalize (x / 255) - 0.5 / 0.5即把像素从[0,255]归一化到[-1,1]。ONNX Runtime Web 的Tensor构造需要 Float32Array所以预处理要手动做function preprocess(imageData, targetSize) { const { data, width, height } imageData; const resized letterboxResize(width, height, targetSize); const input new Float32Array(3 * targetSize * targetSize); // 从 imageData 里取出像素并写入注意忽略 alpha 通道 for (let y 0; y resized.newHeight; y) { for (let x 0; x resized.newWidth; x) { const srcIdx (y * (imageData.width / 1) x) * 4; const dstIdx y * targetSize x; input[dstIdx] (data[srcIdx] - 127.5) / 128; input[dstIdx targetSize * targetSize] (data[srcIdx 1] - 127.5) / 128; input[dstIdx targetSize * targetSize * 2] (data[srcIdx 2] - 127.5) / 128; } } return input; }这里的关键是别直接把整个 imageData 塞给模型YOLO 的输入层是 CHW 布局不是 HWC。4.3 量化模型和 session 配置如果你实测模型推理耗时还是太高可以试试三个策略换 FP16 模型、打开 session 的optimizeModel配置、减少输出张量形状。session 创建时graphOptimizationLevel: all能让 ONNX Runtime 自动融合一些算子比如 ConvRelu、ConvBN 等这部分优化通常能带来 10%~20% 的加速。另外如果你的 ONNX 模型里有图像预处理的原始输入结构比如 Resize Normalize 算子导出时最好去掉这些算子在前端做更可控而且能减少模型体积和推理图复杂度。还有一个很多人都忽略的技巧在导出模型的时候如果只做单类检测可以把类别数从 80 改成 1输出张量的后两个维度会大幅缩小。比如 YOLOv8n 输出 25200x85如果只输出人脸一类就变成 25200x6推理时间和内存占用都降了不少。这个修改可以在训练后的模型 head 部分直接改输出维度或者导出后用 Python 脚本改输出层。5. 常见问题速查与实践总结5.1 报错问题整理我在反复尝试的过程中遇到了不少常见的报错情况这里整理成速查表方便你对着排查。症状可能原因解决方案onnxruntime-web加载模型报403模型文件路径或跨域配置有问题放到同域目录或配置静态资源服务器 CORS推理结果全为 0 或 NaN输入张量形状或归一化方式不对打印输入Tensor的 shape 和 min/max 值逐一核对模型加载后内存崩溃模型过大或 WebGL 纹理内存不足改用 FP16 模型、降低输入尺寸、加 wasm 回退模型速度很慢300ms/帧没开 GPU 加速 / 主线程推理切换 executionProviders加 Worker检测框不准确偏左或偏上letterbox 的 pad 计算误差检查解码 box 时是否减掉了 padX/padYface-aip.js 自带的landmark消失自定义模型没有输出关键点前端降级为只画框或用 MediaPipe 做关键点其中最后一个问题很有意思。face-aip.js 的一大卖点是同时输出人脸框和 68 个关键点但你一旦换上 YOLO 这类纯检测模型关键点就没有了。如果业务需要关键点比如表情识别、贴纸特效我的建议是“组合式方案”先用 YOLO 做人脸检测再把人脸裁剪区域送入另一个轻量级关键点模型比如 MediaPipe Face Landmark两者串联。虽然多了一步但两头都能用上最适合的模型。5.2 从 face-aip.js 迁移出去后的架构思考说实话一旦你开始替换 face-aip.js 的检测模型你很快就发现它提供的方便只是“面上”的背后的推理逻辑、坐标系统、后处理算法都需要自己重新适配。因此我在实际项目里最终做成了一套“三段式”架构第一段是视频输入层负责从摄像头拉流、抽帧、格式转换第二段是推理层在 Worker 中使用 ONNX Runtime Web 加载并执行自定义 ONNX 模型第三段是应用层复用 face-aip.js 提供的画框、关键点绘制、UI 交互逻辑。这样拆开之后换模型只是换一个 ONNX 文件和对应的预处理函数其他逻辑完全不受影响。往后如果模型进一步升级成 4D 毫米波雷达点云检测或者其他多模态输入这套前端的架构依然能套用因为你已经把“输入”和“模型”解耦了。5.3 一些实际的体会折腾下来最大的心得是做前端 AI 不能只盯着 JavaScript api 的调用你对模型的结构理解、对预处理的把握才决定上线后效果的天花板。face-aip.js 帮你解决了“最后 10%”的展示问题但真正决定产品能不能落地的是前面 90% 的模型选型与数据适配。最后分享一个小技巧每次新换一个模型都先在本地同时打开两个页面一个跑新模型一个跑官方 demo用同一路摄像头画面做对比。不要只看检测框对不对还要对比 FPS、漏检率和 CPU/GPU 占用。前端 AI 的评测没有复杂的工具肉眼 一个console.time就够你撑到项目上线了。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进