图像超分最容易被忽略的验收点,不是分辨率有没有翻倍,而是透明边缘有没有被破坏。输入是一张背景透明的贴纸,模型输出在深色底上出现一圈亮边;预览页看起来尚可,打包成 PNG 再打开,轮廓却更明显。这个现象通常不是超分倍率错了,而是模型缓冲区和 PixelMap 对 Alpha 的解释不一致。

本文以 AlphaProof 为示例工程,页面名为 SuperResExportPage,任务 ID 为 SR-ALPHA-0053。输入尺寸 768×768,输出 1536×1536,像素格式为 RGBA_8888。示例边缘像素从非预乘 (240,128,64,64) 转换为预乘 (60,32,16,64);透明 RGB 泄漏从 214 个像素降为 0,边缘异常从 326 降为 0,最终 PNG 为 6.8 MB。这些数据用于说明校验协议,不是某款模型或设备的性能实测。

一、同样是 RGBA,内存里的含义可能完全不同

RGBA_8888 只说明通道顺序和位深,不说明 RGB 是否已经乘过 Alpha。非预乘格式中,半透明橙色可以保存为 (240,128,64,64);预乘格式要求 RGB 先乘 64/255,于是变成约 (60,32,16,64)。如果把前者标记成预乘格式,合成器会把高亮 RGB 当成已经收敛后的值,透明边缘就容易发亮。

反过来也会出错。若模型输出已经预乘,应用又手动乘一次,边缘会发黑。工程上不能靠“看起来像”判断,需要同时记录 pixelFormat、alphaType、行跨距和抽样像素。Alpha 是数据契约,不是导出前随手补的一步。

AlphaProof 的状态流为:

DECODED → UPSCALED → ALPHA_INSPECTED → ALPHA_NORMALIZED → PACKED → PASS

如果 alphaType 不符合目标契约,任务停在 ALPHA_INSPECTED,不会直接打包。只有转换完成并通过像素审计,才进入 PACKED。

二、先读图像信息,再决定要不要转换

超分服务通常返回像素缓冲区或 PixelMap。无论来源是什么,接入层都要把“模型输出契约”写清楚:通道顺序、每行字节数、Alpha 类型和所有权。示例约定模型输出为 RGBA_8888 非预乘,应用展示与后续处理使用预乘 PixelMap。

解码输入图时也不强行假设。页面先创建 PixelMap,读取 getImageInfo(),确认尺寸和像素格式,再进入模型适配层。若输入没有 Alpha,后续审计应走 OPAQUE 路径,不应该为了统一代码人为制造透明通道。

这段代码解决什么问题:用明确的解码选项创建可编辑 PixelMap,并记录超分前的尺寸、像素格式与 Alpha 类型。

import { image } from '@kit.ImageKit';

export interface AlphaSnapshot {
  width: number;
  height: number;
  pixelFormat: image.PixelMapFormat;
  alphaType: image.AlphaType;
}

export async function decodeForSuperResolution(
  source: ArrayBuffer
): Promise<{ pixelMap: image.PixelMap, info: AlphaSnapshot }> {
  const imageSource = image.createImageSource(source);
  try {
    const pixelMap = await imageSource.createPixelMap({
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
      editable: true
    });
    const imageInfo = await pixelMap.getImageInfo();
    return {
      pixelMap,
      info: {
        width: imageInfo.size.width,
        height: imageInfo.size.height,
        pixelFormat: imageInfo.pixelFormat,
        alphaType: imageInfo.alphaType
      }
    };
  } finally {
    imageSource.release();
  }
}

imageSource.release() 与 PixelMap.release() 不是同一件事。创建 PixelMap 后可以释放解码源,但 PixelMap 仍由调用者持有,必须在任务结束或取消时单独释放。若把二者混在一个 finally 里,可能导致还没送入模型就释放像素对象;若都不释放,连续处理大图会形成明显内存压力。

这里没有把 768×768 写进解码选项,因为输入尺寸应从文件读取。示例在业务层检查它是否为 768×768,并把输出目标 1536×1536 交给模型适配器。图像超分接口本身可能由系统能力、原生库或服务实现,本文只约束其输入输出缓冲区,不虚构一个统一的“superResolution()”平台方法。

