视觉处理链封面

前面五篇分别讲了 OCR、超分、文搜图、人脸检测、截图分类。每篇单独都能跑,但真要用到产品里,得把它们串成一条完整的处理链。用户导入一张截图,从图片到最终可搜索、可分类的结构化数据,中间要经过好几个步骤,每一步都可能失败。

这篇就讲怎么设计这条处理链:怎么安排步骤顺序,怎么做状态管理,失败了怎么重试,怎么让整个流程既高效又稳定。

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

最开始我们把所有步骤按顺序串起来:导入图片 → 超分增强 → OCR 识别 → 人脸检测 → 分类 → 建索引。看起来很顺,但实际跑起来问题一堆:

第一,串行太慢。一张图跑完全部步骤要 5-6 秒,用户导入 10 张图就要等一分钟,体验很差。

第二,一步失败全挂。OCR 失败了,后面的人脸检测和分类就不跑了?还是继续跑?整个流程的状态怎么管?

第三,有的步骤不需要跑。一张高清截图不需要超分,一张没有文字的图不需要 OCR。但串行流程不管这些,每一步都跑,浪费性能。

第四,进度怎么反馈?用户导入了 20 张图,现在跑到哪一步了?哪张图处理完了?UI 上怎么展示?

二、这个能力怎么接入

我们最后设计的处理链是一个有向无环图(DAG),不是简单的串行。每个节点是一个处理步骤,节点之间有依赖关系:

节点 1:图片导入与预处理。读取图片、压缩到合适尺寸、生成缩略图。所有后续步骤的基础。

节点 2:条件判断。根据图片尺寸判断要不要做超分;根据图片内容判断要不要做 OCR。不是所有步骤都必跑。

节点 3:并行处理。超分、人脸检测、图像分类这三个互相独立,可以并行跑。OCR 依赖超分的结果,所以 OCR 要等超分跑完。

节点 4:内容分析。把 OCR 结果、人脸结果、分类结果汇总,做关键词提取和标签生成。

节点 5:建搜索索引。生成图片向量,存到本地数据库,供文搜图用。

整个流程用状态机管理,每个节点有 pending / running / success / failed 四种状态。

DAG架构图

三、关键代码怎么写

下面是处理链的核心调度器,文件位置在 entry/src/main/ets/pipeline/PipelineRunner.ets

import { image } from '@kit.CoreImageKit';
import { ImageEnhanceUtil } from '../utils/ImageSuperResolution';
import { OcrUtil } from '../utils/OcrUtil';
import { FaceDetectUtil } from '../utils/FaceDetectUtil';
import { ScreenshotClassifier } from '../utils/ScreenshotClassifier';
import { ImageSearchUtil } from '../utils/ImageSearchUtil';

// 处理状态
enum PipelineState {
  PENDING = 'pending',
  RUNNING = 'running',
  SUCCESS = 'success',
  FAILED = 'failed'
}

// 单张图片的处理任务
interface PipelineTask {
  imageId: string;
  pixelMap: image.PixelMap;
  state: PipelineState;
  currentStep: string;
  result: ProcessResult | null;
}

// 处理结果
interface ProcessResult {
  ocrText: string;
  hasFace: boolean;
  category: string;
  tags: string[];
  vector: Float32Array | null;
}

export class PipelineRunner {
  private static tasks: Map<string, PipelineTask> = new Map();

  // 批量处理入口
  static async processImages(
    images: { id: string; pixelMap: image.PixelMap }[],
    onProgress: (done: number, total: number) => void
  ): Promise<Map<string, ProcessResult>> {
    
    const results = new Map<string, ProcessResult>();
    let done = 0;
    
    // 串行处理每张图(避免内存压力)
    for (const img of images) {
      try {
        const result = await this.processSingle(img.pixelMap);
        results.set(img.id, result);
      } catch (e) {
        console.error('Process failed for ' + img.id + ': ' + e.message);
        // 单张失败不影响其他图
        results.set(img.id, this.emptyResult());
      } finally {
        img.pixelMap.release();
        done++;
        onProgress(done, images.length);
      }
    }
    
    return results;
  }

