AR Engine图像跟踪实战封面

这次做的是一张会“被 AR 召唤”的图片:相机找到参考图后,四角框会贴住图片边缘,中心出现一根能量柱;手机移动或倾斜时,四角和能量柱跟着图像一起变化。

实验没有停在“图片成功加入数据库”。真机先识别了相册加入的实体书封,又识别了工程内置的召唤卡。同一个数据库包含两张图,两张图分别产生自己的名称、索引、状态和位姿。

先看实体书封的结果。顶部已经进入 TRACKING,四角框覆盖书封所在平面,中心准星和能量柱也落在书封上:

实体书封进入TRACKING

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

内置召唤卡进入TRACKING

本次结果

项目 真机结果
设备 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 的实时位姿。

官方资料:

Logo

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

更多推荐