HarmonyOS AR Engine图像跟踪实操——动态加入书封与双目标 AR 叠加

这次做的是一张会“被 AR 召唤”的图片:相机找到参考图后,四角框会贴住图片边缘,中心出现一根能量柱;手机移动或倾斜时,四角和能量柱跟着图像一起变化。
实验没有停在“图片成功加入数据库”。真机先识别了相册加入的实体书封,又识别了工程内置的召唤卡。同一个数据库包含两张图,两张图分别产生自己的名称、索引、状态和位姿。
先看实体书封的结果。顶部已经进入 TRACKING,四角框覆盖书封所在平面,中心准星和能量柱也落在书封上:

随后把相机转向电脑上的内置召唤卡,目标切换为 AR召唤卡,四角重新贴合第二张图:

本次结果
| 项目 | 真机结果 |
|---|---|
| 设备 | HUAWEI Mate 60 Pro |
| 系统 | HarmonyOS 7.0 |
| SDK | API 26 |
| IMAGE 能力 | 支持 |
| 内置卡入库 | state=0 / reason=0 / index=0 |
| 相册书封入库 | state=0 / reason=0 / index=1 |
| 数据库数量 | 2/50 |
| 首次识别 | 8714 ms |
| 书封跟踪 | 真实书封 / index 1 / TRACKING |
| 内置卡跟踪 | AR召唤卡 / index 0 / TRACKING |
| 采样异常 | 0 |
实验准备
本次需要这些内容:
- 一台支持
ARENGINE_FEATURE_TYPE_IMAGE的 HarmonyOS 真机; - CAMERA 权限;
- 一张工程内置参考卡;
- 一本封面纹理清晰的书;
- 一张正对书封拍摄的照片,四边完整、没有明显反光;
- 另一块用于显示内置参考卡的屏幕。
页面使用 ARType.IMAGE,只打开图像跟踪所需的相机和自动对焦。平面、语义、深度和 Mesh 在这个实验里都关闭。
整条数据链路
参考图先变成 PixelMap 并加入 ARAugmentedImageDatabase。相机帧和数据库中的图像匹配成功后,AR Engine 返回 ARAugmentedImage;页面再读取它的状态、名称、索引、范围和中心位姿,最后把图像平面的四个角投影到 Canvas。

