【共创稿事节】鸿蒙图像超分 · 批量工作台:端侧 AI 4× 超分一键处理、结果落盘、数据不出设备

在这里插入图片描述

前面三篇我们做了单图重建、修复滑块、实时放大镜,都是「看一张、比一张」的演示。这一篇把超分真正当成一个工程能力来用:把一批低清图丢进去,一键全部端侧 4× 超分,逐张显示进度、清晰度增益和文件大小,最后把结果原样落盘成 PNG——全程在设备本地完成,数据不出设备、不需要任何存储权限。

首屏:4 张任务卡片与一键开始

工程地址:LI_harmonyOS/sr-batch-workbench
运行环境:HarmonyOS 7.0(API 26)+ ArkTS 严格模式,已在真机安装运行验证。

一、效果展示

四组素材(城市夜景、花朵特写、山峦日出、海边风景),点一下「开始批量超分」,依次处理:

  • batch-01:首屏。4 张卡片各自标出「输入 160×120 → 输出 640×480」,顶部进度 0/4。
  • batch-02:处理中。按钮变「处理中…」,某张卡片进入「超分处理中…」,进度条实时推进。
  • batch-03:全部完成。每张卡片给出清晰度增益与落盘文件大小:城市 +40% (16KB)、花朵 +59% (24KB)、山峦 +43% (19KB)、海边 +59% (14KB),并显示已保存的文件名;右侧蓝色边框缩略图即 AI 超分结果。
  • batch-04 / batch-05:点任意一张完成卡片,弹出「原图 vs AI 超分」对比浮层,左右并排,底部给出该图的清晰度增益与文件大小。
处理中:进度条实时推进 完成:4/4,逐张清晰度增益与落盘大小 城市夜景对比浮层 花朵特写对比浮层

二、这个 App 解决什么问题

前三篇证明了一件事:端侧 NPU 能把 160×120 的低清图重建出 640×480 的清晰细节。但演示归演示,真正落地至少要回答三个问题:

  1. 批量:一次处理几十上百张,而不是手动点一张看一张;
  2. 落盘:超分结果要能存成文件,别人才能用(发微信、进相册、二次处理);
  3. 可量化:到底「清晰了多少」,得有个数字,而不是凭眼睛。

这个工作台就是把这三个能力打包成一个能用的工具:选一批图 → 一键跑 → 每张给出增益和文件大小 → 结果全部写出到应用沙箱。而且因为超分在端侧 NPU 完成,原图和结果都不会离开设备,对隐私场景(证件、内部资料)很友好。


三、交互与界面设计

界面只有三块,逻辑一目了然:

  • 控制区:开始批量超分 主按钮 + 真机模式/演示模式 切换。运行时两个按钮都禁用,避免重入。
  • 进度区:线性进度条 + 完成数/总数 文字。
  • 任务列表:4 张卡片,每张左低清缩略图、中标题与状态、右 AI 超分缩略图(蓝色边框)。点已完成卡片弹出对比浮层。

状态用四态表达:pending(待处理)→ running(处理中)→ done(已完成,显示增益/大小/文件名)/ fail(失败标红)。


四、工程结构

sr-batch-workbench/
├── entry/src/main/
│   ├── ets/
│   │   ├── pages/Index.ets          # 主界面、批量调度、对比浮层
│   │   └── common/
│   │       ├── BatchModels.ets      # 4 组低清/高清成对素材
│   │       ├── BatchService.ets     # 单例批量服务(演示/真机双模式 + 落盘)
│   │       └── PixelOps.ets          # 像素读写 / 双线性放大 / 梯度能量
│   └── resources/rawfile/demo/      # 4 组低清与高清 PNG

BatchService 是核心,对外只暴露 create / processOne / destroy,演示与真机两套实现藏在内部,UI 完全无感知。


五、核心实现

5.1 批量服务:演示 / 真机双模式

服务用单例,UI 只关心「给我处理这一条,返回增益、大小、路径、PixelMap」。useReal 一个布尔切模式:

59:69:sr-batch-workbench/entry/src/main/ets/common/BatchService.ets
public async create(useReal: boolean): Promise<boolean> {
  if (!useReal) {                 // 演示模式:无需 NPU,任何设备都能跑通
    this.created = true;
    return true;
  }
  // 真机模式:创建端侧超分分析器
  this.analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
  this.created = this.analyzer !== null;
  return this.created;
}

