HarmonyOS 7 ImageProcessing:超分导出色彩空间能力门禁
图像超分做到最后,最容易被忽略的一步不是放大,而是导出。预览页里看着正常的结果,一旦编码成 JPEG、发到普通显示链路或交给下游服务,可能出现饱和度变化、暗部抬升甚至 HDR 内容被错误地当成 SDR 处理。只盯着宽高、清晰度和耗时,无法解释这类“像素数量对了,颜色却不对”的问题。
本文把超分后的导出阶段单独做成 SrColorGate 演示项目。页面 ColorExportPage 不负责超分算法本身,而是接收已经生成的 PixelMap,建立输入色彩指纹、查询转换能力、执行 P3 到 sRGB 的转换,再验证目标对象并成对释放 Native 资源。
演示任务为 CSC-1438-206,时间 14:38,电量 79%。输入与目标均为 3200 × 2400,输入是 DISPLAY_P3 / RGBA_8888,目标是 SRGB / RGBA_8888,能力查询结果为 supported=true,进度为 68/100,状态为 VERIFYING_DESTINATION。这些是文图一致的示例数据,不是实机性能证据。

一、导出错误常被误判为超分错误
一张图片从相册进入超分管线后,至少经历解码、像素处理、显示和编码四类语义。PixelMap 的宽高只说明画布大小;pixelFormat 说明通道排列与位深;color space 描述数值如何映射到实际颜色;HDR metadata 又决定高动态范围内容怎样被解释。
如果输入是 Display P3,超分输出仍保留 P3 数值,但导出端直接按 sRGB 解释,同一个三元组就会呈现不同颜色。反过来,把 SDR 数据仅仅打上 HDR 标签也不会凭空获得高动态范围。applyColorSpace() 一类色域操作与 HDR→SDR 色调映射也不能混为一谈。华为当前图片色彩空间转换能力明确列出 HDR2SDR、SDR2HDR 和 SDR2SDR 的支持组合,应先按源、目标、metadata 和 pixel format 查询能力,再决定是否进入转换。
这也是本文不把判断写成 if (isHdr) convert() 的原因。单一布尔值丢失了色彩空间、元数据类型和像素格式三项关键信息。更稳妥的能力键是:
sourceColorSpace + sourceMetadataType + sourcePixelFormat -> destinationColorSpace + destinationMetadataType + destinationPixelFormat。
只有这个六元组被支持,按钮才能从“准备导出”进入“执行转换”。不支持时应给出明确降级选项,例如保留原色域、改用支持的目标组合,或拒绝本次导出,而不是让算法返回值决定用户是否看到错误颜色。
二、先冻结色彩指纹,再创建输出对象
SrColorGate 把一次导出分为五个状态:FINGERPRINTED、SUPPORTED、CONVERTED、VERIFIED、RELEASED。任何异常都进入 FAILED,并携带失败阶段。状态机不允许从 FINGERPRINTED 直接跳到 CONVERTED,因为能力查询必须是显式门槛。
指纹还要绑定超分结果的 generation。用户重新选择图片后,旧 PixelMap 的转换回调可能迟到;即便转换成功,也不能覆盖新任务。演示用 exportGeneration=206 隔离这类结果,并把任务 ID CSC-1438-206 写入日志,不把文件路径或用户相册信息写入日志。
下面的 ArkTS 代码解决“页面如何把 PixelMap 交给 Native 层,同时保持输出所有权清楚”的问题。colorBridge.convert() 是项目自定义 NAPI 封装,不是系统 API;其签名由同模块的 Index.d.ts 声明。
import { image } from '@kit.ImageKit';
import colorBridge from 'libcolor_gate.so';
interface ColorFingerprint {
width: number;
height: number;
sourceColor: string;
targetColor: string;
pixelFormat: string;
generation: number;
}
export class ColorExportCoordinator {
private generation: number = 0;
async convertForExport(source: image.PixelMap): Promise<image.PixelMap> {
const mine = ++this.generation;
const info = await source.getImageInfo();
const fingerprint: ColorFingerprint = {
width: info.size.width,
height: info.size.height,
sourceColor: 'DISPLAY_P3',
targetColor: 'SRGB',
pixelFormat: 'RGBA_8888',
generation: mine
};
const destination = await image.createPixelMap(new ArrayBuffer(
fingerprint.width * fingerprint.height * 4
), {
size: { width: fingerprint.width, height: fingerprint.height },
pixelFormat: image.PixelMapFormat.RGBA_8888,
editable: true
});
try {
const code = colorBridge.convert(source, destination, mine);
if (code !== 0 || mine !== this.generation) throw new Error('STALE_OR_FAILED');
return destination;
} catch (e) {
destination.release();
throw e;
}
}
}
这段代码刻意让调用方拥有成功返回的 destination,失败路径则由协调器释放。成功后调用方在编码或显示完成时必须 release(),不能再由协调器释放,否则会发生双重释放。ArrayBuffer 大小的简化计算只适用于演示中的 RGBA_8888;实际工程应按目标像素格式、行跨距和系统创建接口要求分配,不能把所有格式都乘以 4。
三、能力查询不是装饰性日志
Native 层先调用 OH_ImageProcessing_InitializeEnvironment() 初始化全局环境,再使用 OH_ImageProcessing_IsColorSpaceConversionSupported() 查询组合。创建实例、执行转换和销毁实例必须在同一所有权范围内;全局环境也要在所有处理实例结束后反初始化。
下面的 C++ 片段解决“转换失败时如何保证实例与环境成对释放”的问题。示例用小型 RAII 守卫组织清理,避免在多个 return 分支漏掉 Destroy 或 DeinitializeEnvironment。
#include <multimedia/image_framework/image/pixelmap_native.h>
#include <multimedia/video_processing_engine/image_processing.h>
#include <multimedia/video_processing_engine/image_processing_types.h>
struct ImageProcessingGuard {
OH_ImageProcessing* instance = nullptr;
bool environmentReady = false;
~ImageProcessingGuard() {
if (instance != nullptr) {
OH_ImageProcessing_Destroy(instance);
instance = nullptr;
}
if (environmentReady) {
OH_ImageProcessing_DeinitializeEnvironment();
environmentReady = false;
}
}
};
constexpr int32_t COLOR_GATE_UNSUPPORTED = -1001; // 应用自定义返回码
int32_t ConvertP3ToSrgb(
OH_PixelmapNative* source, OH_PixelmapNative* destination) {
ImageProcessingGuard guard;
auto ret = OH_ImageProcessing_InitializeEnvironment();
if (ret != IMAGE_PROCESSING_SUCCESS) return ret;
guard.environmentReady = true;
ImageProcessing_ColorSpaceInfo srcInfo {
DISPLAY_P3, HDR_METADATA_TYPE_NONE, PIXEL_FORMAT_RGBA_8888
};
ImageProcessing_ColorSpaceInfo dstInfo {
SRGB, HDR_METADATA_TYPE_NONE, PIXEL_FORMAT_RGBA_8888
};
if (!OH_ImageProcessing_IsColorSpaceConversionSupported(&srcInfo, &dstInfo)) {
return COLOR_GATE_UNSUPPORTED;
}
ret = OH_ImageProcessing_Create(
&guard.instance, IMAGE_PROCESSING_TYPE_COLOR_SPACE_CONVERSION);
if (ret != IMAGE_PROCESSING_SUCCESS) return ret;
return static_cast<int32_t>(OH_ImageProcessing_ConvertColorSpace(
guard.instance, source, destination));
}
返回码名称和枚举取值应以当前 SDK 头文件为准;项目升级后要重新编译验证。RAII 守卫解决的是“函数退出时释放”,不等于全局并发管理已经完成。若多个任务共用图片处理环境,初始化与反初始化需要进程级引用计数或单例门闩,不能让任务 A 在任务 B 仍运行时反初始化环境。
OH_ImageProcessing_IsColorSpaceConversionSupported() 必须参与控制流,而不是只打一行日志。如果查询返回 false,仍然创建实例并调用转换,错误会更晚暴露,页面也失去提供降级方案的机会。本文的页面在 supported=false 时保留原始 P3 导出选项,并显式标注目标环境可能不支持;它不会伪装成 sRGB 成功。
配套 DevEco 图展示 ColorExportCoordinator.ets、color_gate.cpp、右侧模拟器和底部 HiLog。画面中的 supported=true、68/100 与 CSC-1438-206 都是演示值,不能替代真机色彩校准或测试报告。

