智绘鸿蒙 · 如 7 而至端侧 AI 能力实战手记

我们想用多目标识别做冰箱清点,结果被 15 个标签教育了

智绘鸿蒙·如 7 而至

前言

需求听起来特别自然:打开冰箱拍一张,App 告诉你"香蕉剩两根、鸡蛋还有六个、牛奶喝完了"。

听起来就是一个目标检测嘛。鸿蒙有现成的端侧能力:多目标识别objectDetection),归属 @kit.CoreVisionKit,不联网、不要权限。我们兴冲冲接上,跑了一张冰箱照片。

结果是这样:

分类汇总:文本×12  食物×2  植物×2

十六个目标里,十二个是"文本"——冰箱里的价签、牛奶盒上的字、酱料瓶的标签。真正跟"食材"有关的只有四个框,而且其中两个是同一颗西兰花,一个被标成"食物"、一个被标成"植物"。

至于香蕉、鸡蛋、牛奶分别在哪?它一个都没说。

这篇就讲这件事:objectDetection 到底能干什么、我们怎么验证的、以及当你发现它不够用时该往哪走。

先认识这个能力给了什么

接口

import { objectDetection, visionBase } from '@kit.CoreVisionKit';

const detector = await objectDetection.ObjectDetector.create();
const response = await detector.process({ inputData: { pixelMap: pm } });
const objects: objectDetection.VisionObject[] = await response.objects;
await detector.destroy();

结构和图像超分是同一套 visionBase.Analyzer 家族:create() / process() / destroy()。所以你已经知道的规律都适用——create() 别放点击回调、process() 要串行、新 API 具名导入在旧设备会直接崩。

返回的每个对象长这样

interface VisionObject {
  boundingBox: visionBase.BoundingBox;   // { left, top, width, height }
  score: number;                         // (0,1)
  labels: Array<number>;                 // 数字标签
  id: number;                            // 从 0 递增
}
  • 目标检测(Object Detection):在图中定位并识别多个目标,输出类别与边框。
  • 置信度 score:模型对该检测结果的确信程度,取值 (0,1)。
  • 端侧推理:识别在设备本地完成,图片不外传。

这里有个新手一定会卡住的地方:labels 是数字数组,d.ts 里没有任何说明。 你在类型定义文件里翻不到 7 是什么、11 是什么。

词表藏在 API 参考文档里,不在代码里。完整的一共 15 个:

编号 含义 编号 含义
0 风景 10 狗头
1 动物 11 食物
2 植物 12 汽车
3 建筑 13 人体
5 人脸 21 文档
6 表格 22 卡证
7 文本
8 人头
9 猫头

请注意两件事:

  1. 只有 15 类。 编号 4、14~20 是空缺的,不是漏抄。
  2. 没有"香蕉""鸡蛋""牛奶"这一层。 它给的是"食物"这个大类。

所以"冰箱清点"这个需求,从能力定义上就不在这个 API 的射程里。我们能拿到的最细粒度就是"这里有个食物"。

那它适合做什么

看完词表反而清楚了。它的 15 类里有几类是很好用的版面/内容分类器,而不是物体检测器:

类别 实用场景
文本(7)、文档(21)、卡证(22)、表格(6) 拍照自动分类:扫描件、身份证、报销单据、表格打印页——文档管理 App 的核心诉求
人脸(5)、人头(8)、人体(13) 隐私打码的粗筛,比逐帧跑人脸检测便宜
猫头(9)、狗头(10) 宠物相册自动归类,单独给了宠物类,比笼统的"动物"实用
风景(0)、建筑(3) 相册按拍摄主题分册

换句话说:把它当"15 类图片内容粗分器"用是对的,当"通用物体识别器"用是错的。

我们是怎么验证的

第一步:把标签词表写进代码

既然 d.ts 里没有,就自己维护一份,并且一定要留未知分支,因为编号可能随模型版本扩充:

export const LABELS: Map<number, string> = new Map<number, string>([
  [0, '风景'], [1, '动物'], [2, '植物'], [3, '建筑'], [5, '人脸'], [6, '表格'], [7, '文本'],
  [8, '人头'], [9, '猫头'], [10, '狗头'], [11, '食物'], [12, '汽车'], [13, '人体'],
  [21, '文档'], [22, '卡证']
]);

labelName: LABELS.get(lid) || `未知(${lid})`

第二步:一定要自己加置信度过滤

接口没有提供阈值参数,process() 把所有检出的框都吐给你。实测一张图返回 18 个,其中大量是 24×5 像素的小框——这种基本可以确定是纹理误检。

const items: Detected[] = [];
for (let i = 0; i < res.objects.length; i++) {
  const o: objectDetection.VisionObject = res.objects[i];
  if (o.score < minScore) {
    continue;                       // 阈值自己定
  }
  // ...
}

