一个字节后端的CV实战:从“我不懂图像”到搞定商品主图检测

变量命名困难户
2025-12-12 17:48
阅读 288

作者:字节跳动基础架构组·五年搬砖人,刚入职新公司两个月,最近沉迷 Rust 不可自拔(但还在用 Python 调模型)


上周五晚上 10 点,我正对着屏幕调试一段 Rust 异步代码,试图把某个中间件的吞吐量再榨出 5%,突然钉钉弹出一条消息:

“兄弟,能帮忙看个 CV 的需求不?急!明天就要 demo 给产品看。”

我差点一口老血喷在键盘上——我是后端啊!虽然在字节干了五年,但那都是和 gRPC、Kafka、etcd 打交道,从来没碰过 OpenCV、PyTorch 这种带“视觉”俩字的东西。但谁让我刚入职两个月,还在试用期呢?只能硬着头皮接了。

于是,就有了这篇技术分享。如果你也像我一样,是个被临时抓壮丁的后端程序员,却要搞计算机视觉实战项目,希望我的踩坑经验能帮你少熬两个通宵。


业务背景:商品主图里有没有“人”?

事情起因是这样的:我们团队最近在重构电商商品展示系统。产品经理提了个看似简单的需求——“自动识别商品主图中是否包含人物,如果有,就打个标签”。

听起来很 straightforward 对吧?但实际上,这个需求背后藏着不少坑:

  • 商品图来源五花八门:有专业摄影棚拍的,也有卖家拿手机随便一拍的
  • 有人物 ≠ 有人脸(比如只拍到手、背影、剪影)
  • 有些图是模特穿衣服展示服装,有些却是真人试吃食品(这俩场景处理策略不同)
  • 最要命的是:不能误判。如果把纯商品图(比如一瓶水)误标为“含人物”,前端展示逻辑会乱套,影响转化率

产品 PM 还补了一句:“最好准确率 > 95%,召回率 > 90%。”
我当时内心 OS:你当这是调个 Redis 配置啊?


技术选型:别 reinvent the wheel

作为老后端,第一反应是:有没有现成服务能调?查了一圈,内部 CV 平台确实有“人体检测” API,但调用量大、成本高,而且 latency 在 200ms+,不符合我们低延迟的要求(商品列表页要秒开)。

那就只能自己训模型了。但问题是——我不会训模型啊

好在团队有个算法实习生小王(感谢他!),给我指了条明路:用开源预训练模型 + 微调(fine-tune)。具体来说:

  • Backbone:YOLOv8(速度快、精度不错、部署友好)
  • 数据集:COCO + 自建商品图样本
  • 框架:Ultralytics YOLO(官方支持 PyTorch,API 超友好)

为什么选 YOLO 而不是 Faster R-CNN 或 DETR?很简单:我们不是算法团队,要的是快、稳、能上线。YOLOv8 推理速度在 GPU 上能跑到 30+ FPS,CPU 上也能跑(虽然慢点),而且 Ultralytics 提供了完整的训练/推理/导出 pipeline,连 ONNX 导出都一键搞定——这对后端集成太友好了。


数据准备:脏活累活躲不掉

模型好不好,七分靠数据。我们从线上捞了 5 万张商品主图,人工标注了“含人”/“不含人”。但很快发现几个问题:

  1. 标注标准模糊:一张图里只有半个手算不算?
    → 和产品对齐:只要出现任何人体部位(头、手、脚、躯干)就算
  2. 负样本太多:90% 都是纯商品图,模型容易偏向预测“无” → 采用类别平衡采样,训练时正负样本 1:1
  3. 边界 case 多:玩偶、雕塑、卡通人物要不要算?
    → 明确规则:只识别人类,其他一律不算

最终整理出:

  • 正样本:8,200 张(含真实人体)
  • 负样本:8,200 张(纯商品 or 含非人类物体)

标注工具用的是 LabelImg,虽然界面复古得像 Windows 98,但胜在轻量、支持 YOLO 格式。


训练过程:调参如炼丹

环境搭好后,直接跑官方示例:

yolo detect train data=custom.yaml model=yolov8n.pt epochs=100 imgsz=640

结果第一轮训练完,验证集 mAP@0.5 只有 0.72。更糟的是,召回率极低——很多明显含人的图被漏掉了。

踩坑 1:输入尺寸太小

imgsz=640 对一般场景够用,但商品图里的人往往很小(比如远处模特)。我把 imgsz 调到 1280,显存直接爆了(我们的训练机只有 24G GPU)。

解决方案:改用 yolov8s(small 版本),配合 mosaic=0(关掉马赛克增强,避免小目标被切碎),imgsz=960。显存占用降下来了,小目标检出率明显提升。

踩坑 2:阈值一刀切

默认的 conf=0.25iou=0.45 在通用场景 OK,但在商品图里会产生大量误检(比如把瓶子反光当成人脸)。

