HarmonyOS 7.0 图像超分实战:压缩存储 + 超分查看,空间画质两不误

本文基于 HarmonyOS 7.0(API 26)的图像超分能力(CoreVisionKit imageSuperResolution),结合真实项目的完整落地实践,讲清楚图像超分"是什么、怎么用、怎么用好"。


一、什么是图像超分,以及使用场景

1.1 官方定义

按官方文档的说法:超分,即超分辨率重建,是指在放大图片尺寸的同时,尽可能恢复和增强图片中的纹理、边缘等细节信息,减少普通插值缩放带来的模糊、锯齿或细节丢失问题。 HarmonyOS 从 API 26.0.0 版本开始,新增支持对输入的低分辨率图像进行超分辨率重建,使图像更加清晰,官方给出的典型场景是提升图片质量、修复老照片。

一个关键事实:超分输出的 PixelMap 在提高图像质量的同时,像素同步放大四倍——也就是说 1000×1000 的输入,会得到 4000×4000 的输出,这不是简单的插值放大,而是 AI 模型重建出的真实细节。

1.2 通用使用场景

  • 图片来源分辨率较低:聊天截图、网络图片、历史存档图,本身清晰度不足;
  • 图片需要放大显示:大图查看、缩放浏览时,普通插值放大一片模糊;
  • 缩略图需要高清展示:缩略图存储小,点开大图时用超分"无损"补清;
  • 修复老照片:低质量历史照片的细节重建。

1.3 本项目的用法:压缩保存 + 大图查看超分,节省空间

本项目的图片链路是"压缩存、超分看、无损出"三段式,核心目标是节省存储空间的同时不牺牲查看体验:

① 保存侧:压缩存储(省空间的源头)

工作日志图片保存时,在 saveWorkLogImage 中做两级瘦身:

  • 解码时等比下采样,最长边压到 1440px(MAX_SIDE),desiredPixelFormat 用 RGBA_8888;
  • 编码时 createImagePacker().packing(pixelMap, { format: 'image/jpeg', quality: 80 }),再写入 filesDir/worklog_images/{logId}/。

代价是图片细节有损,画质上不去——这正是超分登场的前提。

② 查看侧:大图查看时超分(补偿细节)
在这里插入图片描述

打开大图预览页时,标题栏有超分按钮,点击后 AI 重建补偿压缩损失的细节,输出像素放大 4 倍,配合捏合缩放手势看得更清。压缩图平时不占太多空间,只有用户主动想看高清时才花一次超分的算力。

③ 导出侧:超分态无损导出(不污染原图)

超分态下保存/分享时,把超分结果以 quality: 100 编码为临时 jpg 导出(详见第三章),原图永远不动。

三段配合的结果:存储成本由压缩保证,查看画质由超分保证——用"查看时算力"换"存储空间",这是本项目用超分的核心思路。


二、鸿蒙图像超分 API 介绍与基本使用

2.1 API 定位

图像超分能力由 CoreVisionKit 提供,模块为 imageSuperResolution,仅支持 Stage 模型,系统能力为 SystemCapability.AI.Vision.VisionBase,phone / 2in1 / tablet 均从 API 26.0.0 起支持。

导入方式:

import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit';

2.2 核心类与方法

ImageSRAnalyzer(图像超分分析器类,继承自 visionBase.Analyzer)只有三个方法:

方法签名说明
create()create(): Promise<ImageSRAnalyzer>创建分析器实例;失败抛错误码 1018700001(Service exception)
process()process(request: visionBase.Request): Promise<ISPResponse>超分处理,仅支持传入一张图片;返回 ISPResponse.pixelMap(像素放大 4 倍)
destroy()destroy(): Promise<void>释放分析器服务

配套数据结构:

// 输入:ImageData 包裹待处理 PixelMap
const imageData: visionBase.ImageData = { pixelMap: inputImage };
const request: visionBase.Request = { inputData: imageData };
// 输出:ISPResponse 继承 visionBase.Response,pixelMap 即超分结果
const response = await analyzer.process(request);

2.3 基本使用:官方四步法

以下开发步骤与示例代码均复用自官方文档《图像超分》。

第一步:添加导入。

import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit'
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';

第二步:创建与释放分析器。 官方推荐在 aboutToAppear 中 create、在 aboutToDisappear 中 destroy:

private analyzer: imageSuperResolution.ImageSRAnalyzer | null = null;

