Image Kit+ArkTS:人像超分遮罩像素中心对齐与误标控制【鸿蒙心迹】
人像超分最容易被忽略的一层,并不是清晰度,而是遮罩与最终图片有没有落在同一个像素坐标上。图像增强后,头发边缘看起来细了,背景却被错误地划进了人物区域。画质模型可能毫无问题,错的是应用把一张低分辨率分割遮罩当作输出图坐标直接使用。本文讨论的是应用侧几何契约,不声称修改了系统超分算法。为了把争议限定在能检查的地方,我设计了一个名为 MaskAlignLab 的演示数据集:原图为 640×480,分割遮罩为 320×240,假设某个增强流水线返回 1280×960 的结果。三张图的长宽比相同,但并不共享像素索引。需要对齐的是像素中心,不是给遮罩简单乘一次倍数。
## 一、把三种尺寸写进同一个契约图像入口与后处理出口往往由不同模块维护。Image Kit 负责位图读取、解码与变换,分割结果则可能来自业务模型或服务端。它们可能各自返回合法数据,却合起来得到错误叠加。尤其是宽高字段丢了来源标签以后,width 究竟指原图、遮罩还是增强图,很容易在几层函数调用中被误读。我把本例演示任务号定为 MASK-1811,页面文件为 entry/src/main/ets/pages/MaskReviewPage.ets。界面按钮叫“检查坐标契约”,最终通过态写成 MASK_ALIGNED。这些字段是便于读者核对文图的构造样例,不是 DevEco Studio 或真机上已经跑出的测量值。先解决入口问题:读取真正的 PixelMap 信息,而不是信任业务层写死的字符串。下列 getImageInfo() 来自 Image Kit;遮罩和增强图的尺寸则由各自模块以独立参数交给检查器。代码只校验几何关系,不调用未经核实的超分 API。arktsimport { image } from '@kit.ImageKit';interface Size2D { width: number; height: number }interface MaskContract { source: Size2D; mask: Size2D; output: Size2D }async function createContract(sourcePixelMap: image.PixelMap, maskSize: Size2D, outputSize: Size2D): Promise<MaskContract> { const info: image.ImageInfo = await sourcePixelMap.getImageInfo(); const source: Size2D = { width: info.size.width, height: info.size.height }; const sizes: Size2D[] = [source, maskSize, outputSize]; if (sizes.some(s => s.width <= 0 || s.height <= 0)) { throw new Error('INVALID_SIZE'); } if (source.width * maskSize.height !== source.height * maskSize.width || source.width * outputSize.height !== source.height * outputSize.width) { throw new Error('ASPECT_MISMATCH'); } return { source, mask: maskSize, output: outputSize };}这里的比较使用交叉相乘,避免先除法再比较造成不必要的小数误差。它只是“形状准入”,不能证明人物边缘真的被分对,也不能证明图像具有相同的裁切原点。真实项目中一旦有人像裁剪、旋转或镜像,契约还得显式记录 cropRect 和方向变换,否则长宽比完全一致仍然会错位。传入的 PixelMap 由调用者持有,检查函数不擅自释放它;谁创建谁负责在失败和取消时回收。## 二、真正的差错发生在取样公式常见写法是把输出坐标按倍率缩小,再调用四舍五入取遮罩索引。表面上很合理,但最右侧或者最底部会出现越界;而且“取整到最近整数”与“源像素中心对齐”不是一回事。遮罩属于离散标签图,人物像素和背景像素是类别,而不是连续颜色。对标签图做双线性插值,会在边缘制造一个不存在的中间类别。这篇方案约定:输出图的整数坐标表示该像素中心所在网格;先换算到遮罩网格,再选择它覆盖的格子,最后钳制到合法下标。如果遮罩本身采用别的坐标语义,必须替换映射公式,不能把这一种约定当通用定理。arktsinterface GridPoint { x: number; y: number }function outputToMask(point: GridPoint, c: MaskContract): GridPoint { const w: number = c.output.width; const h: number = c.output.height; if (point.x < 0 || point.y < 0 || point.x >= w || point.y >= h) { throw new Error('OUTPUT_POINT_OUT_OF_RANGE'); } const maskX: number = Math.floor((point.x + 0.5) * c.mask.width / w); const maskY: number = Math.floor((point.y + 0.5) * c.mask.height / h); return { x: Math.min(c.mask.width - 1, Math.max(0, maskX)), y: Math.min(c.mask.height - 1, Math.max(0, maskY)) };}按这组样例计算,输出图 (640,480) 映射到遮罩 (160,120)。注意这里没有把遮罩从 320×240 放大成一张新图;做分类查询时直接映射下标即可,既省去额外缓冲,也不会让插值改变类别边界。如果后续要绘制半透明叠层,可以在显示层另外做视觉插值,但业务判断仍读取原始标签。不要混用“供人观看的柔化遮罩”和“供规则计算的离散掩码”。一个容易忽视的分支是极端坐标。输出最右侧的横坐标为 1279,计算时应落在合法范围 0~319 内;上层若传来 x=1280,应该明确报越界而不是静默包回第一列。越界错误意味着坐标空间协议错了,不是普通的一次采样偏差。对抗样例应该同时覆盖边角、贴边头发和临近裁剪边框的点。## 三、让页面显示规则判定,而不是假装展示模型成绩有些技术截图喜欢在图片上写“精度提升 99%”,但没有带标注数据集、指标公式与实测样本,这类数字没有意义。这一页只展示可推算的信息:三组尺寸、中心样例点、规则检查状态。业务上还需要单独的画质验收,但不能由一个通过几何检查的绿色标识推导出来。下面这一段解决页面状态如何防止旧任务覆盖新任务。它不执行分割,也不调用真正的超分服务;checkGeometry() 接收上文已经拿到的契约,用固定的样例点完成演示。任务代次用于拒绝较早的异步请求在用户切换图片以后改写页面。为了集中说明,示例中省略图片选择器和外部服务接口。```arkts@Entry@Componentstruct MaskReviewPage { @State private status: string = ‘WAITING’; @State private pointText: string = ‘尚未检查’; private generation: number = 0; private readonly taskId: string = ‘MASK-1811’; private checkGeometry(): void { const epoch: number = ++this.generation; const c: MaskContract = { source: { width: 640, height: 480 }, mask: { width: 320, height: 240 }, output: { width: 1280, height: 960 } }; try { const p: GridPoint = outputToMask({ x: 640, y: 480 }, c); if (epoch !== this.generation) return; this.pointText = (640,480) → (${p.x},${p.y}); this.status = ‘MASK_ALIGNED’; console.info([MaskAlignLab] ${this.taskId} state=${this.status}); } catch (e) { if (epoch === this.generation) this.status = ‘CHECK_FAILED’; } } build() { Column({ space: 12 }) { Text(‘MaskAlignLab · 人像遮罩核对’).fontSize(22) Text(this.pointText) Text(this.status) Button(‘检查坐标契约’).onClick(() => this.checkGeometry()) }.padding(20) }}````generation是应用自己的会话序号,而不是 Image Kit 回传的任务编号。这个最小组件里计算是同步的;在真正异步场景里,必须把epoch捕获在await之前,并在回到 UI 线程后再比较。若拥有大块遮罩像素数组,取消时还应结束后台任务或废弃缓存引用,避免界面状态已结束但内存继续被占用。错误码CHECK_FAILED保留给调用端诊断,不能自动修改原始图像。## 四、怎样验证同一组样例配图中的 HiLog 文本是演示日志模板,预期依次是[MaskAlignLab] MASK-1811 source=640x480 mask=320x240 output=1280x960、[MaskAlignLab] MASK-1811 point=(640,480)->(160,120)、[MaskAlignLab] MASK-1811 state=MASK_ALIGNED。上面的最小代码只直接打印最后一行;前两行应该由接入时的输入适配层补充,不要把图中全部行误认成这三个片段运行后自动产生。这里还应做一组“反向检查”:先选取遮罩上的格子,再找到它覆盖的输出图范围,核实一个类别在放大后是否被连续地复用。这个检查与上面的点到点映射不是重复测试:前者能看出某一列是否被跳过,后者只能证明选中的几个输出点索引合法。尤其当增强结果是奇数宽高、遮罩尺寸不能整除输出尺寸时,连续性比肉眼看到的平滑边缘更重要。生产环境应把这个结果保存为可复现的输入尺寸与映射规则版本,而非孤立的通过截图。调试时先分别打印 Image Kit 的原图信息、分割服务提供的遮罩信息、增强结果的图像信息,再检查中心点和四角。第二轮才用带人工标注的人像样本核对轮廓误差。若把输出尺寸从 1280×960 改成 1280×800,入口应该给出 ASPECT_MISMATCH,而不是显示成功。若给定 x=1280,应返回 OUTPUT_POINT_OUT_OF_RANGE。这些是预期的规则行为,不是已经通过的真机测试报告。## 五、在实际项目中哪些东西不能省这类坐标门禁适合多分辨率掩码、分割标签、照片编辑辅助区或文搜图区域标注。它不等同于消除模型的边缘光晕:Alpha 预乘、颜色空间、锐度噪声、模型质量属于其他独立层面,这里刻意不重复讨论。真正难的是把坐标身份保存下来,包括原点、裁切、方向、像素格式以及输出图是否发生了非均匀拉伸。还有一个工程选择:不要因为暂时不知道掩码生成时的镜像状态,就猜一个方向然后继续执行。宁愿把结果标成 CHECK_FAILED`,让调用者回到原始图或请用户重选,也不应发布一张边缘标错的处理图。遮罩和原图的内存所有权也必须写在接口约定里,尤其是频繁切换图片时。这篇只给出对齐层的可复核推导与 ArkTS 示例,未执行 DevEco 编译、真机测试或实际人像超分。技术依据是华为 Image Kit 的 PixelMap 信息读取与图像变换文档:使用 PixelMap 完成图像变换、Image Kit 开发文档。图示为生成的演示画面,尺寸、任务号与日志采用构造样例。
更多推荐


所有评论(0)