解决方案:动态调整后处理阈值。我们发现,只要 conf > 0.4 且检测框面积 > 图片总面积的 0.5%,基本就是真阳性。于是在推理时加了后过滤:

def filter_detections(results):
    boxes = results[0].boxes
    valid_boxes = []
    for box in boxes:
        conf = box.conf.item()
        if conf < 0.4:
            continue
        # 计算检测框占图比例
        area_ratio = (box.xywh[0][2] * box.xywh[0][3]) / (IMG_W * IMG_H)
        if area_ratio < 0.005:  # 0.5%
            continue
        valid_boxes.append(box)
    return len(valid_boxes) > 0  # 有人即 True

踩坑 3:过拟合

训练 loss 一路下降,但验证集指标卡在 0.85 上不去。一看可视化结果——模型在记训练集图片!

解决方案

  • 增加 augment=True 中的数据增强(旋转、亮度、裁剪)
  • 加入 CutMix(把两张图的部分区域混合)
  • 早停(early stopping):验证 loss 5 轮不降就停

最终,经过三轮迭代,我们在验证集上达到:

  • 准确率:96.3%
  • 召回率:92.1%
  • mAP@0.5:0.897

部署上线:后端的主场来了

模型训好了,怎么让线上服务用起来?这才是后端的秀场。

我们选择 ONNX + ONNX Runtime 方案:

  • 模型导出为 ONNX(跨平台、无依赖)
  • 用 ONNX Runtime CPU 推理(避免 PyTorch 环境臃肿)
  • 封装成 gRPC 服务(和现有架构无缝集成)

导出命令超简单:

from ultralytics import YOLO
model = YOLO("runs/detect/train/weights/best.pt")
model.export(format="onnx", dynamic=True)  # 支持动态 batch

服务端代码(简化版):

import onnxruntime as ort
import numpy as np
from PIL import Image

class HumanDetector:
    def __init__(self, model_path):
        self.session = ort.InferenceSession(model_path)
        self.input_name = self.session.get_inputs()[0].name

    def preprocess(self, img: Image.Image) -> np.ndarray:
        # Resize + Normalize + CHW -> NCHW
        img = img.resize((960, 960))
        img = np.array(img).astype(np.float32) / 255.0
        img = np.transpose(img, (2, 0, 1))[None, ...]  # add batch dim
        return img

    def predict(self, img: Image.Image) -> bool:
        input_tensor = self.preprocess(img)
        outputs = self.session.run(None, {self.input_name: input_tensor})
        # outputs[0] shape: [1, 84, 8400] —— YOLOv8 的输出格式
        # 这里省略了解码逻辑,实际用了 ultralytics 的 postprocess
        return self._decode_and_filter(outputs[0])

# gRPC handler
def DetectHuman(self, request, context):
    img = Image.open(io.BytesIO(request.image_data))
    has_human = self.detector.predict(img)
    return DetectResponse(has_human=has_human)

性能压测结果

环境 平均延迟 QPS(单核)
Tesla T4 28ms 35
Intel Xeon (8 vCPU) 180ms 5

虽然 CPU 上慢了点,但商品图检测是异步任务(用户上传时触发),完全可接受。而且我们做了缓存:同一张图 hash 值相同,结果直接返回,避免重复计算。


效果与反思

上线一周后,统计数据显示:

  • 日均处理 120 万张图
  • 误判率 3.1%(主要是玩偶/影子)
  • 漏判率 6.8%(多为极小目标或遮挡严重)

产品 PM 居然说:“效果比预期好!” —— 我差点感动哭。

但更重要的是,这次经历让我意识到:后端和 AI 的边界正在模糊。以前觉得“算法是另一个世界”,现在发现,只要掌握核心思路(数据 > 模型 > 部署),加上工程化能力(服务化、监控、压测),后端完全能搞定轻量级 CV 任务。

顺便吐槽一句:运维同事看到我在服务器上跑 nvidia-smi 时的表情,仿佛看到了外星人 😅


给 fellow 后端的建议

如果你也被抓去搞 CV,记住这几点:

  1. 别从零造轮子:优先用 YOLO、SAM、CLIP 这类成熟方案
  2. 数据质量 > 模型 fancy 度:花 80% 时间清洗和标注
  3. 部署友好性很重要:选支持 ONNX/TensorRT 的框架
  4. 监控必须跟上:记录每张图的预测结果、置信度、耗时,方便回溯
  5. 和算法同学搞好关系:一杯瑞幸就能换来救命 advice

最后,虽然我现在还在研究 Rust,但不得不说,Python 在 AI 领域的地位短期内真没法撼动——生态太强了。不过嘛,等我用 Rust 写个 ONNX Runtime 的 binding,说不定又能吹一波了 🤪


本文所有代码已脱敏并整理成 mini 教程,放在公司内部 GitLab(搜索 cv-human-detector-demo)。外部同学可以参考 Ultralytics 官方文档 + COCO 数据集入门。

如果你觉得有用,欢迎点赞转发——毕竟,下一次被拉去搞 NLP 的,可能就是你了 😉

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