测试环境:ALN-AL80 真机,OpenHarmony 7.0.0.32,API 26;测试集为 5 张固定 CC0 照片。

为什么相册需要语义搜索

相册数量少时,文件名、地点和人工标签已经够用。照片增加以后,用户对画面的记忆往往和这些字段对不上:相册记录里写着“城市夜景”,用户输入的却可能是“夜晚亮灯的城市”。

传统关键词检索比较的是文字。字段中没有出现查询词,即使照片内容完全吻合,也不会返回结果。

基线测试使用同一批照片的 fileNametitlecaption 三个字段,查询“夜晚的城市灯光”时执行直接字符串匹配:

const query = '夜晚的城市灯光';
const matches = fixtures.filter(item =>
  [item.fileName, item.title, item.caption]
    .some(text => text.includes(query))
);

结果为 0。测试集中的夜景照片标题是“城市夜景”,描述是“河岸灯光与城市建筑形成高对比夜景”,任何字段都不包含完整查询句。要让传统方案命中,只能补标签、拆词或维护“夜晚—夜景—亮灯”等同义词规则。

验证条件固定为同一批图片和同一句查询词。接入 API 26 后,只比较检索方式是否能够根据画面内容返回夜景照片。

API 26 提供了什么

华为官方将这项能力放在 Core Vision Kit → 通过文本搜索图片textSearchImage 的调用链由六个主要接口组成:init() 初始化服务,insertImage()deleteImage() 维护图片索引,search() 执行文本检索,clearData() 清空索引,release() 释放服务。

Core Vision Kit 官方能力页将该 Kit 定义为机器视觉基础能力;HarmonyOS SDK 文档中心把“通过文本搜索图片”列在 Core Vision Kit 开发指南中;API 变更清单记录了 26.0.0 Beta1 的 Core Vision Kit 新增能力。本机 SDK 中的 @hms.ai.vision.textSearchImage.d.ts 也将上述接口标注为 @since 26.0.0

接口设计说明了它和传统标签检索的区别:

  • insertImage() 接收图片的沙箱路径和 scope,不接收标题、标签或描述;
  • search() 接收自然语言和 scope,返回匹配图片的 imagePathsimilarity
  • 图片内容的特征提取和图文语义匹配由 Core Vision Kit 完成。

应用因此不必为每张图片生成“城市”“夜景”“灯光”等内容标签,也不必自建图文向量模型和近邻检索服务。应用仍要管理图片文件、访问权限,以及哪些路径进入或离开索引。准确地说,API 26 省掉的是内容标注和检索模型,不是文件生命周期管理。

建立五张图片的语义索引

测试集包含湖边日落、长城、森林小路、海岸线和城市夜景。启动测试页后,应用把五张公开图片复制到沙箱,再把路径逐张交给 insertImage();传入索引的只有图片路径,没有 titlecaption

const initialized = await textSearchImage.init();
if (!initialized) {
  throw new Error('系统语义搜图服务暂不可用');
}

for (const imagePath of imagePaths) {
  const inserted = await textSearchImage.insertImage(
    imagePath,
    'PublicPhotos'
  );
  if (!inserted) {
    throw new Error(`索引写入失败:${imagePath}`);
  }
}

5 张公开测试图已建立语义索引

图 1:ALN-AL80 真机完成 5 张测试图片的索引写入。

固定数据集便于复查同一句查询在不同系统版本上的表现,也避免测试过程读取私人相册。

接入后:同一句话搜到夜景照片

搜索阶段不再读取标题和描述,查询词直接传给 search()

const results = await textSearchImage.search(
  '夜晚的城市灯光',
  'PublicPhotos',
  100
);

真机返回一张城市夜景照片。前后两次测试使用同一批图片和同一句“夜晚的城市灯光”,唯一变化是检索方式。

检索方式 交给检索器的数据 返回结果
文件名、标题和描述直接匹配 三个文字字段 0 张
API 26 textSearchImage 五个图片路径 1 张城市夜景

城市夜景命中一张真实照片

图 2:输入图片标题中的“城市夜景”,结果区只显示一张城市夜景照片。

“城市夜景”先确认索引和搜索链路能够返回目标图片;随后换成数据字段中不存在的“夜晚的城市灯光”,验证自然语言与画面内容之间的语义匹配。

“湖边日落”为什么没有搜到

同一轮真机测试还出现了一次漏召回。测试集中有一张湖边日落照片,但“湖边的日落”和“湖边晚霞”都返回 0 张。

相册中有湖边日落照片但本次查询没有返回

图 3:数据集中存在湖边日落图,两种查询表达均返回 0 张,结果区没有混入占位图片。

公开接口只返回命中项,没有提供未命中的内部原因。画面主体、构图和系统模型都可能影响相关性,调用方无法从返回值中确定具体原因。空状态文案因此使用“没有找到相关照片,换一种描述试试”,不把本次检索结果解释为相册中不存在该照片。

