计算机视觉实战:从算法选型到产品落地的血泪经验

张敏△
2026-01-23 06:21
阅读 1559

大家好,我是杭州某厂(不能说名字,但园区里有湖)的一名准新人,普通一本CS大四,已经拿完offer,现在处于“等入职、不摸鱼、但也不敢太浪”的微妙状态。每天早上8点准时睁眼,泡杯速溶咖啡,打开VS Code,开始折腾各种side project——毕竟入职前空窗期太长,怕手生。

前段时间,我接了个朋友公司的外包需求:做一个人脸关键点检测的小工具,用于他们的美颜SDK原型验证。听起来简单?但实际搞起来,光是选什么算法就让我熬了三个通宵,差点以为自己要倒在入职前夜。今天就想和大家聊聊,在有限资源和紧迫deadline下,如何从一堆CV算法中挑出那个“对的人”,并让它顺利跑进产品里


为什么不是直接上YOLO或者Transformer?

很多同学一听到“计算机视觉”,第一反应就是“上YOLOv8”或者“Vision Transformer走起”。但现实是,产品不是论文,跑得快、吃得少、改得动,比SOTA分数重要一百倍

这个项目的需求很明确:

  • 输入:手机前置摄像头实时画面(720p,30fps)
  • 输出:68个人脸关键点(眼睛、鼻子、嘴巴轮廓)
  • 环境:Android端,内存占用<100MB,推理延迟<50ms
  • 额外要求:支持弱光、侧脸、戴口罩场景(产品经理原话:“用户不可能正对着镜头微笑”)

我一开始也热血上头,拉了个MobileViT模型,精度是高,但一部署到真机,帧率直接掉到8fps,还OOM了。测试小哥当场发来灵魂质问:“你这模型是给服务器用的吧?”

那一刻,我深刻理解了什么叫“算法工程师的浪漫,产品经理的噩梦”。


算法选型:精度、速度、部署成本的三角博弈

我花了两天时间,横向对比了几个主流人脸关键点检测方案,总结如下:

算法 模型大小 精度(NME↓) 移动端推理速度 是否支持ONNX 训练难度 适合场景
Dlib (HOG+SVM) ~10MB 5.2% 40ms (CPU) 极低 老旧设备、快速原型
MediaPipe BlazeFace + Custom Regressor ~8MB 3.8% 25ms ✅(需自定义节点) 移动端首选
HRNet-W18 ~90MB 2.1% 120ms+ 高精度服务器端
PFLD (轻量级CNN) ~5MB 4.0% 30ms 资源受限设备
MobileViT + Heatmap ~45MB 2.5% 80ms ✅(需裁剪) 新硬件、GPU加速

注:NME(Normalized Mean Error)是关键点检测常用指标,越低越好;测试环境为骁龙778G,OpenCV 4.8 + ONNX Runtime。

看到没?HRNet虽然精度吊打全场,但模型大、速度慢,直接被产品否了。而Dlib虽然快,但弱光下基本瞎眼,连戴眼镜都识别不准——这年头谁还不戴个防蓝光眼镜?

最后我锁定了两个候选:MediaPipePFLD


为什么最终选了MediaPipe?

说实话,我原本更倾向PFLD,因为代码简单、训练快,GitHub上也有现成的PyTorch实现。但实测发现,它在极端姿态(比如低头45度) 下,鼻尖和下巴的关键点经常“飞”到屏幕外,调试时看着关键点乱跳,感觉自己在玩抽象艺术。

而MediaPipe虽然文档有点迷,但Google团队已经把人脸检测+关键点回归打包成一个pipeline,还做了大量移动端优化。最关键的是——它支持动态分辨率输入,且对光照鲁棒性极强

上周五晚上,我拿自己当小白鼠,在台灯关掉、只开手机闪光灯的环境下录了段视频,MediaPipe依然能稳稳抓住我的五官。那一刻,我仿佛看到了产品经理欣慰的笑容(虽然他可能根本不知道我在加班)。

不过,MediaPipe的坑也不少。比如:

  • 它的输出是归一化坐标(0~1),需要自己转回像素坐标
  • Android端集成要用AAR包,Gradle配置容易翻车
  • 自定义关键点数量?别想了,68点是硬编码的

