前言
前阵子我妈翻出一张 1985 年的全家福,翻拍成电子档之后只有 320×397,人脸糊成一团,让我"想办法弄清楚点"。
第一反应是丢给某个在线修复工具。但一想到要把家里老人的照片传上去,还要按次付费,就有点不愿意。
后来发现 HarmonyOS 7(API 26)把这件事做成了系统能力:图像超分(imageSuperResolution),归属 Core Vision Kit。三行代码调用,不联网、不要权限、不用带模型,全程在设备上跑完。
实测下来,一张 320×397 的老照片,在 Mate 70 Pro 上 201ms 变成 1280×1588。这篇文章就把这个能力从头到尾过一遍,包括官方文档里没写、但我踩出来的那条硬边界。
图像超分能做什么
对低分辨率图像做超分辨率重建,把丢失的高频细节还原出来。
官方给的目标场景是"提升图片质量、修复老照片"。我自己把它归成三类:
- 家庭影像:翻拍纸质老照片、低清截图、被聊天软件压缩过的图
- 工程与台账:现场翻拍的图纸、仪表读数、货物标签
- 相册治理:批量把历史低清图升档,供后续检索和展示
这里要特别说明一点:它不是 AI 重绘。它不会给你重新画一张脸,而是把原来糊在一起的像素细节还原出来。对老照片这种"原件不能改"的场景,这个边界非常重要。

左边是原图,右边是超分结果,中间那条蓝线可以左右拖动对比。
能力速览
| 项目 | 说明 |
|---|---|
| 所属 Kit | @kit.CoreVisionKit |
| 起始版本 | 26.0.0(HarmonyOS 7) |
| 系统能力 | SystemCapability.AI.Vision.VisionBase |
| 支持设备形态 | phone / tablet / 2in1 |
| 需要权限 | 无 |
| 需要联网 | 无,纯端侧推理 |
| 放大倍率 | 固定 4 倍,不可配置 |
调用面只有三个方法:
import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit';
const analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
const imageData: visionBase.ImageData = { pixelMap: src };
const request: visionBase.Request = { inputData: imageData };
const response = await analyzer.process(request);
const sharp: image.PixelMap = response.pixelMap;
await analyzer.destroy();
- 图像超分(Super-Resolution):从低分辨率图像恢复高分辨率细节的技术,这里指系统内置的端侧推理能力。
- PixelMap:鸿蒙里的位图对象,承载像素数据,可以直接交给
Image组件显示。- 端侧推理:模型运行在设备本地,数据不出机器、不依赖网络。
visionBase.Request 目前只有一个有效字段 inputData,需要包一层 ImageData。scene 和 requestId 是预留字段,写了暂时不生效。
环境准备
- DevEco Studio 26.0.0.821,SDK 为
HarmonyOS 26.0.0(API 26) - 工程的
compatibleSdkVersion与targetSdkVersion都要写成26.0.0,低于这个版本连编译都过不去 - 不需要在
module.json5里声明任何requestPermissions
创建工程和跑真机,我直接用 DevEcoCli 一条龙:
devecocli create --project-path ./hmos7-superres --app-name SuperRes --api-level 26
devecocli run --product default --module entry@default --device <serial>run 会把依赖解析、编译、签名、安装、拉起一次做完,日常开发够用。
把引擎封一层
官方示例把 create() 直接写在 aboutToAppear 里。单页面确实够用,但真实应用通常是"选图页 → 修复页 → 结果页"这种结构,页面级持有会导致每次进出都重新加载模型。
所以我封了一个 SrEngine,把生命周期、耗时统计和错误码归一化都收进去:
export class SrEngine {
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 SrError(e.code, `超分服务初始化失败:${e.message}`);
}
}
async superResolve(src: image.PixelMap): Promise<SrResult> {
if (!(await this.ready()) || !this.analyzer) {
throw new SrError(-1, '超分引擎未就绪');
}
const imageData: visionBase.ImageData = { pixelMap: src };
const request: visionBase.Request = { inputData: imageData };
const start: number = Date.now();
const response = await this.analyzer.process(request);
const cost: number = Date.now() - start;
// 这里把 src/dst 尺寸和 cost 一起带出去,给 UI 显示
}
async release(): Promise<void> {
if (this.analyzer) {
await this.analyzer.destroy();
this.analyzer = null;
}
}
}让 process() 自己保证引擎就绪,调用方就不用操心顺序问题。
需要注意的是,create() 可能抛 1018700001 Service exception,一般在低版本系统或者 AI 引擎服务未就绪时出现。这个异常必须 catch 并给降级 UI,不要让它冒到页面外面。
最关键的一步:输入必须限长边
这是整个功能最容易翻车的地方。把相册原图直接 createPixelMap() 解出来再喂进去,会得到:
process failed 401 The parameter check failed.
为什么会 401,我在真机上跑了一轮尺寸阶梯才搞清楚。下面是 Mate 70 Pro 的完整实测数据:
| 输入 | 输出 | 耗时 |
|---|---|---|
| 240×298 | 960×1192 | 148 ms |
| 400×496 | 1600×1984 | 300 ms |
| 600×744 | 2400×2976 | 531 ms |
| 800×992 | 3200×3968 | 939 ms |
| 1000×1240 | 4000×4960 | 1507 ms |
| 1200×1488 | 4800×5952 | 2042 ms |
| 1400×1736 | 5600×6944 | 2907 ms |
| 1600×1984 | 6400×7936 | 3701 ms |
| 1652×2048 | 6608×8192 | 4217 ms |
| 1656×2053 | — | 401 失败 |
| 1700×2108 | — | 401 失败 |

