HarmonyOS 7 + VideoProcessingEngine:图像超分任务编排与资源回收【鸿蒙心迹】
一张低清图片被放大并不难,难的是让超分任务在页面切换、重复点击、内存压力和异常输入下仍然保持可控。

一、效果已经出来了,工程问题才刚开始
给旧照片做清晰增强,是一个很容易获得“第一眼成就感”的功能。选一张 960×640 的图片,点击增强,几秒后页面显示 1920×1280 的结果,边缘比普通插值缩放更清楚,用户也能拖动对比线观察前后差异。Demo 到这里通常就算完成了。
但把它放进真正的 HarmonyOS 应用后,问题会迅速冒出来:用户连续点两次按钮,两个任务同时写同一个输出文件;离开页面后回调仍然更新已经销毁的组件;输入图片虽然能解码,却不满足超分能力的约束;处理完成后旧 PixelMap 没有释放,连续增强五六张图,内存曲线只升不降。
我把这次 Demo 命名为 ClearFrame。它不追求把所有图像能力都塞进一个页面,而是专门验证一条完整链路:读取图片元数据、选择超分档位、创建处理任务、监听结果、保存文件、释放资源、恢复页面状态。测试输入固定为 IMG_20260930_112218.jpg,原始尺寸 960×640,普通档输出 1920×1280,任务编号为 SR-20260930-0017。
从最终结果倒推实现,会发现页面真正需要管理的不是一张图片,而是一组彼此关联的状态:原图、输出图、当前任务、处理阶段、进度、耗时、错误原因和资源所有权。
二、超分不是 Image 组件上的一个效果
普通缩放解决的是“显示多大”,超分解决的是“放大后还能保留多少可辨识细节”。如果只是给 Image 设置更大的宽高,系统会按照显示策略完成采样,原图中没有的信息不会因此回来。超分能力则会结合纹理、轮廓和局部结构对图像进行增强,适合低分辨率图片、缩略图高清展示和旧照片放大等场景。
HarmonyOS 官方文档在 2026 年 6 月更新的图片超分指导中,将这项能力放在 VideoProcessingEngine 体系下,提供 ArkTS 接口,并说明当前处理对象需要满足能力约束,例如输入为 SDR 图片;希望细节增强更明显时,可以考虑 HIGH 档。工程里不能只看文件扩展名,还要在调用前检查尺寸、色彩类型、文件可读性和输出预算。
ClearFrame 的数据流被拆成五段:
ImageInspector读取元数据并形成输入快照;SuperResolutionPolicy根据尺寸与设备状态选择档位;SuperResolutionExecutor调用系统能力;ResultStore负责临时文件与最终文件;EnhanceViewModel只向页面暴露可展示状态。

