文搜图封面

做了 OCR 和超分之后,产品又提了个需求:用户能不能用一句话找图片?比如输入"去年在海边拍的那张",相册就能把海边的照片筛出来。这个功能在 iOS 和安卓上已经有了,鸿蒙端侧能不能做?

研究了一下,HarmonyOS 7 的端侧多模态能力支持文搜图——把文字描述和图片做向量匹配,返回相似度最高的几张。这个能力完全在本地跑,不用上传图片到云端,隐私安全有保障。这篇就讲讲我们怎么把它集成到相册应用里。

一、真实开发中遇到的问题

最开始的想法很简单:用户输入一句话,系统返回匹配的图片。但真做起来才发现问题不少:

第一,相册有几千张图,每张都要算向量,一次性全算完要好久。用户打开搜索页不能等半分钟吧?

第二,自然语言搜索不是精确匹配。用户说"海边的照片",图片本身没有"海边"这个标签,系统怎么知道哪张是海边的?这就需要多模态模型理解图片内容。

第三,搜索结果排序怎么搞?返回 20 张相似的图,哪张排前面?用户最想看的应该是相似度最高的,但也要考虑时间、地点这些辅助因素。

第四,增量更新怎么办?用户拍了新照片,要不要重新算向量?总不能每次打开都全量索引一遍。

二、这个能力怎么接入

HarmonyOS 7 的文搜图能力在 VisionKit 里,核心是两个步骤:给图片生成向量,给文字生成向量,然后算余弦相似度。

接入思路:

1. 离线建索引:应用首次启动时,后台遍历相册所有图片,每张图生成一个向量(float 数组),存到本地数据库。这个过程很慢,但只做一次。

2. 增量更新:监听相册变化,新增的图片单独算向量,追加到索引里。

3. 在线搜索:用户输入文字,先把文字转成向量,然后和所有图片向量算相似度,按相似度排序返回前 N 张。

这里的关键是:图片向量是离线算好的,在线搜索只需要算文字向量 + 向量比对,速度很快。

文搜图架构图

三、关键代码怎么写

下面是封装的文搜图工具类,文件位置在 entry/src/main/ets/utils/ImageSearchUtil.ets

import { vision } from '@kit.CoreVisionKit';
import { image } from '@kit.CoreImageKit';

export class ImageSearchUtil {
  private static encoder: vision.ImageTextEncoder | null = null;

  // 初始化编码器
  static async init(): Promise<void> {
    if (!this.encoder) {
      this.encoder = await vision.ImageTextEncoder.create();
    }
  }

  // 给图片生成向量
  static async encodeImage(
    pixelMap: image.PixelMap
  ): Promise<Float32Array> {
    if (!this.encoder) await this.init();
    
    const result = await this.encoder!.encodeImage(pixelMap);
    return result.embedding; // 图片向量
  }

  // 给文字生成向量
  static async encodeText(text: string): Promise<Float32Array> {
    if (!this.encoder) await this.init();
    
    const result = await this.encoder!.encodeText(text);
    return result.embedding; // 文字向量
  }

  // 余弦相似度计算
  private static cosineSimilarity(
    vecA: Float32Array,
    vecB: Float32Array
  ): number {
    let dot = 0;
    let normA = 0;
    let normB = 0;
    
    for (let i = 0; i < vecA.length; i++) {
      dot += vecA[i] * vecB[i];
      normA += vecA[i] * vecA[i];
      normB += vecB[i] * vecB[i];
    }
    
    return dot / (Math.sqrt(normA) * Math.sqrt(normB));
  }

  // 搜索图片
  static async searchImages(
    query: string,
    imageVectors: Map<string, Float32Array>,
    topK: number = 20
  ): Promise<SearchResult[]> {
    // 1. 文字转向量
    const queryVec = await this.encodeText(query);
    
    // 2. 和所有图片向量算相似度
    const results: SearchResult[] = [];
    for (const [imageId, imageVec] of imageVectors) {
      const score = this.cosineSimilarity(queryVec, imageVec);
      results.push({ imageId: imageId, score: score });
    }
    
    // 3. 按相似度排序,取前 topK
    results.sort((a, b) => b.score - a.score);
    return results.slice(0, topK);
  }
}

// 搜索结果类型
interface SearchResult {
  imageId: string;
  score: number;
}

这段代码的核心:encodeImage 和 encodeText 分别给图片和文字生成向量,cosineSimilarity 算相似度,searchImages 遍历所有图片向量排序返回。

页面里怎么用?文件位置 pages/SearchPage.ets

@Entry
@Component
struct SearchPage {
  @State searchQuery: string = '';
  @State results: SearchResult[] = [];
  @State isSearching: boolean = false;

  // 执行搜索
  async onSearch() {
    if (!this.searchQuery.trim()) return;
    
    this.isSearching = true;
    
    // 从本地数据库读取所有图片向量
    const imageVectors = await DbUtil.getAllImageVectors();
    
    // 执行搜索
    const results = await ImageSearchUtil.searchImages(
      this.searchQuery,
      imageVectors,
      20
    );
    
    this.results = results;
    this.isSearching = false;
  }

  build() {
    Column() {
      // 搜索框
      Search({ placeholder: '用一句话找图片...' })
        .onSubmit(() => this.onSearch())
      
      // 搜索结果
      if (this.isSearching) {
        LoadingProgress().width(40).height(40)
      } else {
        List({ space: 8 }) {
          ForEach(this.results, (result: SearchResult) => {
            ListItem() {
              Image(result.imageId)
                .width('100%')
                .height(200)
            }
          }, (result: SearchResult) => result.imageId)
        }
      }
    }
    .padding(16)
  }
}

文搜图搜索效果

四、运行过程中怎么处理异常

文搜图的异常场景:

1. 索引为空:第一次打开应用,索引还没建完,搜索的时候没有数据。要提示用户"正在建立索引,请稍后再试"。

2. 图片太多:几千张图的向量存在本地,数据库查询要快。我们用了 SQLite 存向量,每个向量存成 blob,查询的时候遍历算相似度。几千条数据毫秒级就能返回。

3. 索引建一半失败了:遍历相册的时候某张图读取失败,跳过这张继续,不要因为一张图失败就中断整个索引过程。

4. 搜索词太短:用户只输入一个词,搜索结果可能不准。UI 上给个提示,建议输入更详细的描述。

五、实际开发中容易忽略的问题

1. 索引要后台建:首次建索引很慢,绝对不能在启动的时候同步做。放后台线程,分批处理,建完了通知用户。

2. 向量维度:端侧模型输出的向量一般是 512 维或者 768 维,存在数据库里要注意格式。float32Array 存成 blob,查询的时候转回来。

3. 增量索引:新拍的照片要及时更新到索引里。监听相册的变化事件,新图单独算向量追加进去。

4. 相似度阈值:不是所有搜索都有好结果。相似度太低的结果(比如低于 0.3)就不要展示了,不然用户觉得搜的不准。

5. 隐私问题:文搜图完全在本地跑,不上传图片。这个是端侧 AI 的优势,要在隐私说明里强调。

文搜图是视觉能力里用户感知最强的功能——一句话就能找到照片,体验很直观。但背后的索引构建、向量存储、增量更新这些工程细节,才是决定好不好用的关键。

Logo

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

更多推荐