  // 单张图片处理链
  private static async processSingle(
    pixelMap: image.PixelMap
  ): Promise<ProcessResult> {
    
    // Step 1: 条件判断 - 要不要超分
    let workingMap = pixelMap;
    const size = pixelMap.getImageInfoSync();
    const needEnhance = Math.max(size.width, size.height) < 1000;
    
    if (needEnhance) {
      try {
        workingMap = await ImageEnhanceUtil.enhanceImage(pixelMap);
      } catch (e) {
        console.warn('Enhance skipped: ' + e.message);
      }
    }

    // Step 2: 并行做 OCR 和人脸检测
    const [ocrResult, hasFace] = await Promise.all([
      OcrUtil.extractTextFromImage(workingMap),
      FaceDetectUtil.hasFace(workingMap)
    ]);

    // Step 3: 分类
    const category = await ScreenshotClassifier.classify(workingMap);

    // Step 4: 生成搜索向量
    let vector: Float32Array | null = null;
    try {
      vector = await ImageSearchUtil.encodeImage(workingMap);
    } catch (e) {
      console.warn('Vector encode failed: ' + e.message);
    }

    // 释放增强后的图
    if (workingMap !== pixelMap) {
      workingMap.release();
    }

    return {
      ocrText: ocrResult.text,
      hasFace: hasFace,
      category: category.type,
      tags: this.generateTags(ocrResult.text, hasFace, category.type),
      vector: vector
    };
  }

  // 生成标签
  private static generateTags(
    text: string,
    hasFace: boolean,
    category: string
  ): string[] {
    const tags: string[] = [category];
    if (hasFace) tags.push('有人脸');
    if (text.includes('发票')) tags.push('票据');
    if (text.includes('聊天')) tags.push('聊天');
    return tags;
  }

  private static emptyResult(): ProcessResult {
    return {
      ocrText: '',
      hasFace: false,
      category: 'unknown',
      tags: [],
      vector: null
    };
  }
}

这段代码的核心:processSingle 方法按条件跑步骤,OCR 和人脸检测用 Promise.all 并行,单步失败不影响整体流程。processImages 串行处理多张图,避免内存压力。

状态管理示意图

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

处理链的异常处理原则:单步失败降级,单图失败跳过。

1. 超分失败:用原图继续做 OCR 和检测,不中断流程。

2. OCR 失败:ocrText 为空,后面的关键词提取和标签生成就基于空文本处理,不影响分类。

3. 人脸检测失败:hasFace 设为 false,分类按无脸处理。

4. 向量生成失败:vector 为 null,这张图就不能参与文搜图,但其他功能正常。

5. 重试机制:瞬时失败(比如模型加载超时)可以重试一次。但不要无限重试,重试一次还失败就放弃。

6. 断点续传:处理到一半用户退出去了,下次进来从上次中断的地方继续,不要从头再来。已完成的图直接读缓存结果。

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

1. 并行不是越多越好:OCR 和人脸检测并行没问题,但如果三个都并行,CPU 和 GPU 都打满了,反而更慢。要根据设备性能调整并行度。

2. 内存是最大瓶颈:每张图的 PixelMap 占用很大。处理链里每一步用完就释放,不要一直持着。中间结果(比如超分后的图)用完就 release。

3. 进度反馈要准确:UI 上显示"正在处理第 5/20 张",比"正在处理..."更有安全感。但不要每处理完一张就刷新 UI,攒一批再更新。

4. 结果要持久化:处理完的结果存到本地数据库。下次打开应用直接读缓存,不要重新跑一遍处理链。

5. 性能监控:每张图每一步花了多少时间,要打日志。上线后如果发现用户反馈慢,看日志就知道哪一步是瓶颈。

处理链设计的核心思路:条件执行 + 并行优化 + 异常降级 + 结果缓存。把这些做好,用户感知到的就是"导入截图之后,App 自动帮我整理好了",而不是"我等了半天不知道它在干嘛"。下一篇是系列最后一篇,把这些能力全部整合起来,做一个完整可用的工具。

Logo

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

更多推荐