HarmonyOS 7.0 图像超分辨率重建:老照片秒变高清
先说两句
前段时间翻到一张老照片,像素太低了,人脸都糊成一团。想着有没有什么办法能把它变清楚,正好看到 HarmonyOS 7.0(API 26)新增了图像超分辨率重建能力,直接用 AI 把低分辨率图重建出更清晰的版本。
试了一下,效果比我想象的好。这篇文章就说说怎么在 HarmonyOS 应用里接入这个功能。
这东西是干什么的
官方定义是「对输入的低分辨率图像进行超分辨率重建,使图像更加清晰」。
说白了就是:给你一张模糊的小图,AI 把它变清晰、变细腻。不是简单拉大,是重建细节。
适合的场景:
- 老照片修复 — 翻拍的旧照片画质不够,超分一下会好很多
- 截图增强 — 某些场景下截图像素不够,超分后文字更清晰
- 缩略图还原 — 从网络加载的缩略图可以用超分提升显示质量
接入方式
需要导入的包
import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit'
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
核心是 @kit.CoreVisionKit 里的 imageSuperResolution 和 visionBase。
权限
需要在 module.json5 里加权限:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:reason_read_imagevideo",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
INTERNET 权限是必须的——首次调用 ImageSRAnalyzer.create() 时,系统需要联网下载 AI 模型。没有网络权限,创建会一直挂起。READ_IMAGEVIDEO 是从图库选图片用的。
代码实现
1. 创建分析器(带超时保护)
超分分析器 ImageSRAnalyzer 是个重量级对象,建议在页面的 aboutToAppear 里创建,aboutToDisappear 里销毁。
这里有个大坑:ImageSRAnalyzer.create() 返回的是 Promise,在以下情况会一直挂起、既不 resolve 也不 reject:
- 在模拟器上运行(CoreVisionKit 不支持模拟器)
- 设备没有联网(首次需要下载 AI 模型)
- 系统版本低于 API 26
所以必须加超时保护:
const INIT_TIMEOUT_MS = 60000;
private async initAnalyzer(): Promise<void> {
this.statusText = '正在初始化超分分析器(首次可能需要下载AI模型)...';
try {
const createTask = imageSuperResolution.ImageSRAnalyzer.create();
const timeoutTask = new Promise<never>((_, reject) => {
setTimeout(() => {
reject({ code: -1, message: '初始化超时,请检查:1.是否真机 2.设备联网 3.系统版本API26+' });
}, INIT_TIMEOUT_MS);
});
this.analyzer = await Promise.race([createTask, timeoutTask]);
if (this.analyzer) {
this.analyzerReady = true;
this.statusText = '超分分析器已就绪';
}
} catch (e) {
this.statusText = '初始化失败: ' + String(e);
}
}
另外,不要并发调用——CoreVisionKit 不支持同一特性被同一进程同一时间多次调用,加了防重复初始化的标志位:
@State isInitializing: boolean = false;
private async initAnalyzer(): Promise<void> {
if (this.isInitializing) {
return;
}
this.isInitializing = true;
// ... 创建逻辑
this.isInitializing = false;
}
销毁要在 aboutToDisappear 里做:
async aboutToDisappear(): Promise<void> {
if (this.analyzer) {
await this.analyzer.destroy();
this.analyzer = null;
}
}
2. 从图库选择图片
推荐使用 PhotoSelectOptions 方式(PhotoViewPicker 旧写法已废弃):
private async openPhoto(): Promise<string> {
try {
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
const picker = new photoAccessHelper.PhotoViewPicker();
const res = await picker.select(options);
if (res.photoUris && res.photoUris.length > 0) {
return res.photoUris[0];
}
return '';
} catch (e) {
hilog.error(0x0000, 'ImageSR', 'PhotoPicker failed: ' + String(e));
return '';
}
}
选到图片后,通过 fileIo.open 打开文件描述符,再用 image.createImageSource 解码成 PixelMap:
private async loadImage(uri: string): Promise<void> {
let fileSource = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
let imageSource = image.createImageSource(fileSource.fd);
this.inputImage = await imageSource.createPixelMap();
await imageSource.release();
await fileIo.close(fileSource);
}
3. 执行超分处理
把 PixelMap 包装成 visionBase.Request,调用 analyzer.process:
let imageData: visionBase.ImageData = {
pixelMap: this.inputImage
};
let request: visionBase.Request = {
inputData: imageData
};
let response: imageSuperResolution.ISPResponse =
await this.analyzer.process(request);
this.outputImage = response.pixelMap;
返回的 response.pixelMap 就是超分后的高清图,直接塞进 Image 组件就能显示。
完整 Demo 示例
import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
const DOMAIN = 0x0000;
const TAG = 'ImageSRDemo';
const INIT_TIMEOUT_MS = 60000;
interface AnalyzerError {
code: number;
message: string;
}
function toAnalyzerError(e: Object): AnalyzerError {
let err: AnalyzerError = { code: -1, message: String(e) };
if (e instanceof Error) {
err.message = e.message;
}
return err;
}
@Entry
@Component
struct ImageSRDemo {
@State inputImage: PixelMap | undefined = undefined;
@State outputImage: PixelMap | undefined = undefined;
@State statusText: string = '正在初始化超分分析器...';
@State analyzerReady: boolean = false;
@State isInitializing: boolean = false;
@State isProcessing: boolean = false;
private analyzer: imageSuperResolution.ImageSRAnalyzer | null = null;
aboutToAppear(): void {
void this.initAnalyzer();
}
aboutToDisappear(): void {
void this.releaseAnalyzer();
}
private async initAnalyzer(): Promise<void> {
if (this.isInitializing) {
return;
}
this.isInitializing = true;
this.analyzerReady = false;
this.statusText = '正在初始化超分分析器(首次可能需要下载AI模型)...';
try {
hilog.info(DOMAIN, TAG, 'Start creating ImageSRAnalyzer');
const createTask = imageSuperResolution.ImageSRAnalyzer.create();
const timeoutTask: Promise<never> = new Promise((_, reject) => {
setTimeout(() => {
reject({ code: -1, message: '初始化超时(' + (INIT_TIMEOUT_MS / 1000).toString() + 's),请检查:1.是否真机 2.设备联网 3.系统版本API26+' });
}, INIT_TIMEOUT_MS);
});
this.analyzer = await Promise.race([createTask, timeoutTask]);
if (this.analyzer) {
this.analyzerReady = true;
this.statusText = '超分分析器已就绪,请选择一张图片';
hilog.info(DOMAIN, TAG, 'ImageSRAnalyzer created successfully');
} else {
this.statusText = '超分分析器创建失败(返回为空)';
hilog.error(DOMAIN, TAG, 'ImageSRAnalyzer.create returned null');
}
} catch (e) {
this.analyzerReady = false;
const err = toAnalyzerError(e as Object);
hilog.error(DOMAIN, TAG, 'Create analyzer failed: code=' + err.code.toString() + ', msg=' + err.message);
this.statusText = '初始化失败: [' + err.code.toString() + '] ' + err.message;
} finally {
this.isInitializing = false;
}
}
private async releaseAnalyzer(): Promise<void> {
if (this.analyzer) {
try {
await this.analyzer.destroy();
hilog.info(DOMAIN, TAG, 'ImageSRAnalyzer released');
} catch (e) {
const err = toAnalyzerError(e as Object);
hilog.error(DOMAIN, TAG, 'Destroy analyzer failed: ' + err.code.toString() + ', ' + err.message);
}
this.analyzer = null;
this.analyzerReady = false;
}
}
build() {
Column() {
Text('图像超分辨率重建')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ top: 20, left: 20, bottom: 4 })
Text('API 26+ | CoreVisionKit ImageSR')
.fontSize(11)
.fontColor('#999999')
.width('100%')
.padding({ left: 20, bottom: 16 })
Text('输入图片(低分辨率)')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
.padding({ left: 20, bottom: 6 })
if (this.inputImage) {
Image(this.inputImage)
.objectFit(ImageFit.Contain)
.width('90%')
.height(160)
.borderRadius(12)
.border({ width: 1, color: '#e0e0e0' })
.backgroundColor('#f8f8f8')
} else {
Column() {
Text('暂无图片')
.fontSize(14)
.fontColor('#999999')
}
.width('90%')
.height(160)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#f8f8f8')
.borderRadius(12)
.border({ width: 1, color: '#e0e0e0' })
}
Text('输出图片(超分结果)')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
.padding({ left: 20, top: 16, bottom: 6 })
if (this.outputImage) {
Image(this.outputImage)
.objectFit(ImageFit.Contain)
.width('90%')
.height(160)
.borderRadius(12)
.border({ width: 1, color: '#e0e0e0' })
.backgroundColor('#f8f8f8')
} else {
Column() {
Text('等待超分处理...')
.fontSize(14)
.fontColor('#cccccc')
}
.width('90%')
.height(160)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#f8f8f8')
.borderRadius(12)
.border({ width: 1, color: '#e0e0e0' })
}
Text(this.statusText)
.fontSize(13)
.fontColor('#666666')
.width('90%')
.margin({ top: 12, bottom: 8 })
Column({ space: 12 }) {
Button('选择图片')
.type(ButtonType.Capsule)
.fontColor(Color.White)
.backgroundColor('#007aff')
.width('80%')
.height(44)
.onClick(() => void this.selectImage())
Button('图像超分')
.type(ButtonType.Capsule)
.fontColor(Color.White)
.backgroundColor(this.inputImage !== undefined && this.analyzerReady ? '#34c759' : '#c0c0c0')
.width('80%')
.height(44)
.enabled(this.inputImage !== undefined && this.analyzerReady && !this.isProcessing)
.onClick(() => {
if (!this.inputImage || !this.analyzer) {
this.statusText = '请先选择图片并等待分析器就绪';
return;
}
void this.processSuperResolution();
})
if (!this.analyzerReady) {
Button(this.isInitializing ? '初始化中...' : '重新初始化')
.type(ButtonType.Capsule)
.fontColor(Color.White)
.backgroundColor('#ff9500')
.width('80%')
.height(44)
.enabled(!this.isInitializing)
.onClick(() => void this.initAnalyzer())
}
Button('清除结果')
.type(ButtonType.Capsule)
.fontColor('#666666')
.backgroundColor('#f0f0f0')
.width('80%')
.height(44)
.onClick(() => {
this.inputImage = undefined;
this.outputImage = undefined;
this.statusText = this.analyzerReady ? '已清除,可以重新选择图片' : '等待分析器就绪...';
})
}
.width('100%')
.padding({ top: 8 })
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
.alignItems(HorizontalAlign.Start)
}
private async selectImage(): Promise<void> {
let uri = await this.openPhoto();
if (!uri) {
this.statusText = '未选择图片';
return;
}
this.statusText = '正在加载图片...';
await this.loadImage(uri);
}
private async openPhoto(): Promise<string> {
try {
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
const picker = new photoAccessHelper.PhotoViewPicker();
const res = await picker.select(options);
if (res.photoUris && res.photoUris.length > 0) {
return res.photoUris[0];
}
return '';
} catch (e) {
const err = toAnalyzerError(e as Object);
hilog.error(DOMAIN, TAG, 'PhotoPicker failed: ' + err.code.toString() + ', ' + err.message);
this.statusText = '选择图片失败: ' + err.message;
return '';
}
}
private async loadImage(uri: string): Promise<void> {
let fileSource: fileIo.File | undefined = undefined;
try {
fileSource = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
const imageSource = image.createImageSource(fileSource.fd);
this.inputImage = await imageSource.createPixelMap();
this.outputImage = undefined;
this.statusText = this.analyzerReady ?
'图片已加载,点击"图像超分"开始处理' :
'图片已加载,等待超分分析器就绪...';
await imageSource.release();
hilog.info(DOMAIN, TAG, 'Image loaded successfully');
} catch (e) {
const err = toAnalyzerError(e as Object);
hilog.error(DOMAIN, TAG, 'Load image failed: ' + err.code.toString() + ', ' + err.message);
this.statusText = '加载图片失败: ' + err.message;
} finally {
if (fileSource) {
await fileIo.close(fileSource);
}
}
}
private async processSuperResolution(): Promise<void> {
if (!this.inputImage || !this.analyzer) {
this.statusText = '分析器未就绪,无法处理';
return;
}
this.isProcessing = true;
this.statusText = '正在超分处理中,请稍候...';
try {
const imageData: visionBase.ImageData = {
pixelMap: this.inputImage
};
const request: visionBase.Request = {
inputData: imageData
};
hilog.info(DOMAIN, TAG, 'Start process super resolution');
const response: imageSuperResolution.ISPResponse =
await this.analyzer.process(request);
this.outputImage = response.pixelMap;
this.statusText = '超分完成!对比上下两张图片的效果差异';
hilog.info(DOMAIN, TAG, 'Super resolution completed successfully');
} catch (e) {
const err = toAnalyzerError(e as Object);
hilog.error(DOMAIN, TAG, 'Super resolution failed: code=' + err.code.toString() + ', msg=' + err.message);
this.statusText = '超分处理失败: [' + err.code.toString() + '] ' + err.message;
} finally {
this.isProcessing = false;
}
}
}
注意事项
1. 必须用真机,不支持模拟器
CoreVisionKit 暂不支持模拟器。在模拟器上 ImageSRAnalyzer.create() 会永远挂起,既不成功也不报错。这是很多人卡住的根本原因。
2. 首次创建需要联网下载 AI 模型
ImageSRAnalyzer.create() 首次调用时,系统会自动下载超分所需的 AI 模型文件。如果设备没联网(或没配 INTERNET 权限),这个下载过程无法完成,Promise 就会一直 pending。所以:
module.json5里必须声明ohos.permission.INTERNET权限- 设备必须联网
- 给
create()加超时保护,别让用户无限等
3. create() 必须加超时
综合前两点,ImageSRAnalyzer.create() 在不满足条件时不会 reject,只会挂起。所以一定要用 Promise.race 配合 setTimeout 做超时兜底,超时后给用户明确的提示(检查真机/联网/系统版本)。
4. 不要并发调用
官方文档明确说:CoreVisionKit 不支持同一进程同一时间多次调用同一个特性。如果 aboutToAppear 里的 create() 还没完成,用户又点了「重新初始化」,会出问题。加个 isInitializing 标志位防重复。
5. 分析器要主动销毁
ImageSRAnalyzer 是 native 层资源,不主动销毁会造成内存泄漏。建议在 aboutToDisappear 里调用 destroy()。
6. 输入图片尺寸限制
官方约束:16px < 宽度 < 2048px,16px < 高度 < 2048px。超出这个范围的图片无法处理。如果原图太大,需要先缩放;如果太小(比如 8x8 的图标),也不行。
7. 图片解码不要阻塞主线程
fileIo.open 和 imageSource.createPixelMap 都是异步的,用 await 没问题。但如果图片比较大,解码耗时较长,界面最好给个 loading 提示,避免用户以为卡死了。
8. 地区限制
CoreVisionKit 仅适用于中国境内(香港特别行政区、澳门特别行政区、中国台湾除外)。境外设备调用会失败。
9. PhotoSelectOptions 替代废弃的 select 参数
旧写法 photoPicker.select({ MIMEType: ..., maxSelectNumber: ... }) 直接传对象参数的方式已废弃,应该先用 new PhotoSelectOptions() 创建选项对象再传给 select()。
10. ArkTS 语法限制
- 不要用
in运算符检查属性(ArkTS 禁止) - 不要用
as BusinessError类型断言(ArkTS 不推荐),用instanceof Error代替 - 不要用模板字符串
`${value}`,用字符串拼接"prefix" + value.toString() catch (e)不要加类型注解
效果怎么样
说实话,超分不是魔法。对于太糊的图片(比如 100x100 的缩略图),重建出来的细节会有一定的 AI 脑补痕迹,不会和原生高清图一模一样。
但对于「稍微有点糊,但还能看出轮廓」的图片,效果是惊喜的。尤其是老照片扫描件、旧手机拍的照片,超分之后文字的边缘、人脸的五官轮廓都会有明显改善。
我拿一张 320x240 的旧图试了一下,处理后分辨率提升明显,细节也丰富了不少。放在手机上对比看,差异很大。
总结
图像超分辨率重建是 HarmonyOS 7.0 里一个很实用的 AI 能力,接入本身不复杂,但有几个容易踩的坑:
- 导入
@kit.CoreVisionKit,声明INTERNET+READ_IMAGEVIDEO权限 - 创建
ImageSRAnalyzer实例,必须加超时保护 - 从图库读取图片 → 解码为 PixelMap
- 调用
analyzer.process得到超分结果 - 用完记得销毁分析器
- 只能在真机上跑,模拟器不行
- 首次需要联网下载模型
如果你在做的应用里有图片展示、老照片修复、缩略图增强之类的场景,可以试试这个功能。代码量不大,但对用户体验的提升是很直接的。
更多推荐
所有评论(0)