图像超分 Demo 最容易写成“选一张图 → process → 显示结果”。真正开始做产品以后,问题会马上变成另一套:用户连续点三次处理怎么办?同一张图重复点要不要重新推理?页面退出时 Analyzer 还在不在?结果 PixelMap 谁持有?这次我就围绕这些工程问题做一个 PhotoSRQueue。

当前 HarmonyOS 7 / API 26 的 Core Vision Kit 已经提供 imageSuperResolution。核心链路很清楚:

ImageSRAnalyzer.create() 创建分析器,process(request) 接收一张图片并返回 ISPResponse,结果通过 pixelMap 获取;不用以后调用 destroy() 释放服务资源。

真正需要业务自己补的,是调度层。

这次运行数据固定为:

  • Task ID:sr_20261001_03
  • State:COMPLETED
  • Source:photo_1280x720.jpg
  • Result PixelMap:READY
  • First run:824 ms
  • Repeat:CACHE_HIT 18 ms
  • Queue:0
  • Cache entries:1
  • Analyzer:ACTIVE

一、第一次能跑通以后,我先把按钮连点三次

最初的版本只有十几行:

const analyzer =
  await imageSuperResolution.ImageSRAnalyzer.create();

const response =
  await analyzer.process(request);

this.resultPixelMap = response.pixelMap;

单次点按钮完全正常。

但当我连续选择多张图、快速点击处理时,页面开始出现两个明显问题:

第一,多个 Promise 同时更新 resultPixelMap,最后显示出来的不一定是最后一次点击对应的结果。

第二,页面状态没有任务概念,只剩一个 RUNNING。前一个任务还没结束,后一个又进来了,UI 根本解释不清现在处理的是谁。

官方接口本身描述的是“单张图片请求”,所以我没有把批量任务能力强行归到系统 API 上,而是在业务层包一层串行队列。

二、Analyzer 只创建一次,不跟着每个任务反复创建

这段代码解决的问题,是页面生命周期内复用同一个分析器。

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

private analyzer:
  imageSuperResolution.ImageSRAnalyzer | null = null;

async aboutToAppear(): Promise<void> {
  try {
    this.analyzer =
      await imageSuperResolution.ImageSRAnalyzer.create();

    this.analyzerState = 'ACTIVE';
  } catch (error) {
    this.analyzerState = 'ERROR';
    console.error(
      `create analyzer failed: ${JSON.stringify(error)}`
    );
  }
}

这里我没有把 create() 放进每一次“开始处理”按钮。

原因很直接:Analyzer 是一个服务对象,不是一个一次性临时变量。

页面还在使用这项能力时,重复创建没有必要;页面离开后再统一销毁,生命周期更清楚。

这也让任务队列只关心“什么时候执行 process”,不需要每个任务都重新处理初始化逻辑。

三、我给 process 外面包了一层串行任务队列

当前代码解决的是:同一时间只让一个 SRTask 进入实际 process()。

interface SRTask {
  id: string;
  key: string;
  pixelMap: PixelMap;
  state: 'WAITING' | 'RUNNING' |
    'COMPLETED' | 'FAILED';
}

private queue: SRTask[] = [];
private running: boolean = false;

async enqueue(task: SRTask): Promise<void> {
  this.queue.push(task);

  if (this.running) {
    return;
  }

  await this.processNext();
}

private async processNext(): Promise<void> {
  if (this.queue.length === 0) {
    this.running = false;
    return;
  }

  this.running = true;
  const task = this.queue.shift()!;

  try {
    task.state = 'RUNNING';
    await this.executeTask(task);
    task.state = 'COMPLETED';
  } catch (error) {
    task.state = 'FAILED';
  } finally {
    await this.processNext();
  }
}

它没有试图证明“Core Vision Kit 不能并发”。

我只是基于当前产品需求做了一个更稳定的调度选择:一次处理一张,UI 状态按任务顺序收口。

这种写法还有一个好处。

后面如果要加取消、失败重试、任务优先级,不需要去改 ImageSRAnalyzer 调用层,只改队列就行。

四、真正调用官方 API 的部分反而很短

这段代码解决的是把输入 PixelMap 封装成 visionBase.Request,再拿回结果。

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

private async runSR(
  input: PixelMap
): Promise<PixelMap> {
  if (!this.analyzer) {
    throw new Error('Analyzer not ready');
  }

  const imageData: visionBase.ImageData = {
    pixelMap: input
  };

  const request: visionBase.Request = {
    inputData: imageData
  };

  const response:
    imageSuperResolution.ISPResponse =
      await this.analyzer.process(request);

  return response.pixelMap;
}

这才是系统能力真正负责的部分。

官方 API 26 文档当前给出的接口也是这条链路:ImageSRAnalyzer.create()、process(request)、ISPResponse.pixelMap 和 destroy()。

另外,图像超分一次请求只处理一张图片,所以本文的“队列”完全属于应用侧调度,不是把多张图片一起塞进一次 process。

五、第二次点同一张图,我不想再跑一遍推理

PhotoSRQueue 的第二个改动是缓存。

当前 Demo 给输入图生成一个业务 key。正式项目可以把文件 URI、大小、修改时间或者内容摘要组合起来,本文为了讲清逻辑,只假设已经得到稳定 key。

这段代码解决的是相同输入重复处理:

private cache:
  Map<string, PixelMap> = new Map();

