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

买家发来的糊图,怎么直接上详情页

智绘鸿蒙·如 7 而至

前言

做电商工具的都懂这个流程:买家在聊天里甩过来一张商品图,说"用这个做主图"。你点开一看——250 像素宽,压缩过,糊得连拉链都看不清。

扔给超分就完事了?功能上是的,鸿蒙 7 的 imageSuperResolution 三行代码就能放大四倍。

但我这次特意换了台设备跑,结果让我改了这篇文章的结论:

同一张 250×333 的图,Mate 70 Pro 上大约 165ms,MatePad Pro 上要 820ms。差五倍。

所以这篇不讲 API 长什么样(那部分文档里都有),讲把它塞进一条真实业务流水线时你会撞上的几件事

这条流水线长什么样

一张聊天截图要变成能上架的主图,中间有四步,超分只是其中一步:

拿到图(聊天转发/截图) → 解码并限住长边 → 超分重建 → 出图(白底 / 尺寸合规)

第三步是系统能力,另外三步都是你自己的活。多数人栽在第二步和第四步。

第二步的坑:超分固定 4 倍,输出单边硬上限 8192px,所以输入长边必须 ≤ 2048,超了直接抛 401 The parameter check failed

有意思的是,聊天工具转发出来的图往往已经被压得很小,反而不会触发这个限制;真正会触发的是用户点"原图"发过来的那种 4000×3000 扫描件。所以两种都要防。

第四步的坑:超分只补细节,不改背景、不去污。买家图里的沙发、床单、桌面杂物,它会清清楚楚地给你放大。电商主图要的白底它不会给,后面必须接一步分割。

开发步骤

第一步:起引擎,别在点击回调里 create

import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class SrEngine {
  static readonly MAX_INPUT_EDGE: number = 2048;
  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 Error(`超分服务初始化失败(${e.code}):${e.message}`);
    }
  }
}
  • 超分(Super-Resolution):把低分辨率图像重建为高分辨率图像,恢复丢失的细节。
  • 端侧推理:模型跑在设备本地,图片不出机器。对电商场景来说,"商品原图不外传"本身就是卖点。

需要注意的是,create() 可能抛 1018700001 Service exception(低版本系统、AI 引擎未就绪),必须 catch 并给降级 UI

第二步:解码时就限长边

这是内存和 401 的双重防线。先读元数据,再决定解码尺寸:

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,内存先炸为敬,然后才轮到超分报错。

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

第三步:跑超分,并且把耗时记下来

async superResolve(src: image.PixelMap): Promise<SrResult> {
  if (!(await this.ready()) || !this.analyzer) {
    throw new Error('超分引擎未就绪');
  }
  const imageData: visionBase.ImageData = { pixelMap: src };
  const request: visionBase.Request = { inputData: imageData };
  const start = Date.now();
  const response = await this.analyzer.process(request);
  const cost = Date.now() - start;
  // 把 cost 记进埋点,后面你会感谢自己
}

为什么强调记耗时?因为不同设备能差五倍。你不埋点,就永远以为自己手机上那 200ms 是普遍水平,然后被用户投诉"点了没反应"。

第四步:批量要串行,别并发

聊天里一次转发十张图是常事。ImageSRAnalyzer 是重资源,实测并发调用没有收益、还会拉长单次耗时。老老实实串行加进度条:

for (let i = 0; i < jobs.length; i++) {
  const r: SrResult = await this.engine.superResolve(jobs[i]);
  this.progress = `${i + 1}/${jobs.length}`;
  results.push(r.output);
}

十张 250×333 的图,在平板上按 820ms 一张算就是 8.2 秒。这个进度必须可见、可取消。

第五步:出图前接分割

超分完直接上架是不合格的。接 subjectSegmentation 抠主体、垫白底,才得到平台要的合规主图。这一步本文不展开,但流水线位置要留出来。

真机运行效果

测试机:HUAWEI MatePad Pro(MRDI-W00),HarmonyOS 7.0.0.107,API 26。

