前言
做电商工具的都懂这个流程:买家在聊天里甩过来一张商品图,说"用这个做主图"。你点开一看——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
}model 用 deviceInfo.productModel 取,配合 deviceInfo.sdkApiVersion 一起上报,回来按机型分桶。跑几天你就能得到一张"机型 → 每千像素耗时"的表,产品里的超时阈值直接查这张表,而不是拍一个 3 秒了事。
分桶之后你会发现两件事:
- 同类芯片的手机之间差异不大,但手机和平板之间能差到五倍,所以至少按 phone / tablet / 2in1 三档设阈值。
- 同一台机器连续调用会略微变快(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——先按机型埋点分桶,再定超时阈值。
参考文献
- Core Vision Kit 开发指南 · 图像超分:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution
- Core Vision Kit 开发指南 · 主体分割:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-subject-segmentation
- Core Vision Kit API 参考 · imageSuperResolution(图像超分):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-image-super-resolution-api