图鉴里的神兽插画如果按高清随包分发,安装体积会随篇目一起增长;如果全部压到低清,放大观察时绒毛和纹样又会发糊。端侧图像超分让第三种做法成立:素材按低清随包,读者点开某一篇时,在设备本地把细节重建出来,处理过程不出设备。

这项能力先在一个独立实验页验证过输入准备、同尺度对照与图像资源释放。把它搬进图鉴详情页时,多出来的是业务约束:页面要一次排布多张卡片、素材尺寸各不相同、读者可能随时返回。这次搬迁里真正决定结果的是三件事。

重建入口放在详情页的哪一段

详情页在主图下方依次排布导言、知识与故事、馆长讲解几块内容。重建入口放在主图之后、正文之前,读者刚看完插画就能就地比较,不用跳页。

入口本身是一个独立卡片组件,只接收当前神兽的标识与主题状态,不持有页面的其他数据。这样它在双栏与单栏两套布局里都能挂载,窄屏也不会因为没有位置而丢掉入口。

if (this.isTwoPane) {
  Row({ space: 14 }) {
    Column({ space: 12 }) {
      ShanhaiBeastDetailHeroCard({ beast: this.beast, isDarkMode: this.isDarkMode })
      this.IntroductionCard()
      ShanhaiBeastSuperResolutionCard({
        beastId: this.beast.id,
        beastName: this.beast.name,
        isDarkMode: this.isDarkMode
      })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Start)

    ShanhaiBeastDetailReadingCards({ beast: this.beast, isDarkMode: this.isDarkMode })
      .layoutWeight(1)
  }
  .width('100%')
  .alignItems(VerticalAlign.Top)
}

卡片只在能力可用时渲染,判定同时看系统版本与运行时能力查询:

private srAvailable(): boolean {
  if (deviceInfo.sdkApiVersion < 26) {
    return false;
  }
  return canIUse('SystemCapability.AI.Vision.VisionBase');
}

纯 OpenHarmony 模拟器没有这一项能力,查询返回假,卡片不渲染,页面其余部分照常工作。这条分支让模拟器可以继续做布局回归,真机再做能力验收。

输入尺寸由输出倍率反推

官方给出的规格里有一条直接决定内存占用:服务把输入放大固定的四倍输出。输入边长乘四就是输出边长,两者是平方关系。

第一次接入时,我把馆藏原图直接交给服务,设备上应用随即退出。按四倍反推,一张一千二百余像素见方的插画会生成约两千五百万像素的输出缓冲,这已经超出一次页面渲染应当占用的内存。

修法是把降尺度提前到解码阶段,避免先解出大图再缩小——后者在解码那一刻就已经吃满内存:

const raw = await context.resourceManager.getMediaContent(shanhaiBeastImage(this.beastId));
const source = image.createImageSource(raw.buffer as ArrayBuffer);
const info = await source.getImageInfo();
const longSide = Math.max(info.size.width, info.size.height);
const ratio = SR_INPUT_LONG_SIDE / longSide;
const options: image.DecodingOptions = {
  desiredSize: {
    width: Math.max(1, Math.round(info.size.width * ratio)),
    height: Math.max(1, Math.round(info.size.height * ratio))
  }
};
const pixelMap = await source.createPixelMap(options);
await source.release();
return pixelMap;

长边取三百八十四像素,短边按原图比例算,非方形素材不会被拉成正方形,输出落在 1536 见方。

这个取值同时把能力用在了正确的地方:素材按低清存储、查看时重建,正是压缩存储加超分查看的用法;拿超分去放大本来就够大的原图,只会白白增加内存。

请求对象用字面量构造

接口声明里,请求类型写成一个类。照着声明直接构造实例,设备上会抛出构造函数不存在的类型错误,日志原文是 TypeError: Constructor is false。

声明文件只描述类型形状,运行时不保证该类型可以构造。改成对象字面量后请求正常下发:

const imageData: visionBase.ImageData = { pixelMap: source };
const request: visionBase.Request = { inputData: imageData };
const response = await analyzer.process(request);
this.enhanced = response.pixelMap;

