HarmonyOS AR Engine 运动跟踪实操:实时读取设备位姿

设备位姿运动轨迹封面

本次实验做什么

第一篇已经完成设备能力检测、相机权限申请和 AR 会话管理。本次在同一个工程中增加“实验 02”,从 AR 会话的每一帧中读取设备位姿,并把以下数据直接显示在相机画面上:

  • 设备在 AR 世界坐标系中的 X、Y、Z 位置;
  • 表示设备朝向的四元数 x、y、z、w;
  • 设备相对当前原点移动的距离;
  • 设备相对当前原点旋转的角度;
  • 当前跟踪状态和已经处理的采样数。

页面同时保留“重置原点、暂停/恢复、销毁退出”三个操作,用于验证位姿计算和 AR 会话生命周期。

本次最终在 Mate 60 Pro 真机上取得连续位姿数据。重置原点后,相对位移和转角回到 0.000 m / 0.0°;移动并旋转手机后,画面记录到 0.727 m / 21.2°;暂停后采样数保持在 802,恢复后继续增长。

实验环境与准备

项目 本次使用值
开发工具 DevEco Studio
工程类型 HarmonyOS Phone,ArkTS
compileSdk / targetSdk API 26
真机 HUAWEI Mate 60 Pro(ALN-AL80)
真机系统 HarmonyOS 7.0.0.100,API 26
相机权限 PERMISSION_GRANTED
运动跟踪能力 SLAM 支持

本次直接沿用第一篇已经完成的权限和能力检测。首页只有在相机权限已授予、SLAM 能力受支持时,才开放“实验 02:进入运动跟踪”入口。

运动跟踪实验入口

设备位姿来自 AR Engine 建立的局部世界坐标系,单位是米;它不是经纬度,也不是 GPS 位置。数据链路如下:

AR帧到位姿数据链路

实现过程

1. 增加运动跟踪实验页

工程保留第一篇的 AR 会话页面,另外新增:

entry/src/main/ets/features/arengine/MotionTrackingPage.ets

首页点击实验 02 后,先复用相机权限与 SLAM 检查,再显示 MotionTrackingPage。退出运动跟踪页时恢复首页。

if (this.showMotionTracking) {
  MotionTrackingPage({
    onExit: () => {
      this.showMotionTracking = false;
    }
  })
} else {
  // 首页内容
}

2. 接收 AR 帧回调

位姿数据随 AR 帧更新。这里实现 ARViewCallback,把 onFrameUpdate() 收到的上下文交给页面处理。

class MotionTrackingCallback extends arViewController.ARViewCallback {
  private readonly handler:
    (context: arViewController.ARViewContext, timestamp: number) => void;

  constructor(handler:
    (context: arViewController.ARViewContext, timestamp: number) => void) {
    super();
    this.handler = handler;
  }

  onFrameUpdate(context: arViewController.ARViewContext,
    timestamp: number): void {
    this.handler(context, timestamp);
  }

  onAnchorAdd(context: arViewController.ARViewContext,
    node: Node, anchor: arEngine.ARAnchor): void {
  }

  onAnchorUpdate(context: arViewController.ARViewContext,
    node: Node, anchor: arEngine.ARAnchor): void {
  }
}

本篇只验证设备位姿,不创建锚点,所以两个锚点回调保持空实现。

3. 创建 WORLD 类型 AR 会话

页面出现时加载 ArkGraphics 3D 场景,创建 ARViewContext,设置回调和 WORLD 会话参数,然后调用 init()

private async initARView(): Promise<void> {
  const startTime: number = Date.now();
  const scene: Scene = await Scene.load();
  const context = new arViewController.ARViewContext();

  this.callback = new MotionTrackingCallback(
    (viewContext: arViewController.ARViewContext,
      timestamp: number) => {
      this.handleFrameUpdate(viewContext, timestamp);
    });

  context.scene = scene;
  context.callback = this.callback;
  context.config = {
    type: arEngine.ARType.WORLD,
    planeFindingMode:
      arEngine.ARPlaneFindingMode.HORIZONTAL_AND_VERTICAL,
    powerMode: arEngine.ARPowerMode.NORMAL,
    semanticMode: arEngine.ARSemanticMode.NONE,
    poseMode: arEngine.ARPoseMode.GRAVITY,
    depthMode: arEngine.ARDepthMode.DISABLED,
    meshMode: arEngine.ARMeshMode.DISABLED,
    focusMode: arEngine.ARFocusMode.AUTO
  };

  await context.init();
  this.arContext = context;
  hilog.info(DOMAIN, TAG,
    'MOTION_SESSION_INIT success costMs=%{public}d',
    Date.now() - startTime);
}

