夜雨聆风学习资料网

ARTICLE · 1266937

#向光而行 码向未来 我给记账 App 的凭证图做了次"瘦身手术" | HarmonyOS 7 图像超分实战

#向光而行 码向未来 我给记账 App 的凭证图做了次"瘦身手术" | HarmonyOS 7 图像超分实战
点击蓝字 | 关注我们

作者 | 开发者“在人间耕耘”

空间是怎么被吃掉的

我的记账 App 里有个不起眼但挺常用的功能:拍凭证。买菜的小票、装修材料验收单、给爸妈买电器的保修卡,随手拍一张挂在账单上,月底对账特别管用。

功能好用,代价就是占内存。有阵子我注意到这个 App 的存储占用涨得不对劲——账单数据满打满算才几 MB,占用却眼看要往 1 个 G 走。一开始还怀疑是数据库出了问题,打开 DevEco 的沙箱文件浏览器才找到真凶:凭证文件夹里躺着的全是没压缩的相机原图,随手一拍就是 4000 像素长边,单张 3 到 8MB,一个月拍上三五十张,小几百兆就这么没了。而且这还只是本地,这套图后面要上云,OSS 的存储和流量账单会把这个数字再放大一遍。

解法不用想,压缩呗。但凭证图和风景照不一样,风景照糊点没人计较,凭证图用户点开就是要看金额、看商家名、看订单号的,压糊了等于白存。我在这对矛盾里纠结了一阵子,直到刷到社区里有开发者聊 HarmonyOS 7 新开放的一个端侧 AI 能力——图像超分(imageSuperResolution),输入一张低分辨率图,AI 重建出宽高各放大 4 倍的结果。

我当时盯着这个 API 介绍看了会儿,越看越觉得这俩东西是天生一对:存的时候放心压,看的时候让 AI 把清晰度补回来。

存:压到 1280,别忘了展示做处理

原来的选图逻辑很糙,图库里选完,原始文件按字节原样复制进沙箱,图库多大、沙箱就多大。改造后,复制这一步变成了"解码、下采样、重编码":

import { image } from '@kit.ImageKit';

import { fileIo as fs } from '@kit.CoreFileKit';

// 长边压到 1280,JPEG 72 重编码

const MAX_EDGE = 1280;

async function compressToSandbox(srcUri: string, destPath: string): Promise<number> {

 const src = image.createImageSource(srcUri);

 // desiredSize 在解码阶段直接下采样,别全尺寸解码完再缩

 const decodeOpts: image.DecodingOptions = {

   desiredSize: calcFitSize(await src.getImageInfo(), MAX_EDGE)

 };

 let pm = await src.createPixelMap(decodeOpts);

 // 重编码会丢 EXIF 方向,竖拍的照片会躺平——按 EXIF 转正再编码

 const orientation = await src.getImageProperty(image.PropertyKey.ORIENTATION);

 pm = await rotateByOrientation(pm, orientation);

 const packer = image.createImagePacker();

 const data = await packer.packToData(pm, { format: 'image/jpeg', quality: 72 });

 const file = fs.openSync(destPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);

 fs.writeSync(file.fd, data);

 fs.closeSync(file);

 pm.release();

 return data.byteLength;

}

这段代码里我踩过一个很隐蔽的坑:压缩完自己先翻着看,怎么所有竖着拍的小票全躺平了。查了半天才知道,原始 JPEG 是靠 EXIF 里的 Orientation 标记让系统显示时自动转正的,重编码一次这个标记就丢了,得自己在编码前读出方向、手动转回去。这种问题不真机走一遍流程根本发现不了,模拟器里横竖屏看着都对。

效果上,4000 像素、3-8MB 的原图,压完长边 1280,单张 100 到 300KB,体积只剩原来的零头。

看:超分把清晰度补回来

存压完了,看的时候怎么办。API 26 的图像超分接口本身非常简单:

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

import { image } from '@kit.ImageKit';

// analyzer 全局单例:create 是秒级耗时操作,不能每次点开大图都来一遍

let analyzer: imageSuperResolution.ImageSRAnalyzer | null = null;