async aboutToAppear(): Promise<void> {
  this.analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
  hilog.info(0x0000, 'ImageSRSample', 'ImageSRAnalyzer created');
}

async aboutToDisappear(): Promise<void> {
  if (this.analyzer) {
    await this.analyzer.destroy();
    hilog.info(0x0000, 'ImageSRSample', 'ImageSRAnalyzer released successfully');
  }
}

第三步:选图并解码为 PixelMap。 通过 photoAccessHelper.PhotoViewPicker 拉起图库,用 fileIo 与 image 模块将 URI 转换为 PixelMap:

private async openPhoto(): Promise<string> {
  return new Promise<string>((resolve) => {
    let photoPicker: photoAccessHelper.PhotoViewPicker = new photoAccessHelper.PhotoViewPicker();
    photoPicker.select({
      MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
      maxSelectNumber: 1
    }).then(res => {
      resolve(res.photoUris[0]);
    }).catch((err: BusinessError) => {
      hilog.error(0x0000, 'ImageSRSample', `Failed to get photo image uri.code: ${err.code}, message: ${err.message}`);
      resolve('');
    });
  });
}

private loadImage(name: string) {
  setTimeout(async () => {
    let imageSource: image.ImageSource | undefined = undefined;
    let fileSource = await fileIo.open(name, fileIo.OpenMode.READ_ONLY);
    imageSource = image.createImageSource(fileSource.fd);
    this.inputImage = await imageSource.createPixelMap();
    await fileIo.close(fileSource);
  }, 100);
}

第四步:构造 Request 并调用超分。

Button('图像超分')
  .onClick(() => {
    if (!this.inputImage || !this.analyzer) {
      return;
    }
    // 调用图像超分接口
    let imageData: visionBase.ImageData = { pixelMap: this.inputImage };
    let request: visionBase.Request = { inputData: imageData };
    request.inputData = imageData
    this.analyzer.process(request)
      .then((response: imageSuperResolution.ISPResponse) => {
        this.outputImage = response.pixelMap;
      })
      .catch((error: BusinessError) => {
        hilog.error(0x0000, 'ImageSRSample', `Image super resolution failed. Code: ${error.code}, message: ${error.message}`);
      });
  })

2.4 显示质量配套:Image 组件参数

超分结果在 Image 组件上显示时,官方对 Image 组件也有清晰度建议:图片放大显示时设置 .interpolation(.High);若解码图源与显示尺寸不匹配出现失真,可选择 .autoResize(false) 按原图尺寸解码(会增加内存占用)。本项目超分结果的显示层正是按放大场景配置插值,保证 4 倍像素在缩放下渲染到位。

补充:Image Kit 的 image-processing-arkts(图片细节增强)也提供清晰度增强/缩放能力,与 CoreVisionKit 的 AI 超分是两条独立路径——前者是图像处理级的轻量增强,后者是模型级的重建放大,按效果需求选型。

示例:
图片超分前:
在这里插入图片描述
图片超分后:在这里插入图片描述
也许这样看效果并不明显,但是如果两张图片放到统一同一尺寸下对比还是挺明显的:
在这里插入图片描述
(示例图片来源于互联网,如有侵权请联系删除)


三、结合本项目的最佳实践

官方示例是"最短可用路径",生产环境还差好几层防护。以下是本项目实际落地的工程化改造。
在这里插入图片描述

3.1 兼容性与版本控制:三级能力探测

本工程中 compatibleSdkVersion: "6.0.2(22)"、targetSdkVersion: "26.0.0"——应用要跑在 API 22 的老设备上,而图像超分是 API 26 新增能力。在低版本设备上 imageSuperResolution 模块根本未定义,直接访问属性就会 crash,所以能力探测必须前置且分三级:

/** 设备超分能力探测:API 版本不满足直接隐藏按钮;syscap 不满足同上;否则预创建分析器验证
 *  (图像超分为 API 26 新增,低版本设备上 imageSuperResolution 模块未定义,
 *  直接访问属性会 crash,必须先用 API 版本拦截;不支持超分的设备 create 抛 801,
 *  创建成功则复用避免二次开销) */
private probeSrSupport(): void {
  if (getSdkApiVersion() < 26 || !canIUse('SystemCapability.AI.Vision.VisionBase')) {
    this.srSupported = false;
    return;
  }
  imageSuperResolution.ImageSRAnalyzer.create()
    .then((analyzer: imageSuperResolution.ImageSRAnalyzer) => {
      if (this.srDisposed) {
        void analyzer.destroy();   // 离页后 create 才 resolve:立即销毁,防无主泄漏
        return;
      }
      this.srAnalyzer = analyzer;  // 探测成功的实例直接复用
    })
    .catch(() => {
      this.srSupported = false;    // 不支持超分的设备 create 抛 801
    });
}

