AR Engine 平面语义实战封面:8 个平面全部 UNKNOWN

封面是根据本次实测数据制作的视觉摘要;真正承担验证的仍是下面的真机截图和 Hilog。

这篇文章只写一次真实复测得到的结果。

在 HUAWEI Mate 60 Pro、HarmonyOS 7.0(API 26)上,相机画面扫过室内柜体、墙顶和附近表面后,AR Engine 最终跟踪到 8 个平面;几何范围、中心坐标和边界顶点都可以读取,但 8 个平面的 ARPlane.label 始终都是 UNKNOWN

本次结果不是“平面语义识别成功”。准确结论是:

  • 相机权限、SLAM 和 SEMANTIC 能力门禁通过;
  • semanticMode=PLANE 的 AR 会话初始化成功;
  • 平面几何跟踪可运行,最大同时跟踪数为 8;
  • 本次会话没有取得任何非 UNKNOWN 的平面语义标签;
  • 独立的二维拍照识别产生过模型原始输出,但它既不能证明标签正确,也不能替代 ARPlane.label

下面是同一次会话的真实结果页。它显示 8 个总平面、8 个跟踪中平面、0 个已分类平面和 8 个未知平面:

真机复测:8 个平面全部为未知语义

一、实验环境与可运行案例

项目 本次实测值
设备 HUAWEI Mate 60 Pro
系统 HarmonyOS 7.0 / API 26
应用包 com.example.csdn
Ability EntryAbility
案例入口 实验 05:识别平面语义
完整页面源码 entry/src/main/ets/features/arengine/PlaneSemanticPage.ets
复测时间 2026-08-25 16:36:11—16:45:33

工程不是代码片段演示:签名 HAP 已覆盖安装到真机,随后完成初始化、扫描、四次二维识别、暂停、恢复和销毁。使用的安装与启动命令如下:

hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.csdn

模块声明了相机、加速度计和陀螺仪权限:

"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:permission_reason_camera",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.ACCELEROMETER",
    "reason": "$string:permission_reason_accelerometer",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.GYROSCOPE",
    "reason": "$string:permission_reason_gyroscope",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

二、先分清三个概念

1. 平面数量不是语义分类数量

session.getAllTrackables(arEngine.ARTrackableType.PLANE) 返回的是平面 Trackable。它能提供范围、姿态、方向和边界多边形,但不代表语义标签一定有效。

本次页面同时统计:

  • total:当前返回的平面总数;
  • tracking:状态为 TRACKING 的平面数;
  • classifiedlabel !== UNKNOWN 的平面数;
  • unknownlabel === UNKNOWN 的平面数。

所以 total=8 / tracking=8classified=0 / unknown=8 可以同时成立。

2. planeType 不是 label

plane.planeType 描述几何朝向,例如水平向上、水平向下或垂直;plane.label 才是地面、墙面、桌面等语义类别。

本次代表平面显示“水平向上”,只能说明其几何方向,不能把它写成“地面”。因为同一条记录的 label 仍然是 UNKNOWN

AR 平面语义面向多种室内表面。下面三张图只用于帮助理解类别与几何表面的关系,不是本次真机返回的分类结果。

平面语义概念示意:地面与墙面

概念示意:地面与墙面。图中的高亮效果不是本次真机截图。

平面语义概念示意:桌面与座椅

概念示意:桌面与座椅。本次复测没有取得这两类有效标签。

平面语义概念示意:门窗、床与天花板

概念示意:门窗、床与天花板。本次复测对应计数仍全部为 0。

3. 二维模型输出不是 AR 平面语义

页面还提供“识别当前画面”按钮:它读取 AR 相机帧,转换成 PixelMap,再交给 Core Vision Kit 多目标识别。这是二维图像链路,与 AR 平面的 label 是两份独立数据。

即使二维模型返回了某个对象名称,也不能据此修改或推断 ARPlane.label

AR 平面语义与二维原始模型输出的双链路

原理示意:上方是本次 8 个平面全部 UNKNOWN 的 AR 链路;下方是独立二维推理链路。二维输出没有正确性验证,也不会回填平面标签。

三、能力门禁:不支持就不创建会话