平面识别模式保留在会话配置中,但本篇不读取平面、命中结果和锚点;深度、Mesh、语义能力保持关闭。

4. 从每个有效帧中读取位姿

回调触发后,先从 ARViewContext.session 取得当前会话,再依次获取 ARFrameARCameraARPose

只有相机状态为 TRACKING 时才读取位姿。初始化阶段或暂时失去跟踪时,页面只更新状态,不把无效数据写入统计结果。

private handleFrameUpdate(
  context: arViewController.ARViewContext,
  timestamp: number): void {
  const now: number = Date.now();
  if (this.isDestroyed || this.isPaused ||
    now - this.lastFrameTimestamp < 100) {
    return;
  }
  this.lastFrameTimestamp = now;

  const session: arEngine.ARSession | undefined = context.session;
  if (!session) {
    return;
  }

  let frame: arEngine.ARFrame | undefined = undefined;
  let pose: arEngine.ARPose | undefined = undefined;
  try {
    frame = session.getFrame();
    const camera: arEngine.ARCamera = frame.getCamera();
    this.updateTrackingStatus(camera.state, camera.stateReason);

    if (camera.state !== arEngine.ARTrackingState.TRACKING) {
      return;
    }

    pose = camera.getPose();
    this.updatePose(pose.translation, pose.rotation);
  } finally {
    if (pose) {
      pose.release();
    }
    if (frame) {
      frame.release();
    }
  }
}

页面按约 100 ms 一次,也就是约 10 Hz,更新文字和日志。ARView 相机预览仍按自身帧率运行,节流只用于本实验的数据展示。

每次处理完成后释放 ARPoseARFrame,避免持续采样时累积原生资源。

5. 显示平移和旋转数据

ARPose.translation 提供 X、Y、Z 三个位置分量;ARPose.rotation 提供四元数 x、y、z、w。页面保留三位小数显示位置和四元数,转角保留一位小数。

首个有效位姿自动保存为原点:

private setOrigin(translation: Vec3, rotation: Quaternion): void {
  this.originTranslation = {
    x: translation.x,
    y: translation.y,
    z: translation.z
  };
  this.originRotation = {
    x: rotation.x,
    y: rotation.y,
    z: rotation.z,
    w: rotation.w
  };
  this.originReady = true;
}

相对位移使用当前位置与原点之间的三维欧氏距离:

private distanceBetween(current: Vec3, origin: Vec3): number {
  const dx: number = current.x - origin.x;
  const dy: number = current.y - origin.y;
  const dz: number = current.z - origin.z;
  return Math.sqrt(dx * dx + dy * dy + dz * dz);
}

相对转角通过两个单位四元数的点积计算。四元数 q-q 表示同一姿态,因此点积先取绝对值。

private rotationBetween(
  current: Quaternion,
  origin: Quaternion): number {
  const dot: number = Math.abs(
    current.x * origin.x +
    current.y * origin.y +
    current.z * origin.z +
    current.w * origin.w);
  const normalizedDot: number =
    Math.min(1, Math.max(0, dot));
  return 2 * Math.acos(normalizedDot) * 180 / Math.PI;
}

“重置原点”不会重建 AR 会话,只把当前有效位置和朝向保存为新的比较基准,同时把相对位移和转角清零。

6. 处理跟踪状态

实验页区分三种状态:

AR 状态 页面显示 数据处理
TRACKING 跟踪中 读取并更新位姿
PAUSED 跟踪暂停 不读取位姿,显示暂停原因
STOPPED 跟踪停止 不读取位姿,等待会话退出

EXCESSIVE_MOTION 显示为“设备移动过快”,INSUFFICIENT_FEATURES 显示为“环境纹理不足”。没有更具体原因时显示“暂时无法建立稳定跟踪”。

7. 暂停、恢复和销毁

暂停和恢复直接调用 ARViewContext.pause()resume()。暂停标记同时阻止帧处理;恢复后清空节流时间,下一次有效帧可以立即进入采样。

private pauseARView(): void {
  if (!this.arContext || this.isDestroyed || this.isPaused) {
    return;
  }
  this.arContext.pause();
  this.isPaused = true;
}