三条结论可以直接抄进设计文档:
- 输出单边硬上限是 8192px。 1652×2048 刚好输出 8192 能通过,1656×2053 输出 8212 就报错。换算回来就是:输入长边 ≤ 2048。
- 耗时和"输入像素数"线性相关,不是输出像素数。 取 45 万像素以上的稳定区间,大约是
输入像素数 × 1.18 μs + 50 ms。有了这个模型,用户选了一张 1200 万像素的图,先压到长边 2048(约 300 万像素),就能预估 3.6 秒,进度条文案和超时策略提前就能定。 - 首次调用会多花约 30ms。 同一张图连跑三次是 229 → 199 → 195 ms,多出来的是模型加载。所以
create()不要放在点击回调里。
正确的写法是在解码阶段就把尺寸压住:先读元数据,再决定解码尺寸。这样既不会 401,也省掉一张全尺寸位图的内存。
export class SrEngine {
/** 超分固定 4 倍,输出单边 > 8192 直接抛 401,故输入长边须 <= 2048。 */
static readonly MAX_INPUT_EDGE: number = 2048;
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,内存先炸为敬,然后才轮到超分报错。
版本兼容:这个坑会直接白屏
如果你的工程要同时跑鸿蒙 6 和鸿蒙 7,这一点需要特别注意:
import { imageSuperResolution } from '@kit.CoreVisionKit'; // 在低版本设备上直接崩运行时判断救不了你。 ES 的具名导入在模块加载阶段就解析,imageSuperResolution 是 26.0.0 才有的导出,在 API 24 设备上会在你的任何一行代码执行之前抛:
Error type:SyntaxError ... does not provide an export name 'imageSuperResolution'随后进程以 254 退出。canIUse() 或者 deviceInfo.sdkApiVersion >= 26 这类判断写在函数体里,根本来不及执行。
唯一的解法是把新 API 隔离到旧设备不会导航进去的页面(或者独立 HSP)。我在页面里只放一个状态徽章:
@State supported: boolean = deviceInfo.sdkApiVersion >= 26;真正的超分调用只存在于这个页面,旧设备装的是不含此页面的产物。这条规律对鸿蒙 7 的所有新能力都成立——文搜图、FAST Kit、3DGS 都一样。
保存结果:不申请权限也能写相册
老照片涉及隐私,"全程不出设备"是这类产品的卖点,所以不该去申请受限权限 ohos.permission.WRITE_IMAGEVIDEO(申请它上架还要额外过审)。
用 SaveButton 安全控件 + MediaAssetChangeRequest 就够了:
saveCallback: SaveButtonCallback = async (event, result, error) => {
if (result !== SaveButtonOnClickResult.SUCCESS || !this.after) {
return;
}
const packer = image.createImagePacker();
const bytes = await packer.packToData(this.after,
{ format: 'image/jpeg', quality: 96 } as image.PackingOption);
const request = photoAccessHelper.MediaAssetChangeRequest
.createAssetRequest(this.ctx!, photoAccessHelper.PhotoType.IMAGE, 'jpg');
request.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, bytes);
await photoAccessHelper.getPhotoAccessHelper(this.ctx!).applyChanges(request);
};用户点一下,系统弹这个确认框,授权只对本次点击有效:

这里有个小坑:SaveButton 不支持 margin()。安全控件只开放极小的属性白名单,写 .margin() 会直接编译报错:
Property 'margin' does not exist on type 'SaveButtonAttribute'要控制位置就在外面套一层 Column 加 margin。
顺手记一个 ArkUI 老坑
指标卡(原分辨率 / 输出分辨率 / 耗时)我一开始写成:
@Builder MetricCell(label: string, value: string) { ... }
this.MetricCell('原分辨率', this.srcSize ? `${this.srcSize.w}×${this.srcSize.h}` : '—')真机上状态文案正常刷新,但三个指标永远停在"—"。原因是 @Builder 的简单类型参数是值传递,不建立状态依赖。改成传对象(引用传递)就好了:
interface Metric { label: string; value: string }
@Builder MetricCell(m: Metric) { ... }
this.MetricCell({ label: '原分辨率', value: this.srcSize ? `${this.srcSize.w}×${this.srcSize.h}` : '—' })这个坑和鸿蒙 7 没关系,但编译器和 linter 都不报,踩一次能白耗半小时。
真机运行效果
测试机:HUAWEI Mate 70 Pro(PLR-AL00),HarmonyOS 7.0.0.107,API 26。
首页,右上角是能力就绪徽章:

选一张内置样片,原图 320×397:

点「开始修复」,201ms 后出结果:

换一张 300×241 的毕业照,输出 1200×964,耗时 95ms:

至此整条链路就跑通了。
几条实测之外但值得知道的
不同长宽比的图行为一致。 第二张样片是 300×241 的宽图,输出 1200×964,倍率同样是 4。没有针对长宽比做特殊处理,所以横竖混合的相册可以放心批量跑。
重复调用的收益会饱和。 同一张图连跑四次是 229 → 199 → 195 → 195 ms 这个量级,第三次基本就平了。也就是说"预热一次"值得做,"预热十次"没意义。
批量要自己排队。 process() 返回 Promise,但底层是重资源。我在页面上用一个 busy 标志把按钮锁住、串行执行,比并发提交更稳。真要批量,就把"解码 → 超分 → 落盘"做成队列,进度按张数报("第 3/10 张"),别报百分比。
和文搜图串联很自然。 超分解决"看不清",textSearchImage 解决"找不到"。老照片批量升档之后再建语义索引,检索质量会明显好于对糊图建索引——两个能力都在 Core Vision Kit 里,可以放进同一条离线流水线。
一个诚实的提醒:超分只补细节,不去污。 第一张对比图里那些白色斑点,是我故意在源图上加的灰尘噪点——超分之后它们被完整保留,甚至更锐利。
- 模糊、低分辨率、JPEG 压缩块 → 明显改善
- 划痕、霉点、水渍、褪色 → 基本不动
真要端到端的"老照片修复",得把去污、上色和超分串起来,而且超分放最前面(它对输入质量最不挑)。
总结
什么场景值得用它:家庭影像、工程台账、相册批量治理这类"图已经糊了、又不能重拍"的场合。它只补细节不重绘内容,所以老照片这种"原件不能改"的场景特别合适。
反过来,划痕、霉点、水渍这类污损它不管,要端到端修复得自己串去污和上色;另外它是 26.0.0 才有的导出,低版本设备上不要碰。
💡 一句话:能力只有三行代码,真正要记住的是解码时限住长边 2048,以及别在 API 24 设备上导入它。
参考文献
- HarmonyOS 7(API 26) 官方新能力解读 & 开发者实战开发案例合集:https://developer.huawei.com/consumer/cn/forum/topic/0208224160644765091
- Core Vision Kit 开发指南 · 图像超分:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution
- Core Vision Kit API 参考 · imageSuperResolution(图像超分):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-image-super-resolution-api
- Core Vision Kit 错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/errorcode-core-vision