进入实验前依次检查相机权限、SLAM 和 SEMANTIC。任何一项不满足都直接拦截,避免在不支持的设备上制造“识别失败”的假象。

private startPlaneSemantic(): void {
  if (!this.cameraGranted) {
    this.latestMessage = '拦截成功:未获得相机权限,不启动平面语义。';
    hilog.info(DOMAIN, TAG, 'SEMANTIC_GUARD camera=false');
    return;
  }
  if (!this.canStartAR()) {
    this.latestMessage = '拦截成功:当前设备不支持 SLAM,不启动平面语义。';
    hilog.info(DOMAIN, TAG, 'SEMANTIC_GUARD slam=false');
    return;
  }
  if (!this.canStartSemantic()) {
    this.latestMessage = '拦截成功:当前设备不支持平面语义,不创建 AR 会话。';
    hilog.info(DOMAIN, TAG, 'SEMANTIC_GUARD semantic=false');
    return;
  }
  hilog.info(DOMAIN, TAG,
    'SEMANTIC_GUARD camera=true slam=true semantic=true');
  this.showPlaneSemantic = true;
}

本次真机日志为:

SEMANTIC_GUARD camera=true slam=true semantic=true

它只证明设备允许进入实验,不保证每个环境平面都能取得语义标签。

四、创建平面语义 AR 会话

