在这里插入图片描述

《HarmonyOS技术精讲-Scan Kit:图像识码——从图片中提取信息》

实际开发中的“扫图”需求

很多开发者在接入 Scan Kit 时,第一个想到的场景往往是实时扫描——也就是打开摄像头直接扫码。这个能力官方文档讲得很详细,示例代码也跑得通。

但实际项目里,非实时扫描的需求一点也不少:

  • 图库选图识别:用户相册里有一张二维码截图,想识别里面的内容。
  • 截图识别:应用内业务截图后,提取其中的条码信息。
  • 视频抽帧识别:监控视频或者视频文件里,有一个二维码短暂闪过,需要从帧中识别出来。

摄像头扫码是“主动感知”,而图像/视频帧扫码是“被动提取”。前者依赖相机流的持续输入,后者则基于静态资源(PixelMap / 文件路径)。这个区别决定了 API 的选择和使用方式完全不同。

核心:imageDecode 接口

Scan Kit 用来处理静态图像和视频帧的核心接口是 imageDecode,属于 @hms.sdk.scan.kit 模块。

比起相机扫码的 startScan 流程,imageDecode 的参数简单很多,但坑也不少。它的核心输入是 InputImage 对象。

InputImage 支持两种构建方式:

输入源 适用场景 创建方式
PixelMap 内存中的图片(如截图、本地已读入内存的图片) InputImage.create(pixelMap, width, height)
文件路径 文件系统中的图片(如从图库获取的 uri) InputImage.createFromPath(filePath, width, height)

实际开发中,从图库或者拍照拿到的通常是 URI 或者 PixelMap,所以两种方式都得会。

环境说明

  • DevEco Studio 版本:DevEco Studio 6.1.0 及以上
  • HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
  • 目标设备:手机

核心实现:图库选图 + Camera拍照识别

1. 准备权限与配置

Scan Kit 本身依赖于权限,但 imageDecode 不要求相机权限。如果你有拍照流程,相机权限是必须的。另外,从图库读取图片也需要文件读取权限。

// module.json5
{
  "module": {
    // ...
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA"
      },
      {
        "name": "ohos.permission.READ_MEDIA"
      }
    ]
  }
}

2. 页面布局与状态管理

这里我直接用一个完整的页面来展示整个流程。包含一个 Image 组件展示图片,两个按钮分别触发“从图库选择”和“拍照扫描”,以及一个文本区域展示识别结果。

// pages/ImageDecodeDemo.ets
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct ImageDecodeDemo {
  @State selectedImagePixelMap: image.PixelMap | null = null;
  @State scanResult: string = '请选择图片或拍照';
  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;

  build() {
    Column() {
      // 显示图片
      if (this.selectedImagePixelMap) {
        Image(this.selectedImagePixelMap)
          .width('80%')
          .height(300)
          .objectFit(ImageFit.Contain)
          .margin({ bottom: 20 })
      } else {
        Text('请选择一张包含二维码/条码的图片')
          .width('80%')
          .height(300)
          .backgroundColor('#f0f0f0')
          .textAlign(TextAlign.Center)
          .margin({ bottom: 20 })
      }

      // 扫描结果
      Text('识别结果:')
      Text(this.scanResult)
        .width('80%')
        .maxLines(5)
        .margin({ bottom: 10 })

      // 按钮区域
      Row({ space: 20 }) {
        Button('从图库选择')
          .onClick(() => this.selectFromGallery())
        Button('拍照识别')
          .onClick(() => this.takePhotoAndDecode())
      }
    }
    .width('100%')
    .padding(20)
  }

  // ... 方法实现
}

3. 从图库选择图片并识别