三、用 Image_NativeModule 做 Alpha 类型转换

手动遍历像素做预乘并不难,但容易忽略行跨距、整数舍入和格式差异。HarmonyOS Image_NativeModule 提供 OH_PixelmapNative_ConvertAlphaFormat,可在非预乘与预乘 PixelMap 之间转换。示例在 Native 层创建目标 PixelMap,并明确目标 alphaType 为 PIXELMAP_ALPHA_TYPE_PREMULTIPLIED。

Native 接口需要把资源释放写完整。初始化参数、图像信息和 PixelMap 都有对应 Release;任何中间步骤失败,都要走统一清理路径。尤其是 1536×1536 RGBA 缓冲区,一个对象约占 9 MB,泄漏几次就会影响后续任务。

这段代码解决什么问题:把模型返回的非预乘 PixelMap 转换为预乘 PixelMap,避免应用层重复实现像素乘法。

#include <multimedia/image_framework/image/pixelmap_native.h>

Image_ErrorCode ConvertToPremultiplied(
    OH_PixelmapNative* src,
    uint32_t width,
    uint32_t height,
    OH_PixelmapNative** dst)
{
    if (src == nullptr || dst == nullptr) return IMAGE_BAD_PARAMETER;

    OH_Pixelmap_InitializationOptions* opts = nullptr;
    OH_PixelmapInitializationOptions_Create(&opts);
    OH_PixelmapInitializationOptions_SetWidth(opts, width);
    OH_PixelmapInitializationOptions_SetHeight(opts, height);
    OH_PixelmapInitializationOptions_SetSrcPixelFormat(opts, PIXEL_FORMAT_RGBA_8888);
    OH_PixelmapInitializationOptions_SetPixelFormat(opts, PIXEL_FORMAT_RGBA_8888);
    OH_PixelmapInitializationOptions_SetAlphaType(
        opts, PIXELMAP_ALPHA_TYPE_PREMULTIPLIED);

    const size_t bytes = static_cast<size_t>(width) * height * 4;
    std::vector<uint8_t> empty(bytes, 0);
    Image_ErrorCode code = OH_PixelmapNative_CreatePixelmap(
        empty.data(), empty.size(), opts, dst);
    if (code == IMAGE_SUCCESS) {
        code = OH_PixelmapNative_ConvertAlphaFormat(src, *dst, true);
    }
    OH_PixelmapInitializationOptions_Release(opts);

    if (code != IMAGE_SUCCESS && *dst != nullptr) {
        OH_PixelmapNative_Release(*dst);
        *dst = nullptr;
    }
    return code;
}

第三个参数 true 表示转换为预乘格式,目标 PixelMap 的初始化信息也使用预乘类型,二者必须一致。若模型输出实际已预乘,再调用这条路径会产生二次处理,因此转换前必须读取并核对源契约。实际桥接时还要确认 Native PixelMap 与 ArkTS PixelMap 的所有权,避免双方都释放或都不释放。

示例使用 width * height * 4 创建目标缓冲区,是因为明确限定 RGBA_8888;生产代码应读取 rowStride,并在格式不是 RGBA_8888 时拒绝进入该分支。不能把适用于演示尺寸的连续缓冲区假设扩展到所有像素格式。

四、转换成功不等于边缘已经正确

API 返回成功只说明格式转换完成,不能证明模型输出没有把透明区域的 RGB 污染。Alpha 为 0 时,RGB 理论上不应影响合成,但某些打包或后处理链可能保留隐藏颜色;在缩放和滤波时,这些颜色可能重新进入半透明边缘。

AlphaProof 增加两个指标:transparentRgbLeaks 统计 A=0 但 RGB 非零的像素;fringePixels 统计预乘格式下任一 RGB 大于 A 的像素。对 RGBA_8888 预乘数据,R、G、B 都不应超过 Alpha。这个规则简单,却能快速抓住最明显的契约错误。

