深夜喂完奶后,我用 JavaScript 给运营团队搞了个“图像识别外挂”

掘金独行侠
2025-12-12 22:43
阅读 725

凌晨 2:17,小宝终于睡熟了。我把奶瓶轻轻放在灶台上,蹑手蹑脚回到书房,打开那台贴满卡通贴纸的 MacBook——没错,这就是我的“深夜第二战场”。白天是全职妈妈,晚上是码农,最近刚入职新公司两个月,正在适应“带娃+上班”双线程运行的硬核生活。

上周五下班前,运营组的老张突然在钉钉上@我:“姐,能不能帮忙搞个能自动识别商品主图里有没有‘真人模特’的功能?我们双11素材审核快爆了。” 我一口咖啡差点喷出来——你管这叫“能不能”?这明明是个正经的计算机视觉任务啊!

但转念一想,我司做的是 C2M 电商平台,每天上传的商家图片成千上万,人工审核确实不现实。而且我本来就对底层原理有点执念(被前司架构师毒打出来的),既然都踩进来了,不如干脆撸起袖子干一票大的。

于是,就有了这个用 JavaScript 实现的商品图像智能分类项目。今天这篇水文,就是记录一下我这个“深夜带娃码农”是怎么在奶香和代码香之间,硬生生把 CV 项目跑通的全过程——包括那些让我想砸电脑的坑,和最后看到准确率飙升时嘴角疯狂上扬的瞬间。


起因:运营的“小需求”,程序员的“大工程”

先说清楚背景。我们平台允许商家上传商品图,但平台规范要求:服饰类商品主图必须包含真人模特试穿效果,不能只有平铺图或白底图。以前靠人工抽检,漏检率高不说,运营小姐姐们眼睛都快看瞎了。

老张原本以为加个“判断图里有没有人”的开关就行,殊不知——

“有没有人” 和 “有没有穿这件衣服的人” 是两码事!

比如这张图:

  • 有真人,但穿的是牛仔裤,商品却是 T 恤 → 不合格
  • 有真人,但只是手拿着衣服没穿 → 不合格
  • 有真人,全身穿着商品同款 → 合格

所以问题本质不是通用人体检测,而是 “商品-人物关联性判断”。更麻烦的是,我们前端技术栈以 React + Node.js 为主,后端微服务也重度依赖 JavaScript 生态。老板明确说了:“能用 JS 解决的,别给我引入 Python 服务,运维会骂人的。”

得,那就用 JavaScript 干 CV。


技术选型:在 TensorFlow.js 和 ONNX 之间反复横跳

一开始我天真地以为直接用 TensorFlow.js 加载个预训练模型就行。毕竟官网示例里 mobilenet 几行代码就能分类 ImageNet,多爽。

import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';

const model = await mobilenet.load();
const predictions = await model.classify(imageElement);

结果一跑测试集就傻眼了:

  • 模型说“有人”,但其实是衣架上的假人模特
  • 模型说“无人”,但其实角落里有个穿同款的小朋友(像素太小)
  • 最离谱的是,一张纯白底图被识别成“浴袍”(因为训练集里很多浴袍是白的……)

显然,通用图像分类模型根本不理解我们的业务语义。得重新训练。

数据从哪来?

我厚着脸皮找数据团队要了过去半年被人工标记为“合格/不合格”的主图,大概 8000 张。但问题来了:

  • 合格样本只有 3000 张(商家懒,很多人传平铺图)
  • 不合格样本里混杂了各种情况:无图、logo 图、场景图……

我花了两个晚上(趁娃睡了)手动清洗数据,用 LabelImg 标出“人物区域”和“商品区域”,最后整理出一个干净的二分类数据集:合格 vs 不合格

提醒自己:下次一定要推动建立标准标注流程,不然数据脏得像我家地板(别问,问就是娃刚打翻了米糊)。

模型选择:轻量 vs 精准

考虑到要部署到前端(部分场景需实时反馈),模型不能太大。我对比了几种方案:

方案 框架 模型大小 推理速度 (CPU) 准确率 (验证集) 是否支持浏览器
MobileNetV2 TF.js ~14MB 120ms 82.3%
EfficientNet-Lite0 TF.js ~21MB 180ms 86.7%
YOLOv5s (ONNX) ONNX Runtime Web ~28MB 350ms 91.2% ✅(需转 ONNX)
自定义 CNN TF.js ~8MB 90ms 79.5%

看起来 YOLO 最准,但它输出的是目标检测框,我还需要额外写逻辑判断“人物是否穿着目标商品”——这又回到了最初的问题。

最终我决定:用 EfficientNet-Lite0 做端到端二分类。理由很现实:

  • 运营不需要知道“为什么不合格”,只需要“合不合格”
  • 训练简单,调参少
  • 能直接集成到现有 React 组件里

训练过程:从 loss 不降 到 准确率起飞

我在本地用 Python + Keras 先训了个原型(别打我,我只是借个环境),数据增强用了:

  • 随机裁剪(模拟不同构图)
  • 颜色抖动(应对不同光线)
  • 水平翻转(衣服左右对称嘛)

训练脚本核心就这几行:

model = tf.keras.applications.EfficientNetLite0(
    include_top=True,
    weights=None,
    input_shape=(224, 224, 3),
    classes=2
)

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

history = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=30,
    callbacks=[EarlyStopping(patience=5)]
)

但第一次跑完,验证准确率卡在 75% 不动。我盯着 loss 曲线看了半小时,突然意识到——类别不平衡!不合格样本是合格的两倍多。

赶紧加上 class_weight:

from sklearn.utils.class_weight import compute_class_weight

class_weights = compute_class_weight(
    'balanced',
    classes=np.unique(train_labels),
    y=train_labels
)
class_weight_dict = dict(enumerate(class_weights))

再跑,准确率直接冲到 88%。那一刻我差点在书房跳起来(然后想起隔壁睡着娃,默默坐下)。


转换与部署:让模型跑在浏览器里

训练完的 .h5 模型不能直接给前端用,得转成 TensorFlow.js 格式:

tensorflowjs_converter \
  --input_format=keras \
  ./model.h5 \
  ./tfjs_model

生成一堆 .binmodel.json 文件,丢到 CDN 上。

前端调用代码也很简单:

// hooks/useImageClassifier.js
import * as tf from '@tensorflow/tfjs';

let model;

export const loadModel = async () => {
  if (!model) {
    model = await tf.loadLayersModel('/models/tfjs_model/model.json');
  }
  return model;
};

export const classifyImage = async (img) => {
  const m = await loadModel();
  const tensor = tf.browser.fromPixels(img)
    .resizeNearestNeighbor([224, 224])
    .toFloat()
    .div(tf.scalar(255.0))
    .expandDims();

  const predictions = await m.predict(tensor).data();
  tf.dispose(tensor); // 别忘了内存管理!

  return {
    isQualified: predictions[1] > 0.7, // 阈值可调
    confidence: predictions[1]
  };
};

重点来了

  • 必须 tf.dispose(),否则内存泄漏,浏览器卡死(亲身踩坑)
  • 图片预处理尺寸必须和训练时一致(224x224)
  • 置信度阈值别设 0.5!我调到 0.7 才平衡了误杀率和漏杀率

上线后的“惊喜”:运营说“好像有点不准?”

模型上线第一天,运营反馈:“有些明显合格的图被打了回票!” 我心里一紧,赶紧查日志。

发现罪魁祸首是——图片加载时机
前端在 <img>onLoad 回调里调用分类,但如果图片还没完全渲染(比如网络慢),tf.browser.fromPixels 就会读到空白 canvas。

解决方案:加个 requestAnimationFrame 确保渲染完成:

const classifyWhenReady = (img) => {
  return new Promise(resolve => {
    const check = () => {
      if (img.complete && img.naturalHeight !== 0) {
        resolve(classifyImage(img));
      } else {
        requestAnimationFrame(check);
      }
    };
    check();
  });
};

另外,还有一次线上事故:某商家上传了一张 超大分辨率图(8000x6000),直接导致低端手机浏览器崩溃。现在前端会先压缩图片再送入模型:

const resizeImage = (file, maxWidth = 1024) => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = URL.createObjectURL(file);
    img.onload = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      let { width, height } = img;
      if (width > maxWidth) {
        height = (height * maxWidth) / width;
        width = maxWidth;
      }
      canvas.width = width;
      canvas.height = height;
      ctx.drawImage(img, 0, 0, width, height);
      resolve(canvas);
    };
  });
};

效果如何?运营终于不用加班了

上线两周后,我拉了份数据:

指标 上线前 上线后
日均人工审核量 5200 张 800 张
审核平均耗时 3.2 秒/张 0.4 秒/张
模型准确率 - 89.6%
误杀率(合格被判不合格) - 6.2%
漏杀率(不合格被判合格) - 4.1%

老张昨天请我喝了杯奶茶(无糖,带娃后戒糖了),说:“现在我们只复核模型打低分的图,效率翻倍!”

最让我开心的是,这个模型现在不仅用于审核,还被产品拿去做 “商家素材质量评分”,甚至反哺到推荐系统——高质量主图的商品更容易被曝光。技术真的能驱动业务,这话不假。


写在最后:当妈后,我更懂“鲁棒性”了

说实话,做这个项目最大的挑战不是算法,而是在碎片时间里保持专注。有时候刚调好一行代码,娃就哭了;有时候半夜想到 loss 不降的原因,爬起来改配置又怕吵醒家人。

但正是这种“被打断”的日常,让我对系统的鲁棒性有了更深的理解——

就像带娃,你永远不知道下一秒会发生什么,但你得确保整个系统(家庭+工作)还能稳稳运行。

这次用 JavaScript 做 CV,虽然绕了些路,但也证明了:JS 生态早已不是当年那个“玩具语言”了。TensorFlow.js、ONNX Runtime Web、WebGL 加速……前端工程师也能玩转 AI。

如果你也在带娃写代码,或者被运营提了“小需求”,别慌。深呼吸,泡杯咖啡(或奶粉),一行一行 debug。毕竟——

我们既能哄睡人类幼崽,也能驯服机器学习模型,还有什么搞不定的?

(完)

P.S. 下次打算试试用 MediaPipe 做实时姿态估计,看看能不能判断模特是不是“自然展示商品”……不过得等小宝睡整觉再说 😴

评论 0

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