图像超分做到最后,最容易被忽略的一步不是放大,而是导出。预览页里看着正常的结果,一旦编码成 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 起始版本应以项目实际使用的最新头文件与能力矩阵为准;无法验证的组合应在运行前查询并降级,而不是硬编码为可用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