配置中的关键项是 planeFindingModesemanticMode。本案例同时启用水平、垂直平面检测,并请求平面语义;深度模式设为自动,Mesh 在本实验中关闭。

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

  this.callback = new PlaneSemanticCallback(
    (arContext: arViewController.ARViewContext, timestamp: number) => {
      this.handleFrameUpdate(arContext, 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.PLANE,
    poseMode: arEngine.ARPoseMode.GRAVITY,
    depthMode: arEngine.ARDepthMode.AUTOMATIC,
    meshMode: arEngine.ARMeshMode.DISABLED,
    focusMode: arEngine.ARFocusMode.AUTO
  };

  await context.init();
  this.arContext = context;
}

本次初始化和深度探测的真实日志如下:

SEMANTIC_SESSION_INIT success mode=PLANE depth=AUTOMATIC costMs=39
SEMANTIC_DEPTH_PROBE attempt=13 depthSuccess=true depth=256x256 depthFormat=4 depthPlanes=1 confidenceSuccess=true confidence=256x256 confidenceFormat=3 confidencePlanes=1

深度图可读取仍不能推出平面语义必然成功,它只是另一项设备与帧能力证据。

五、逐帧读取并严格统计 UNKNOWN

帧回调只在相机处于 TRACKING 时继续处理。取得平面 Trackable 后,代码必须直接读取 ARPlane.label,不能根据朝向或尺寸自行猜测语义。

private updateSemanticSnapshot(
  trackables: Array<arEngine.ARTrackable>
): void {
  let tracked: number = 0;
  let classified: number = 0;
  let unknown: number = 0;

  trackables.forEach((trackable: arEngine.ARTrackable) => {
    if (trackable.type !== arEngine.ARTrackableType.PLANE ||
      trackable.state !== arEngine.ARTrackingState.TRACKING) {
      return;
    }

    const plane: arEngine.ARPlane = trackable as arEngine.ARPlane;
    tracked += 1;
    if (plane.label === arEngine.ARSemanticPlaneLabel.UNKNOWN) {
      unknown += 1;
    } else {
      classified += 1;
    }
  });

  this.totalPlanes = trackables.length;
  this.trackedPlanes = tracked;
  this.classifiedPlanes = classified;
  this.unknownPlanes = unknown;
}

代表平面的几何详情来自真实 API:

private updateRepresentativePlane(plane: arEngine.ARPlane): void {
  let pose: arEngine.ARPose | undefined = undefined;
  try {
    pose = plane.getPose();
    const polygon: ArrayBuffer = plane.getPolygonXZ();
    const vertexCount: number = Math.floor(polygon.byteLength / 4 / 2);

    this.representativeLabel = this.semanticLabelText(plane.label);
    this.representativeDirection = this.planeTypeText(plane.planeType);
    this.representativeSize =
      `${Math.abs(plane.extendX).toFixed(2)} × ` +
      `${Math.abs(plane.extendZ).toFixed(2)} m`;
    this.representativeCenter =
      `X ${pose.translation.x.toFixed(2)}  ` +
      `Y ${pose.translation.y.toFixed(2)}  ` +
      `Z ${pose.translation.z.toFixed(2)}`;
    this.representativeVertices = `${vertexCount} 个`;
  } finally {
    if (pose) {
      pose.release().catch((error: BusinessError) => {
        hilog.error(DOMAIN, TAG,
          'SEMANTIC_POSE_RELEASE error=%{public}d', error.code);
      });
    }
  }
}

实际扫描中,首次平面在初始化后 5834 ms 出现,随后总数逐步增长到 8。稳定阶段的日志是:

SEMANTIC_FIRST_PLANE latencyMs=5834 total=1
SEMANTIC_COUNTS sample=104 total=8 tracking=8 classified=0 unknown=8 floor=0 wall=0 table=0 seat=0 ceiling=0 doorWindow=0 bed=0 other=0
SEMANTIC_REPRESENTATIVE label=未知 direction=水平向上 extent=4.71 × 2.63 m center=X 0.70  Y -1.04  Z -0.92 vertices=10
SEMANTIC_COUNTS sample=370 total=8 tracking=8 classified=0 unknown=8 floor=0 wall=0 table=0 seat=0 ceiling=0 doorWindow=0 bed=0 other=0

sample=104sample=370,8 个平面持续跟踪,分类数仍为 0。这比单帧截图更能说明:本次不是页面刚进入时的短暂等待,而是在持续扫描后仍没有取得有效语义标签。

六、二维拍照识别:只记录原始输出

API 26 的相机帧先检查多目标识别系统能力,再读取 YUV_420_888 图像、转换为 PixelMap 并发起推理:

private requestPhotoRecognition(): void {
  if (!canIUse('SystemCapability.AI.Vision.ObjectDetection')) {
    this.photoRecognitionState = '当前设备不支持';
    this.photoRecognitionResult = '缺少多目标识别系统能力';
    hilog.error(DOMAIN, TAG, 'PHOTO_OBJECT_GUARD syscap=false');
    return;
  }
  this.photoCaptureRequested = true;
  this.photoRecognitionState = '等待下一帧';
}

private async runPhotoRecognition(pixelMap: image.PixelMap): Promise<void> {
  try {
    if (!this.objectDetector) {
      this.objectDetector = await objectDetection.ObjectDetector.create();
    }
    const request: visionBase.Request = {
      inputData: { pixelMap: pixelMap }
    };
    const response = await this.objectDetector.process(request);
    // 页面只展示 response.objects 的原始标签与分数,
    // 不把结果写入任何 ARPlane。
  } finally {
    this.photoRecognitionBusy = false;
    pixelMap.release().catch((error: BusinessError) => {
      hilog.error(DOMAIN, TAG,
        'PHOTO_OBJECT_PIXELMAP_RELEASE error=%{public}d', error.code);
    });
  }
}

用户改变镜头后,四次调用依次留下:

PHOTO_OBJECT_COMPLETE count=1 summary=人物 42%
PHOTO_OBJECT_COMPLETE count=4 summary=文本 43% · 植物 40% · 植物 36%
PHOTO_OBJECT_COMPLETE count=10 summary=人头 83% · 人头 80% · 人脸 69%
PHOTO_OBJECT_COMPLETE count=0 summary=画面中没有识别到目标

这些是模型原始输出,没有人工标注或独立正确性验证。本文不把它们称为“识别正确”,也不利用它们解释 8 个 AR 平面的语义。首图显示的是最后一次调用后的页面状态,所以二维区域为“画面中没有识别到目标”。

七、暂停、恢复与销毁必须闭环

AR 页面退出时要释放持有的平面、二维检测器和 ARViewContext。暂停与恢复则直接调用上下文的生命周期接口:

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.lastSampleTime = 0;
}

