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

前面三篇我们做了单图重建、修复滑块、实时放大镜,都是「看一张、比一张」的演示。这一篇把超分真正当成一个工程能力来用:把一批低清图丢进去,一键全部端侧 4× 超分,逐张显示进度、清晰度增益和文件大小,最后把结果原样落盘成 PNG——全程在设备本地完成,数据不出设备、不需要任何存储权限。
工程地址:
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 超分」对比浮层,左右并排,底部给出该图的清晰度增益与文件大小。
二、这个 App 解决什么问题
前三篇证明了一件事:端侧 NPU 能把 160×120 的低清图重建出 640×480 的清晰细节。但演示归演示,真正落地至少要回答三个问题:
- 批量:一次处理几十上百张,而不是手动点一张看一张;
- 落盘:超分结果要能存成文件,别人才能用(发微信、进相册、二次处理);
- 可量化:到底「清晰了多少」,得有个数字,而不是凭眼睛。
这个工作台就是把这三个能力打包成一个能用的工具:选一批图 → 一键跑 → 每张给出增益和文件大小 → 结果全部写出到应用沙箱。而且因为超分在端侧 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,不申请媒体权限 |
| 真机模式启动慢/失败 | 首帧就创建 analyzer | aboutToAppear 只存 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 错误,已在真机安装运行验证。
更多推荐




所有评论(0)