HarmonyOS 7 Core Vision Kit + ImageSource:图像超分输出的色域继承、HDR 降级与灰阶回归验收【鸿蒙心迹】
这次超分任务没有崩,边缘也确实更清楚。真正让人停下来的,是结果图比原图“灰”了一层:夕阳里的橙色还在,亮部却像被一块半透明白布盖住。

一、清晰度提高以后,颜色反而成了新问题
Demo 名为 ToneGuard SR,任务 ID 是 srcolor_20261001_15。输入是一张 3024×4032 的 Display P3 照片,经过 2× 超分得到 6048×8064 输出。第一版只验收尺寸、耗时和锐度,页面显示 SUCCESS,测试人员却很快发现同一张图在支持广色域的设备上明显褪色。
追日志后才发现,解码阶段能够读到输入色彩特征,模型适配器却只接收像素缓冲;输出 PixelMap 重建时沿用了默认 sRGB。更亮的区域还经过了一次没有元数据依据的压缩,结果就是高光变平、饱和度下降。它不是模型把细节“算坏了”,而是模型前后的图像管线丢失了颜色语义。
这类问题很容易被一张普通显示器截图掩盖。sRGB 页面上看着差不多,换到广色域屏幕或继续导出后,错误才明显。我们因此把验收拆成三层:输入特征必须形成快照;模型工作空间必须可解释;输出编码必须明确是保留广色域还是降级到 sRGB。
修复后,本轮色差指标 ΔE00 从 6.7 降到 1.8,高光裁剪占比从 3.4% 降到 0.6%,灰阶 16 级全部保持单调,峰值内存 412 MB,耗时 2.46 s,状态 COLOR_SAFE。
最初定位花了不少时间,因为普通截图无法完整保留广色域差异。我们先怀疑模型权重,又怀疑预览组件插值,甚至把锐化强度逐级关掉,褪色仍然存在。直到把原图、模型输入、模型输出和最终编码四个节点的色彩特征并排打印,才看到第三个节点之后的 outputSpace 被重建为 sRGB。
这也改变了 Demo 的数据结构。以前 SuperResolutionResult 只有 pixelMap、width、height 和 cost,页面一旦拿到 SUCCESS 就直接展示;现在结果必须同时包含 profileSnapshot、toneMode 和 ToneReport。缺少任意一项都只能进入 NEEDS_VALIDATION,不能让 UI 把“推理完成”写成“处理成功”。
二、色彩信息要和像素一起进入任务
旧接口只传 PixelMap,调用方无法知道这个对象来自 Display P3、sRGB,还是带 HDR 增益信息的源文件。页面重试时又重新解码,前后两次甚至可能走不同路径。
下面的代码解决“像素有了,解释像素的元数据却丢了”。任务创建时一次性冻结色彩快照,后续解码、模型推理和导出都引用同一个 snapshotId。
export interface ColorProfileSnapshot {
snapshotId: string
sourceSpace: 'SRGB' | 'DISPLAY_P3'
transfer: 'SRGB' | 'PQ' | 'HLG'
hdr: boolean
bitDepth: 8 | 10
createdAt: number
}
export async function captureProfile(source: ImageSource,
taskId: string): Promise<ColorProfileSnapshot> {
const meta = await ColorMetaAdapter.read(source)
return {
snapshotId: [taskId, meta.revision].join(':'),
sourceSpace: meta.space === 'DISPLAY_P3' ? 'DISPLAY_P3' : 'SRGB',
transfer: meta.transfer,
hdr: meta.transfer === 'PQ' || meta.transfer === 'HLG',
bitDepth: meta.bitDepth >= 10 ? 10 : 8,
createdAt: Date.now()
}
}
ColorMetaAdapter 是工程层封装,用来隔离不同图像格式的元数据读取。这里没有把读取不到的信息强行猜成 P3:缺失时回到 sRGB,并在结果里标记 PROFILE_ASSUMED。正式产品可以提示用户,但不能因为文件扩展名或设备型号就臆测色域。
snapshotId 还解决了重复调用风险。srcolor_20261001_15 重试时必须继续使用同一快照;如果源文件在任务中途被编辑,新的 revision 会创建新任务,而不是让前半段用旧像素、后半段用新元数据。页面离开不会销毁快照,只有任务结束或取消后才释放关联对象。
这里还有一个容易忽略的边界:相册缩略图不能替代原始解码。缩略图可能已经被系统转换为 sRGB,如果用它做颜色快照,再拿原文件做超分,元数据和像素就来自两条链路。ToneGuard SR 的预览可以用缩略图,正式任务则重新打开原始资产,并让 profile 与首个解码块在同一读取会话内产生。
如果源图元数据冲突,例如容器声明 Display P3、嵌入配置却损坏,任务不会悄悄选择更鲜艳的一项。策略会降级为 sRGB,同时把 PROFILE_CONFLICT 写入报告。正式产品可以允许用户继续,但结果页必须说明发生了降级,否则后续色差指标没有可信基准。
三、模型工作空间不等于最终输出空间
超分模型往往在固定范围的 RGB 数据上工作。直接把广色域数值塞进去,模型可能把超出训练范围的颜色当成异常;反过来,推理前粗暴压成 sRGB,又会让可恢复的颜色细节提前消失。
我们把流程改成“源空间→线性工作空间→模型空间→目标空间”。模型只处理经过受控归一的亮度和颜色数据,输出后再依据快照恢复目标编码。工作空间转换参数与模型版本一起写入任务摘要,不能靠隐藏默认值。
下面的 TonePolicy 解决“HDR 内容应该保留还是降级”的产品判断。设备、导出格式和业务目标共同决定策略,而不是所有图片都走同一条曲线。
export type ToneMode = 'KEEP_WIDE_GAMUT' | 'MAP_TO_SDR' | 'PASSTHROUGH_SDR'
export interface OutputTarget {
supportsWideGamut: boolean
supportsHdr: boolean
exportFormat: 'HEIF' | 'JPEG' | 'PNG'
}
export function chooseToneMode(profile: ColorProfileSnapshot,
target: OutputTarget): ToneMode {
if (!profile.hdr && profile.sourceSpace === 'SRGB') {
return 'PASSTHROUGH_SDR'
}
if (profile.hdr && (!target.supportsHdr || target.exportFormat === 'JPEG')) {
return 'MAP_TO_SDR'
}
return target.supportsWideGamut ? 'KEEP_WIDE_GAMUT' : 'MAP_TO_SDR'
}
本 Demo 的目标页面支持广色域,因此保留 Display P3;分享为普通 JPEG 时则执行 MAP_TO_SDR。两种结果不能共用缓存键,否则先生成的 SDR 版本可能被误当成广色域结果。缓存键包含 source revision、模型版本、倍率、ToneMode 与输出格式。
降级也不是简单截断。映射曲线要保护高光层次与中灰稳定,避免把天空亮部全部压成一块。当前参数来自固定样本集,只对这个 Demo 的输出链路负责;不同模型、显示目标和内容类型都需要重新验收。
我们还比较了“推理前映射”和“推理后映射”两条方案。推理前压到 SDR 能降低模型输入范围,但霓虹与夕阳的高光细节提前合并;推理后映射保留更多层次,却要求模型工作空间能够接住线性化数据。当前模型适配器使用后者,因此摘要必须记录 workingSpace=LINEAR_P3。换模型时若训练范围不同,不能继续复用这个决定。
ToneMode 也会影响交互。用户在结果页切换“保持广色域”和“兼容分享”时,应用不会重新执行昂贵的超分推理,而是复用线性模型结果重新编码。前提是工作缓冲尚在受控缓存中;若已释放,则从无损中间结果恢复。这个缓存有明确容量与过期时间,不能为了切换顺滑无限保留全尺寸浮点图。
四、用灰阶和高光比例检查,不靠“看起来差不多”
最早的验收只有前后对比滑杆,评审结论很容易受屏幕、亮度和环境光影响。后来我们加入 16 级灰阶卡、色块差值和高光裁剪比例。它们不能替代主观观察,却能及时发现明显的管线错误。
下面的回归器解决“输出尺寸正确但色调曲线失真”。它检查灰阶是否单调,并统计接近上限的像素比例;若任意一项超出阈值,任务不会进入 COLOR_SAFE。
export interface ToneReport {
monotonicSteps: number
clippedHighlightRatio: number
deltaE00: number
passed: boolean
}
export function validateTone(grayLevels: number[], clippedPixels: number,
totalPixels: number, deltaE00: number): ToneReport {
let monotonicSteps = 0
for (let i = 1; i < grayLevels.length; i++) {
if (grayLevels[i] >= grayLevels[i - 1]) monotonicSteps++
}
const clippedHighlightRatio = clippedPixels / Math.max(1, totalPixels)
return {
monotonicSteps,
clippedHighlightRatio,
deltaE00,
passed: monotonicSteps === 15 &&
clippedHighlightRatio <= 0.01 && deltaE00 <= 2.0
}
}
16 级灰阶有 15 个相邻关系,所以 monotonicSteps 必须等于 15。高光裁剪阈值设置为 1%,色差门槛为 2.0。本轮分别得到 15/15、0.6% 和 1.8。若只看平均色差,少量严重爆白可能被大量正常像素稀释,因此三项必须一起通过。
测试样本包含夕阳、霓虹灯、白色瓷器和低饱和文档照片。文档照片重点看中灰和白底,霓虹灯重点看高光与饱和度;同一门槛不代表所有内容获得相同观感,而是先挡住可量化的退化。
色差测量使用固定色块区域,而不是拿每个像素与原图直接比较。超分本来会改变边缘与局部纹理,逐像素差异会把清晰度提升误判成颜色错误。固定区域避开高频边缘,比较转换前后的代表色;高光裁剪则在亮度通道统计,两项各自回答不同问题。
灰阶回归还专门加入了重复执行。第一次输出 COLOR_SAFE 后,把结果重新解码再走一遍导出检查,确认编码器写入的标记能被读取。只有内存结果通过、文件回读失败时,状态改为 ENCODE_MISMATCH,不能继续沿用之前的绿色状态。这样能把显示正确但落盘错误的问题拦在分享之前。