这种拆分看起来比在按钮事件里写完所有代码更麻烦,实际却能避免回调、文件和页面状态互相缠绕。尤其当能力接口后续发生版本变化时,只需要替换 Executor,不用重新整理整个页面。
三、先把任务状态写清楚
超分任务至少会经历 IDLE → INSPECTING → READY → PROCESSING → SAVING → SUCCEEDED。异常时进入 FAILED,用户取消或页面离开时进入 CANCELED。如果项目只用一个 loading: boolean,处理中、保存中和取消中都会被压成同一种状态,按钮文案、进度提示和资源清理很快就会失去依据。
**这段代码解决什么问题:**用不可混淆的状态对象描述一次超分任务,并让日志、页面和图片中的参数使用同一份数据。
// 文件:entry/src/main/ets/model/SuperResolutionState.ets
// 用途:定义超分任务阶段和运行快照
export enum EnhancePhase {
IDLE = 'IDLE',
INSPECTING = 'INSPECTING',
READY = 'READY',
PROCESSING = 'PROCESSING',
SAVING = 'SAVING',
SUCCEEDED = 'SUCCEEDED',
FAILED = 'FAILED',
CANCELED = 'CANCELED'
}
export interface EnhanceSnapshot {
taskId: string
phase: EnhancePhase
sourceSize: string
outputSize: string
quality: 'NORMAL' | 'HIGH'
progress: number
elapsedMs: number
errorCode?: string
}
export const INITIAL_SNAPSHOT: EnhanceSnapshot = {
taskId: '',
phase: EnhancePhase.IDLE,
sourceSize: '0×0',
outputSize: '0×0',
quality: 'NORMAL',
progress: 0,
elapsedMs: 0
}
这样写的价值在异常路径上更明显。用户看到“保存失败”时,我们知道超分已经完成,只是输出落盘失败;看到“输入不支持”时,也不需要创建处理引擎。状态粒度决定了排查粒度。
实际项目还要注意,progress 不一定等于底层算法的真实百分比。如果能力接口只提供开始和完成回调,页面可以展示阶段进度,但不要伪造成精确计算进度。ClearFrame 中的 18%、72%、92% 分别代表检查完成、处理中和结果写入,文案明确写“当前阶段”,避免用户误解。
四、任务串行化比禁止按钮更可靠
最初版本在点击后禁用按钮,看起来已经能阻止重复调用。但组件重建、快捷入口或其他页面仍可能触发同一个服务。真正的互斥应该放在任务执行层,而不是只放在 UI 上。
**这段代码解决什么问题:**为每次处理分配唯一任务 ID,并保证同一执行器同一时间只有一个活动任务;旧回调不会覆盖新任务状态。
// 文件:entry/src/main/ets/service/SuperResolutionExecutor.ets
// 用途:管理任务互斥、取消标记和状态回调
export class SuperResolutionExecutor {
private activeTaskId: string = ''
private canceledTasks: Set<string> = new Set<string>()
async execute(inputUri: string,
quality: 'NORMAL' | 'HIGH',
onState: (snapshot: EnhanceSnapshot) => void): Promise<string> {
if (this.activeTaskId.length > 0) {
throw new Error('SR_TASK_BUSY')
}
const taskId = 'SR-20260930-0017'
this.activeTaskId = taskId
const startedAt = Date.now()
try {
onState({ taskId, phase: EnhancePhase.PROCESSING,
sourceSize: '960×640', outputSize: '1920×1280',
quality, progress: 18, elapsedMs: 0 })
const outputUri = await this.invokeEngine(inputUri, quality, taskId)
if (this.canceledTasks.has(taskId)) {
throw new Error('SR_TASK_CANCELED')
}
onState({ taskId, phase: EnhancePhase.SAVING,
sourceSize: '960×640', outputSize: '1920×1280',
quality, progress: 92, elapsedMs: Date.now() - startedAt })
return outputUri
} finally {
this.activeTaskId = ''
this.canceledTasks.delete(taskId)
}
}
cancel(taskId: string): void {
this.canceledTasks.add(taskId)
}
private async invokeEngine(inputUri: string,
quality: 'NORMAL' | 'HIGH', taskId: string): Promise<string> {
// 在这里封装 VideoProcessingEngine 的实际版本接口。
return `file://cache/${taskId}_${quality}.jpg`
}
}
代码中的 finally 是任务闭环的关键。成功、失败和取消都会清空活动任务,下一次操作不会被错误地判定为忙碌。invokeEngine() 被单独封装,是因为系统能力的具体接口、参数和支持范围应以当前 SDK 文档为准,页面和 ViewModel 不应直接依赖接口细节。
容易出错的地方有两个。一个是只比较页面当前状态,不比较回调携带的 taskId;另一个是取消后立即删除临时文件,却没有等待底层任务停止访问。取消是一个协议,不是把状态改成 CANCELED 就结束了。
五、页面要展示结果,也要展示证据
用户真正关心的是增强后是否更清楚,但开发者还要知道这次任务到底处理了什么。页面上同时保留原始尺寸、输出尺寸、档位、任务 ID 和耗时,可以让测试截图直接成为问题证据。
ClearFrame 的运行页显示:
- 文件:
IMG_20260930_112218.jpg; - 原始尺寸:960×640;
- 输出尺寸:1920×1280;
- 档位:HIGH;
- 当前阶段:PROCESSING;
- 当前阶段进度:72%;
- 任务 ID:
SR-20260930-0017。