但比起从头训练一个模型,这些“小问题”简直可以忽略。毕竟,能跑起来的产品,才是好产品


训练自己的回归头:不是所有数据都来自WFLW

虽然MediaPipe提供了基础人脸检测,但它的关键点回归头是针对3D人脸建模训练的,和我们产品要的2D平面关键点(比如嘴唇内外轮廓)有偏差。

于是,我决定保留MediaPipe的检测器,只替换回归头,并在自己的数据集上微调。

数据集来源:

  • WFLW(公开数据集,含遮挡、夸张表情)
  • 自采数据:用手机拍了200张不同角度、光照、口罩佩戴的照片(包括我室友睡眼惺忪的样子)
  • 数据增强:随机旋转(±30°)、亮度抖动、模拟模糊

训练脚本核心逻辑(PyTorch):

# 冻结MediaPipe backbone(通过ONNX导出特征)
backbone = load_onnx_model("mediapipe_blazeface.onnx")
backbone.eval()

# 自定义轻量回归头
class KeyPointRegressor(nn.Module):
    def __init__(self, num_points=68):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(256, num_points * 2)  # x, y for each point
        )
    
    def forward(self, x):
        return self.fc(x).view(-1, 68, 2)

# 损失函数:加权MSE(嘴部关键点权重x2)
def weighted_mse_loss(pred, target, weights):
    loss = (pred - target) ** 2
    return (loss * weights).mean()

训练时用了Cosine Annealing LR,batch size=32,8个epoch就收敛了。最终在验证集上NME降到3.5%,比原始MediaPipe的4.1%有明显提升。

最爽的是——整个模型导出ONNX后只有7.2MB,加载到Android端,平均推理时间28ms,完美达标。


从算法到产品:那些没人告诉你的细节

你以为模型跑通就结束了?Too young.

1. 前处理:别让OpenCV背锅

一开始我用OpenCV的cv2.resize直接缩放图像,结果关键点位置偏移。后来发现是因为插值方式不对。改成INTER_AREA后,边缘锯齿少了,定位更准。

# 错误示范
img_resized = cv2.resize(img, (128, 128))

# 正确姿势
img_resized = cv2.resize(img, (128, 128), interpolation=cv2.INTER_AREA)

2. 后处理:平滑比精度更重要

实时视频中,关键点抖动会让人“晕3D”。我加了个简单的指数平滑滤波

# prev_pts: 上一帧的关键点
# curr_pts: 当前帧预测
smoothed_pts = 0.7 * curr_pts + 0.3 * prev_pts

效果立竿见影——用户反馈“终于不卡了”,虽然他们根本不知道背后是数学在拯救体验。

3. 产品埋点:别等上线才后悔

我在关键点输出后加了日志上报(脱敏后),记录失败case。结果发现,戴透明口罩的用户占比高达18%,而我们的训练数据里几乎没有。立马补采,两周内迭代了v1.1版本。


总结:算法是手段,产品是目的

这次项目让我彻底明白:在工业界,没有“最好的算法”,只有“最适合产品的算法”

在学校里,我们追求SOTA、刷榜、发论文;但在真实产品中,你得考虑:

  • 手机发热会不会让用户卸载?
  • 模型更新要不要重新过审?
  • 测试能不能用脚本自动化?
  • 运维能不能一键回滚?

现在回头看,如果当初死磕HRNet,可能现在还在调内存泄漏。而用MediaPipe+微调,三天搞定原型,一周交付,产品经理请我喝了杯瑞幸(虽然只是9.9券)。

入职前这段空窗期,我最大的收获不是技术,而是学会用产品思维做技术决策。毕竟,咱们不是在造火箭,而是在做一个让人愿意每天打开的App。

对了,下周就要去阿里园区报到了。听说那边食堂的东坡肉不错,希望别让我第一天就debug到半夜——毕竟,我已经习惯了8点起床,可不想变成“凌晨四点的杭州程序员”。

(完)

评论 0

最热最新
暂无评论
张敏△Lv.1
0
影响力
0
文章
0
粉丝