这段代码解决什么问题:读取 Native PixelMap 的行跨距和像素数据,量化透明 RGB 泄漏与预乘边缘异常。

struct AlphaAudit {
    uint32_t transparentRgbLeaks = 0;
    uint32_t fringePixels = 0;
};

Image_ErrorCode AuditPremultiplied(
    OH_PixelmapNative* pixelmap,
    AlphaAudit& result)
{
    OH_Pixelmap_ImageInfo* info = nullptr;
    OH_PixelmapImageInfo_Create(&info);
    Image_ErrorCode code = OH_PixelmapNative_GetImageInfo(pixelmap, info);
    if (code != IMAGE_SUCCESS) {
        OH_PixelmapImageInfo_Release(info);
        return code;
    }

    uint32_t width = 0, height = 0, rowStride = 0;
    OH_PixelmapImageInfo_GetWidth(info, &width);
    OH_PixelmapImageInfo_GetHeight(info, &height);
    OH_PixelmapImageInfo_GetRowStride(info, &rowStride);
    OH_PixelmapImageInfo_Release(info);

    size_t size = static_cast<size_t>(rowStride) * height;
    std::vector<uint8_t> pixels(size);
    code = OH_PixelmapNative_ReadPixels(pixelmap, pixels.data(), &size);
    if (code != IMAGE_SUCCESS) return code;

    for (uint32_t y = 0; y < height; ++y) {
        for (uint32_t x = 0; x < width; ++x) {
            const size_t p = static_cast<size_t>(y) * rowStride + x * 4;
            const uint8_t r = pixels[p], g = pixels[p + 1];
            const uint8_t b = pixels[p + 2], a = pixels[p + 3];
            if (a == 0 && (r != 0 || g != 0 || b != 0)) ++result.transparentRgbLeaks;
            if (r > a || g > a || b > a) ++result.fringePixels;
        }
    }
    return IMAGE_SUCCESS;
}

这里严格使用 rowStride,而不是默认每行等于 width * 4。内存对齐可能让行跨距更大,忽略它会从第二行开始读错位置。OH_Pixelmap_ImageInfo 创建后无论成功失败都要释放;像素缓冲由 std::vector 管理,函数退出会自动回收。

边缘审计并不是视觉质量的全部。它抓不到模型纹理失真、锯齿或色彩偏移,也不代表 0 泄漏就一定好看。它只是 Alpha 契约的自动化门槛,后面仍应加入透明/白色/黑色三种背景预览,让设计与测试观察轮廓。

图中的 DevEco Studio 是演示配图:左侧工程包含 AlphaBridge.cpp、AlphaAudit.cpp 与 PngExporter.ets;中间突出 ConvertAlphaFormat;右侧模拟器显示任务 SR-ALPHA-0053 停在 ALPHA_NORMALIZED;底部日志使用同一组 326→0、214→0 数据。它不构成真机性能证明。

五、PNG 回写后必须重新解码一次

内存 PixelMap 通过审计,还不能直接宣布 PASS。打包参数、编码器与文件写入都可能引入新的差异。示例使用 PNG,是因为目标需要透明通道;若误选 JPEG,Alpha 会被丢弃,前面的转换全部失去意义。

打包后不只检查文件大小和扩展名,而是把 6.8 MB PNG 重新创建 ImageSource,再读取 PixelMap 信息和边缘样本。只有回读尺寸仍是 1536×1536、Alpha 通道存在、两个审计指标继续为 0,任务才进入 PASS。

这段代码解决什么问题:把预乘 PixelMap 打包为 PNG,并通过回读确认尺寸和 Alpha 契约没有在编码阶段丢失。

export async function packAndVerifyPng(
  normalized: image.PixelMap
): Promise<ArrayBuffer> {
  const packer = image.createImagePacker();
  let encoded: ArrayBuffer;
  try {
    encoded = await packer.packToData(normalized, {
      format: 'image/png',
      quality: 100
    });
  } finally {
    packer.release();
  }

  const source = image.createImageSource(encoded);
  try {
    const decoded = await source.createPixelMap({
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
      editable: false
    });
    try {
      const info = await decoded.getImageInfo();
      if (info.size.width !== 1536 || info.size.height !== 1536) {
        throw new Error(`PNG_SIZE_MISMATCH:${info.size.width}x${info.size.height}`);
      }
      await NativeAlphaAudit.assertClean(decoded, 'SR-ALPHA-0053');
    } finally {
      decoded.release();
    }
  } finally {
    source.release();
  }
  return encoded;
}