这个流程是:打开系统相册 → 用户选图 → 获取图片 URI → 读取为 PixelMap → 调用 Scan Kit 的 imageDecode

  async selectFromGallery() {
    try {
      // 1. 获取相册访问能力
      let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);

      // 2. 启动系统相册选择器
      let uri = await phAccessHelper.selectPhotoUri();

      // 3. 获取Uri并创建PixelMap
      let file = this.context.getFile(uri);
      let imageSource = image.createImageSource(file);
      let pixelMap = await imageSource.createPixelMap();

      // 4. 更新UI,显示图片
      this.selectedImagePixelMap = pixelMap;

      // 5. 调用识别
      await this.decodePixelMap(pixelMap);
    } catch (err) {
      console.error(`选择图片失败: ${err}`);
      this.scanResult = '选择图片失败';
    }
  }

  async decodePixelMap(pixelMap: image.PixelMap) {
    try {
      // 动态导入 Scan Kit 模块
      const scanKit = await import('@hms.sdk.scan.kit');

      // 获取图片尺寸
      let imageInfo = await pixelMap.getImageInfo();
      let width = imageInfo.size.width;
      let height = imageInfo.size.height;

      // 创建 InputImage
      let inputImage = scanKit.InputImage.create(pixelMap, width, height);

      // 配置识别选项(可选)
      let options = new scanKit.DecodeOptions();
      // 支持所有码制,可以根据业务需要限制
      // options.scanTypes = [scanKit.ScanType.QR_CODE, scanKit.ScanType.CODE128];

      // 调用识别
      let result = await scanKit.ScanService.imageDecode(inputImage, options);

      if (result && result.originValue) {
        this.scanResult = result.originValue;
      } else {
        this.scanResult = '未识别到有效码';
      }

    } catch (err) {
      console.error(`识别失败: ${err}`);
      this.scanResult = `识别失败: ${err.message}`;
    }
  }

关键点说明

  • 动态导入@hms.sdk.scan.kit 是远程模块,必须用动态导入,否则编译期会报错。
  • selectPhotoUri():这个方法在部分旧版本SDK中可能返回 undefined,需要做好防御。最新版本已经稳定。
  • getImageInfo():PixelMap 的尺寸信息是构造 InputImage 的必填参数,尺寸必须和 PixelMap 实际尺寸一致,否则底层图像裁剪会出问题。

4. 拍照识别

拍照的流程是:打开相机 → 获取照片 → 读取为 PixelMap → 调用 imageDecode

  async takePhotoAndDecode() {
    try {
      // 1. 获取相机实例(省略详细的相机初始化代码,只做示例)
      let cameraManager = camera.getCameraManager(this.context);
      let cameraDevice = await cameraManager.getSupportedCameras();
      if (cameraDevice.length === 0) {
        this.scanResult = '未找到相机设备';
        return;
      }

      // 2. 创建相机输入和输出(这里仅展示核心逻辑,真实开发需要完整的生命周期管理)
      let photoOutput = await cameraManager.createPhotoOutput(
        // ..... 相机配置代码
      );

      // 3. 拍照并获取照片
      let captureResult = await new Promise<image.PixelMap>((resolve, reject) => {
        photoOutput.on('photoAvailable', (err, photo) => {
          if (err) {
            reject(err);
            return;
          }
          let mainImage = photo.main;
          // 读取为 PixelMap
          let receiver = image.createImageReceiver(mainImage.size.width, mainImage.size.height);
          // ..... 完整的像素读取代码
          resolve(pixelMap);
        });
        photoOutput.capture(/* 配置参数 */);
      });

      // 4. 显示并识别
      this.selectedImagePixelMap = captureResult;
      await this.decodePixelMap(captureResult);

    } catch (err) {
      console.error(`拍照识别失败: ${err}`);
      this.scanResult = '拍照识别失败';
    }
  }

注意:上面的相机代码只是一个示意结构,实际 Camera Kit 的初始化、会话管理、输出配置代码量比较大(大约 100+ 行)。这里为了专注 Scan Kit 部分进行了简化。

5. 视频帧扫描示例(重要)

视频帧扫描和图片扫描本质一样,只是输入源变成了视频的某一帧。如果你在视频播放中截取了一帧,拿到它的 PixelMap,就可以通过 imageDecode 识别。

  async decodeVideoFrame(pixelMap: image.PixelMap) {
    // 和图片识别的代码完全一致
    await this.decodePixelMap(pixelMap);
  }