四、转换成功之后,还要验证目标对象
函数返回成功只能证明调用链没有报告错误。工程上还要验证目标 PixelMap 的宽高、像素格式、目标色彩空间和是否可编码。尺寸应保持 3200 × 2400;目标格式为 RGBA_8888;若目标对象信息与请求不一致,状态进入 DESTINATION_MISMATCH,不继续打包。
验证阶段也要避免“重新解码产物看起来能显示”这种弱证据。显示组件可能做隐式适配,肉眼预览也受屏幕色域与亮度影响。更可靠的做法是同时保存结构指纹、编码结果摘要和少量标准色块的容差结果。本文不虚构 Delta E 实测数字,只把接口和验收位置固定下来。
下面的 ArkTS 代码解决“迟到转换结果如何与当前任务隔离,以及何时释放输入输出”的问题。它把预览引用和导出引用分开,页面销毁时只释放自己仍持有的对象。
@Entry
@Component
struct ColorExportPage {
@State phase: string = 'FINGERPRINTED';
@State progress: number = 0;
@State supported: boolean = false;
private taskGeneration: number = 206;
private source?: image.PixelMap;
private converted?: image.PixelMap;
private async runGate(): Promise<void> {
const mine = this.taskGeneration;
this.supported = colorBridge.isSupported(
'DISPLAY_P3', 'RGBA_8888', 'SRGB', 'RGBA_8888');
if (!this.supported) {
this.phase = 'UNSUPPORTED';
return;
}
this.phase = 'CONVERTING';
const next = await coordinator.convertForExport(this.source!);
if (mine !== this.taskGeneration) {
next.release();
return;
}
const old = this.converted;
this.converted = next;
old?.release();
this.progress = 68;
this.phase = 'VERIFYING_DESTINATION';
hilog.info(0xC206, 'SrColorGate',
'CSC-1438-206 DISPLAY_P3->SRGB 3200x2400 progress=68');
}
aboutToDisappear(): void {
this.taskGeneration++;
this.converted?.release();
this.converted = undefined;
this.source?.release();
this.source = undefined;
}
}
交换顺序是先把新对象放进字段,再释放旧对象,避免 UI 在同一帧引用已释放资源。页面离开先递增 generation,使迟到结果只能自释放,随后再释放页面仍持有的对象。如果 source 由上层缓存拥有,页面就不应释放;实际工程必须在接口上明确“借用”还是“转移所有权”,不能仅靠注释猜测。
五、68% 是验证阶段,不是转换完成率
运行页将进度划成指纹 20%、能力查询 15%、转换 33%、目标验证 20%、编码抽检 12%。因此 68/100 恰好表示转换调用完成并进入目标验证。这个比例是演示管线的权重,不是 OH_ImageProcessing_ConvertColorSpace() 提供的原生进度回调。

