相册里看着正常的竖图交给图像处理链路后可能横了。页面预览没问题超分输出却旋转了九十度再补一个rotate(90)另一批图片又倒过来。这个现象很适合提醒开发者文件像素矩阵的宽高和用户看到的视觉方向不一定是一回事。本文构造UprightSRDemo页面名为NormalizePreviewPage任务编号SR-0816。源文件像素矩阵为 3024×4032EXIF 方向归一化值为RIGHT_TOP应用侧按规则顺时针旋转 90°生成送入超分能力的 1440×1080 PixelMap示例输出为 4320×3240最终状态READY。这些数字用于固定正文与配图不冒充设备性能或真实模型效果。一、预览方向正确不能证明像素方向正确很多图片查看器会读取 EXIF Orientation在显示时自动旋转。开发者看到的是一张竖直照片文件里的像素仍可能以横向矩阵保存。后续组件若再次尊重元数据看起来一切正常只接收 PixelMap 像素的处理能力若没有同样的方向语义就可能得到横图。UprightSR 不把“Image 组件显示正确”当输入验收。流程先创建 ImageSource读取 Orientation再解码 PixelMap执行方向归一化最后构造 Core Vision Kit 的请求。方向处理是超分前置阶段不混进结果展示层。Demo 冻结以下调试字段时间16:32、任务SR-0816、源矩阵3024×4032、方向RIGHT_TOP、动作rotate 90°、请求输入1440×1080、示例输出4320×3240、阶段DECODED → ORIENTED → PROCESSING → READY、输入所有者REQUEST、输出所有者PAGE。诊断页还显示source released true与analyzer destroyed true。二、Orientation 不是一个角度而是八种映射EXIF Orientation 不只有 0、90、180、270 四种旋转。完整语义还包含镜像组合。若只写一个角度表遇到镜像方向时人像文字可能左右反转。工程上更稳妥的做法是先把平台返回的字符串或枚举映射成应用自己的方向类型再生成一组有顺序的变换动作。本文使用RIGHT_TOP表示常见的顺时针 90° 情况。真实返回文本的大小写、连字符和枚举值应以当前 SDK 文档与设备结果为准边界层负责标准化业务层不直接比较随版本变化的原始字符串。这段代码解决什么问题把 EXIF 朝向变成可测试的旋转与翻转计划避免在页面里散落条件分支。typeNormalizedOrientationTOP_LEFT|TOP_RIGHT|BOTTOM_RIGHT|BOTTOM_LEFT|LEFT_TOP|RIGHT_TOP|RIGHT_BOTTOM|LEFT_BOTTOMinterfaceTransformPlan{rotate:0|90|180|270flipX:booleanflipY:boolean}exportfunctionplanFor(orientation:NormalizedOrientation):TransformPlan{constplans:RecordNormalizedOrientation,TransformPlan{TOP_LEFT:{rotate:0,flipX:false,flipY:false},TOP_RIGHT:{rotate:0,flipX:true,flipY:false},BOTTOM_RIGHT:{rotate:180,flipX:false,flipY:false},BOTTOM_LEFT:{rotate:0,flipX:false,flipY:true},LEFT_TOP:{rotate:90,flipX:true,flipY:false},RIGHT_TOP:{rotate:90,flipX:false,flipY:false},RIGHT_BOTTOM:{rotate:270,flipX:true,flipY:false},LEFT_BOTTOM:{rotate:270,flipX:false,flipY:false}}returnplans[orientation]}映射表的好处是可以独立测试。准备一张四角分别写 A、B、C、D 的小图针对八个方向生成结果检查文字和角标是否到位。只拿风景照测试不够左右镜像很难凭肉眼察觉。不同工具对镜像后旋转与旋转后镜像的定义可能不同。上表表达的是 Demo 的应用侧约定接入时必须用标记图验证 PixelMap 变换顺序不能只照抄枚举名称。若目标系统版本已经提供自动处理旋转角度的解码选项也要确认是否涵盖镜像、是否保留原元数据以及结果 PixelMap 的宽高语义再决定由系统还是应用负责两边不能同时做。三、先冻结“谁负责纠正方向”方向错误最常见的根因不是不会旋转而是多个层都认为自己该旋转。相册选择器预览一次、Image 组件显示一次、图片解码选项一次、业务代码再一次最终表现取决于走了哪条路径。UprightSR 的协议很简单ImageSource 读取原始属性OrientationNormalizer唯一负责把 PixelMap 变成视觉正向后续超分模块只接收已经归一化的像素不再查看 EXIF结果页面也不附加旋转。输出保存时将方向视为 TOP_LEFT避免下游重复解释旧标签。若项目选择“解码阶段自动处理”那就删除手工 rotate/flip并把自动处理选项写进输入快照。重要的不是哪一种更高级而是只有一个事实源。四、解码尺寸必须按旋转后的宽高思考源矩阵是 3024×4032RIGHT_TOP 旋转后视觉宽高应是 4032×3024。若解码参数仍按旋转前的长短边设置可能得到 1080×1440然后旋转成 1440×1080这是本文需要的请求尺寸。若误把目标写成 1440×1080 再旋转最终会变成 1080×1440。因此尺寸规划应先确认方向是否交换宽高再反推解码目标。UprightSR 的输入约束写成“视觉长边 1440、视觉短边 1080”不是“文件 width1440、height1080”。这段代码解决什么问题读取方向、按视觉目标解码并在同一 PixelMap 上完成旋转/翻转同时保证 ImageSource 在解码结束后释放。import{image}fromkit.ImageKitinterfaceNormalizedInput{pixelMap:image.PixelMap orientation:NormalizedOrientation visualSize:{width:number,height:number}}functionnormalizeOrientation(raw:string):NormalizedOrientation{constkeyraw.trim().replaceAll(-,_).toUpperCase()constknown:string[][TOP_LEFT,TOP_RIGHT,BOTTOM_RIGHT,BOTTOM_LEFT,LEFT_TOP,RIGHT_TOP,RIGHT_BOTTOM,LEFT_BOTTOM]returnknown.includes(key)?keyasNormalizedOrientation:TOP_LEFT}exportasyncfunctiondecodeUpright(fd:number):PromiseNormalizedInput{constsourceimage.createImageSource(fd)letpixelMap:image.PixelMap|undefinedtry{constrawawaitsource.getImageProperty(image.PropertyKey.ORIENTATION)constorientationnormalizeOrientation(raw)constplanplanFor(orientation)constswapplan.rotate90||plan.rotate270pixelMapawaitsource.createPixelMap({desiredSize:swap?{width:1080,height:1440}:{width:1440,height:1080},editable:true})if(plan.flipX||plan.flipY)awaitpixelMap.flip(plan.flipX,plan.flipY)if(plan.rotate!0)awaitpixelMap.rotate(plan.rotate)constinfoawaitpixelMap.getImageInfo()return{pixelMap,orientation,visualSize:info.size}}catch(error){pixelMap?.release()throwerror}finally{source.release()}}代码里 PixelMap 在成功路径不能于 finally 释放因为所有权已经交给调用方失败路径才释放已创建对象。ImageSource 的异步读取和解码完成后不再需要因此在 finally 中释放。官方开发指导同样强调相关异步操作完成后PixelMap 和 ImageSource 都应按生命周期释放。editable: true是因为 rotate/flip 会修改 PixelMap。若当前 API 版本或解码选项对可编辑性有不同要求应按 SDK 声明调整。这里没有把所有图像都先复制一份因为复制会增加峰值内存所有权足够清楚时原地变换更直接。默认把未知方向当 TOP_LEFT 只是 Demo 的降级策略。正式产品应区分“属性不存在”和“属性无法解析”。前者可以按正常方向继续后者更适合进入ORIENTATION_UNKNOWN并保留诊断以免损坏图片被悄悄送入后续能力。五、超分请求接收的是归一化像素HarmonyOS 7/API 26 的 Core Vision Kit 图像超分能力公开了ImageSRAnalyzer.create()、process(request)与destroy()响应持有 PixelMap。visionBase.Request的inputData接收图像数据。工程代码应以当前 SDK 的类型声明、设备支持和官方限制为准。UprightSR 把分析器封装在一次会话中。输入 PixelMap 至少存活到process()完成响应 PixelMap 交给页面分析器无论成功失败都 destroy。输入与输出不是同一个所有权槽位不能因为页面已经拿到结果就忘记释放输入。这段代码解决什么问题用明确的资源所有者调用图像超分并在异常路径同样销毁分析器和输入 PixelMap。import{imageSuperResolution,visionBase}fromkit.CoreVisionKitimport{image}fromkit.ImageKitinterfaceSRResult{output:image.PixelMap inputSize:stringoutputSize:string}exportasyncfunctionrunSuperResolution(fd:number):PromiseSRResult{constnormalizedawaitdecodeUpright(fd)constanalyzerawaitimageSuperResolution.ImageSRAnalyzer.create()letoutput:image.PixelMap|undefinedtry{constrequest:visionBase.Request{inputData:{pixelMap:normalized.pixelMap}}constresponseawaitanalyzer.process(request)outputresponse.pixelMapconstoutputInfoawaitoutput.getImageInfo()return{output,inputSize:${normalized.visualSize.width}x${normalized.visualSize.height},outputSize:${outputInfo.size.width}x${outputInfo.size.height}}}catch(error){output?.release()throwerror}finally{normalized.pixelMap.release()awaitanalyzer.destroy()}}成功返回后output的所有者是调用方。函数不能在 finally 释放它否则页面拿到的是失效对象。输入 PixelMap 已经完成 process可以释放analyzer 也在同一会话结束。若官方能力允许复用 analyzer项目可以提升到仓储层但必须定义引用计数、并发串行化和页面退出策略不能把一个全局实例永久留着。本文示例输出 4320×3240 是文图一致字段不用于宣称固定倍率。实际输出尺寸、格式、设备支持和输入限制以当前官方文档与返回结果为准。代码读取getImageInfo()而不是用1440 * 3猜结果。上图是开发环境演示配图不是实际 DevEco Studio 截图或真机测试证据。工程树包含OrientationNormalizer.ets、SuperResolutionSession.ets和NormalizePreviewPage.ets中间标出 RIGHT_TOP 与资源释放右侧模拟器显示SR-0816、1440×1080、4320×3240、READYHiLog 固定使用 16:32。六、状态机要把方向处理单独列出来如果页面只有 LOADING 和 READY方向读取失败、解码失败、超分失败都会落到同一个提示。UprightSR 使用IDLE → DECODING → DECODED → ORIENTED → PROCESSING → READY。失败状态带阶段例如FAILED_ORIENTATION或FAILED_PROCESS。阶段拆开后日志也更有意义。DECODED表示 PixelMap 已创建但尚未保证视觉正向ORIENTED表示宽高和角标测试通过可以交给超分READY表示输出 PixelMap 已由页面接管。资源所有权和状态变化同步排查时不需要猜当前对象还能不能用。这段代码解决什么问题页面替换结果时先释放旧输出并在离开页面时完成最后一次回收。typeSRPhaseIDLE|DECODING|ORIENTED|PROCESSING|READY|FAILEDEntryComponentstruct NormalizePreviewPage{Statephase:SRPhaseIDLEStatetaskId:stringSR-0816Stateorientation:stringRIGHT_TOPStateinputSize:string1440x1080StateoutputSize:string--Statepreview?:image.PixelMapundefinedprivateasyncstart(fd:number):Promisevoid{this.phaseDECODINGtry{this.phaseORIENTEDthis.phasePROCESSINGconstresultawaitrunSuperResolution(fd)this.preview?.release()this.previewresult.outputthis.inputSizeresult.inputSizethis.outputSizeresult.outputSizethis.phaseREADY}catch(error){this.phaseFAILED}}aboutToDisappear():void{this.preview?.release()this.previewundefined}build(){Column({space:12}){Text(任务${this.taskId}).fontSize(20).fontWeight(FontWeight.Bold)Image(this.preview).width(100%).aspectRatio(4/3)Text(${this.orientation}·${this.inputSize}→${this.outputSize})Text(this.phase)}.padding(20)}}这里把ORIENTED快速写入页面是为了展示状态结构真实实现应由decodeUpright返回阶段事件或由会话对象统一驱动不能在尚未完成方向处理时提前标记。示例也省略了文件选择和任务取消以免混淆文章焦点。页面释放旧 preview 后再接管新 output。若 Image 组件仍在绘制旧 PixelMap立即 release 是否安全需要按组件与当前 SDK 行为验证更稳妥的实现是先切换 UI 引用在下一次安全时机释放或者由专门的资源仓储协调。原则是“最后一个消费者退出后释放”而不是机械地在赋值前后调用。运行页固定显示 16:32、SR-0816、EXIF RIGHT_TOP、源矩阵 3024×4032、动作 rotate 90°、输入 1440×1080、输出 4320×3240、状态 READY。红色箭头指向“方向已归一化”说明进入超分前的像素语义不把视觉清晰度当作可量化测试结论。七、诊断页先看宽高交换再看模型结果出现横图时不要先怀疑超分算法。第一步核对源矩阵和视觉矩阵RIGHT_TOP 是否导致宽高交换1440×1080 是否是归一化后的结果。第二步用角标图确认是否镜像。第三步才检查 process 的输入与输出。UprightSR 的日志是[16:32] SR-0816 EXIFRIGHT_TOP rotate90随后是[16:32] input1440x1080 output4320x3240 READY。若第一条缺失问题在元数据或归一化若第一条正确而请求仍是 1080×1440问题在尺寸规划若输入正确、输出方向错误再检查能力边界与输出处理。诊断配图中的186ms是为了说明阶段耗时字段应放在 PROCESSING 节点的固定演示值不是设备测量结果也不用于比较 Core Vision Kit 性能。真实工程应同时记录设备、系统、输入摘要和测量区间否则单独一个毫秒数字没有可比性。诊断图展示完整阶段、所有权和释放结果DECODED、ORIENTED、PROCESSING、READYsource released trueinput released trueanalyzer destroyed trueoutput owner PAGE。03 解释用户看到的结果04 解释为何没有重复旋转和资源遗留。八、失败路径比成功路径更容易泄漏图片解码成功、创建 analyzer 失败时normalized PixelMap 仍需释放。process 抛错时输入、可能已生成的临时输出和 analyzer 都要收口。页面接管输出后发生路由切换则由页面释放。每个对象都应有恰好一个当前所有者。可以画一张很简单的所有权表ImageSource 属于 decodeUpright输入 PixelMap 成功时移交 runSuperResolution失败时仍由 decodeUprightAnalyzer 属于会话响应 PixelMap 成功时移交页面失败时留在会话。这个表比“记得 release”更有执行力。资源释放日志不应打印成业务成功。analyzer destroyed true只说明清理动作完成不证明输出质量合格。质量验收需要独立的图像样本、指标和人工观察。九、不要同时保留原图、归一化图和结果图3024×4032 的 RGBA 像素占用远大于压缩文件体积。若流程先全尺寸解码再复制旋转再生成请求图再保留三倍结果峰值内存会迅速上升。UprightSR 在解码时直接设置请求所需尺寸在同一 PixelMap 上做变换process 完成后释放输入。若产品需要原图预览不必一直持有全尺寸 PixelMap。可以让 Image 组件使用可访问的 URI 展示处理链路单独解码受控尺寸。若必须比较前后效果也应限制同时存在的结果数量并在切换任务时回收旧对象。内存优化不能以破坏证据为代价。诊断页保存宽高、方向、动作、规则版本和错误码即可不要把完整 PixelMap 放进长期日志或状态快照。十、自动方向能力上线后要避免“双重修正”近期 Image Kit 版本说明提到自动处理 PixelMap 旋转角度的能力。项目升级 SDK 时这是一个需要专门回归的变化旧代码可能手工 rotate新解码选项又自动处理正常图片会再次转向。回归测试应记录三项事实解码选项是否启用自动方向输出 PixelMap 的实际宽高原 Orientation 是否仍可读。然后用八方向标记图跑完矩阵。不能看到新 API 就把旧代码删掉也不能忽略新默认行为。本文保留应用侧显式方案是为了把判断过程讲清楚不代表所有 API 26 工程都必须手工旋转。实际选择应以目标 SDK、系统版本和官方接口说明为准。十一、测试需要覆盖镜像、损坏元数据和任务切换基础用例包含八种方向每张图四角带不同文字。断言内容不仅是 width/height还包括四个角的视觉位置。再加入无 Orientation、非法值、属性读取失败、解码失败、不可编辑 PixelMap、process 失败和 destroy 失败。资源测试连续进入退出页面检查输出替换和路由离开后不再持有旧 PixelMap。并发测试启动 SR-0816 后切到 SR-0817旧任务即使完成也不能把输出交给新页面。这一项属于通用生命周期保护不改变本文“方向归一化”的核心问题。尺寸测试不要硬编码所有输出必须三倍。输入只断言 1440×1080 与视觉方向一致输出从响应读取再按官方约束检查。若能力返回不支持或参数错误页面应展示能力错误不回退到一张方向错误的旧结果。十二、上线检查从输入快照开始发布前为每次请求保存轻量快照taskId、源 URI 摘要、源宽高、原始方向文本、标准化方向、变换计划、请求宽高、输出宽高、SDK/规则版本和最终阶段。涉及用户照片时只留必要的非敏感元数据原 URI 与图片内容不上传普通日志。验收文案也应具体SR-0816 的 3024×4032 源矩阵读取为 RIGHT_TOP顺时针 90° 后请求尺寸为 1440×1080process 完成后响应 PixelMap 由页面接管输入、ImageSource 与 Analyzer 在各自生命周期结束后释放页面离开释放输出任何未知方向不得静默重复旋转。这套流程真正解决的不是“加一行 rotate”。它建立了像素方向的事实源、尺寸规划、请求边界和资源所有权。图像超分只应该接收已经解释清楚的输入否则输出越清晰方向错误也越醒目。十三、透明通道和色彩信息不能被方向问题遮住方向归一化通过后输入仍可能因为像素格式、透明通道或色彩空间出现视觉差异。带透明区域的素材在旋转时若预乘语义处理不一致边缘可能出现黑边广色域图片若被默认路径转换前后对比也可能让人误判为“超分改变了颜色”。UprightSR 的诊断快照除了尺寸和方向还应记录 PixelMapFormat、alphaType 以及可获得的色彩信息。本文不把这些字段塞进配图是为了保持主线清楚但生产排查不能只盯 Orientation。方向正确、颜色异常是另一个独立问题域。输入约束应在 process 前完成确认 PixelMap 可用、尺寸满足能力要求、像素格式处于支持范围、没有零宽高。不能为了让请求成功而无条件转换多次每一次格式转换都可能增加内存峰值和画质损失。若必须转换要把转换步骤写进快照。十四、保存结果时不要把旧 Orientation 写回去PixelMap 已经真正旋转到视觉正向后输出文件不应继续携带源图的 RIGHT_TOP。若编码器把旧 EXIF 原样复制新文件在支持 Orientation 的查看器里还会再旋转一次不支持元数据的查看器则显示正常同一个文件在不同应用里方向不一致。保存协议应明确像素已经归一化方向元数据写为正常方向或不再携带旧方向拍摄时间、设备型号、地理位置等其他 EXIF 是否保留按隐私和产品需求单独决定。不要用“复制全部元数据”作为默认策略尤其是用户将结果分享出去时。编码完成后需要重新创建 ImageSource读取新文件尺寸和 Orientation 做闭环校验。只检查文件存在或能打开不够。验收样本至少覆盖一个 RIGHT_TOP 和一个镜像方向确保新文件在相册、应用 Image 组件和不解析 EXIF 的基础查看器里视觉一致。如果业务只在内存中显示结果、不落盘也要防止把源 Orientation 附加到网络 DTO。下游看到标签后可能再次修正。元数据和像素必须作为同一份语义版本管理。十五、批量处理需要背压而不是同时启动全部图片单图流程资源闭环不代表批量相册处理就安全。十张图片同时解码、归一化和超分会同时持有多个输入与输出 PixelMap峰值内存取决于最慢任务。页面上看见十个进度条不等于底层适合十路并发。队列可以限制同一时刻的解码和 process 数量。任务进入队列时只保存 URI 与轻量元数据轮到执行再创建 ImageSource输出交给持久化或页面后立即释放输入。用户取消尚未开始的任务不应产生 PixelMap取消进行中的任务则按官方接口能力决定能否中断不能假装 Promise 已取消。批量任务的状态至少区分 QUEUED、NORMALIZING、PROCESSING、READY、FAILED 和 CANCELED。方向错误重试要复用同一 taskId 并增加 attempt避免诊断系统把一次图片处理拆成多条互不关联的记录。当页面进入后台产品可以暂停取新任务让正在执行的一项收口。若能力不支持后台场景不应靠保持页面对象来强行运行。恢复时从轻量快照重建队列不能持久化 PixelMap 实例。十六、上线验收要把“方向正确”说成可执行条件“横竖图都正常”不够具体。可以写成RIGHT_TOP 输入在归一化后宽高从竖向矩阵语义变为 1440×1080 视觉横图四角文字保持阅读方向镜像方向没有左右反转送入 process 的 PixelMap 与预览使用同一归一化结果保存文件不携带会导致二次旋转的旧 Orientation。资源条件也写进验收ImageSource 在属性读取和解码结束后释放输入 PixelMap 在 process 完成后释放Analyzer 在会话结束时 destroy输出 PixelMap 在页面替换、保存完成或页面退出后释放任何失败路径不遗留所有权不明的对象。最后再验收能力结果输入输出尺寸从对象真实读取错误码可诊断不把 Demo 的 4320×3240 当所有设备固定结论。方向、资源、模型结果三组断言分开问题出现时才知道该查哪一层。十七、参考资料与能力边界华为开发者图像超分专题HarmonyOS Image Kit使用 PixelMap 完成图像变换HarmonyOS Image KitEXIF Orientation 常见问题Core Vision Kit API 变更ImageSRAnalyzerCore Vision KitVisionBaseCore Vision Kit 图像超分属于 HarmonyOS 7/API 26 新能力设备范围、Beta 状态、输入限制和接口签名可能随 SDK 演进。本文只使用已核对的create/process/destroy、Request 与 PixelMap 边界接入时仍应以当前 SDK 类型声明和官方文档为最终依据。