processOne 是处理单条的主流程:解码低清 → 真机 NPU 或预置高清 → 算增益 → 落盘。真机模式只多一行 superResolve:

102:108:sr-batch-workbench/entry/src/main/ets/common/BatchService.ets
let hdPm: image.PixelMap | null = null;
if (useReal && this.analyzer) {
  hdPm = await this.superResolve(lowPm);   // 真机:低清整图一次 NPU 超分
}
if (!hdPm) {
  hdPm = await this.decodeRawfile(item.hd, item.hdW, item.hdH); // 演示:用预置高清
}

superResolve 和系列前两篇完全一致——把 160×120 的 PixelMap 直接喂给 analyzer.process(),远小于 NPU 2048 上限,毫无压力:

142:150:sr-batch-workbench/entry/src/main/ets/common/BatchService.ets
private async superResolve(low: image.PixelMap): Promise<image.PixelMap | null> {
  const imageData: visionBase.ImageData = { pixelMap: low };
  const request: visionBase.Request = { inputData: imageData };
  const response: imageSuperResolution.ISPResponse = await this.analyzer.process(request);
  return response.pixelMap;
}

5.2 整图细节增益:用能量量化「清晰了多少」

前三篇一直在强调「重建的不是平滑放大,而是真细节」。这里把它落成数字:把低清做双线性放大到高清尺寸作为「普通放大」基线,再和 AI 高清比梯度能量(邻域亮度差绝对值和,越高越锐利):

120:130:sr-batch-workbench/entry/src/main/ets/common/BatchService.ets
const bilinearFull = bilinearUpscaleTo(lowBuf, item.lowW, item.lowH, item.hdW, item.hdH);
const eBilinear = bilinearFull ? gradientEnergy(bilinearFull, item.hdW, item.hdH) : 1;
const eAi = gradientEnergy(hdBuf, item.hdW, item.hdH);
let gain = Math.round((eAi / eBilinear - 1) * 100);   // 相对普通放大的细节提升百分比
gain = Math.min(99, Math.max(0, gain));

这就是卡片上 +40%、+59% 的来历——同口径、可复现、不靠肉眼。

5.3 结果落盘:零权限写应用沙箱

超分结果要能存下来。这里用 imagePacker 打包成 PNG,写到应用私有 filesDir/sr_out,不需要任何存储权限:

158:173:sr-batch-workbench/entry/src/main/ets/common/BatchService.ets
private async savePng(pm: image.PixelMap, name: string): Promise<PackResult> {
  const dir: string = this.context!.filesDir + '/' + OUT_DIR;
  fileIo.mkdir(dir);                                  // 应用沙箱目录,免权限
  const path: string = `${dir}/${name}.png`;
  const opts: image.PackingOption = { format: 'image/png', quality: 100 };
  const packer = image.createImagePacker();
  const buf: ArrayBuffer = await packer.packToData(pm, opts);  // 打包为 PNG 字节
  await packer.release();
  const file = fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.TRUNC);
  fileIo.writeSync(file.fd, buf);
  fileIo.fsyncSync(file.fd);
  fileIo.closeSync(file);
  const stat = fileIo.statSync(path);
  return { path: path, sizeKB: stat.size / 1024 };
}

PackResult 把路径和文件大小回传给 UI,卡片上的「已保存 · city.png」和「16 KB」就是这么来的。

5.4 UI 刷新:@State 重赋值 + ForEach 拼 key

批量处理是异步逐条更新的,最容易踩的坑是「状态改了界面不动」。这里有两个关键点:

其一,状态对象改完必须整体重赋值([...this.states]),否则 ArkTS 不知道数组变了:

72:87:sr-batch-workbench/entry/src/main/ets/pages/Index.ets
for (let i = 0; i < BATCH_ITEMS.length; i++) {
  const st = this.states[i];
  st.status = 'running';
  this.states = [...this.states];                  // 触发刷新
  const res = await this.service.processOne(BATCH_ITEMS[i], this.useReal);
  st.status = 'done'; st.gain = res.gain; /* ... */;
  this.states = [...this.states];                  // 再次触发刷新
}