三级递进的逻辑:

  1. API 版本拦截(getSdkApiVersion() < 26):先于一切模块访问,防 crash 的生命线;
  2. syscap 校验(canIUse('SystemCapability.AI.Vision.VisionBase')):系统级能力声明过滤;
  3. 预创建探测(ImageSRAnalyzer.create()):前两级通过但设备实际不支持(抛 801)的最终兜底,且创建成功的实例直接复用,不浪费。

探测不通过时隐藏超分按钮而非点击报错,用户无感。

3.2 输入治理:2048px 硬约束下的下采样解码

超分接口对输入尺寸有硬约束(单边不超过 2048px),而用户图库里的照片动辄 4000px+。本项目的 decodeInputPixelMap 先取图源信息,超限时等比下采样再解码:

/** 解码超分输入 PixelMap:沙箱路径直接创建,photoUri 按 fd 创建;
 *  输入超 2048px(API 硬约束)时等比下采样解码 */
private async decodeInputPixelMap(uri: string): Promise<image.PixelMap | undefined> {
  let imageSource: image.ImageSource | undefined = undefined;
  let fd: number = -1;
  try {
    if (uri.includes('data/storage/el2')) {
      imageSource = image.createImageSource(uri);        // 沙箱路径
    } else {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      fd = file.fd;
      imageSource = image.createImageSource(file.fd);    // 媒体 uri 按 fd
    }
    const info = await imageSource.getImageInfo(0);
    const options: image.DecodingOptions = {};
    const maxSide = Math.max(info.size.width, info.size.height);
    if (maxSide > 2048) {
      const scale = 2048 / maxSide;
      options.desiredSize = {
        width: Math.floor(info.size.width * scale),
        height: Math.floor(info.size.height * scale)
      };
    }
    return await imageSource.createPixelMap(options);
  } catch (e) {
    return undefined;
  } finally {
    imageSource?.release();   // ImageSource 用完即释放,只留 PixelMap
    if (fd !== -1) fs.closeSync(fd);
  }
}

注意输入 PixelMap 在 process 完成后在 finally 中 release(),超分输入只服务一次。

3.3 状态管理:@Observed 整体替换 + 缓存 + 离页防御

结果显示状态用 @Observed 类整体替换实例驱动刷新,携带三个语义字段:

@Observed
export class SrResult {
  index: number = -1;                                        // 当前应用超分的图片下标,-1 表示未开启
  pixelMap: image.PixelMap | undefined = undefined;          // 超分输出
  animate: boolean = false;                                  // 新结果播扫描揭示动画,缓存恢复不重播
}

结果缓存:srCache: Map<number, image.PixelMap> 按图片下标缓存超分结果,切图往返不重复计算;但处理期间用户可能已切图,用下标守卫避免串图:

const response = await this.srAnalyzer.process(request);
if (this.srDisposed) {
  response.pixelMap.release();   // 离页后 process 才 resolve:结果无主,直接释放防内存幽灵
  return;
}
this.srCache.set(targetIndex, response.pixelMap);
if (targetIndex === this.curIndex) {          // 仅当仍停留在该图时切换显示
  this.srResult = this.buildSrResult(targetIndex, response.pixelMap, true);
}

揭示动画:新结果用 @Watch onSrResultChange 驱动左→右扫描揭示(1600ms 属性动画:揭示层宽度 0→全宽 clip 裁剪 + 扫描线 position 同步移动),缓存恢复态直接完整显示不重播

3.4 生命周期:统一资源释放

页面离页时按序释放全部超分资源(releaseSrResources):

/** 离页释放超分资源:分析器、缓存 PixelMap、临时文件 */
private releaseSrResources(): void {
  this.srDisposed = true;                    // 置位后迟到的 process 结果直接释放
  void this.srAnalyzer?.destroy();
  this.srAnalyzer = null;
  this.srCache.forEach((pm: image.PixelMap) => pm.release());
  this.srCache.clear();
  this.clearSrTemp();
  this.srResult = new SrResult();
}

超分输出是 4 倍像素的大图,缓存多张就是可观的内存占用,离页必须清干净。

3.5 超分态导出:临时文件 + 安全组件保存

