智绘鸿蒙 · 如 7 而至端侧 AI 能力实战手记

一张 320 像素的老照片,端侧 201ms 变 1280

智绘鸿蒙·如 7 而至

前言

前阵子我妈翻出一张 1985 年的全家福,翻拍成电子档之后只有 320×397,人脸糊成一团,让我"想办法弄清楚点"。

第一反应是丢给某个在线修复工具。但一想到要把家里老人的照片传上去,还要按次付费,就有点不愿意。

后来发现 HarmonyOS 7(API 26)把这件事做成了系统能力:图像超分imageSuperResolution),归属 Core Vision Kit。三行代码调用,不联网、不要权限、不用带模型,全程在设备上跑完。

实测下来,一张 320×397 的老照片,在 Mate 70 Pro 上 201ms 变成 1280×1588。这篇文章就把这个能力从头到尾过一遍,包括官方文档里没写、但我踩出来的那条硬边界。

图像超分能做什么

对低分辨率图像做超分辨率重建,把丢失的高频细节还原出来。

官方给的目标场景是"提升图片质量、修复老照片"。我自己把它归成三类:

  • 家庭影像:翻拍纸质老照片、低清截图、被聊天软件压缩过的图
  • 工程与台账:现场翻拍的图纸、仪表读数、货物标签
  • 相册治理:批量把历史低清图升档,供后续检索和展示

这里要特别说明一点:它不是 AI 重绘。它不会给你重新画一张脸,而是把原来糊在一起的像素细节还原出来。对老照片这种"原件不能改"的场景,这个边界非常重要。

左边是原图,右边是超分结果,中间那条蓝线可以左右拖动对比。

能力速览

项目 说明
所属 Kit @kit.CoreVisionKit
起始版本 26.0.0(HarmonyOS 7)
系统能力 SystemCapability.AI.Vision.VisionBase
支持设备形态 phone / tablet / 2in1
需要权限
需要联网 无,纯端侧推理
放大倍率 固定 4 倍,不可配置

调用面只有三个方法:

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

const analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
const imageData: visionBase.ImageData = { pixelMap: src };
const request: visionBase.Request = { inputData: imageData };
const response = await analyzer.process(request);
const sharp: image.PixelMap = response.pixelMap;
await analyzer.destroy();
  • 图像超分(Super-Resolution):从低分辨率图像恢复高分辨率细节的技术,这里指系统内置的端侧推理能力。
  • PixelMap:鸿蒙里的位图对象,承载像素数据,可以直接交给 Image 组件显示。
  • 端侧推理:模型运行在设备本地,数据不出机器、不依赖网络。

visionBase.Request 目前只有一个有效字段 inputData,需要包一层 ImageDatascenerequestId 是预留字段,写了暂时不生效。

环境准备

  • DevEco Studio 26.0.0.821,SDK 为 HarmonyOS 26.0.0(API 26)
  • 工程的 compatibleSdkVersiontargetSdkVersion 都要写成 26.0.0,低于这个版本连编译都过不去
  • 不需要在 module.json5 里声明任何 requestPermissions

创建工程和跑真机,我直接用 DevEcoCli 一条龙:

devecocli create --project-path ./hmos7-superres --app-name SuperRes --api-level 26
devecocli run --product default --module entry@default --device <serial>

run 会把依赖解析、编译、签名、安装、拉起一次做完,日常开发够用。

把引擎封一层

官方示例把 create() 直接写在 aboutToAppear 里。单页面确实够用,但真实应用通常是"选图页 → 修复页 → 结果页"这种结构,页面级持有会导致每次进出都重新加载模型。

所以我封了一个 SrEngine,把生命周期、耗时统计和错误码归一化都收进去:

export class SrEngine {
  private analyzer: imageSuperResolution.ImageSRAnalyzer | null = null;

  async ready(): Promise<boolean> {
    if (this.analyzer) {
      return true;
    }
    try {
      this.analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
      return true;
    } catch (err) {
      const e = err as BusinessError;
      throw new SrError(e.code, `超分服务初始化失败:${e.message}`);
    }
  }

  async superResolve(src: image.PixelMap): Promise<SrResult> {
    if (!(await this.ready()) || !this.analyzer) {
      throw new SrError(-1, '超分引擎未就绪');
    }
    const imageData: visionBase.ImageData = { pixelMap: src };
    const request: visionBase.Request = { inputData: imageData };
    const start: number = Date.now();
    const response = await this.analyzer.process(request);
    const cost: number = Date.now() - start;
    // 这里把 src/dst 尺寸和 cost 一起带出去,给 UI 显示
  }