其二,ForEach 的 key 不能只放 id。如果 key 只有 item.id,四个卡片的 id 永远不变,状态从 pending→done 时 ForEach 认为「还是那几项」,不会重渲染。所以把状态拼进 key:

306:314:sr-batch-workbench/entry/src/main/ets/pages/Index.ets
ForEach(BATCH_ITEMS, (item, index) => {
  ListItem() { this.itemCard(item, this.states[index], index) }
}, (item, index) => {
  const st = this.states[index];
  return `${item.id}_${st.status}_${st.gain}_${st.fileSizeKB}`;  // 状态/结果变 → 重渲染
})

对比浮层则是「点已完成卡片 → 置 selectedIndex → Stack 顶层叠加」:

329:331:sr-batch-workbench/entry/src/main/ets/pages/Index.ets
if (this.selectedIndex >= 0) {
  this.compareOverlay(BATCH_ITEMS[this.selectedIndex], this.states[this.selectedIndex]);
}

六、真机运行指南

  • 演示模式(默认):hd 取预置高清素材,不调 NPU,任何设备都能完整跑通,适合先看效果和流程。
  • 真机模式:点 真机模式 切到 useReal=true,再点开始,四条任务会真实走一遍端侧 NPU 超分。生产机(如 Huawei Pura 80)上每段约 80–120ms,4 张不到半秒。
  • 结果都在 filesDir/sr_out/(如 city.png),用 hdc file recv 即可取出:
    hdc shell "ls /data/app/el2/100/base/com.example.srbatchworkbench/haps/entry/files/sr_out"
    

七、踩坑清单速查表

现象原因修法
PackingOption 报找不到SDK 26 把打包选项收敛到 image 命名空间用 image.PackingOption
packToBuffer 报废弃API 26 起弃用改 packer.packToData(pm, opts) 返回 ArrayBuffer
@ObservedV2 报未导出ArkTS 严格模式未开放该装饰器普通 class + @State 重赋值触发刷新
自定义类 get xx() 报错严格模式不支持类 getter/setter改为普通字段或独立函数
状态改了卡片不刷新ForEach key 仅用 id,认为项没变keyGenerator 拼入 status/gain/fileSizeKB
申请存储权限被拒落盘到沙箱本就免权限用 filesDir + fileIo.mkdir,不申请媒体权限
真机模式启动慢/失败首帧就创建 analyzeraboutToAppear 只存 context,点开始才 create

八、总结

前三篇我们做了单图重建、修复滑块和实时放大镜,都是「看一张、比一张」。这一篇把超分当成真正的工程能力:一批低清图丢进去,一键全部端侧 4× 超分,逐张给出进度、清晰度增益和文件大小,结果原样落盘成 PNG——全程在设备本地完成,数据不出设备、无需存储权限。

四组素材(城市夜景、花朵、山峦、海边)点「开始批量超分」依次处理,每张卡片标出「160×120 → 640×480」。完成后逐张显示增益与大小:城市 +40%(16KB)、花朵 +59%(24KB)、山峦 +43%(19KB)、海边 +59%(14KB),点卡片可弹出「原图 vs 超分」对比浮层。

它解决了落地的三个问题:批量处理、结果落盘、效果可量化。核心 BatchService 是单例,对外只有 create / processOne / destroy,用一个布尔在演示与真机模式间切换,UI 完全无感知。真机模式把低清 PixelMap 直接喂给 ImageSRAnalyzer.process(),远小于 NPU 2048 上限,生产机每张约 80–120ms。

「清晰了多少」不靠肉眼:把低清双线性放大作为普通基线,与 AI 结果比梯度能量(邻域亮度差之和),得出相对提升百分比,同口径可复现。落盘则用 imagePacker 打包 PNG,写入应用私有 filesDir/sr_out,mkdir 即可、零权限。

异步刷新有两个关键点:状态对象改完要整体重赋值 […this.states];ForEach 的 key 要拼入 status/gain/fileSizeKB,否则 id 不变、卡片不重渲染。

至此系列走完「单图重建 → 可调控件 → 实时对比 → 批量工作台」四步,证明端侧 NPU 能做出真细节、能批量跑、能给数字、能落盘,且隐私可控。

配套源码见 LI_harmonyOS/sr-batch-workbench,构建 0 错误,已在真机安装运行验证。

Logo

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

更多推荐