HarmonyOS 7 端侧 AI 图像超分的工程化落地

在这轮产品迭代中,运营团队希望在相册功能里加入"老照片修复"的卖点。第一版方案我们直接调用了云端的超分 API,结果上线不到一周,客诉就爆了。用户抱怨网络差的时候图片传不上去,更严重的是,大量的私密照片上传到云端,引发了严重的隐私合规审查危机。痛定思痛后,我们决定把 AI 模型直接塞进手机里,用端侧的 NPU 来做推理,但这绝非易事。

模型瘦身与格式转换的阵痛

云端跑的 PyTorch 模型动辄几百兆,直接塞进鸿蒙的 HAP 包里完全不现实。我们首先在服务端对模型进行了知识蒸馏,把参数砍掉了一大半,然后利用 MindSpore Lite 的转换工具,将模型转成了鸿蒙底层支持的 .ms 格式。即便如此,模型依然有 20 多兆,我们最终选择了 FP16 量化,牺牲了极其微小的画质,换取了内存占用减半。

在工程目录下,你不能把模型放在常规的 resources 里,必须放进 rawfile 目录。加载模型时,通过 context.resourceManager 去读取文件流。

NPU 设备后端的动态降级

很多开发者以为只要配置了 NPU,系统就会自动用硬件加速。实际上,如果在没有独立 NPU 的老旧机型或者模拟器上强行指定 NPU,底层的推理引擎会直接抛异常崩溃。

这里有一个极其致命的警示:必须在初始化推理引擎时,写好健壮的降级(Fallback)逻辑。先用 NPU 尝试加载,一旦捕获到异常,立刻退回到 CPU 推理。

typescript

复制代码
import mindspore from '@ohos.ai.mindSporeLite';

async function initModel(context: Context) {
  const modelBuffer = await context.resourceManager.getRawFileContent('super_resolution.ms');
  
  // 尝试使用 NPU
  let model = await mindspore.loadModelFromBuffer(modelBuffer.buffer, {
    deviceType: mindspore.DeviceType.NPU,
    threadNum: 2
  });

  if (!model) {
    // 警示:降级处理,防止低端机闪退
    model = await mindspore.loadModelFromBuffer(modelBuffer.buffer, {
      deviceType: mindspore.DeviceType.CPU,
      threadNum: 4
    });
  }
  return model;
}

像素重排与内存对齐的深渊

这是整个项目中最折磨人的环节。你从相册拿到的 PixelMap 是 RGBA 格式,但超分模型通常只接受 RGB 或者灰度图,并且要求输入的数据类型是 Float32。

如果你直接在 ArkTS 层用循环去遍历每一个像素做归一化,那几千乘几千的像素点,会让应用瞬间卡死。我们只能写 NAPI,在 C++ 层利用指针的高效内存访问,把 PixelMap 的数据按通道拆分出来,归一化到 0 到 1 之间,再重新组装成模型需要的张量(Tensor)形状。

主线程冻结与 TaskPool 的终极救赎

推理过程极其消耗 CPU 和内存,稍微大一点的图,处理时间长达几秒。如果你把推理函数写在 aboutToAppear 里,界面马上就会变成幻灯片。

千万不要尝试用 setTimeout 去糊弄,必须老老实实使用 TaskPool。而且要注意,传进 TaskPool 的必须是可序列化的数据。如果是几兆的 ArrayBuffer,传递过程中的深拷贝也会引发 OOM。对于大数据,必须使用 Transferable 转移所有权,或者在底层通过 NAPI 暴露接口直接操作内存地址。

去网上找一个极简的图像分类模型(比如辨别猫和狗),通过 MindSpore 转换为 .ms 格式。写一个页面,点击按钮调用 TaskPool 进行推理,并把置信度最高的结果打印在屏幕上。跑通这个流程,你就算摸到了端侧 AI 的门槛。