超分态保存/分享时,把超分结果编码为临时 jpg,走完导出即删(getCurrentImagePath):

const packer: image.ImagePacker = image.createImagePacker();
data = await packer.packToData(this.srResult.pixelMap, { format: 'image/jpeg', quality: 100 });
const path = `${context.cacheDir}/${SR_EXPORT_FILE}`;   // cacheDir/sr_export.jpg,固定文件名
const file = fs.openSync(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
fs.writeSync(file.fd, data);

保存到系统相册用 SaveButton 安全组件(点击即获临时授权,免 WRITE_IMAGEVIDEO 权限)+ photoAccessHelper.createAsset + 按 fd 拷贝——注意 createAsset 只创建空相册资源,内容必须 fs.openSync(uri, READ_WRITE) 后按 fd 拷贝,直接 copyFileSync(srcPath, uri) 传媒体 uri 字符串会保存失败:

const helper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
const outUri: string = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
const dstFile = fs.openSync(outUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
try {
  fs.copyFileSync(srcPath, dstFile.fd);
} finally {
  fs.closeSync(dstFile);
}

幽灵文件兜底:分享面板打开期间进程被杀/闪退时,离页清理不会执行,cacheDir/sr_export.jpg 会残留。解法是固定文件名 + 启动时按名单清扫幽灵文件。


四、注意事项

结合官方文档与本项目踩坑,图像超分落地时重点盯住以下几条:

  1. API 版本拦截必须最先做。图像超分是 API 26 新增,低版本设备上 imageSuperResolution 模块未定义,任何属性访问都会 crash——先 getSdkApiVersion() 再碰模块,这是生命线。
  2. 设备能力三级探测。API 版本 → canIUse('SystemCapability.AI.Vision.VisionBase') → 预创建探测(不支持设备 create() 抛 801),三级全过才亮按钮。
  3. 仅支持单张图片。process(request) 不支持批量,列表场景需自行循环排队。
  4. 输入单边 ≤2048px(硬约束)。超限需自行等比下采样解码(DecodingOptions.desiredSize),否则处理失败。
  5. 内存是 4 倍放大。输入 2048px 输出即 8192px,RGBA_8888 下单张结果约 268MB 像素数据——缓存多张超分结果要掂量内存,离页必须全部 release()。
  6. process 是耗时 AI 操作。要有 loading 态(本项目标题栏 loading + 完成后扫描揭示动画),且要防御异步时序:处理期间切图用下标守卫,离页后迟到结果直接释放。
  7. 资源配对使用。create/destroy 配对、ImageSource 解码后即释放、输入 PixelMap 用完释放、导出临时文件即用即删——漏掉任何一对都是泄漏。
  8. 临时文件加双保险。页面离页清理 + 启动时按固定文件名清扫,消灭进程被杀残留的幽灵文件。
  9. 保存权限走安全组件。SaveButton 点击即获临时授权,免敏感权限申请;媒体 uri 内容必须按 fd 拷贝,不能直接传 uri 字符串。
  10. 错误码兜底。1018700001(Service exception)是 create/process 共同的错误码,用户侧给 Toast + 可重试,不要静默失败。

五、总结

HarmonyOS 7.0(API 26)的图像超分给了应用一张"画质后悔药":存储时大胆压缩省空间,查看时用 AI 重建把细节补回来。本项目"压缩存→ 超分看(4 倍像素重建)→ 无损出(q100 临时导出)"的三段式链路,验证了这条"用查看时算力换存储空间"的路线完全可行。

API 层面的心智模型一句话:create() 创建分析器 → 构造 visionBase.Request → process() 拿 4 倍像素的 ISPResponse.pixelMap → destroy() 释放——三步四注意(版本拦截、尺寸约束、资源配对、时序防御)。

而真正决定落地质量的,是官方示例之外的工程化功夫:三级能力探测让老设备无感降级,2048px 下采样解码守住硬约束,@Observed 整体替换 + 缓存 + 离页防御管住状态与内存,临时文件清扫消灭幽灵。API 决定能不能用,工程实践决定好不好用——两者齐备,图像超分才能从演示走向生产。


参考文档

  • 图像超分(开发指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution
  • imageSuperResolution(图像超分)API 参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-image-super-resolution-api
  • Image 组件(autoResize / interpolation 最佳清晰度配置):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-image
  • 图片细节增强(image-processing-arkts):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/image-processing-arkts
Logo

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

更多推荐