我们最后用了两道过滤,缺一不可:

const bigEnough: boolean = o.boundingBox.width >= imgW * 0.05
  && o.boundingBox.height >= imgH * 0.05;

只按 score 过滤会留下一堆高置信度的小框;只按尺寸过滤会留下低置信度的大框。

第三步:把 labels 数组当成"可能多个"来处理

实测每个对象 labels 长度都是 1(日志里是 rawLabels=[7] 这样)。但类型是数组,就意味着同一区域可能给多个标签——事实上真的发生了,见下一节。所以别写死 labels[0],至少留个兜底:

const lid: number = o.labels.length > 0 ? o.labels[0] : -1;

真机数据

测试机:HUAWEI MatePad Pro(MRDI-W00),HarmonyOS 7.0.0.107,API 26。

输入是一张 720×540 的冰箱内部照片,画面里有鸡蛋、香蕉、番茄、牛奶、酸奶、西兰花、酱料瓶、几个保鲜盒。

阈值 0.30 时的结果:

total=18 kept=16 cost=679ms
分类汇总:文本×12  食物×2  植物×2

明细里几个关键项:

id label 置信度 我们的解读
0 7 文本 76.1% 651,138 56×14 包装上的印刷字,误检成目标
4 11 食物 48.3% 83,301 197×159 西兰花
8 2 植物 44.2% 83,301 197×159 同一个框,另一个标签
11 11 食物 41.4% 243,55 354×184 上层那排东西
6 7 文本 46.8% 653,221 24×5 24×5 像素的噪声框

界面结果:

逐条明细(标签、置信度、框坐标):

这次实验教给我们的四件事

第一件:15 类是硬上限,别指望细粒度识别。

"还剩几根香蕉"这种需求,objectDetection 做不到。要么接受只到"食物"这一层,要么换方案(见下一节)。

第二件:同一区域会被打多个标签,统计前先按框去重。

西兰花同时是"食物"和"植物",两个对象 id 不同但 boundingBox 完全一致(83,301,197×159)。如果你按对象数统计"图里有几个东西",会重复计数。

去重的做法是按框的重叠度合并:

function sameBox(a: Detected, b: Detected): boolean {
  return a.left === b.left && a.top === b.top
    && a.width === b.width && a.height === b.height;
}

完全相同只是最省事的一种;真实场景要算 IoU。

第三件:误检主要来自"像字的纹理",必须双重过滤。

12/16 是"文本",绝大多数是几十像素宽的条状框。这是因为冰箱里确实到处是标签和印刷字——模型没错,是我们的需求假设错了:"食品包装上的字"也是文本。

阈值 0.30 时留下 16 个,提到 0.50 只剩 4 个。所以阈值不是越高越好,越高越容易把真正的目标(食物最高才 48.3%)也砍掉。

这个 48.3% 值得记住:在这类"物体不是画面主体"的场景里,正确目标的置信度天然偏低,用通用阈值 0.6 会直接漏检。

第四件:耗时可以接受,但别在预览帧上跑。

单次 679 ms(首次)/ 521 ms(热态)。做"拍一张分析一次"完全够用,做实时预览则每秒只能跑一两次,需要降分辨率。

那我们到底该怎么实现冰箱清点

三条路,按投入排序:

路线 做法 代价 适用
一、换需求 把"识别每样食材"改成"有没有食物、有几处食物",用于"该补货了"这类粗提醒 几乎为零 能接受粗粒度
二、串联文搜图 给常备食材建参考图库并索引进 textSearchImage,对检测到的"食物"框逐个裁剪检索 每框一次检索,十几个框要几秒 需要"这是香蕉"级别
三、自己接模型 引入真正的细粒度分类器 工作量最大 效果要求高

我们最后选了路线二。因为对"还剩什么"这个用户问题,"按相似度匹配参考图"给出的答案已经足够好,而且用户能自己往参考库里加新食材。

总结

什么场景值得用它:把图片按内容粗分——文本 / 文档 / 卡证 / 表格做拍照归档,猫头狗头做宠物相册,人脸人头做打码粗筛。

不适合:细粒度的"这是什么东西"。15 类是硬上限,香蕉、鸡蛋、牛奶都不在那张表里。

💡 一句话:当 15 类内容粗分器用是对的,当通用物体识别器用是错的——要细粒度就串文搜图。

参考文献

  1. Core Vision Kit 开发指南 · 多目标识别:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-object-detection
  2. Core Vision Kit API 参考 · objectDetection(多目标识别):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-object-detection-api
  3. Core Vision Kit 开发指南 · 通过文本搜索图片(细粒度串联方案):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-text-search-image