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

端侧文搜图:先搞清它按什么匹配,再决定搜索框怎么写

智绘鸿蒙·如 7 而至

前言

假设你在做一个旅行相册应用。用户拍了几千张照片,文件名全是 IMG_20260924_183207.jpg,按时间排序帮不上忙,标签系统又没人愿意手动打。

用户实际会怎么找照片?他会说:"把海边日落那张给我看看。"

传统两条路都不划算:云端多模态检索要上传用户全部相册,隐私和带宽都是问题;本地自建向量库要引模型、做推理、管索引,工程量劝退。

HarmonyOS 7(API 26)在 Core Vision Kit 里新增了这个能力:文搜图textSearchImage)。把图片特征存进本地索引,然后用一句自然语言把匹配的图片捞回来,全程不出设备。

这篇先讲怎么用,再讲一个更重要的事:它到底按什么在匹配——这个直接决定你的产品文案怎么写。

能用在哪些场景

  • 旅行、家庭相册的自然语言检索(最贴合)
  • 自媒体、设计类的本地素材库
  • 工程、家装类的参考图归档
  • 任何"图片多、命名烂、标签没人打"的本地相册

需要注意的是,它不是通用图片搜索引擎。第二节末尾会看到它明确的脾气边界。

接口与约束

调用面六个方法,都是 textSearchImage 命名空间下的静态函数,不需要 create/destroy 分析器:

import { textSearchImage } from '@kit.CoreVisionKit';

init(): Promise<boolean>
insertImage(imagePath: string, scope: string): Promise<boolean>
search(query: string, scope: string, topKey?: number): Promise<ImageObject[]>
deleteImage(imagePath: string, scope: string): Promise<boolean>
clearData(): Promise<boolean>
release(): Promise<boolean>

返回的 ImageObject 有三个字段:imagePathscopesimilarity(取值范围 [-1, 1],越大越相似)。

  • 文搜图(Text-to-Image Search):用文本作为查询条件,从已建立索引的图片集合中检索匹配图像。
  • scope:图片索引的逻辑分区,用来隔离不同图库,类似一张表里的命名空间。
  • 端侧索引:图片特征提取与检索都在设备本地完成,不上传原图。

参数约束(来自 SDK 声明,实测有效):

参数 约束
imagePath 应用沙箱路径,1–128 字符
scope 1–32 字符,只允许字母或数字
query 1–100 字符,允许中文
topKey 整数 0–100,默认 100

三个错误码值得提前处理:

  • 1013100001 Invalid image path —— 传了媒体库 URI 而不是沙箱路径
  • 1013100002 Service abnormal —— 服务异常,需要降级
  • 1013100003 The capability has been updated —— 模型能力升级后旧索引失效,官方要求先 clearData() 再重新 insertImage()

最后这个坑比较隐蔽:用户升级系统后你的索引可能整体作废。不处理 1013100003,表现就是"搜索突然全空",而且界面上没有任何提示。

开发步骤

第一步:把图片落到沙箱

这是最容易卡住的地方。insertImage 只吃沙箱路径,你从相册选图拿到的 file://media/Photo/... URI 直接传进去就是 1013100001

所以流程必须是:取图 → 拷进 context.filesDir → 用沙箱路径建索引