用户看到的是“正在验证目标色彩空间”,而不是“转换还剩 32% 像素”。这种文案区分很重要。同步 Native 调用若耗时较长,应移出 UI 主线程,并通过受控并发机制返回结果;但 PixelMap 是否可跨并发实例传递、Native 对象如何封装,要按当前 Sendable 与模块接口约束设计,不能简单把 JS 对象扔进 Worker。
诊断页展示完整状态链:ENV_READY -> SUPPORTED -> CONVERTED -> VERIFYING_DESTINATION,并列出源、目标、尺寸、任务代次和资源计数。它与运行页明显不同,承担的是问题定位,而不是重复一张结果卡。

六、异常路径比成功路径更值得先画出来
第一类异常是环境初始化失败。此时没有实例可销毁,但若初始化已成功,所有退出分支都必须反初始化。第二类是能力不支持。它不应被当成崩溃,而是产品分支。第三类是创建实例失败。此时仍要反初始化环境。第四类是转换失败。目标 PixelMap 可能已经分配,ArkTS 侧应释放。第五类是页面换图或退出导致 generation 失效,迟到输出必须释放但不能提交。
还有一个容易遗漏的边界:输出 PixelMap 成功转换后,编码器仍可能不支持目标组合或配置。因此“转换成功”与“导出成功”要用两个状态。编码阶段若失败,可以保留转换结果供预览或重试,但不能把文件写入成功的 UI 状态提前展示。
如果输入是 HDR,不能把 P3→sRGB 的 SDR 示例直接套用。应根据官方支持矩阵填写 BT2020_HLG、BT2020_PQ、对应 HDR metadata 和 10 bit pixel format,再查询能力。HDR2SDR 涉及色调映射,不是简单改标签。无法确认元数据时,宁可拒绝自动降级,也不要产生看似正常、实际语义错误的文件。
七、把色彩门禁放进验收,而不是藏在算法后面
建议至少准备四组用例:Display P3 RGBA_8888 到 sRGB RGBA_8888 的受支持路径;故意构造不支持组合,确认按钮禁用且保留源图;转换过程中切换任务,确认旧结果释放且不覆盖新页面;连续进入退出页面,确认实例数和 PixelMap 持有数回到零。
结构验收检查宽高、format、color space 和返回码;视觉验收使用标准色卡与多台目标设备;资源验收观察 Native 实例、PixelMap 和全局环境引用计数。三者不能互相替代。只看截图无法证明色彩正确,只看返回码也无法证明资源已经释放。
日志应记录色彩枚举、像素格式、metadata 类型、generation、阶段和错误码,不记录用户文件路径。生产环境还应限制高频日志,避免一张图片的逐阶段信息淹没真正异常。
最终要保留的不是一句“超分后做了色彩转换”,而是一条可复查的证据链:源指纹是什么、目标组合是什么、系统是否声明支持、转换返回什么、目标对象是否匹配、谁持有并释放资源。只有这条链闭合,导出颜色偏差才有机会被定位,而不是回头怀疑超分模型。
八、参考资料与边界声明
本文依据当前公开文档描述接口和资源边界,未声称完成真机色彩测量。具体枚举、错误码、设备支持和 SDK 起始版本应以项目实际使用的最新头文件与能力矩阵为准;无法验证的组合应在运行前查询并降级,而不是硬编码为可用。
更多推荐


所有评论(0)