private async destroyARView(): Promise<void> {
  if (!this.arContext || this.isDestroyed) {
    return;
  }
  this.isDestroyed = true;
  if (this.retainedProbePlane) {
    await this.retainedProbePlane.release();
    this.retainedProbePlane = undefined;
  }
  if (this.objectDetector) {
    await this.objectDetector.destroy();
    this.objectDetector = undefined;
  }
  await this.arContext.destroy();
  this.arContext = undefined;
  this.callback = undefined;
}

暂停发生在 sample=789,页面保持 8 个跟踪平面和 0 个已分类平面:

真机复测:会话暂停

SEMANTIC_SESSION_PAUSE success sample=789 tracking=8 classified=0

恢复后,下一次采样从 790 继续。恢复瞬间有 6 个平面处于跟踪状态,这属于重新建立跟踪过程,不能把它写成“平面数据丢失”:

真机复测:会话恢复后继续采样

SEMANTIC_SESSION_RESUME success sample=789 tracking=8 classified=0
SEMANTIC_COUNTS sample=790 total=8 tracking=6 classified=0 unknown=6 floor=0 wall=0 table=0 seat=0 ceiling=0 doorWindow=0 bed=0 other=0

最后点击销毁,应用关闭相机并返回七项实验首页:

真机复测:销毁会话并返回首页

SEMANTIC_SESSION_DESTROY success samples=944 maxTracking=8 maxClassified=0 total=8

八、本次结果如何解释

验证项 2026-08-25 同一次真机会话结果
能力门禁 camera=true / slam=true / semantic=true
会话初始化 成功,39 ms
深度与置信度 第 13 次探测取得 256 × 256 图像
首个平面 5834 ms
最大平面总数 8
最大同时跟踪数 8
有效语义数 0
稳定阶段未知数 8
代表平面 水平向上,4.71 × 2.63 m,10 个边界顶点
二维拍照识别 产生过多组原始输出,最终为“画面中没有识别到目标”;正确性未验证
暂停与恢复 789 暂停,恢复后从 790 继续采样
销毁 samples=944 / maxTracking=8 / maxClassified=0 / total=8

从证据可以确定:

  1. 几何平面检测链路可运行,因为平面数量、范围、位姿与边界均有真实数据。
  2. 本次平面语义分类没有成功,因为整个会话的最大有效分类数为 0。
  3. semanticMode=PLANE、能力门禁通过和深度图可用,都不是“必然返回非 UNKNOWN 标签”的证明。
  4. 仅凭这一次测试无法确定始终为 UNKNOWN 的唯一原因;场景、光照、纹理、模型适配和设备实现都可能影响结果,不能选一个原因当作已证实结论。
  5. 如果业务必须依赖地面、墙面或桌面标签,应把 UNKNOWN 设计为正常分支:保留几何能力、提示用户继续扫描,或降级为只依赖平面朝向与点击命中,但不要伪造语义类别。

九、排查清单

遇到“检测到平面但没有语义”时,可以按以下顺序检查:

  1. 确认相机权限、SLAM 和 SEMANTIC 能力均通过。
  2. 确认会话配置确实为 semanticMode: ARSemanticMode.PLANE
  3. 分开记录 planeTypelabel,不要把水平向上直接写成地面。
  4. 同时记录 total / tracking / classified / unknown,并持续采样,避免只看一帧。
  5. 扫描多种真实表面,保持移动缓慢、画面清晰,并记录失败分支。
  6. 如果接入二维模型,把其结果放在独立区域,不回填 ARPlane.label
  7. 验证暂停、恢复和销毁,确保退出页面后释放相机、Trackable、Frame、Image、Pose 与检测器资源。

十、结论

这次实操得到的是一个明确但未达到语义目标的结果:AR Engine 在真机上跟踪到了 8 个环境平面,几何数据完整,生命周期闭环可复现;然而整个会话没有出现任何非 UNKNOWNARPlane.label

因此第五篇仍不能标记为“平面语义验证成功”。后续只有在真机日志和截图中取得至少一个非 UNKNOWN 标签,并能与同一会话的页面数据对应,才可以更新这一结论。

参考资料

本文所有结果截图均来自 2026-08-25 同一次真机会话;横向封面、原理图和场景图由内置 ImageGen 制作,只承担视觉摘要与概念说明,不作为成功证据。

Logo

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

更多推荐