  async release(): Promise<void> {
    if (this.analyzer) {
      await this.analyzer.destroy();
      this.analyzer = null;
    }
  }
}

process() 自己保证引擎就绪,调用方就不用操心顺序问题。

需要注意的是,create() 可能抛 1018700001 Service exception,一般在低版本系统或者 AI 引擎服务未就绪时出现。这个异常必须 catch 并给降级 UI,不要让它冒到页面外面。

最关键的一步:输入必须限长边

这是整个功能最容易翻车的地方。把相册原图直接 createPixelMap() 解出来再喂进去,会得到:

process failed 401  The parameter check failed.

为什么会 401,我在真机上跑了一轮尺寸阶梯才搞清楚。下面是 Mate 70 Pro 的完整实测数据:

输入 输出 耗时
240×298 960×1192 148 ms
400×496 1600×1984 300 ms
600×744 2400×2976 531 ms
800×992 3200×3968 939 ms
1000×1240 4000×4960 1507 ms
1200×1488 4800×5952 2042 ms
1400×1736 5600×6944 2907 ms
1600×1984 6400×7936 3701 ms
1652×2048 6608×8192 4217 ms
1656×2053 401 失败
1700×2108 401 失败

三条结论可以直接抄进设计文档:

  1. 输出单边硬上限是 8192px。 1652×2048 刚好输出 8192 能通过,1656×2053 输出 8212 就报错。换算回来就是:输入长边 ≤ 2048
  2. 耗时和"输入像素数"线性相关,不是输出像素数。 取 45 万像素以上的稳定区间,大约是 输入像素数 × 1.18 μs + 50 ms。有了这个模型,用户选了一张 1200 万像素的图,先压到长边 2048(约 300 万像素),就能预估 3.6 秒,进度条文案和超时策略提前就能定。
  3. 首次调用会多花约 30ms。 同一张图连跑三次是 229 → 199 → 195 ms,多出来的是模型加载。所以 create() 不要放在点击回调里。

正确的写法是在解码阶段就把尺寸压住:先读元数据,再决定解码尺寸。这样既不会 401,也省掉一张全尺寸位图的内存。

export class SrEngine {
  /** 超分固定 4 倍,输出单边 > 8192 直接抛 401,故输入长边须 <= 2048。 */
  static readonly MAX_INPUT_EDGE: number = 2048;

  static async decodeCapped(source: image.ImageSource): Promise<image.PixelMap> {
    const info: image.ImageInfo = await source.getImageInfo();
    const long: number = Math.max(info.size.width, info.size.height);
    if (long <= SrEngine.MAX_INPUT_EDGE) {
      return source.createPixelMap();
    }
    const k: number = SrEngine.MAX_INPUT_EDGE / long;
    return source.createPixelMap({
      desiredSize: {
        width: Math.round(info.size.width * k),
        height: Math.round(info.size.height * k)
      }
    } as image.DecodingOptions);
  }
}

相册导入、内置样片、以后要加的相机拍照,三条路径共用这一个入口,上限只改一处。

反过来讲,如果先 createPixelMap()scale() 缩小——一张 4000×3000 的 ARGB 位图是 48MB,内存先炸为敬,然后才轮到超分报错。

版本兼容:这个坑会直接白屏

如果你的工程要同时跑鸿蒙 6 和鸿蒙 7,这一点需要特别注意:

import { imageSuperResolution } from '@kit.CoreVisionKit';  // 在低版本设备上直接崩

运行时判断救不了你。 ES 的具名导入在模块加载阶段就解析,imageSuperResolution 是 26.0.0 才有的导出,在 API 24 设备上会在你的任何一行代码执行之前抛:

Error type:SyntaxError ... does not provide an export name 'imageSuperResolution'

随后进程以 254 退出。canIUse() 或者 deviceInfo.sdkApiVersion >= 26 这类判断写在函数体里,根本来不及执行。

唯一的解法是把新 API 隔离到旧设备不会导航进去的页面(或者独立 HSP)。我在页面里只放一个状态徽章:

@State supported: boolean = deviceInfo.sdkApiVersion >= 26;

真正的超分调用只存在于这个页面,旧设备装的是不含此页面的产物。这条规律对鸿蒙 7 的所有新能力都成立——文搜图、FAST Kit、3DGS 都一样。

保存结果:不申请权限也能写相册

老照片涉及隐私,"全程不出设备"是这类产品的卖点,所以不该去申请受限权限 ohos.permission.WRITE_IMAGEVIDEO(申请它上架还要额外过审)。

SaveButton 安全控件 + MediaAssetChangeRequest 就够了:

saveCallback: SaveButtonCallback = async (event, result, error) => {
  if (result !== SaveButtonOnClickResult.SUCCESS || !this.after) {
    return;
  }
  const packer = image.createImagePacker();
  const bytes = await packer.packToData(this.after,
    { format: 'image/jpeg', quality: 96 } as image.PackingOption);
  const request = photoAccessHelper.MediaAssetChangeRequest
    .createAssetRequest(this.ctx!, photoAccessHelper.PhotoType.IMAGE, 'jpg');
  request.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, bytes);
  await photoAccessHelper.getPhotoAccessHelper(this.ctx!).applyChanges(request);
};

用户点一下,系统弹这个确认框,授权只对本次点击有效:

这里有个小坑:SaveButton 不支持 margin()。安全控件只开放极小的属性白名单,写 .margin() 会直接编译报错:

Property 'margin' does not exist on type 'SaveButtonAttribute'

要控制位置就在外面套一层 Column 加 margin。

顺手记一个 ArkUI 老坑

指标卡(原分辨率 / 输出分辨率 / 耗时)我一开始写成:

@Builder MetricCell(label: string, value: string) { ... }
this.MetricCell('原分辨率', this.srcSize ? `${this.srcSize.w}×${this.srcSize.h}` : '—')

真机上状态文案正常刷新,但三个指标永远停在"—"。原因是 @Builder 的简单类型参数是值传递,不建立状态依赖。改成传对象(引用传递)就好了:

interface Metric { label: string; value: string }

@Builder MetricCell(m: Metric) { ... }
this.MetricCell({ label: '原分辨率', value: this.srcSize ? `${this.srcSize.w}×${this.srcSize.h}` : '—' })

这个坑和鸿蒙 7 没关系,但编译器和 linter 都不报,踩一次能白耗半小时。

真机运行效果

测试机:HUAWEI Mate 70 Pro(PLR-AL00),HarmonyOS 7.0.0.107,API 26。

首页,右上角是能力就绪徽章:

选一张内置样片,原图 320×397:

点「开始修复」,201ms 后出结果:

换一张 300×241 的毕业照,输出 1200×964,耗时 95ms:

至此整条链路就跑通了。

几条实测之外但值得知道的

不同长宽比的图行为一致。 第二张样片是 300×241 的宽图,输出 1200×964,倍率同样是 4。没有针对长宽比做特殊处理,所以横竖混合的相册可以放心批量跑。

重复调用的收益会饱和。 同一张图连跑四次是 229 → 199 → 195 → 195 ms 这个量级,第三次基本就平了。也就是说"预热一次"值得做,"预热十次"没意义。

批量要自己排队。 process() 返回 Promise,但底层是重资源。我在页面上用一个 busy 标志把按钮锁住、串行执行,比并发提交更稳。真要批量,就把"解码 → 超分 → 落盘"做成队列,进度按张数报("第 3/10 张"),别报百分比。

和文搜图串联很自然。 超分解决"看不清",textSearchImage 解决"找不到"。老照片批量升档之后再建语义索引,检索质量会明显好于对糊图建索引——两个能力都在 Core Vision Kit 里,可以放进同一条离线流水线。

一个诚实的提醒:超分只补细节,不去污。 第一张对比图里那些白色斑点,是我故意在源图上加的灰尘噪点——超分之后它们被完整保留,甚至更锐利。

  • 模糊、低分辨率、JPEG 压缩块 → 明显改善
  • 划痕、霉点、水渍、褪色 → 基本不动

真要端到端的"老照片修复",得把去污、上色和超分串起来,而且超分放最前面(它对输入质量最不挑)。

总结

什么场景值得用它:家庭影像、工程台账、相册批量治理这类"图已经糊了、又不能重拍"的场合。它只补细节不重绘内容,所以老照片这种"原件不能改"的场景特别合适。

反过来,划痕、霉点、水渍这类污损它不管,要端到端修复得自己串去污和上色;另外它是 26.0.0 才有的导出,低版本设备上不要碰。

💡 一句话:能力只有三行代码,真正要记住的是解码时限住长边 2048,以及别在 API 24 设备上导入它。

参考文献

  1. HarmonyOS 7(API 26) 官方新能力解读 & 开发者实战开发案例合集:https://developer.huawei.com/consumer/cn/forum/topic/0208224160644765091
  2. Core Vision Kit 开发指南 · 图像超分:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution
  3. Core Vision Kit API 参考 · imageSuperResolution(图像超分):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-image-super-resolution-api
  4. Core Vision Kit 错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/errorcode-core-vision