private async executeTask(
  task: SRTask
): Promise<void> {
  const cached =
    this.cache.get(task.key);

  if (cached) {
    this.resultPixelMap = cached;
    this.lastResult = 'CACHE_HIT';
    return;
  }

  const result =
    await this.runSR(task.pixelMap);

  this.cache.set(task.key, result);
  this.resultPixelMap = result;
  this.lastResult = 'PROCESSED';
}

这就是截图里第一次 824ms、第二次 18ms 的来源。

18ms 不是 ImageSRAnalyzer 第二次突然变快,而是第二次根本没有进入超分推理,只完成缓存查找和 UI 更新。

如果文章不把这一点说清楚,很容易把业务缓存效果误写成系统性能提升。

缓存同样有边界。

PixelMap 占内存,正式项目不能让 Map 一直增长。至少要做容量限制、最近使用淘汰和主动释放策略。

当前 Demo 只保留一个结果,所以截图里 Cache entries = 1。

六、图片资源不是只有 Analyzer 需要释放

做图像类能力时,最容易把注意力全放到 AI 服务上。

但输入图通常还经历:

  • Photo Picker 返回 URI;
  • fileIo 打开文件;
  • ImageSource 解码;
  • createPixelMap;
  • Analyzer process;
  • 输出 PixelMap;
  • UI 展示。

如果这些对象全部长期持有,页面即使只处理三张图,内存也可能迅速增大。

我现在的做法是:

输入 PixelMap 在任务结束、确认后续不再使用时释放;临时 ImageSource 和 file 及时关闭;输出结果如果进入缓存,就由缓存层统一管理,而不是页面和缓存各留一份引用。

官方示例本身也会在图像处理完成后释放输入 PixelMap、ImageSource、文件句柄,最后销毁 Analyzer。

七、destroy 一定要和页面退出形成闭环

这段代码解决的,是用户离开页面以后不再保留超分服务。

async aboutToDisappear(): Promise<void> {
  this.queue = [];
  this.running = false;

  if (this.analyzer) {
    try {
      await this.analyzer.destroy();
    } finally {
      this.analyzer = null;
      this.analyzerState = 'DESTROYED';
    }
  }
}

这里有一个工程细节。

清空队列不代表已经在执行的 Promise 被“强制取消”。

如果业务要求处理中立即退出,还要在任务结果回来时判断页面是否已经 disposed,避免继续写 UI。

所以正式版本我会再加一个 session token:

页面进入时生成 token,任务完成时比对 token;页面离开以后旧 token 失效。

这样即使底层处理已经发出,也不会让过期结果覆盖新页面。

八、我更愿意把 Analyzer 看成页面级资源,而不是按钮函数

做到这里以后,工程结构也从单页变成:

entry/src/main/ets/
├── model/
│   └── SRTaskState.ets
├── pages/
│   └── SRLabPage.ets
└── service/
    ├── SRCache.ets
    └── SRTaskQueue.ets

DevEco 图里,中间区域主要是 SRTaskQueue.ets。

右侧运行页显示:

Task: sr_20261001_03
State: COMPLETED
First run: 824 ms
Repeat: CACHE_HIT 18 ms
Queue: 0
Cache entries: 1
Analyzer: ACTIVE

底部日志同样对应这组数据。

这张图我最想表达的不是“代码很多”,而是 Analyzer 已经从 UI 页面里被剥离出来。

页面负责选图和显示状态;队列负责顺序;缓存负责复用;Core Vision Kit 只负责真正的图像超分。

九、为什么最终截图里 Analyzer 还是 ACTIVE

运行截图里任务已经 COMPLETED,但 Analyzer 仍然显示 ACTIVE。

这是故意保留的状态。

因为用户还停留在 Photo SR Queue 页面里,下一秒还可能再选一张图。这个时候销毁 Analyzer,再点一次又 create,一来一回没有必要。

真正的释放时机,是页面离开或者业务明确结束超分功能以后。

所以当前状态组合是:

Task = COMPLETED
Queue = 0
Analyzer = ACTIVE

这三个状态没有冲突。

任务完成描述的是业务任务,Queue=0 描述的是没有等待项,Analyzer=ACTIVE 描述的是服务仍可复用。

把这三层混成一个“完成/未完成”,后面一定会越来越难维护。

十、这次最重要的不是 824ms,而是边界被拆清楚了

图像超分的系统 API 其实很短。

真正让一个 Demo 变成可继续扩展的工程,需要把几个边界拆开:

Analyzer 生命周期:页面级资源,进入创建,退出 destroy。

Task 生命周期:每一张图自己的 WAITING / RUNNING / COMPLETED / FAILED。

Cache 生命周期:结果复用,但必须有容量和释放策略。

UI 生命周期:只展示当前仍然有效的任务结果。

当前截图里的 824ms、18ms 只是这一版 Demo 的一次运行数据,不应该当成设备性能结论。

真正值得留下的,是这条调用闭环:

选图 → PixelMap → 入队 → 查缓存 → process → 缓存结果 → UI 展示 → 页面退出 destroy。

后面无论要加批量导入、任务取消、磁盘缓存还是后台处理,都可以继续沿着这条结构扩展,而不是把所有逻辑重新塞回一个按钮回调里。

参考资料

  • HarmonyOS 7 Core Vision Kit API 变更
    https://developer.huawei.com/consumer/en/doc/harmonyos-releases/js-apidiff-corevisionkit-7001
  • 图像超分开发者实践合集
    https://developer.huawei.com/consumer/cn/forum/topic/0204224159448225375
  • PixelMap 图像变换
    https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/image-transformation
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