所以核心不在于 Scan Kit 是否有专门的高性能接口,而在于你的业务逻辑如何从视频中抽帧。 如果需要实时抽帧(比如监控视频),可以考虑使用 AVCodec Kit 的视频解码能力,在解码后的帧中选取关键帧,再调用 imageDecode

常见问题与踩坑记录

问题 1:动态模块加载失败

现象import('@hms.sdk.scan.kit') 在真机上提示模块不存在或者导入超时。

原因@hms.sdk.scan.kit 是一个远程模块,需要提前在 DevEco Studio 中添加依赖。很多新手会漏掉 oh-package.json5 中的配置。

解决方案
检查 oh-package.json5 是否有如下依赖:

{
  "dependencies": {
    "@hms.sdk.scan.kit": "^3.0.0"
  }
}

同时确认设备已登录华为账号,并且网络正常(远程库需要下载)。

问题 2:InputImage 尺寸不匹配导致识别不到

现象:图片有清晰的二维码,但识别结果始终是空或者“未识别到”。

原因InputImage.create() 传入的 width 和 height 如果大于 PixelMap 的实际尺寸,或者尺寸和 PixelMap 不匹配,底层图像解码会失败。

解决方案必须通过 pixelMap.getImageInfo() 获取真实尺寸,不要假设或硬编码。

let imageInfo = await pixelMap.getImageInfo();
let inputImage = scanKit.InputImage.create(pixelMap, imageInfo.size.width, imageInfo.size.height);

问题 3:权限申请在拍照场景失效

现象:点击“拍照识别”按钮,直接闪退或报 Permission denied

原因:相机权限是运行时权限,需要在调用相机 API 之前动态申请。很多人只配了 module.json5 的权限列表,但忘了 requestPermissionsFromUser

解决方案:调用相机前手动请求权限。

async function requestCameraPermission(context: common.UIAbilityContext): Promise<void> {
  let atManager = abilityAccessCtrl.createAtManager();
  let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
  if (grantStatus.authResults[0] !== 0) {
    throw new Error('相机权限被拒绝');
  }
}

最佳实践总结

  1. 统一使用 imageDecode 处理所有非实时场景:不管是图库选图、拍照、截图还是视频帧,只要最终能拿到 PixelMap,就可以复用同一套识别逻辑。

  2. 注意 InputImage 的尺寸来源InputImage 创建时传入的尺寸必须和 PixelMap 实际尺寸一致,否则识别率会急剧下降。不要手动计算或猜测。

  3. 动态模块加载务必异步处理:Scan Kit 是远程模块,import() 是异步操作。如果在 UI 主线程中同步调用,会导致应用卡死。

  4. 限定码制可以提升准确率:如果业务场景只支持二维码或者只支持 CODE128,可以在 DecodeOptions 中指定 scanTypes,避免错误识别其他码制。这能显著提升用户体验。

  5. 生命周期管理:相机和图片读取都涉及 Activity(Ability)的生命周期,建议在 onPageShow / onPageHide 中统一管理相机关闭和资源释放。

FAQ:真实开发视角

Q:为什么模拟器上 selectPhotoUri() 报错,真机正常?
A:模拟器通常没有完整的媒体库服务,photoAccessHelper 的很多接口在模拟器上行为不确定。建议一律以真机为准。

Q:imageDecode 是否支持批量识别(一张图里多个二维码)?
A:支持。DecodeOptions 中可以开启 multiScan 开关。开启后,ScanService.imageDecode 返回的 originValue 会是一个数组。注意:默认只识别第一个

Q:为什么我使用 InputImage.createFromPath() 总是失败?
A:最常见的原因是路径权限问题。确保文件路径是应用可读的,并且传入的是沙箱路径,而不是 content:// 类型的 URI。如果是 URI,需要先拷贝到应用沙箱再读取。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