async stageFiles(ctx: Context, names: string[]): Promise<string[]> {
  const paths: string[] = [];
  for (let i = 0; i < names.length; i++) {
    const bytes: Uint8Array = await ctx.resourceManager.getRawFileContent(names[i]);
    const dest: string = `${ctx.filesDir}/${names[i]}`;
    const file = fileIo.openSync(dest,
      fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
    fileIo.writeSync(file.fd,
      bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
    fileIo.closeSync(file);
    paths.push(dest);
  }
  return paths;
}

从相册导入同理:fileIo.open(uri) 读出内容,写进 filesDir,再拿这个路径建索引。

还有一点:scope 只能用字母和数字。我一开始想用 trip_2026,下划线不在允许字符集里,最后用了 triplens2026。scope 是索引的逻辑分区,多用户、多图库都靠它隔离,别浪费它。

第二步:建索引,并把耗时暴露出来

async build(paths: string[], scope: string): Promise<number> {
  await this.init();
  const start: number = Date.now();
  for (let i = 0; i < paths.length; i++) {
    try {
      await textSearchImage.insertImage(paths[i], scope);
    } catch (err) {
      const e = err as BusinessError;
      if (e.code === 1013100003) {
        await this.reset();                 // 模型升级,旧索引作废
        return this.build(paths, scope);    // 重建一次
      }
      throw new TsError(e.code, `建索引失败(${e.code}):${e.message}`);
    }
  }
  return Date.now() - start;
}

实测单张 640×480 图片的 insertImage380 ms,6 张约 2.3 秒。

这个数字决定了产品形态:建索引必须是可后台执行、可中断、有进度的任务,不能放在首屏等待里。

第三步:检索

async search(query: string, scope: string, top: number): Promise<Hit[]> {
  await this.init();
  const list: textSearchImage.ImageObject[] = await textSearchImage.search(query, scope, top);
  // 转成 UI 需要的结构:文件名 + similarity
}

检索很快,实测 13–36 ms,用户体感是即时的。

第四步:生命周期

init() 自己保证幂等;release()aboutToDisappear。因为 insertImage 是同步等返回的,索引期间不要并发调用 search

真机运行效果

测试机:HUAWEI MatePad Pro(MRDI-W00),HarmonyOS 7.0.0.107,API 26。图库是 6 张旅行照:海边日落、雪山湖泊、都市夜景、古镇小巷、森林溪流、沙漠星空。

精确命中一张:

搜「夜景」命中两张(都市夜景 + 沙漠星空):

搜「风景」返回空:

单特征词「城市夜景」:

它到底按什么匹配

我用 9 个查询词做了完整对照,结果如下:

查询词 命中 相似度 检索耗时 解读
海边日落 1 张(海边) 33.7% 36 ms 精确
雪山湖泊 1 张(雪山) 35.9% 21 ms 精确
城市夜景 1 张(都市夜景) 35.3% 17 ms 精确
沙漠星空 1 张(沙漠星空) 34.8% 19 ms 精确
夜景 2 张(都市夜景 32.9% / 沙漠星空 31.7%) 17 ms 跨图共同特征
1 张(雪山) 31.2% 单字也能命中
一张猫 0 张 13 ms 图库无猫,正确拒绝
风景 0 张 17 ms 6 张全是风景,却返回 0
海边的雪山 0 张 跨图元素拼接,正确拒绝

两个结论,直接决定产品怎么写。

结论一:它匹配"画面里有什么",不匹配类别。

「风景」返回 0 是最有信息量的一条——6 张图全是风景,但"风景"不描述任何画面内容,模型无从对齐。而「夜景」返回 2 张,因为都市夜景和沙漠星空在画面上共享"夜"这个可视特征

所以搜索框的引导文案不能写"输入图片类别",要写**"描述照片里的东西"**。示例词要具体到画面元素:海边日落下雪的山夜晚的霓虹灯,而不是 风景旅行好看

结论二:相似度绝对值不能直接给用户看。

精确命中的分数也只有 31%–36%,因为返回值落在 [-1,1] 全区间,实际分布被压缩在 0.3 附近。如果 UI 上显示"匹配度 33.7%",用户会认为结果很差。

建议做法:只做相对排序,不做绝对展示;一定要显示,就按本次结果集归一化(最高分记 100),或者干脆显示名次而不是百分比。

上生产前还要补的三件事

  1. 索引持久化与增量。 insertImage 是往系统数据库里写,但你需要自己记住"哪些文件已经建过",否则每次冷启重建,2.3 秒/6 张会放大成几百秒。
  2. 删除同步。 用户从你的应用里删照片时,必须同步 deleteImage,否则结果里会返回已经不存在的文件。
  3. 1013100003 的自动恢复。 别把它当普通错误抛给用户,检测到就静默 clearData() + 重建,并在 UI 上提示"正在重新整理相册索引"。

总结

什么场景值得用它:图片多、命名烂、标签没人打的本地相册——旅行、家庭、自媒体素材、工程参考图都算,全程端侧跑完,原图不出设备。

不适合:需要按"类别、事件"检索的场景。它认的是画面里有什么,不认"风景""旅行"这种抽象标签,硬套只会得到一片空白。

💡 一句话:搜索框要引导用户"描述照片里的东西",别让他选分类;相似度只做相对排序。

参考文献

  1. Core Vision Kit 开发指南 · 通过文本搜索图片:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-text-search-image
  2. Core Vision Kit API 参考 · textSearchImage(通过文本搜索图片):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-text-search-image-api
  3. Core Vision Kit 错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/errorcode-core-vision
  4. HarmonyOS 7(API 26) 官方新能力解读 & 开发者实战开发案例合集:https://developer.huawei.com/consumer/cn/forum/topic/0208224160644765091