HarmonyOS 7 PixelForge 图像超分工程实录 03:ImageSR × Region Decode:大图分块、重叠边界与拼接缝治理【鸿蒙心迹】
前两篇把 PixelForge 的两个基础问题收稳了。
01 解决的是单图链:解码出 PixelMap、进入超分、拿到真实输出尺寸、编码、释放;02 把这条链放进批量队列,确认每张图都能在自己的 finally 里释放 input / output PixelMap,并把并发 3 的 287.9MB 峰值压到并发 1 的 118.6MB。
做到这里以后,第三篇才适合碰真正的大图。
这次我把输入换成:
city_panorama_4439x2959.jpg
4439 × 2959
4.8MB
如果仍然整图解码、整图超分、整图输出,3x 结果理论尺寸会达到:
13317 × 8877
仅最终 RGBA_8888 PixelMap 的像素数据就已经非常大。
更麻烦的是,AI 中间缓冲和编码阶段还会继续抬高峰值。
所以第三篇不再问“整图能不能跑”,而是问:
能不能只把当前分块解码成 PixelMap,做 3x 超分以后立即释放,再把结果按可控重叠区重新拼回去。
HarmonyOS 当前 Image Kit 的解码选项本身支持 region 概念,Native Image Kit 也明确提供图像区域描述结构;PixelMap 侧则支持裁剪、缩放、位图读写等能力。也就是说,“区域解码 + PixelMap 局部处理”本身是一个很合理的工程方向。
本轮统一数据:
taskId:
sr_20261002_03
source:
city_panorama_4439x2959.jpg
inputSize:
4439 × 2959
sourceFileSize:
4.8MB
tileCount:
4
horizontalCore:
2220 / 2219
verticalCore:
1480 / 1479
inputOverlap:
96px
outputOverlap:
288px
requestedScale:
3x
targetOutput:
13317 × 8877
actualOutput:
13317 × 8877
averageTileSrCost:
356ms
stitchCost:
126ms
fullFrameMemoryEstimate:
451.2MB
peakMemory:
168.9MB
seamDiffBeforeBlend:
12.8%
seamDiffAfterBlend:
1.9%
releasedPixelMaps:
8
status:
TILED_SR_READY