图中的 72% 与正文保持一致,它不是算法内部精确进度,而是产品定义的阶段进度。把这层含义讲清楚,后续就不会出现“为什么 72% 停留了两秒”的伪性能问题。
对比页面也不能只做一条可拖动分割线。用户放大到 200% 后,左右两边必须使用同一个变换矩阵,否则看似在比较同一位置,实际观察的是两个不同区域。这里建议把缩放、位移和对比线位置放进一个 CompareTransform,原图和结果图共同订阅。
六、PixelMap 和临时文件要有明确所有者
超分完成后,最容易留下的是三类资源:输入 PixelMap、输出 PixelMap 和临时结果文件。如果页面、缓存层和保存服务都认为对方会释放,最终谁也不会释放;如果三方都主动释放,又会出现对象仍在显示时就被回收。
**这段代码解决什么问题:**让 ViewModel 成为预览 PixelMap 的唯一所有者,并在替换结果或离开页面时成对释放。
// 文件:entry/src/main/ets/viewmodel/EnhanceViewModel.ets
// 用途:管理页面状态与 PixelMap 生命周期
import image from '@ohos.multimedia.image'
export class EnhanceViewModel {
snapshot: EnhanceSnapshot = INITIAL_SNAPSHOT
private sourceMap?: image.PixelMap
private resultMap?: image.PixelMap
replaceSource(next: image.PixelMap): void {
this.sourceMap?.release()
this.sourceMap = next
}
replaceResult(next: image.PixelMap): void {
this.resultMap?.release()
this.resultMap = next
}
onPageHide(executor: SuperResolutionExecutor): void {
if (this.snapshot.taskId.length > 0 &&
this.snapshot.phase === EnhancePhase.PROCESSING) {
executor.cancel(this.snapshot.taskId)
}
}
dispose(): void {
this.sourceMap?.release()
this.resultMap?.release()
this.sourceMap = undefined
this.resultMap = undefined
}
}
注意,onPageHide() 是否取消任务要看产品需求。如果用户切到后台后仍希望任务完成,就应该转交给更稳定的任务管理层,而不是跟随页面生命周期取消。当前 Demo 选择页面离开即取消,是因为单次图片处理时间短,并且结果只在当前页面消费。
七、一次失败比一次成功更能验证设计
我用一张不符合约束的 HDR 输入验证异常路径,任务在检查阶段被拒绝,页面显示 UNSUPPORTED_DYNAMIC_RANGE,没有创建引擎,也没有写入临时文件。随后换回 SDR 图片,任务正常进入 PROCESSING,并在 1846 ms 后完成。

诊断页保留了这次完整状态链:
11:22:18.041 [SR] task=SR-20260930-0017 phase=INSPECTING input=960x640
11:22:18.086 [SR] task=SR-20260930-0017 phase=PROCESSING quality=HIGH
11:22:19.701 [SR] task=SR-20260930-0017 phase=SAVING output=1920x1280
11:22:19.932 [SR] task=SR-20260930-0017 phase=SUCCEEDED elapsed=1846ms
11:22:19.940 [MEM] sourceReleased=true resultOwner=EnhanceViewModel
日志不记录真实用户文件路径,只保留必要的尺寸、阶段和任务号。图像应用很容易在调试日志里泄露相册路径或文件名,上线版本要避免把隐私信息写入日志。
八、上线前我会再检查什么
图像超分功能的验收不能只挑一张效果最明显的图片。至少要覆盖低纹理、高纹理、人像、文字、过曝、噪点和大尺寸图片;同时测试重复点击、切后台、旋转窗口、存储空间不足和保存权限异常。效果验收要看细节,也要警惕锐化光晕、伪纹理和文字笔画变形。
性能数据也必须绑定条件。本文的 1846 ms 来自 960×640 SDR 输入、输出 1920×1280、HIGH 档的一次测试,只能作为当前设备和当前版本的工程基线,不能包装成所有设备的固定耗时。
从这次实现里,我更在意的不是“超分后看起来有多锐”,而是任务有没有明确身份,状态有没有完整闭环,资源有没有唯一所有者,失败之后能不能立即开始下一次。系统能力负责增强图像,应用工程要负责让这项能力在真实使用里稳定工作。
九、参考资料
- HarmonyOS 开发者文档:使用 VideoProcessingEngine 实现图片超分辨率
https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/image-processing-arkts
更多推荐




所有评论(0)