先检查权限和 IMAGE 能力
页面入口先检查相机权限和 IMAGE 能力。任何一个条件不满足,都不会创建 IMAGE 会话。
private startImageTracking(): void {
if (!this.cameraGranted) {
this.latestMessage =
'未获得相机权限,不启动图像跟踪';
return;
}
const supported: boolean =
arViewController.isARTypeSupported(
arEngine.ARFeatureType
.ARENGINE_FEATURE_TYPE_IMAGE);
if (!supported) {
this.latestMessage =
'当前设备不支持 IMAGE 能力';
return;
}
hilog.info(DOMAIN, TAG,
'IMAGE_GUARD camera=true image=true');
this.showImageTracking = true;
}
本次真机门禁通过:
IMAGE_GUARD camera=true image=true
IMAGE_GUARD image=true
建立 IMAGE 会话
IMAGE 会话的配置很短。addAugmentedImageMode 使用普通模式,自动对焦保持开启:
const scene: Scene = await Scene.load();
const context =
new arViewController.ARViewContext();
context.scene = scene;
context.callback = this.callback;
context.config = {
type: arEngine.ARType.IMAGE,
planeFindingMode:
arEngine.ARPlaneFindingMode.DISABLED,
powerMode: arEngine.ARPowerMode.NORMAL,
semanticMode: arEngine.ARSemanticMode.NONE,
poseMode: arEngine.ARPoseMode.GRAVITY,
depthMode: arEngine.ARDepthMode.DISABLED,
meshMode: arEngine.ARMeshMode.DISABLED,
focusMode: arEngine.ARFocusMode.AUTO,
addAugmentedImageMode:
arEngine.ARAddAugmentedImageMode.NORMAL
};
await context.init();
this.arContext = context;
真机会话建立用了 35 ms:
IMAGE_SESSION_INIT success costMs=35
type=128 addMode=0 focus=1
把内置参考卡加入数据库
内置 PNG 从资源中读取,解码为 1200 × 900 的 PixelMap,随后创建图像数据库并调用 addImage():
const mediaBytes: Uint8Array =
await this.hostContext.resourceManager
.getMediaContent(
$r('app.media.image_tracking_target').id);
const source: image.ImageSource =
image.createImageSource(mediaBytes.buffer);
const pixelMap: image.PixelMap =
await source.createPixelMap({
desiredSize: {
width: 1200,
height: 900
},
desiredPixelFormat:
image.PixelMapFormat.RGBA_8888,
editable: false
});
const database =
await arEngine
.createARAugmentedImageDatabase();
database.setImageAddMode(
arEngine.ARAddAugmentedImageMode.NORMAL);
const result = await database.addImage(
'AR召唤卡', pixelMap, 0.18);
这一步的真实返回是:
IMAGE_DATABASE_ADD state=0 reason=0 index=0
count=1 capacity=50 widthMeters=0.180
state=0 只表示图片通过了入库检查,不等于相机已经识别成功。真正的成功还要等 ARAugmentedImage.state 进入 TRACKING。
从相册动态加入书封照片
页面底部的“选择书封”会拉起系统 Photo Picker。它只选择一张照片,不读取整个图库:
const options =
new photoAccessHelper.PhotoSelectOptions();
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes
.IMAGE_TYPE;
options.maxSelectNumber = 1;
options.isOriginalSupported = true;
const picker =
new photoAccessHelper.PhotoViewPicker();
const result = await picker.select(options);
const uri: string = result.photoUris[0];
Picker 返回的是媒体 URI。本次第一次直接执行 image.createImageSource(uri),得到的 ImageSource 为空,随后出现:
IMAGE_GALLERY_ADD error=-1
Cannot read property getImageInfo of undefined
最终处理方式是先以只读方式打开媒体 URI,再把文件描述符交给图片解码器:
const selectedFile: fileIo.File =
fileIo.openSync(
uri, fileIo.OpenMode.READ_ONLY);
const imageSource: image.ImageSource =
image.createImageSource(selectedFile.fd);
try {
const info: image.ImageInfo =
await imageSource.getImageInfo();
// 按原始比例计算解码尺寸并创建 PixelMap
} finally {
await imageSource.release();
fileIo.closeSync(selectedFile.fd);
}
书封原图是 3072 × 4096。页面把最长边限制到 1600,同时保持原始宽高比,因此解码结果是 1200 × 1600,没有把竖版书封硬拉成横图。
const longEdge: number =
Math.max(sourceWidth, sourceHeight);
const scale: number =
longEdge > 1600 ? 1600 / longEdge : 1;
const decodeWidth: number =
Math.round(sourceWidth * scale);
const decodeHeight: number =
Math.round(sourceHeight * scale);
同一个数据库继续调用 addImage(),书封成为第二张图:
IMAGE_GALLERY_ADD state=0 reason=0 index=1 count=2
sourceWidth=3072 sourceHeight=4096
decodeWidth=1200 decodeHeight=1600
widthMeters=0.180
从每一帧读取图像 Trackable
相机回调每 100 ms 最多采样一次。每轮先读取更新的 AUGMENTED_IMAGE;当前帧没有更新对象时,再从会话读取全部图像对象,避免已经识别的目标在静止时突然从页面消失。
frame = session.getFrame();
const updated = frame.getUpdatedTrackables(
arEngine.ARTrackableType.AUGMENTED_IMAGE);
trackables = updated.length > 0
? updated
: session.getAllTrackables(
arEngine.ARTrackableType.AUGMENTED_IMAGE);
const candidate =
this.firstTrackedImage(trackables);
if (!candidate ||
candidate.state !==
arEngine.ARTrackingState.TRACKING) {
return;
}
const pose: arEngine.ARPose =
candidate.getPose();
this.trackedName = candidate.name;
this.trackedIndex = candidate.index;
this.extendX = candidate.extendX;
this.extendZ = candidate.extendZ;
书封首次进入跟踪用了 8714 ms:
IMAGE_TRACKING_ENTER count=1 sample=83 index=1
IMAGE_FIRST_TRACK latencyMs=8714
name=真实书封 index=1
extendX=1.0000 extendZ=1.3333
IMAGE_POSE sample=83 state=TRACKING
centerX=0.023 centerY=0.111 centerZ=-1.307
distance=1.312
手机继续移动后,中心位姿持续变化:
IMAGE_POSE sample=104 name=真实书封 index=1
centerX=0.042 centerY=-0.003 centerZ=-1.511
distance=1.511
IMAGE_POSE sample=109 name=真实书封 index=1
centerX=-0.073 centerY=0.044 centerZ=-1.565
distance=1.567
把图像四角投影到屏幕
ARAugmentedImage 的位姿位于参考图中心。页面先在图像局部坐标中构造四个角,再依次经过图像 Pose、相机 View 矩阵和 Projection 矩阵,得到 Canvas 坐标。
const localCorners: number[][] = [
[-extendX / 2, 0, -extendZ / 2],
[ extendX / 2, 0, -extendZ / 2],
[ extendX / 2, 0, extendZ / 2],
[-extendX / 2, 0, extendZ / 2]
];
const camera: arEngine.ARCamera =
frame.getCamera();
const viewMatrix: Array<number> =
camera.viewMatrix;
const projectionMatrix: Array<number> =
camera.getProjectionMatrix(0.05, 20.0);
四个屏幕点连成目标轮廓,中心点用于绘制准星和能量柱。它们不是固定在屏幕中央的装饰,所以相机倾斜后仍会贴住参考图。
两张图如何区分
数据库给每张参考图分配独立索引:
| 图像 | 名称 | 索引 | 真机状态 |
|---|---|---|---|
| 工程内置卡 | AR召唤卡 |
0 | TRACKING |
| 相册书封 | 真实书封 |
1 | TRACKING |
页面优先选择当前处于 TRACKING 的图像对象。相机从书封转向内置卡时,目标名和索引随 Trackable 切换,四角也用新目标的 Pose 重新计算。
遇到的三个情况
1. 参考图质量不够
最早的纯矢量卡没有通过图像质量检查:
IMAGE_DATABASE_ADD state=1009200012
reason=3 FEATURE_LIMIT count=0
提高纹理后又遇到光照异常:
IMAGE_DATABASE_ADD state=1009200012
reason=2 LIGHT_ANOMALY count=0
最终参考图增加了分散纹理,并重新调整亮度,才得到 state=0 / reason=0。
2. 能入库,但相机一开始匹配不到
底层日志曾连续出现:
matches too few and take little region
can't estimate scale
homography is invalid
这表示相机画面中的有效匹配区域太小,或者透视、清晰度、反光导致尺度与单应矩阵无法建立。把完整图片放进取景框、保持对焦,并使用纹理更清晰的真实书封后,两个目标都进入了 TRACKING。
3. 本机返回的范围不是实际厘米值
两张图片登记宽度都传入 0.18 m,但本机返回的数据分别是:
真实书封 extendX=1.0000 extendZ=1.3333
AR召唤卡 extendX=1.0000 extendZ=0.7500
它们与两张图的 3:4、4:3 宽高比一致,但不是实际 18 cm。早期调试页直接乘以 100 并显示 cm,真机截图中因此出现了 100.0×133.3cm。最终页面改为显示不带物理单位的“范围”和“位姿”,文章保留这次真实现象,不把归一化数据伪装成物理测量结果。
暂停、恢复和销毁
拉起相册前先暂停 AR 会话,选图返回后再恢复:
IMAGE_PICKER_PAUSE sample=5
IMAGE_PICKER_RESUME sample=5
页面按钮也覆盖暂停、恢复和销毁。销毁顺序是先停止 ARViewContext,再释放图像数据库和所有仍持有的 PixelMap。
最终真机复测先在第 531 次采样暂停,等待约 2 秒后恢复时仍然是第 531 次;恢复后继续采样到 543,再执行销毁。汇总中的 pending=0 / errors=0:
IMAGE_SESSION_PAUSE sample=531
IMAGE_SESSION_RESUME sample=531 locked=false
IMAGE_SESSION_DESTROY sample=543 tracks=4 lost=3
pending=0 errors=0 locked=false
private async destroyARView(): Promise<void> {
if (this.isDestroyed) {
return;
}
this.isDestroyed = true;
const context = this.arContext;
this.arContext = undefined;
if (context) {
await context.destroy();
}
await this.releaseImageResources();
}
每轮采样取得的 Pose、Trackable 和 ARFrame 也要释放:
if (pose) {
await pose.release();
}
for (const trackable of trackables) {
await trackable.release();
}
if (frame) {
await frame.release();
}
最终效果
本次完成了从资源图片入库、系统相册动态选图、双目标识别、图像位姿读取到四角叠加的完整链路。实体书封和屏幕召唤卡都在同一数据库中进入了 TRACKING,名称与索引能够区分,移动后的中心位姿持续变化,异常计数保持为 0。
实验里同样保留了失败分支:低特征图、亮度异常、媒体 URI 直接解码失败、相机匹配点不足和范围单位不一致都没有被隐藏。最后能看到的四角、中心准星和能量柱,全部来自真机 IMAGE Trackable 的实时位姿。
官方资料:
更多推荐


所有评论(0)