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

《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('相机权限被拒绝');
}
}
最佳实践总结
-
统一使用
imageDecode处理所有非实时场景:不管是图库选图、拍照、截图还是视频帧,只要最终能拿到PixelMap,就可以复用同一套识别逻辑。 -
注意
InputImage的尺寸来源:InputImage创建时传入的尺寸必须和PixelMap实际尺寸一致,否则识别率会急剧下降。不要手动计算或猜测。 -
动态模块加载务必异步处理:Scan Kit 是远程模块,
import()是异步操作。如果在 UI 主线程中同步调用,会导致应用卡死。 -
限定码制可以提升准确率:如果业务场景只支持二维码或者只支持 CODE128,可以在
DecodeOptions中指定scanTypes,避免错误识别其他码制。这能显著提升用户体验。 -
生命周期管理:相机和图片读取都涉及 Activity(Ability)的生命周期,建议在
onPageShow/onPageHide中统一管理相机关闭和资源释放。
FAQ:真实开发视角
Q:为什么模拟器上 selectPhotoUri() 报错,真机正常?
A:模拟器通常没有完整的媒体库服务,photoAccessHelper 的很多接口在模拟器上行为不确定。建议一律以真机为准。
Q:imageDecode 是否支持批量识别(一张图里多个二维码)?
A:支持。DecodeOptions 中可以开启 multiScan 开关。开启后,ScanService.imageDecode 返回的 originValue 会是一个数组。注意:默认只识别第一个。
Q:为什么我使用 InputImage.createFromPath() 总是失败?
A:最常见的原因是路径权限问题。确保文件路径是应用可读的,并且传入的是沙箱路径,而不是 content:// 类型的 URI。如果是 URI,需要先拷贝到应用沙箱再读取。
更多推荐



所有评论(0)