分析器按一次调用创建和销毁,销毁放在 finally,保证处理抛错时也尝试释放:

analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
try {
  const imageData: visionBase.ImageData = { pixelMap: source };
  const request: visionBase.Request = { inputData: imageData };
  const response = await analyzer.process(request);
  this.enhanced = response.pixelMap;
  this.enhancedSize = this.sizeOf(response.pixelMap);
  this.phase = ShanhaiSuperResolutionPhase.DONE;
} catch (error) {
  const failure = error as BusinessError;
  this.phase = ShanhaiSuperResolutionPhase.FAILED;
  this.note = 'AI 超分不可用(' + failure.code + '),已保留低清预览。';
} finally {
  if (analyzer !== undefined) {
    try { await analyzer.destroy(); } catch (ignored) {}
  }
}

四类资源的归属分开处理,不共用一个释放标志:

对象释放时机责任方
ImageSource低清 PixelMap 解码完成解码流程
Analyzer本次 process 结束或失败卡片的 finally
低清 PixelMap页面不再展示卡片状态清理
重建 PixelMap被新结果替换接收结果的卡片

对比画面跟着状态刷新

左右两格的图像来自两个状态变量。把这两格抽成一个带参数的构建函数之后,设备上出现了一个现象:结论行已经显示低清 384 × 384、重建 1536 × 1536,两格却一直停在占位符。

原因是构建函数的参数按值传递,参数变化不会让它重新执行。图像从无到有这一步,格子拿到的还是最初那份空值。把两格直接写在 build 里,让它跟着状态走,刷新恢复正常:

Row({ space: 10 }) {
  Column() {
    Stack() {
      if (this.lowRes !== undefined) {
        Image(this.lowRes)
          .width('100%')
          .height('100%')
          .objectFit(ImageFit.Cover)
          .scale({ x: SR_ZOOM, y: SR_ZOOM })
      } else {
        Text('—').fontSize(18).fontColor(this.secondaryText())
      }
    }
    .width('100%')
    .height(SR_BOX_HEIGHT)
    .clip(true)
    Text('低清原图').fontSize(12).fontColor(this.gold()).width('100%').textAlign(TextAlign.Center)
  }
  .layoutWeight(1)
}

同一区域放大后才看得见差异

低清素材和重建结果整图并排时,两边都会被缩到同一个显示尺寸,肉眼看不出差别。两格使用相同的显示区域与相同的放大倍数,由外层裁剪出中心区域,比较条件才一致。

两格各占 512 × 608 像素的显示区域,取同一块中心区域放大两倍。左格保留低清的块状感,右格的绒毛分缕、眼睛与橙色纹样更锐。用同一区域的梯度能量做客观对照,重建格约为低清格的一点四五倍。

真机回读:输出尺寸、耗时与细节

在 API 26 真机上点击重建按钮,页面回读为低清 384 × 384、重建 1536 × 1536,单次流程耗时约四点二秒,运行期间没有异常日志。

图鉴详情页的 AI 高清重建:左为低清输入,右为重建输出

耗时统计的是从解码、创建分析器、处理到销毁的整段流程,包含输出信息读取,不能当作纯推理时间。

透明区域在业务素材里的处理约定

回读两格的图像数据可以看到一处现象:低清素材的透明角落,在重建输出里变成了不透明黑,页面上表现为右格四角出现黑色。

观察项低清输入重建输出
角落像素全透明不透明黑
页面表现透出卡片底色四角显示黑色

这属于输出数据本身的变化,不改动服务返回结果去掩盖。业务侧的处理办法是给比较区域约定不透明底色,或在展示前预合成一层背景,让透明区域不参与最终画面。

接入后的验收清单

检查项预期结果
能力不可用卡片不渲染,页面其余内容正常
重建成功两格都出图,结论行显示实际输入与输出尺寸
连续点击运行中的按钮禁用,不产生重叠请求
素材非方形按长边缩放,画面不变形
页面返回迟到的结果不写入已退出页面

参考:图像超分官方指南、API 26 Core Vision Kit 增量。

Logo

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

更多推荐