一、第三篇先把“整图超分”改成“区域计划”
如果继续沿用前两篇:
ImageSource
→ createPixelMap
→ SuperResolution
整张 4439×2959 原图会一次性进入 PixelMap。
3x 后输出宽高:
4439 × 3 = 13317
2959 × 3 = 8877
这时整帧内存估算已经到:
451.2MB
这不是最终系统占用,只是 PixelForge 当前按输入、输出和中间工作区做的工程估算。
真实值会随设备、模型实现、编码路径变化。
但它已经足够说明:
这张图不适合再把整帧 PixelMap 长时间留在内存里。
所以 03 新增:
tile/
├─ RegionDecodeService.ets
├─ TilePlanBuilder.ets
├─ TileStitcher.ets
└─ FeatherBlender.ets
页面不再关心 PixelMap,只订阅 TiledSrSnapshot。
二、4 块不是平均切,而是保留 96px 输入重叠
原图:
4439 × 2959
横向无法平均分成两个完全一样的整数宽度。
所以核心区域固定:
左:
2220
右:
2219
上:
1480
下:
1479
这四个 core 正好覆盖完整原图。
但真正解码区域不会只取 core。
靠近拼接边的位置额外多取:
96px
重叠。
Tile Plan:
export interface TileCore {
x: number
y: number
width: number
height: number
}
export interface TilePlan {
core: TileCore
decodeRegion: TileCore
overlapPx: number
}
export class TilePlanBuilder {
build():
TilePlan[] {
return [
{
core:
{ x: 0, y: 0, width: 2220, height: 1480 },
decodeRegion:
{ x: 0, y: 0, width: 2316, height: 1576 },
overlapPx: 96
},
{
core:
{ x: 2220, y: 0, width: 2219, height: 1480 },
decodeRegion:
{ x: 2124, y: 0, width: 2315, height: 1576 },
overlapPx: 96
},
{
core:
{ x: 0, y: 1480, width: 2220, height: 1479 },
decodeRegion:
{ x: 0, y: 1384, width: 2316, height: 1575 },
overlapPx: 96
},
{
core:
{ x: 2220, y: 1480, width: 2219, height: 1479 },
decodeRegion:
{ x: 2124, y: 1384, width: 2315, height: 1575 },
overlapPx: 96
}
]
}
}
这一段代码解决的是“分块以后边缘上下文不够”的问题。
如果四块严格无重叠,超分模型在边缘看不到邻块内容,很容易形成明显接缝。
三、Region Decode 的价值不是“裁完再超分”,而是“不创建整图 PixelMap”
Image Kit 当前解码选项支持图像 region 描述;Native 接口也有 OhosImageRegion / Image_Region 这类区域结构。
PixelForge 的 RegionDecodeService 把具体 SDK 细节隔离在 Adapter 里,业务层只看到:
export interface DecodeRegion {
x: number
y: number
width: number
height: number
}
export interface RegionDecodeService {
decodeRegion(
sourcePath: string,
region: DecodeRegion
): Promise<image.PixelMap>
}
这里最关键的不是接口长什么样,而是:
每次只得到当前 tile 的 PixelMap
不会为了截四块,先完整创建一次 4439×2959 PixelMap 再 crop。
这正是大图分块能真正降低峰值的前提。
四、每个 tile 仍然沿用 02 的单项生命周期
Tile 处理没有重新发明资源模型。
依然是:
decode region
→ tile PixelMap
→ SR
→ output tile PixelMap
→ crop overlap
→ 保存拼接块
→ release output
→ release input
代码:
export class TileRunner {
async run(
sourcePath: string,
plan: TilePlan
): Promise<TileResult> {
let input:
image.PixelMap | null =
null
let output:
image.PixelMap | null =
null
try {
input =
await this.regionDecoder
.decodeRegion(
sourcePath,
plan.decodeRegion
)
const sr =
await this.adapter
.process(
input,
{ scale: 3 }
)
output =
sr.pixelMap
return await this.cropper
.cropToCore(
output,
plan,
3
)
} finally {
output?.release()
input?.release()
this.metrics
.releasedPixelMaps += 2
}
}
}
4 个 tile:
4 input
4 output
所以最终:
releasedPixelMaps=8
五、为什么输出重叠变成 288px
输入重叠:
96px
请求:
3x
所以输出重叠区理论尺寸:
96 × 3 = 288px
这 288px 不是直接丢掉。
我们先保留它,用于:
比较两侧输出
做过渡融合
最后再根据 core 边界裁切到真实输出坐标。
这比“每块超分后直接按核心区域硬拼”更容易抑制接缝。
六、拼接缝 12.8% 是怎么来的
为了让“接缝好不好”不完全靠肉眼,我加了一个简单工程指标。
对相邻 tile 的重叠带:
取同一逻辑位置的 RGB 差值
求平均归一化差异
羽化前:
12.8%
这说明两个 tile 的独立超分结果在边缘有明显差异。
注意它不是 PSNR / SSIM,也不是官方质量指标。
只是 PixelForge 当前用于:
接缝回归
的工程量。
七、Feather Blend 只处理重叠带,不重新算整张图
第三段代码解决的是“怎么让 288px 重叠区平滑过渡”。
示意实现:
export function featherWeight(
index: number,
length: number
): number {
if (length <= 1) {
return 1
}
return index /
(length - 1)
}
export function blendChannel(
left: number,
right: number,
weight: number
): number {
return Math.round(
left * (1 - weight) +
right * weight
)
}
横向接缝:
左块权重 1 → 0
右块权重 0 → 1
纵向同理。
四块交叉中心区域则使用二维权重。
当前融合后:
seamDiffAfterBlend=
1.9%
相比 12.8% 明显下降。
八、1.9% 不是“完全无缝”的绝对证明
这点必须克制。
1.9% 只是当前测试图、当前重叠宽度、当前指标下的结果。
真实视觉还会受到:
建筑直线
文字
人脸
高频纹理
天空渐变
AI 伪影
影响。
所以 PixelForge 仍然保留:
接缝放大预览
人工检查。
工程指标和视觉确认两条都保留。
九、为什么没有把 overlap 开到 256px
overlap 越大:
边缘上下文越多
但代价也很明显:
重复 SR 区域更多
单 tile 更大
内存更高
总耗时更长
当前 96px 是 PixelForge 的项目参数,不是系统推荐值。
本轮先用:
96 → 288
形成可稳定回归的基线。
06 才会对比其他 overlap。
十、168.9MB 和整帧估算 451.2MB 的差别才是分块价值
本轮:
fullFrameMemoryEstimate:
451.2MB
actualPeak:
168.9MB
下降非常明显。
这两个数不能理解成系统承诺。
但在同一设备、同一测试输入、同一实现下,它们说明:
分块把大对象的同时存活数量压下来了。
和 02 的“并发 3 → 并发 1”是同一类工程思路。
不是让算法变轻,而是控制同时活着的资源。
十一、最终输出宽高必须回到完整 3x 几何
四块独立处理以后,最终输出不能因为:
奇数宽高
overlap
裁剪
多 1px 或少 1px。
原图:
4439 × 2959
最终必须:
13317 × 8877
所以 TileStitcher 的最终校验:
export function validateFinalSize(
inputWidth: number,
inputHeight: number,
scale: number,
outputWidth: number,
outputHeight: number
): boolean {
return (
outputWidth ===
inputWidth * scale &&
outputHeight ===
inputHeight * scale
)
}
本轮:
actualOutput=
13317 × 8877
通过。
十二、拼接阶段不要同时保留 4 个巨大输出 PixelMap
这也是第三篇最容易重新把内存抬回去的地方。
如果:
4 tile SR 完成
→ 4 个 output PixelMap 全部放数组里
→ 最后一起拼
分块意义会被削弱。
PixelForge 当前策略:
tile SR
→ 裁出需要部分
→ 写入 stitch buffer / 文件块
→ release PixelMap
→ 下一 tile
TileResult 保存的是轻量坐标和缓存文件引用,不是大 PixelMap 本体。
这样才能把 peak 保持在 168.9MB 左右。
十三、拼接耗时 126ms 单独记录
当前:
averageTileSrCost=
356ms
stitchCost=
126ms
如果以后结果慢了,可以明确:
AI 推理变慢
还是:
拼接变慢
尤其当 tile 数量从 4 增加到 12 时,拼接成本会明显变化。
分块方案不能只看内存,不看总时延。
十四、DevEco 图要同时展示 core、overlap 和实际输出
开发图:

HiLog:
taskId=
sr_20261002_03
input=
4439x2959
tiles=
4
overlap=
96
scale=
3
tile1=
2220x1480
tile2=
2219x1480
tile3=
2220x1479
tile4=
2219x1479
averageTileSrCost=
356ms
peakMemory=
168.9MB
fullFrameEstimate=
451.2MB
stitchCost=
126ms
seamDiff:
12.8%
→
1.9%
releasedPixelMaps=
8
output=
13317x8877
status=
TILED_SR_READY
这比“分成 4 块”有用得多。
十五、运行图让接缝治理变成可视结果
最终运行图:

能直接看到:
4 个 core
96px 输入重叠
288px 输出重叠
3x 结果
13317 × 8877
以及:
12.8%
→
1.9%
真正表达的是:
分块以后,结果不是机械拼图,而是有明确重叠策略和接缝治理。
十六、如果 tile 中间失败,不能重跑整图
4 块里任何一块失败,PixelForge 记录:
tileId
decode region
SR state
cache path
重试只重新执行失败 tile。
已经完成的 tile 不重复跑。
这会直接成为 04 缓存设计的基础:
完整图可缓存
分块结果也可缓存
十七、Region Decode 本身也需要边界 clamp
靠近原图边缘时:
core + overlap
可能超出源图。
所以 Tile Plan Builder 必须做:
x >= 0
y >= 0
right <= sourceWidth
bottom <= sourceHeight
本轮四个 decodeRegion 都经过 clamp。
右侧 tile 实际宽度:
2315
而不是强行写成和左边一样的 2316。
这就是奇数尺寸必须认真算的地方。
十八、TILED_SR_READY 的验收条件
最终状态至少代表:
4 个 tile 全部完成
每 tile input/output PixelMap 都释放
最终宽高正确
输入 overlap=96px
输出 overlap=288px
拼接耗时可观察
接缝差异从 12.8% 降到 1.9%
实际峰值明显低于整帧估算
失败 tile 可单独重试
这些全部成立以后,第三篇才算真正完成。
十九、下一篇:算力省下来以后,别每次都重新算
03 的大图已经能跑稳。
接下来最浪费的事情反而变成:
同一张图
同一倍率
同一模型版本
每次打开都重新超分
04 会给 PixelForge 加本地结果缓存。
但缓存不能只用文件名。
同名文件被重新编辑,或者模型版本升级以后,旧结果必须自动失效。
下一篇会围绕:
sourceHash
scale
modelRevision
cacheKey
MISS / HIT / INVALIDATE
继续推进。
二十、分块方案还要保存 Tile Manifest,不能只靠内存数组
第三篇跑通以后,我又补了一层很实际的东西:TileManifest。
原因是大图处理时间明显比单图长,中途如果应用发生异常,只靠内存里的 tiles[] 很难知道已经完成到哪里。
Manifest 至少记录:
taskId
sourceHash
scale
tileCount
每个 tile 的 core
decodeRegion
resultPath
status
srCost
当前 4 个 tile 都是:
DONE
如果第 3 块失败,下一次重试只需要重新跑:
tile_03
而不是重新超分 1、2、4。
这和 04 的缓存设计也能直接衔接:tile 结果本身可以成为可复用的中间产物。
二十一、拼接缓冲区也不能无限大
即使不保留 4 个输出 PixelMap,最终生成 13317×8877 大图时仍然需要一个结果容器。
如果一次性创建完整 RGBA 拼接缓冲:
13317 × 8877 × 4
依然很重。
所以 PixelForge 当前把 Stitcher 设计成“条带写入”:
上半区
→ 处理 / 写入
下半区
→ 处理 / 写入
或直接面向目标文件做分区写入。
这篇手机图里为了方便理解,展示的是完整结果预览;真实实现不会为了“拼接方便”再次把所有中间结果同时堆回内存。
分块优化如果只优化 AI 阶段、不优化 stitch 阶段,很容易把省下来的内存又在最后一步用掉。
二十二、羽化只处理亮度差还不够,后面还要关注结构连续性
当前 1.9% 指标主要反映像素差异。
但城市全景里更敏感的是:
桥梁直线
楼体边缘
栏杆
文字
窗格
如果两块 SR 在边缘生成的结构稍有偏移,即使平均 RGB 差值不大,人眼仍然能看出断线。
所以 PixelForge 给后续质量回归预留:
edge continuity
line continuity
local sharpness
这些指标不会在 03 一次全部做完。
当前先把“接缝明显偏色 / 偏亮”解决,再把结构问题留到 06 的多图回归。
二十三、Tile 顺序也会影响峰值和首屏反馈
4 块可以:
1 → 2 → 3 → 4
顺序执行。
也可以优先处理当前用户正在查看的区域。
如果用户放大浏览图像左上角,PixelForge 可以优先:
tile_01
先生成局部高清结果,再后台完成其余 tile。
第三篇还没有做这种调度,但 Tile Manifest 已经有独立 tile 状态,因此后面不需要修改核心数据结构。
这也是分块设计除了“降内存”以外的另一个价值:处理单位更小以后,调度策略会更灵活。
二十四、分块失败时必须把临时文件和 PixelMap 两边都清理
某个 tile 在 SR 成功后、写入临时结果时失败,可能留下:
tile_02.tmp
如果下一轮直接读取这个文件,很容易把半成品当完成结果。
所以 tile 的最终状态只在:
临时文件完整写入
→ rename final
→ Manifest 标记 DONE
以后成立。
失败路径:
PixelMap release
临时文件删除
Manifest = FAILED
大图任务的可靠性最终仍然回到“资源和状态一起收口”,而不是只看 AI 调用有没有返回。
二十五、为什么 03 不追求把 4 块并行处理
02 已经证明并发 3 会把内存推到 287.9MB。
03 每块又比 02 的最大输入更大。
所以当前仍然坚持:
tile concurrency=1
先得到:
168.9MB
稳定峰值。
后面如果高内存设备要做 tile concurrency=2,必须在 06 的设备基线里单独验证。
我更愿意先牺牲一点总耗时,也不愿意让“大图支持”变成一条不可预测的 OOM 风险。
二十六、第三篇最终留下的是一套可以重试、可以观测的大图协议
到这里,大图处理已经从“切四张图拼一下”变成:
Tile Plan
→ Region Decode
→ Tile SR
→ Overlap Crop
→ Feather Blend
→ Manifest
→ Final Size Check
→ Resource Cleanup
每个步骤都有明确输入、状态和证据。
以后输入从 4439×2959 换成更大尺寸,只需要重新规划 tile 数量和 overlap,不需要把整个 PixelForge 的任务生命周期重做一遍。
二十七、最终还要把 TilePlan 写进结果元数据
最终输出文件不能只保存一张大图。
PixelForge 会同时写一份轻量 sidecar:
sourceSize
scale
tileCount
overlap
tilePlanRevision
seamDiff
generatedAt
以后用户反馈“这张大图中间有一道缝”,开发者可以直接知道它来自哪一版分块策略,而不是只拿到一张已经拼好的 JPEG 猜原因。
这种结果元数据在算法持续迭代时很有价值:图片是结果,TilePlan 则是结果能够被复现的证据。
参考资料
- Core Vision Kit API 26 图像超分 API 变更:
https://developer.huawei.com/consumer/en/doc/harmonyos-releases/js-apidiff-corevisionkit-7001 - Image Kit 区域解码选项:
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-v5/_ohos_image_decoding_ops-V5 - Image Kit PixelMap 图像变换:
https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/image-transformation
更多推荐



所有评论(0)