NativeAlphaAudit 是示例工程对前一段 C++ 审计函数的桥接层,不是 Image Kit 自带类。文章明确给它加上 Native 前缀,就是避免读者误认为平台存在同名 API。打包器、回读 PixelMap 和 ImageSource 都按创建顺序反向释放,异常路径也不会跳过清理。

PNG 的 quality 参数并不等价于 JPEG 的视觉质量语义,项目应以当前 SDK 的 ImagePacker 文档为准。示例保留它是为了让打包参数完整;真正的验收仍是回读结果,而不是把 100 当作“绝对无损”的口号。

六、运行页展示结果,诊断页解释边缘像素

最终运行页显示输入 768×768、输出 1536×1536、RGBA_8888、PREMULTIPLIED、PNG 6.8 MB,以及状态 PASS。状态链完整列出,避免用户只看到 2× 便误以为任务成功。

页面上的 fringe 326 → 0 和 RGB leaks 214 → 0 都来自审计结果。红色标注只指出 Alpha 已归一化,不把界面装饰成错误清单。真实项目还可让用户切换棋盘格、黑底和白底背景,快速观察透明边缘。

诊断页则把同一个边缘像素拆开:转换前 (240,128,64,64),转换后 (60,32,16,64),并展示 R≤A、G≤A、B≤A 三项检查。它还记录回写后重新解码为 1536×1536,最终 Alpha 范围仍是 0…255。

这张图承担技术解释,而不只是展示 UI。看到转换后的 RGB 正好约为原值乘 64/255,开发者能确认这里做的是预乘,而不是简单清零颜色。完全透明像素可以清零隐藏 RGB,半透明像素则要按 Alpha 比例保留颜色信息。

七、边界:不要把 Alpha 修复写成模型质量万能药

Alpha 语义正确,只解决合成契约问题。超分模型若在透明边缘生成了错误纹理,格式转换不会恢复真实轮廓;输入 PNG 若本身有污染,输出也可能继承。项目需要区分三种问题:模型生成错误、缓冲区解释错误、编码回写错误。本文的审计主要覆盖后两种。

还要考虑 OPAQUE 图片。没有透明通道的照片不应强制走预乘转换,既浪费时间,也会让日志产生无意义的 Alpha 指标。适配层先读 alphaType,再选择 PASS_THROUGH 或 CONVERT;格式未知时阻断,而不是猜测。

Native 资源释放也要做取消测试:模型任务取消、转换失败、审计失败、打包失败、回读失败。每个路径都检查 PixelMap、初始化参数、ImageInfo、ImagePacker 和 ImageSource 是否成对释放。若 ArkTS 与 Native 共享同一个 PixelMap,所有权协议要写进接口注释。

本文核对的官方一手资料包括 Image Kit 的 PixelMap Native 操作、Alpha 格式转换、图片解码与 ImagePacker:

  • https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/pixelmap-c
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/image-decoding
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/image-packer
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-image

八、结语:2× 只是尺寸,PASS 需要完整链路

AlphaProof 没有把 1536×1536 当作终点。它先确认模型输出契约,再把非预乘转换为预乘,使用 rowStride 做像素审计,最后打包 PNG 并重新解码。只有尺寸、Alpha 类型、边缘指标和资源释放都闭环,任务 SR-ALPHA-0053 才进入 PASS。

对透明素材来说,超分后的那一圈亮边不是“小瑕疵”,而是数据契约已经断开。把 (240,128,64,64) 校准为 (60,32,16,64),再把 326 与 214 两项异常归零,才能说明应用交付的是可组合的图像资产,而不只是像素数量翻倍的缓冲区。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