async function superResolve(input: image.PixelMap): Promise<image.PixelMap> {

 if (analyzer === null) {

   analyzer = await imageSuperResolution.ImageSRAnalyzer.create();

 }

 const request: visionBase.Request = {

   inputData: { pixelMap: input }

 };

 const response = await analyzer.process(request);

 return response.pixelMap; // 宽高各 4 倍的重建结果

}

代码是不难,难的是它的内存脾气。超分输出是宽高各 4 倍,像素总量就是 16 倍——我最早直接拿 1280 的存档图喂进去,输出 5120 长边,一张 RGBA_8888 几十 MB,连着点开几张预览,内存曲线相当刺激。后来把超分的输入单独压到长边 720,输出 2880,已经超过任何手机屏幕的物理宽度,全屏看小票上的文字足够清楚,单张内存 25MB 左右,看完立刻 release。process 一次只吃一张图,多张凭证就自己排队串行处理。

交互上做了个小设计:点开大图预览时先用压缩图秒开,体验和改造前完全一样、无感,超分在后台跑,完成后把预览数据替换掉。用户感知到的就是图打开一会儿后"自己变清晰了",而不是盯着 loading 干等。

记得要做兼容处理

图像超分是端侧 AI 能力,不是"系统到了 API 26 就一定有"。官方文档列了支持条件:API 26.0.0 起、Phone/Tablet/PC(2in1)、具备系统能力集 SystemCapability.AI.Vision.VisionBase、仅 Stage 模型、元服务不支持。

所以我的判断逻辑写了三层:版本号过 26、canIUse 查能力集,最后 create 实测一次。

import { deviceInfo } from '@kit.BasicServicesKit';

const SR_SYS_CAP = 'SystemCapability.AI.Vision.VisionBase';

/** ①② 常规判断:版本下限 + 系统能力集 */

function superResSupported(): boolean {

 // imageSuperResolution 这个成员是 API 26 才开放的

 if (deviceInfo.sdkApiVersion < 26) {

   return false;

 }

 // 官方的机型适配机制:只看版本号会被部分设备骗过

 return canIUse(SR_SYS_CAP);

}

failed /system/lib64/module/libcorevisionkit.z.so not exist

系统声明了这个能力,但那台设备的固件包里压根没带这个 native 库。HarmonyOS 7 的 AI 能力是分机型分批次推送的,部分设备的系统包里会出现这种"声明了但没带上"的情况。

所以第三层只能真刀真枪 create 一次:

/** ③ create 实测:前两层都过也只是"理论上支持",跑起来才算数;结果进程级缓存,别每次选图都探测一遍 */

let probed: boolean | null = null;

async function probeByCreate(): Promise<boolean> {

 if (probed !== null) {

   return probed;

 }

 try {

   await imageSuperResolution.ImageSRAnalyzer.create();

   probed = true;

 } catch (e) {

   // 缺库设备在这一层现出原形:成员导出为 undefined 抛 TypeError,或 create 直接抛 801

   probed = false;

 }

 return probed;

}

版本、canIUse、create 三关全过,才敢把"支持超分"写进功能开关。

顺着这个结论我做了个产品上的取舍:压缩和超分能力绑定。有超分能力的设备,选图时压缩存档,看的时候超分补清晰度;没能力的设备,选图原样复制,多占点空间但画质有保障——没有超分,压了就真糊了。

这样降级策略特别干净:任何设备装上 App,要么"压缩+超分",要么"原图直显",不存在"压了却看不清"的中间态。

写在 1024

真机效果(Mate 90 Pro,HarmonyOS 7 正式版):100 张凭证的存储占用从几百 MB 降到 20MB 上下;预览秒开不变,一两秒后画面切到重建图,小票上的文字笔画明显变实,金额数字放大看不再是那种涂抹感。

最有意思的是这台设备反过来给我留了个念想——另一台 7.0 内测版的旧旗舰,到现在系统还没把 AI 组件包推下来,App 里它永远走原图直显的分支。哪天系统更新把库带上了,重启 App 探测通过,压缩和超分自动就启用了,一行代码都不用改。这种把能力检测做扎实之后"等系统追上来"的从容感,还挺新鲜的。

从最早照着文档敲第一行 ArkTS,到现在能把端侧 AI 编进自己产品的存储链路里,鸿蒙这几年的迭代速度是实打实被我这样一个普通开发者的真机日历记下来的。1024,向光而行,码向未来。

点击“阅读原文”

相关学习资料