五、结果页展示的是颜色链路,而不是一张漂亮照片
DevEco Studio 的日志按顺序打印 profile、toneMode、模型倍率、回归指标和最终状态。若输出丢了色域,能够直接看到 sourceSpace=DISPLAY_P3 而 outputSpace=SRGB 的不合理组合,不再从“是不是模型问题”开始猜。
手机运行图时间为 15:28,任务 srcolor_20261001_15,输入 3024×4032,输出 6048×8064,倍率 2×。页面同时展示 Display P3、KEEP_WIDE_GAMUT、ΔE00=1.8、高光裁剪 0.6%、灰阶 15/15、峰值内存 412 MB 与耗时 2.46 s。

六、资源、缓存和取消路径同样会影响颜色结果
色彩转换会产生中间 PixelMap 和浮点缓冲区,内存峰值比普通 sRGB 路径更高。ToneGuard SR 不把全尺寸输入、线性工作图和最终输出同时长期保留:解码块完成后释放源块,模型输出完成后释放工作缓冲,页面只持有可显示结果。异常分支也通过统一 scope 回收。
取消任务时,先递增 generation,让晚到的模型回调失效,再停止新的分块,最后等待正在执行的颜色转换退出。不能在任务线程仍使用缓冲区时直接 release;也不能只取消 UI 进度,后台继续占用 400 MB。重复点击“开始超分”会创建新 generation,旧任务无法覆盖新页面。
缓存也保存 ProfileSnapshot 的摘要。若用户把输出目标从广色域页面切到 JPEG 分享,缓存会生成另一条 MAP_TO_SDR 结果。相同像素尺寸不代表相同颜色产品,缓存层必须把色彩策略当成业务参数。
导出后还要重新读取文件元数据做闭环检查。内存里的 PixelMap 正确,不代表编码器一定写入预期标记。正式项目应验证文件色彩空间、位深和传递函数,再用同一组灰阶与高光指标复测解码结果。
异常恢复方面,颜色验证失败不会覆盖用户原图,也不会删除模型中间结果。页面保留“重新编码”和“保留原图”两个动作:前者只调整 ToneMode 与导出参数,后者彻底结束任务并释放缓存。若模型推理失败,则没有重新编码入口,避免把两类失败混成同一个重试按钮。
监控指标也分开记录 inferCost、colorCost 与 encodeCost。总耗时 2.46 s 中,模型、颜色转换和编码的占比不同;若未来颜色转换成为瓶颈,可以调分块与并发,而不必误以为模型变慢。指标只记录尺寸、模式和耗时,不上传用户图片或色块内容。
对于批量任务,COLOR_SAFE 必须逐图成立,不能用平均值掩盖个别失败。队列可以继续处理下一张,但最终报告会列出未通过项目,批量导出按钮保持禁用。用户选择跳过后,未通过图片保持原图版本,并在结果清单中明确标记,避免悄悄输出一张颜色错误的超分图。
这次问题让我重新划分了“超分成功”的定义。宽高翻倍、边缘清晰只证明模型产生了结果;输入色彩有快照、工作空间可解释、输出标记正确、回归指标通过,才证明这张图能安全交给用户。COLOR_SAFE 不是视觉上的一句好评,而是一条从 ImageSource 到最终文件都能对账的工程结论。
更多推荐


所有评论(0)