private resumeARView(): void {
  if (!this.arContext || this.isDestroyed || !this.isPaused) {
    return;
  }
  this.arContext.resume();
  this.isPaused = false;
  this.lastFrameTimestamp = 0;
}

点击“销毁退出”或页面消失时调用 destroy(),再清空页面持有的上下文和回调。

构建与安装

工程使用已配置好的签名执行 clean 构建:

$env:JAVA_HOME='D:\Program Files\Huawei\DevEco Studio\jbr'
$env:DEVECO_SDK_HOME='D:\Program Files\Huawei\DevEco Studio\sdk'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
$hvigor='D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat'

& $hvigor clean --no-daemon
& $hvigor assembleHap --mode module `
  -p product=default `
  -p module=entry@default `
  -p buildMode=debug `
  --no-daemon

本次 CompileArkTSPackageHapSignHap 全部成功,构建结果为零告警。签名包位置:

entry/build/default/outputs/default/entry-default-signed.hap

最终 HAP 大小为 312812 bytes,SHA-256 为:

B1EB789CD168F17E65F67E40E778747C5B7D3C226AC95C8734027E28D251A146

安装到当前连接的真机:

$hdc='D:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe'
& $hdc install `
  'entry\build\default\outputs\default\entry-default-signed.hap'

安装结果为 install bundle successfully

真机实操与结果

1. 进入运动跟踪

首页确认相机权限为 PERMISSION_GRANTED,设备能力矩阵中 SLAM 为“支持”,点击“实验 02:进入运动跟踪”。

ARView 初始化后先短暂处于“跟踪暂停”,相机看到有纹理的地毯后切换为“跟踪中”。最终回归日志中,会话初始化耗时 17 ms,从暂停切换到跟踪约 1.8 秒。

2. 确认位姿持续更新

进入跟踪状态后,X/Y/Z、四元数和采样数开始变化。第一次真机测试记录到第 45 个采样:位置为 X=0.097 mY=0.641 mZ=-0.051 m,相对自动原点的位移为 0.650 m

位姿已更新但重置原点按钮未启用

这三个坐标描述手机在本次 AR 会话局部坐标系中的位置,坐标正负方向随会话建立的坐标系确定。图中“重置原点”仍是灰色,这是第一次测试时发现的问题,修正过程记录在后文“情况四”;最终重置结果使用修正后的页面重新采集。

3. 重置当前原点

手机保持当前姿态,点击“重置原点”。页面继续显示绝对位姿,同时把相对位移和相对转角改为 0.000 m / 0.0°

重置原点后数据归零

日志同步记录重置时的采样数和位置:

MOTION_ORIGIN_RESET sample=17 position=(-0.047,0.016,-0.064)

4. 暂停后检查采样数

点击“暂停”,状态标签变为“会话已暂停”,按钮变为“恢复”。采样数停在 802;间隔 5 秒再次读取页面,仍为 802。

暂停后采样停止

对应日志:

MOTION_SESSION_PAUSE success sample=802

5. 恢复后移动并旋转手机

点击“恢复”,跟踪重新建立,采样数继续增长。随后平移并旋转手机,画面记录到第 825 个采样:

  • X/Y/Z:-0.050 m / 0.076 m / -0.126 m
  • 相对位移:0.727 m
  • 相对转角:21.2°
  • 四元数:x=-0.045, y=0.705, z=0.706, w=0.039

移动旋转后的位姿变化

本轮销毁会话时,日志汇总的最大相对位移为 0.791 m,最大相对转角为 21.2°

6. 销毁会话并返回首页

点击“销毁退出”后,ARViewContext.destroy() 成功,页面返回实验工作台,相机占用指示消失。

销毁会话返回首页

实操中遇到的情况与处理

情况一:初始化后暂时没有位姿

真机刚进入 ARView 时,相机状态先是 PAUSED,原因显示“暂时无法建立稳定跟踪”;镜头对准有纹理的地毯后才切换为 TRACKING

处理方式是在帧回调中先判断 camera.state。只有 TRACKING 才调用 getPose();其他状态只显示原因,不参与位移计算。

情况二:数据在日志中变化,页面数字没有刷新

第一次实现把 X/Y/Z 数值作为参数传给带参数的 @Builder。Hilog 中的位置持续变化,但构建器中的文字停留在首次值。

状态未刷新且颜色显示异常

处理后,构建器只接收坐标轴名称,内部直接读取 @State

@Builder
private positionItem(axis: string, color: string): void {
  Text(this.positionValue(axis))
}

修改后,页面数值与每 10 次采样输出的 Hilog 一致更新。

情况三:半透明深色卡片显示成黄色

最初把八位颜色写成了 RRGGBBAA 顺序。ArkUI 按 AARRGGBB 解释八位颜色,导致透明度和颜色通道错位。

颜色改为 #E812233D#12FFFFFFAARRGGBB 格式后,叠加卡恢复为半透明深蓝色,相机画面仍可见。

情况四:已经有位姿,“重置原点”仍不可点击

按钮最初绑定 latestTranslation !== undefined,但 latestTranslation 不是 @State,值改变后没有触发按钮重新构建。

增加 @State originReady,首个有效位姿保存为原点时设为 true,按钮改为:

.enabled(this.originReady)

修正后,位姿建立时按钮立即可用,重置原点结果可以稳定复现。

情况五:按回调时间戳节流后,真机仍接近每帧刷新

SDK 回调提供了 timestamp 参数。第一版直接用它做 100 ms 差值判断,但真机观察到采样仍接近相机帧率,界面更新约 70 次/秒。

本实验只需要稳定观察数据,节流基准改为 Date.now()。最终连续采样约 10 次/秒,暂停和恢复的采样数也更容易核对。回调参数仍保留,后续需要与相机帧时间对齐时可以单独处理其时间单位。

最终验证结果

验证项 真机结果
WORLD 会话初始化 成功,最终回归耗时 17 ms
跟踪状态 从 PAUSED 切换为 TRACKING
X/Y/Z 平移 持续更新,单位为米
四元数 x/y/z/w 持续更新
重置原点 0.000 m / 0.0°
移动和旋转 截图 0.727 m / 21.2°
暂停 采样数 802,等待 5 秒不变
恢复 采样继续增长,截图达到 825
销毁 成功返回首页,相机资源释放
clean 签名构建 成功,零告警

真机验证日志

下面是最终短回归中保留的完整关键日志。从会话初始化、跟踪状态变化、自动原点、手动重置,到暂停、恢复和销毁,整个流程均有对应记录。

08-19 16:59:27.359 MOTION_SESSION_INIT success costMs=17
08-19 16:59:27.413 MOTION_TRACKING state=跟踪暂停 reason=暂时无法建立稳定跟踪
08-19 16:59:29.178 MOTION_TRACKING state=跟踪中 reason=暂时无法建立稳定跟踪
08-19 16:59:29.179 MOTION_ORIGIN_AUTO position=(-0.025,-0.005,0.002)
08-19 16:59:30.082 MOTION_POSE sample=10 position=(-0.033,0.004,-0.024) distance=0.028 rotation=0.0
08-19 16:59:30.856 MOTION_ORIGIN_RESET sample=17 position=(-0.047,0.016,-0.064)
08-19 16:59:31.134 MOTION_POSE sample=20 position=(-0.056,0.022,-0.085) distance=0.023 rotation=0.0
08-19 16:59:32.172 MOTION_POSE sample=30 position=(-0.091,0.034,-0.178) distance=0.123 rotation=0.1
08-19 16:59:32.955 MOTION_SESSION_PAUSE success sample=31
08-19 16:59:34.935 MOTION_SESSION_RESUME success sample=31
08-19 16:59:35.027 MOTION_TRACKING state=跟踪暂停 reason=暂时无法建立稳定跟踪
08-19 16:59:38.134 MOTION_SESSION_DESTROY success samples=31 maxDistance=0.135 maxRotation=0.1

长时间移动、旋转和生命周期验证日志如下:

MOTION_SESSION_PAUSE success sample=802
暂停后等待 5 秒,页面采样数仍为 802
MOTION_SESSION_RESUME success sample=802
MOTION_POSE sample=810 position=(-0.031,0.037,-0.042) distance=0.775 rotation=21.2
MOTION_POSE sample=820 distance=0.746 rotation=21.2
恢复后画面:sample=825,distance=0.727 m,rotation=21.2°
MOTION_SESSION_DESTROY success samples=832 maxDistance=0.791 maxRotation=21.2

本篇完成的是“从 AR 帧取得设备位姿并验证数据变化”的链路,不包含平面命中、锚点和 3D 模型放置。

官方资料

Logo

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

更多推荐