HarmonyOS 7 AI图像超分:分块推理调度与结果缓存落盘【鸿蒙心迹】 把一张 1120 × 1584 的海报放大到 2240 × 3168最开始我只想做一个“选择图片、处理、保存”的小工具。真正把页面做成任务型应用后问题立刻从“算法能不能返回一张清晰图”变成了“用户如何知道它还活着”。尤其在端侧推理耗时不固定、预览图需要先显示、系统随时可能打断页面的时候简单地在按钮上挂一个转圈动画并不够。这篇围绕SuperResLab的一条任务记录展开。输入图片poster_1120.jpg业务任务 ID 为sr_20261009_02Demo 调度配置叫SR-X2-Lite目标放大倍率2x应用层切成16 个 tiles最终输出sr_20261009_02.png。界面上正在处理的断面显示12 / 16个分块已交付、总进度68%完成后详情页显示16 / 16、SAVED、尺寸2240 × 3168、文件大小4.8 MB。需要先把边界说清HarmonyOS 7 / API 26 的 Core Vision Kit 提供imageSuperResolution.ImageSRAnalyzer包括创建、处理、销毁等能力并没有把本文的TileScheduler、SRWorker、ResultCache、进度 68% 或SR-X2-Lite作为官方接口公布。这些是我们为了任务管理设计的应用层包装。具体是否适合切片、输入尺寸约束、推理并发度和图像边界处理都必须依据实际设备、官方限制和实验结果决定。文中统一配图是 Demo 的设计复现不是已验证的设备性能截图。一、问题是从一张处理中的预览图开始的原型只有两个画面左边显示原图右边显示“高清结果”。选择图片以后程序立刻调用超分分析器等待返回再替换右侧图片。这个流程演示单图效果没问题放到真实产品的交互链路里却有三个明显缺口。第一图片只要大一点等待时间就让用户没有反馈。第二单次处理过程中如果导航返回、进入后台或者用户再次点击“开始”会遇到并发任务和资源管理问题。第三结果在内存里显示过不等于文件已保存。最容易被忽视的是第三点界面已经有高清预览用户点“分享”时才发现没有可读的输出文件。因此我做了一个取舍把图像算法调用和任务生命周期分开。UpscaleTaskPage只描述用户希望处理哪张图、当前采用什么策略以及进度到了哪里TileScheduler处理业务切片和排队SRWorker是分析器调用的封装层ResultCache只负责输出结果是否真实落盘。最后再由TaskDebugPage解释算法输出、拼接和缓存的全过程。这里的SR-X2-Lite只是本 Demo 的业务策略标识用于区别不同的尺寸、内存和调度方案不能误认为系统可选的公开模型名称。输入宽高各扩大两倍像素总数扩大四倍正好对应示例从 1120 × 1584 到 2240 × 3168 的变化。这也意味着即使 JPEG 最终只有几 MB解码成 RGBA 缓冲后仍会占用显著更大的内存不能拿磁盘文件大小直接推断推理峰值。二、项目分层后按钮不再直接驱动算法先看这次整理后的工程目录。页面、调度器、分析器适配和持久化各自独立依赖方向由外往内方便替换底层策略SuperResLab/ └── entry/src/main/ets/ ├── pages/ │ ├── UpscaleTaskPage.ets │ └── TaskDebugPage.ets ├── engine/ │ └── TileScheduler.ets ├── worker/ │ └── SRWorker.ets ├── cache/ │ └── ResultCache.ets └── model/ ├── SRTaskInfo.ets └── TileResult.ets一条任务采用四个核心阶段READY表示入参已经验证PREPROCESS表示源图解码、尺寸检查与预处理UPSCALING表示执行推理和拼接RESULT_READY表示输出像素已可用。只有文件写入与校验完成整个任务才进入SAVED。注意RESULT_READY与SAVED不能合并。前者属于内存和计算结果后者属于持久化承诺。页面采用另外一组可见状态RUNNING / PAUSED / FAILED / SAVED。一旦采用分块策略每个 tile 还有自己的QUEUED / PROCESSING / DONE / FAILED子状态。某一块失败不应直接让整个界面变成成功也不应无限次静默重试。做成任务对象以后这些状态能在测试用例里被逐个校验而不用盯着一张进度条猜测。我的经验是图像处理工具尤其要控制“重入”。用户在同一张图上快速点击两次不应该同时启动两个高成本分析器如果切换图片也应由旧任务明确取消或完成释放后再创建新任务。这个限制在业务层实现比在按钮上临时禁用更可靠因为从通知、路由或恢复入口仍可能发起任务。三、第一段代码任务信息先有统一的数据模型项目最初的问题是不同页面都自己计算进度。任务页用finishedTiles / totalTiles详情页用“当前步骤 × 权重”造成同一个时刻两张图显示不同百分比。解决方法是让进度只由调度器产出一个快照页面只负责读取。下面的模型把分块数、阶段和进度单独保存// model/SRTaskInfo.ets应用层状态不是系统官方的任务结构exporttypeSRStageREADY|PREPROCESS|UPSCALING|RESULT_READY;exporttypeSRStatusRUNNING|PAUSED|FAILED|SAVED;exportinterfaceSRTaskInfo{taskId:string;source:string;model:string;scale:number;totalTiles:number;finishedTiles:number;memoryCapMB:number;stage:SRStage;status:SRStatus;progress:number;revision:number;}exportconstDEMO_TASK:SRTaskInfo{taskId:sr_20261009_02,source:poster_1120.jpg,model:SR-X2-Lite,scale:2,totalTiles:16,finishedTiles:12,memoryCapMB:256,stage:UPSCALING,status:RUNNING,progress:68,revision:12};revision是最容易漏掉的字段。tile 回调是异步的如果同一个 tile 被重试旧结果可能晚于新结果回到页面。没有版本控制就可能出现“进度从 70 跳回 65”的假回退。每次发布新的任务快照应当让版本单调增加页面看到旧版本直接忽略。至于memoryCapMB: 256它描述 Demo 的应用层缓存和工作队列预算并非声明 Core Vision Kit 能提供“设置 256 MB 上限”的系统接口。实际峰值还包括解码图、输入输出PixelMap、模型内部工作空间、合成目标图、预览图以及系统其他图形内存。做预算必须分别统计不能把业务阈值当作系统硬限制。四、12 / 16 为什么不是 75%而显示 68%这是我认为这篇最值得单独解释的一点。截图显示Tiles 12 / 16如果按简单除法确实是75%。然而页面进度是68%。这不是疏忽而是我们明确采用了任务加权进度预处理占 8%、tile 结果计算与交付占 80%、拼接 / 修缝占 8%、缓存写入与校验占 4%。运行到 12 块已交付时预处理已完成tile 阶段占比为0.8 × 12 / 16 0.6因此综合进度就是8% 60% 68%。这时输出图并没有拼接完成缓存也还没落盘所以不能提前给满进度。等 16 块都提交完算法与分块部分到88%拼接和修缝完成到96%文件实际写入、校验完成后才显示100%。有些应用会直接用 12/16 作为“分块进度”同时用 68% 作为“总任务进度”这当然也可以。问题不在采用哪个公式而在于口径必须写清。如果 UI 只写一个大大的“68%”用户很容易以为还有 32% 的模型推理工作事实上剩下可能主要是分块结果拼接、编码与缓存。进度条越漂亮越容易掩盖真正的等待阶段。五、第二段代码调度器聚合结果而不是相信回调次序我把每个 tile 的唯一编号、重试代数与结果提交状态保存下来调度器收到回调以后先判断能否入账再计算综合进度。这样即使同一块因为超时被重试旧回调也不会让完成数重复加一。下面展示的是关键聚合逻辑// engine/TileScheduler.ets应用层调度与加权进度exportinterfaceTileResult{tileId:number;attempt:number;outputPath:string;}exportclassTileScheduler{privatecommitted:Mapnumber,numbernewMap();privatelatestAttempt:Mapnumber,numbernewMap();privatetask:SRTaskInfo;constructor(task:SRTaskInfo){this.tasktask;}markAttempt(tileId:number,attempt:number):void{this.latestAttempt.set(tileId,attempt);}mergeTileProgress(result:TileResult):boolean{if(result.tileId0||result.tileIdthis.task.totalTiles){returnfalse;}if(this.latestAttempt.get(result.tileId)!result.attempt){returnfalse;// 旧重试代数不再入账}if(this.committed.has(result.tileId)){returnfalse;}this.committed.set(result.tileId,result.attempt);this.task.finishedTilesthis.committed.size;this.task.progress8Math.floor(80*this.task.finishedTiles/this.task.totalTiles);this.task.revision;returntrue;}}调用侧还要管理队列背压不能把 16 个原图 tile 一口气都解码成高分辨率PixelMap。这个设计里的“16 tiles”只是业务切片数量不等于底层 Core Vision Kit 同时执行 16 路推理。在一些设备上串行执行更稳在另一些设备上也许可以有限并行具体上限需要跑内存、温度和失败率测试。还有一个被切片方案天然放大的边界问题如果直接把源图切成不重叠方块再分别超分拼回去可能出现缝隙、颜色过渡不一致或边缘纹理断裂。实际处理必须给 tile 加重叠边界、记录有效区域、选择融合权重并在目标图中使用统一的坐标系。不是所有模型和系统能力都适合外部分块官方分析器的输入约束也可能决定最合理的方案是整图处理或先缩放到可支持范围。因此对应用层而言调度器只是可替换模块当设备无法满足切片输入条件时选择整图调用当图片或模型需要分块时才启用带重叠区的 tile pipeline。把这点写出来比把所有图像都强制塞进“16 块并行”更符合实际工程。六、真正的系统能力调用应该放在哪里Core Vision Kit 的imageSuperResolution.ImageSRAnalyzer才是官方分析器。按照官方参考典型步骤是创建分析器、构造包含PixelMap的visionBase.Request等待process()返回处理后的PixelMap最后释放分析器以及相关图像资源。本文应用层的 Worker 只是把这套调用包起来并没有发明新的系统超分 API。下面是一个单张输入的关键调用片段。为了突出资源释放省略了图库选择、图片解码、模型约束预检查和输出写文件这些必须在正式工程里补齐。// worker/SRWorker.ets调用官方 Core Vision Kit 的关键部分import{imageSuperResolution,visionBase}fromkit.CoreVisionKit;exportasyncfunctionsuperResolveOne(input:PixelMap):PromisePixelMap{constanalyzerawaitimageSuperResolution.ImageSRAnalyzer.create();try{constdata:visionBase.ImageData{pixelMap:input};constreq:visionBase.Request{inputData:data};constresponseawaitanalyzer.process(req);returnresponse.pixelMap;}finally{awaitanalyzer.destroy();}}要特别强调两个生命周期细节。第一函数返回的输出PixelMap所有权交给调用方调用方必须在编码、缓存或显示完成以后自行释放不能在这里提前释放。第二输入PixelMap的释放要由创建它的那一层负责避免一层释放后另一层还在显示。创建失败、推理抛错、页面销毁都需要通往同一个可靠的资源回收路径。try / finally能保证大部分正常异常分支执行释放逻辑但不能代替系统能力中止或进程终止时的恢复机制。如果真实项目要允许用户“取消”必须有明确定义取消排队中的 tile 很容易中止一个已经进入系统分析器的process()则要按当前版本公开的取消 / 释放语义验证不能假定销毁对象就能安全强杀正在运行的底层任务。七、DevEco Studio 对照一次分块进度在示意开发图里左侧工程结构包含UpscaleTaskPage.ets、TileScheduler.ets、SRWorker.ets和ResultCache.ets中间是任务提交、进度聚合和缓存保存右侧预览是同一个任务sr_20261009_02底部 HiLog 用相同 ID 把几种阶段串起来。它的价值是解释“哪个模块拥有哪种状态”不是作为真机编译通过的证据。特别要提醒图中编辑器片段用Math.floor(finishedTiles / totalTiles * 100)表示了纯分块进度的简化写法。如果直接用于真实应用总进度会变成 75%与手机图 03 的 68% 不一致。因此本文正文采用上面的8 / 80 / 8 / 4 加权方案作为最终任务进度口径。把两种公式并排讲清比勉强宣称所有图完全无差异更能帮助读者理解版本迭代。这也揭示出“真实开发截图感”和“工程真实性”不是一回事。界面可以非常像 IDE但数据一致性仍需要通过源码、构建产物和实际日志验证。读者复刻时建议用同一个任务快照驱动界面与日志不要分别手工拼数字。日志中记录taskId、tileId、attempt、当前阶段、内存样本和最终结果路径会让一次分块异常可追溯。八、任务页展示的内容应该让用户判断下一步这张UpscaleTaskPage设计图中当前任务RUNNING / UPSCALING模型策略SR-X2-Lite分块12 / 16综合进度68%输入poster_1120.jpg业务缓存预算256 MB。预览区显示原图1120 × 1584和目标超分结果2240 × 3168。由于任务尚未SAVED右侧更适合显示可快速刷新的低分辨率预览而不是误导用户“高清输出已经完成”。我会在产品里把“预览”和“输出”做成两份资源。预览可以是低分辨率合成图、缩略图或者上一次稳定结果用来保证交互反馈正式输出必须经过全部 tile、融合修缝、图像编码、磁盘写入和校验。这样既避免 UI 长时间空白也不需要每次刷新进度条就重新生成完整大图。这张图里有一个值得调试的矛盾当进度还是 68% 时界面下方已经出现“保存结果”按钮。这个按钮在实际应用中应当置灰或者明示“处理中暂不可保存”示意图用它说明 UI 操作入口但不代表可以绕过SAVED门槛。我宁可在文章里指出这一点也不愿把不完整的交互当作已验收的产品界面。至于“暂停任务”含义也不能随便承诺。本文的暂停首先作用于队列分发不再启动新的 tile正在执行中的任务按底层能力安全完成或自然返回。恢复时读取调度器已确认的 tile 列表跳过真正完成的分块不能简单重启一批没有持久化记录的任务后宣称“断点续推”。九、第四段代码结果落盘比显示一张图更重要在 SuperResLab 里输出文件的目标位置是应用沙箱缓存目录下的sr_20261009_02.png最终详情显示完整路径/data/storage/el2/base/cache/sr_20261009_02.png。这里刻意不用“相册路径”这个说法应用沙箱文件和用户相册资源的授权、访问、可见性、清理策略都不同。未来要发布到相册或分享给其他应用需要遵守相应的媒体资源和 URI 授权流程。落盘最好采用临时文件与原子替换策略避免写了一半却显示SAVED。下面给的是存储层协议而不是偷懒用某个不存在的系统函数假装写盘。writeEncodedImage()、verifyFile()和atomicCommit()都必须在ResultCache里用可用文件 API 实现。// cache/ResultCache.ets持久化事务的业务接口示意exportinterfaceEncodedImage{bytes:Uint8Array;width:number;height:number;checksum:string;}exportasyncfunctionsaveResultToCache(taskId:string,image:EncodedImage,cache:ResultCache,store:SRTaskStore):Promisestring{consttarget/data/storage/el2/base/cache/${taskId}.png;consttemp${target}.writing;awaitcache.writeEncodedImage(temp,image.bytes);constverifiedawaitcache.verifyFile(temp,image.checksum);if(!verified){awaitcache.removeIfExists(temp);thrownewError(CACHE_VERIFY_FAILED);}awaitcache.atomicCommit(temp,target);awaitstore.markSaved(taskId,target,image.width,image.height);returntarget;}atomicCommit()的行为要结合实际文件系统 API 验证而不是因为函数名字里有“atomic”就默认可靠。如果写入接口没有提供应用需要的原子语义至少要做到“临时文件、完整校验、成功后发布路径、失败清理”。记录数据库状态时也要考虑文件已写成但事务尚未提交、事务已提交但文件被系统清理两类不一致情况。这要求读取结果时再做存在性与完整性检查。图里的4.8 MB、校验摘要3E9A-A210、推理6.1s、拼接1.2s、总耗时8.4s都是本演示预设的结果字段不是对某台 HarmonyOS 设备跑出的性能承诺总耗时还包括预处理、编码和其他业务开销不一定等于两个子耗时简单相加。工程测试应该记录冷启动与热启动、不同图片类别、不同设备温度、长短边尺寸、编码格式才能给用户可信的预期。十、结果详情应当回答的是“真的保存成功了吗”在TaskDebugPage中任务完成态为SAVED阶段为RESULT_READY总分块数16 / 16。它还保留一条时间线READY → PREPROCESS → UPSCALING → SAVED。有的人会问既然阶段叫RESULT_READY为什么整体状态叫SAVED因为一个描述算法结果是否形成另一个描述用户可用结果是否已经可靠存储这是两条不同的判断轴。详情页还标出了“缝隙修复完成”。这一步不能仅凭肉眼看缩略图确认。常见的检查方法是把所有 tile 的重叠边缘单独抽出来做差分分析重点看边界是否出现亮度突变、色彩断层、重复纹理或者被错误裁切的像素。若应用支持文字海报也要在标题边缘检查模型有没有凭空生成笔画超分提高观感并不意味着能恢复原图里不存在的真实文字细节。我会给结果校验加两层文件层检查是否可读取、尺寸是否符合目标、编码内容是否完整视觉层检查色彩、边缘缝隙和关键区域细节。文件校验通过只能证明落盘成功不能保证图像看起来正确视觉验收通过也不能替代文件一致性。对用户来说这两个步骤最终都折叠成一个绿色状态对开发者来说则必须留两条日志。最后还有一个经常被忽略的风险系统或用户可能清理 cache 目录。既然它是缓存就不能保证永久存在。页面在重新打开任务时应重新检查输出文件是否还可读取如果已失效状态应明确转为“结果已过期 / 可重新生成”而不是继续展示一个历史SAVED标签让用户点击后报错。对需要长期保存的文件应转移至适当的持久位置并处理用户授权。十一、验收方案比一张对比图更有价值这次我列的验收项没有堆成几十个按钮测试而是集中在五种行为。快速重复提交同一任务 ID 不会启动两个推理作业。旧回调迟到版本号落后的 tile 结果不能覆盖新状态。强制退出恢复已确认的分块可以追踪无法恢复的中间状态有明确说明。磁盘空间不足临时文件写入失败后任务不得显示SAVED。结果被清理重新进入详情页必须发现缓存缺失并展示可操作的失败态。如果设备采用整图处理策略还要有一套旁路测试不拆 tile 时进度不应虚构12 / 16如果用户取消只能按分析器真实支持的生命周期处理如果输入图不满足尺寸和像素约束应该在创建昂贵资源之前给出可解释提示。这里涉及 Stage 模型、API 版本、设备支持范围以及 Core Vision Kit 当前的输入限制均以官方文档和实际运行环境为准。每个耗时指标也必须标注统计范围。比如所谓“推理 6.1s”可能不包含模型加载、图片解码和写盘因此不等于用户从点击到获得结果的时间。没有口径的性能数字不如不放。在数据量大、发热明显的端侧场景里分阶段记录耗时、峰值内存和失败原因后续优化才有真正的抓手。十二、这次重构留下的判断一个图像超分 Demo 能不能调用 API并不等于它已经是稳定的图片处理工具。真正的工程工作发生在分析器之外异步任务是否可观察、每个分块是否可幂等、进度是否有统一口径、资源是否及时释放、结果是否经过校验、缓存是否能在下一次打开时找到。图片“更清晰”是目标但过程“可信”才是产品能长期使用的基础。SuperResLab 当前的模型仍是一个演示约定不代表任何设备都能稳定以 256 MB 跑完 16 个 tile也不能据此承诺每次都在 8.4 秒内完成。下一步如果把它做成实际产品我会优先测三种情况一张满是文字的复杂海报、一张大面积平滑渐变的人像背景、以及一张高频细节密集的夜景。它们暴露的问题完全不同足够检验分块、融合和质量提示是否站得住。最值得保留的一条原则是把“算法处理完”与“用户确实拿到了可使用的文件”分开。进度可以加权预览可以渐进显示任务可以在安全边界下暂停但SAVED必须是经过文件层确认的事实。这才是图像超分从一次按钮演示走向工程能力的关键。参考资料华为开发者Core Vision Kit — imageSuperResolution 图像超分 API华为开发者图像与视觉能力相关文档图示说明四张图片围绕 SuperResLab 统一设计IDE 与手机截图为生成的技术示意不是实际编译、真机性能或相册保存的证据。TileScheduler、SRWorker、ResultCache、SR-X2-Lite和图片中的耗时、大小均为业务层演示定义需依据 SDK 文档和真机测试调整。