另一个负例“白色北极熊驾驶红色跑车”同样返回 0 张:

明显不存在的查询进入空结果状态

图 4:测试集中没有对应画面,页面返回 0 张,结果区保持为空。

五张图片适合验证接口链路,无法用于评价模型召回率。阈值和查询策略需要在人物、食物、城市、自然、夜景和文字等更大数据集上测试。

命中结果怎样回到相册详情

search() 返回 ImageObject 数组,其中包含图片路径和相似度。双镜记忆相机根据 imagePath 查找当前相册记录,过滤已经删除或改为私密的照片,再按 similarity 从高到低展示。

一条双拍记录可能包含前后两张图片。如果两个路径同时命中,页面只保留相似度较高的一项,避免同一组照片重复出现。

for (const result of results) {
  const record = findPublicRecordByPath(records, result.imagePath);
  if (!record) {
    continue;
  }

  const index = matches.findIndex(item => item.recordId === record.id);
  if (index < 0) {
    matches.push({
      recordId: record.id,
      imagePath: result.imagePath,
      similarity: result.similarity
    });
  } else if (result.similarity > matches[index].similarity) {
    matches[index] = {
      recordId: record.id,
      imagePath: result.imagePath,
      similarity: result.similarity
    };
  }
}

matches.sort((left, right) => right.similarity - left.similarity);

点击唯一命中的城市夜景进入照片详情

图 5:点击搜索结果卡片后进入照片详情。该页面验证 imagePath 与相册记录的跳转关系,不作为搜索命中的证据。

Core Vision Kit 决定哪些图片与查询相关;相册记录决定这张图片当前是否允许展示。两部分通过返回的 imagePath 接在一起。

新增和删除照片时怎样更新索引

应用不需要管理图片的内容标签,但需要让系统索引与当前相册保持一致。双镜记忆相机在 Preferences 中保存上次成功写入的路径集合,每次同步只处理差异:

const desiredPaths = collectPublicImagePaths(records);
const indexedPaths = await loadIndexedPaths(context);

for (const path of indexedPaths) {
  if (!desiredPaths.includes(path)) {
    await textSearchImage.deleteImage(path, SEARCH_SCOPE);
  }
}

for (const path of desiredPaths) {
  if (!indexedPaths.includes(path)) {
    await textSearchImage.insertImage(path, SEARCH_SCOPE);
  }
}

await saveIndexedPaths(context, desiredPaths);

新增照片调用 insertImage(),删除照片或公开状态变化调用 deleteImage()。本地路径集合只能在系统接口成功后更新,否则一次写入异常会让后续同步误判。

SDK 错误码 1013100003 表示能力已经更新。收到该错误后调用 clearData(),再以当前公开照片路径重建索引。scope 用于区分检索集合,示例使用含义明确的 PublicPhotos;照片是否公开仍以相册记录为准。

模拟器和真机的结果不同

同一个 HAP 安装到 API 26 三折叠模拟器后,Core Vision 返回 1013100002 Service abnormal;相同代码在 ALN-AL80 真机上完成索引,并成功返回夜景照片。

模拟器可检查输入框、加载状态、空状态和折叠布局。Core Vision 语义检索的能力结论来自真机结果。

最终效果:一句话返回唯一夜景照片

输入夜晚的城市灯光后唯一命中城市夜景

图 6:输入“夜晚的城市灯光”,页面同屏显示查询词、“找到 1 组相关照片”和唯一的城市夜景结果卡。

图 6 构成搜索成功的完整证据:查询词没有出现在测试图片的文件名、标题和描述中,结果页仍只返回城市夜景;搜索结果卡与图 5 的照片详情承担不同职责,前者证明语义命中,后者验证点击后的业务跳转。

API 26 让相册获得了一条新的找图路径:照片入库时提交图片路径,Core Vision Kit 建立内容语义索引;搜索时提交自然语言,系统返回相关图片。应用不需要为每张照片补齐“城市、夜景、灯光”等内容标签,也不需要部署自己的图文检索模型。

这项能力适合作为相册浏览之外的第二入口。用户记得画面却记不清时间和地点时,可以直接输入一句描述;imagePath 返回后仍要经过当前相册记录和可见性检查,再进入照片详情。

漏召回决定了搜索页还要保留两个出口:换一种说法重新搜索,或者返回时间、地点和相册分类继续查找。空状态写“没有找到相关照片”即可,不能把一次检索结果解释为相册中不存在这张照片。

接入真实相册时,核心工作集中在三处:新增和删除照片时同步索引;索引能力更新后使用当前路径重建;展示前重新检查照片权限。完成这三步,语义搜索才能从一段 API 示例变成可以长期使用的相册功能。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