先说两句

前段时间翻到一张老照片,像素太低了,人脸都糊成一团。想着有没有什么办法能把它变清楚,正好看到 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 里的 imageSuperResolutionvisionBase

权限

需要在 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 < 宽度 < 2048px16px < 高度 < 2048px。超出这个范围的图片无法处理。如果原图太大,需要先缩放;如果太小(比如 8x8 的图标),也不行。

7. 图片解码不要阻塞主线程

fileIo.openimageSource.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 能力,接入本身不复杂,但有几个容易踩的坑:

  1. 导入 @kit.CoreVisionKit,声明 INTERNET + READ_IMAGEVIDEO 权限
  2. 创建 ImageSRAnalyzer 实例,必须加超时保护
  3. 从图库读取图片 → 解码为 PixelMap
  4. 调用 analyzer.process 得到超分结果
  5. 用完记得销毁分析器
  6. 只能在真机上跑,模拟器不行
  7. 首次需要联网下载模型

如果你在做的应用里有图片展示、老照片修复、缩略图增强之类的场景,可以试试这个功能。代码量不大,但对用户体验的提升是很直接的。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