首页三张待处理图,第三张就是"买家发来的商品图",250×333:

载入后,商品图糊到看不清包上的拉链:

超分后 1000×1332,包型、提手缝线、价签轮廓都回来了:

至此功能就跑通了,但先别急着定 SLA。

五倍差异是怎么测出来的

同一张 250×333 输入,在平板上连跑四次:

次序 耗时
1 820 ms
2 780 ms
3 772 ms
4 845 ms

稳定在 800ms 上下,不是首帧抖动

对照另一台设备:71.5k px(240×298)输入实测 148ms、198k px(400×496)实测 300ms,按线性内插,83k px 在手机上约 165ms。

所以:耗时 ≈ 输入像素数 × 1.18 μs 这个模型只在特定机型上成立,系数本身是设备相关的。

把"跨设备"做成埋点,而不是口头承诺

具体怎么落地,我这套是这么埋的。每次调用只记四个字段,成本极低:

interface SrStat {
  edge: number;      // 输入长边
  px: number;        // 输入像素总数
  cost: number;      // 实测耗时 ms
  model: string;     // deviceInfo.productModel
}

modeldeviceInfo.productModel 取,配合 deviceInfo.sdkApiVersion 一起上报,回来按机型分桶。跑几天你就能得到一张"机型 → 每千像素耗时"的表,产品里的超时阈值直接查这张表,而不是拍一个 3 秒了事。

分桶之后你会发现两件事:

  1. 同类芯片的手机之间差异不大,但手机和平板之间能差到五倍,所以至少按 phone / tablet / 2in1 三档设阈值
  2. 同一台机器连续调用会略微变快(229 → 199 → 195 ms),这是模型驻留的效果。意味着冷启动后第一次调用的耗时才是你该拿来定超时的数,别用热身后的漂亮数字。

进度、取消与失败兜底

进度。串行循环里更新一个 @State progress: string,文案写"第 3/10 张"比写百分比实在——用户不知道总数对应的百分比有什么意义,但知道还剩几张。

取消。给循环加一个标志位,每张处理完检查一次:

private cancelled: boolean = false;

for (let i = 0; i < jobs.length; i++) {
  if (this.cancelled) {
    break;
  }
  const r: SrResult = await this.engine.superResolve(jobs[i]);
  results.push(r.output);
}

注意取消检查放在两张之间,不要强行中断 process()——单张 800ms 左右,等它跑完再退出的代价可以接受,强行打断反而要处理引擎状态不一致。

兜底。三类失败分开处理:

错误 原因 处理
401 输入超限 走解码期限长边,本来就不该遇到
1018700001 服务异常 提示用户重启应用
其它 未知 保留原图并明确告知"这张没处理成功"

绝不能静默把糊图当成品交给用户——电商场景里这比不处理更糟。

还有两个实际会遇到的

深色/浅色主题:超分结果 PixelMap 直接丢给 Image 组件显示没问题,但要存文件得走 ImagePacker.packToData,格式选 image/jpeg,质量 96 左右比较平衡。

写相册别申请权限:用 SaveButton 安全控件 + MediaAssetChangeRequest,用户点一次授权一次,不需要 ohos.permission.WRITE_IMAGEVIDEO 那个受限权限。顺带提醒,SaveButton 不支持 margin(),要控制位置就在外面套一层容器。

总结

什么场景值得用它:电商工具里"用户传来的图糊、又要求直接上架"这一段。超分补清晰度,后面接分割垫白底再交付。

不适合:需要去污、换背景、改角度的场景,那些得靠别的能力,超分只会把杂物一起放大。

💡 一句话:别把一台设备上的 165ms 当成全世界的 165ms——先按机型埋点分桶,再定超时阈值。

参考文献

  1. Core Vision Kit 开发指南 · 图像超分:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution
  2. Core Vision Kit 开发指南 · 主体分割:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-subject-segmentation
  3. Core Vision Kit API 参考 · imageSuperResolution(图像超分):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-image-super-resolution-api