HarmonyOS AR Engine深度估计实战——把毫米距离画成实时热力图

HarmonyOS AR Engine深度估计实战------把毫米距离画成实时热力图

打开实验页面后,相机画面上会多出一层彩色网格。它不是滤镜:每个小格都保存着手机到画面中物体的大致距离。手机对准近处木纹地面时,中心距离是 791 mm ;对准约 3 米外的区域后,中心距离变成 2.91 m

两次锁定都取得了 784/784 个有效抽样点。热力镜里的暖色区域代表近处,绿色和蓝色逐步对应更远的区域,画面变化和毫米距离能够互相对上。

锁定场景 中心距离 抽样范围 有效抽样 高置信度
中近距离木纹地面 791 mm 0.48--0.80 m 784/784 11%
远距离区域 2.91 m 1.43--3.10 m 784/784 9%

这里的"深度图"可以先理解为一张距离表:普通照片的像素记录颜色,深度图的像素记录距离。页面再把距离换成颜色,才得到容易观察的"热力图"。锁定按钮只把当前画面停住,数字不会被重新计算或美化。

先看懂页面

第一次进入页面,先认下面五个信息:

  • 中心距离:准星附近一小块区域到手机的距离,不是整张画面的平均值;
  • 红色、黄色:距离较近;
  • 绿色、蓝色:距离逐渐变远;
  • 有效抽样 :成功读到距离的格子数量,784/784 表示 784 个格子都有值;
  • 高置信度:系统认为"这次距离更可靠"的格子比例,它不是有效格子的比例。

实际操作时只看两个变化就够了:手机靠近物体,中心数字会变小,网格偏暖色;手机对准远处,中心数字会变大,网格逐渐变成绿色或蓝色。

实验准备

本次运行环境如下:

项目 实际环境
设备 HUAWEI Mate 60 Pro
系统 HarmonyOS 7.0
SDK API 26
工程 ArkTS Phone 应用,targetSdkVersion 26
必需权限 CAMERA、ACCELEROMETER、GYROSCOPE
必需能力 SLAM、DEPTH
AR 类型 WORLD
深度模式 AUTOMATIC

工程已经完成签名,真机连接正常。应用入口先检查相机权限、SLAM 和 DEPTH,三项全部满足后才显示可用的"实验 09:实时深度热力镜"。

权限声明沿用 AR Engine 工程的基础配置:

json5 复制代码
"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"
    }
  }
]

入口页在创建会话前做能力门禁:

ArkTS/ets 复制代码
private canStartDepth(): boolean {
  const depth: FeatureItem | undefined = this.features.find(
    (item: FeatureItem) =>
      item.type === arEngine.ARFeatureType.ARENGINE_FEATURE_TYPE_DEPTH
  );
  return this.canStartAR() && depth !== undefined && depth.supported;
}

private startDepthEstimation(): void {
  if (!this.cameraGranted) {
    this.latestMessage = '未获得相机权限,不启动深度估计。';
    hilog.info(DOMAIN, TAG, 'DEPTH_GUARD camera=false');
    return;
  }
  if (!this.canStartAR()) {
    this.latestMessage = '当前设备不支持 SLAM,不启动深度估计。';
    hilog.info(DOMAIN, TAG, 'DEPTH_GUARD slam=false');
    return;
  }
  if (!this.canStartDepth()) {
    this.latestMessage = '当前设备不支持深度估计,不创建 AR 会话。';
    hilog.info(DOMAIN, TAG, 'DEPTH_GUARD depth=false');
    return;
  }

  hilog.info(DOMAIN, TAG,
    'DEPTH_GUARD camera=true slam=true depth=true');
  this.showDepthEstimation = true;
}

这张彩色网格是怎么来的

这张原理图只说明数据处理过程,不是真机结果。正文中的距离、热力图和日志均来自后面的真实运行记录。

相机每更新一次画面,就会产生一份 ARFrame。可以把它理解为"带有 AR 数据的一帧相机画面"。页面对每一帧做下面五件事:

  1. 收到新的相机帧;
  2. 取得一张"距离表"和一张"可信程度表";
  3. 把底层缓冲区转成 ArkTS 可以按下标读取的整数数组;
  4. 256 × 256 的原始数据中抽出 28 × 28 个格子,并计算准星附近的代表距离;
  5. 距离决定颜色,可信程度决定透明度,最后用 Canvas 盖到相机画面上。

第一步:打开能返回距离的 AR 会话

页面底层使用 ARView 显示真实相机画面,上层使用 Canvas 画彩色网格和准星。两层叠在一起后,既能看到现实环境,也能看到距离变化。

下面这段配置只打开"自动返回深度"和"自动对焦"。平面、语义和 Mesh 都关闭,本次页面只处理距离数据。

ArkTS/ets 复制代码
const scene: Scene = await Scene.load();
const context = new arViewController.ARViewContext();

this.callback = new DepthEstimationCallback(
  (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.DISABLED,
  powerMode: arEngine.ARPowerMode.NORMAL,
  semanticMode: arEngine.ARSemanticMode.NONE,
  poseMode: arEngine.ARPoseMode.GRAVITY,
  depthMode: arEngine.ARDepthMode.AUTOMATIC,
  meshMode: arEngine.ARMeshMode.DISABLED,
  focusMode: arEngine.ARFocusMode.AUTO
};

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

相机出帧速度很快,没有必要每一帧都重新画 784 个格子。下面的判断把读取频率限制为每 250 ms 最多一次;页面暂停、结果锁定、页面退出或上一轮还没读完时,都不会重复读取。

ArkTS/ets 复制代码
const DEPTH_SAMPLE_INTERVAL_MS: number = 250;

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

  this.lastSampleTime = now;
  this.samplingInFlight = true;
  this.sampleCurrentDepth(context).finally(() => {
    this.samplingInFlight = false;
  });
}

第二步:从当前画面读出距离和可信程度

只有页面显示相机正在稳定跟踪时才读取数据。acquireDepthImage16Bits() 返回每个位置的毫米距离;acquireDepthConfidenceImage() 返回同一位置的可信程度。两张图尺寸相同,下标也一一对应。

ArkTS/ets 复制代码
frame = session.getFrame();
const camera: arEngine.ARCamera = frame.getCamera();
this.updateTrackingStatus(camera.state, camera.stateReason);

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

depthImage = frame.acquireDepthImage16Bits();
confidenceImage = frame.acquireDepthConfidenceImage();

if (depthImage.planes.length === 0 ||
  confidenceImage.planes.length === 0) {
  throw new Error('深度图或置信度图没有可读取的平面');
}

const depthPlane: arEngine.ImageComponent = depthImage.planes[0];
const confidencePlane: arEngine.ImageComponent =
  confidenceImage.planes[0];

深度功能刚启动时,普通相机画面往往已经出现,但距离数据还没准备好。真机返回 1009200001 时,页面把它显示成"正在准备",继续等待,而不是立即判定失败:

ArkTS/ets 复制代码
const DEPTH_NOT_READY_CODE: number = 1009200001;

if (err.code === DEPTH_NOT_READY_CODE) {
  this.pendingCount += 1;
  this.guideText =
    '深度数据正在准备,保持镜头稳定并扫过有纹理的物体边缘';
} else {
  this.errorCount += 1;
  this.guideText = `深度读取异常:${err.code}`;
}

最终真机返回的距离表大小为 256 × 256。两轮测试分别等待约 7.4 秒和 7.7 秒才出现首份有效数据,所以进入页面后短时间只看到相机并不异常。

遇到的问题:数组读错后出现竖向空带

第一版页面虽然能画出颜色,却出现了规则的竖向空带,有效格子固定为 448/784。问题不在相机,而在数组读取方式。

当时把 ImageComponent.buffer 当成一个个原始字节,又使用 rowStridepixelStride 跳着取值。但 ArkTS 转换完成后,每个数组元素已经是一个完整数值,再按字节步长跳读,就会有一部分位置永远被跳过。

处理方式是先把 ArrayBuffer 转成 Int32Array,然后用最普通的二维数组下标 y * width + x 读取。简单说:数组已经按数值展开,不能再按字节跳着读。

修订后的代码同时读取距离和可信程度,并从中心 5 × 5 小区域取代表值:

ArkTS/ets 复制代码
private sampleDepthGrid(
  width: number,
  height: number,
  depthPlane: arEngine.ImageComponent,
  confidencePlane: arEngine.ImageComponent
): DepthSampleResult {
  const depthValues: Int32Array =
    new Int32Array(depthPlane.buffer);
  const confidenceValues: Int32Array =
    new Int32Array(confidencePlane.buffer);

  const depths: number[] = [];
  const confidences: number[] = [];
  const centerValues: number[] = [];
  let validCount: number = 0;
  let highConfidenceCount: number = 0;
  let minDepth: number = 65535;
  let maxDepth: number = 0;

  const centerColumn: number = Math.floor(GRID_COLUMNS / 2);
  const centerRow: number = Math.floor(GRID_ROWS / 2);

  for (let row: number = 0; row < GRID_ROWS; row += 1) {
    const sourceY: number = Math.min(
      height - 1,
      Math.floor((row + 0.5) * height / GRID_ROWS)
    );

    for (let column: number = 0;
      column < GRID_COLUMNS;
      column += 1) {
      const sourceX: number = Math.min(
        width - 1,
        Math.floor((column + 0.5) * width / GRID_COLUMNS)
      );
      const planeIndex: number = sourceY * width + sourceX;
      const depth: number = depthValues[planeIndex];
      const confidence: number = confidenceValues[planeIndex];

      depths.push(depth);
      confidences.push(confidence);

      if (depth > 0) {
        validCount += 1;
        minDepth = Math.min(minDepth, depth);
        maxDepth = Math.max(maxDepth, depth);

        if (confidence >= 2) {
          highConfidenceCount += 1;
        }
        if (Math.abs(column - centerColumn) <= CENTER_RADIUS &&
          Math.abs(row - centerRow) <= CENTER_RADIUS) {
          centerValues.push(depth);
        }
      }
    }
  }

  return {
    depths: depths,
    confidences: confidences,
    validCount: validCount,
    highConfidenceCount: highConfidenceCount,
    minDepth: validCount > 0 ? minDepth : 0,
    maxDepth: maxDepth,
    centerDepth: this.median(centerValues)
  };
}

修订后有效抽样从 448/784 恢复到 784/784,竖向空带消失。rowStridepixelStride 仍保留在日志中作为原始信息,但不再用来计算数组下标。

中心距离没有只取准星下的一个点,而是收集中心 5 × 5 区域的有效值,再取排在中间的数。这样某一个点突然为空或跳得很远时,页面数字不会立刻跟着乱跳。

ArkTS/ets 复制代码
private median(values: number[]): number {
  if (values.length === 0) {
    return 0;
  }
  const ordered: number[] = values.slice().sort(
    (first: number, second: number) => first - second
  );
  const middle: number = Math.floor(ordered.length / 2);
  if (ordered.length % 2 === 1) {
    return ordered[middle];
  }
  return Math.round(
    (ordered[middle - 1] + ordered[middle]) / 2
  );
}

第三步:把毫米距离换成颜色

页面固定绘制 28 × 28,也就是 784 个格子。距离决定颜色,可信程度只决定颜色是更实还是更淡:

ArkTS/ets 复制代码
private depthColor(depth: number, confidence: number): string {
  if (depth <= 0) {
    return 'rgba(38, 49, 62, 0.20)';
  }

  const alpha: string = confidence >= 2 ? '0.88' :
    (confidence === 1 ? '0.58' : '0.30');

  if (depth < 700) {
    return `rgba(255, 117, 92, ${alpha})`;
  }
  if (depth < 1500) {
    return `rgba(255, 209, 90, ${alpha})`;
  }
  if (depth < 3000) {
    return `rgba(93, 225, 168, ${alpha})`;
  }
  return `rgba(85, 181, 255, ${alpha})`;
}

Canvas 每次清空旧画面,再按行列绘制全部色块:

ArkTS/ets 复制代码
for (let index: number = 0;
  index < result.depths.length;
  index += 1) {
  const row: number = Math.floor(index / GRID_COLUMNS);
  const column: number = index % GRID_COLUMNS;

  this.heatmapContext.fillStyle = this.depthColor(
    result.depths[index],
    result.confidences[index]
  );
  this.heatmapContext.fillRect(
    lensX + column * cellWidth,
    lensY + row * cellHeight,
    Math.max(1, cellWidth - 0.35),
    Math.max(1, cellHeight - 0.35)
  );
}

修订版的实时画面已经没有规则空带,中心距离为 1.52 m,范围为 0.91--2.44 m,784 个网格全部取得有效深度。

这里最容易看错的是"高置信度"。例如高置信度为 11%,不代表只有 11% 的格子有距离。本次仍然是 784/784 个格子有效,只是其中 11% 被系统标为更可信;其他格子照样显示,只是颜色更淡。

跟着真机做一遍

1. 启动深度实验

进入实验 09 后,先拿着手机缓慢扫过木纹、家具边缘或明暗变化明显的区域。需要移动的是手机,不是在屏幕上滑动。

页面开始时显示"等待深度";出现彩色网格、中心距离和 784/784 后,说明距离数据已经可用。

2. 锁定中近距离结果

把准星对准 0.4--1 米内的木纹地面。等中心数字不再大幅跳动后,点击"锁定当前深度证据"。按钮变成"继续实时采样",说明当前画面已经冻结。第 204 帧结果为:

text 复制代码
DEPTH_EVIDENCE_LOCK sample=204 centerMm=791 minMm=483 maxMm=804 valid=784 highConfidenceRatio=11

这行日志翻译成页面数据就是:中心 791 mm,画面范围约 0.48--0.80 m,784 个格子全部有效,其中 11% 为高置信度。锁定只停止页面采样,不会关闭相机。

3. 锁定远距离结果

点击"继续实时采样",把手机对准更远区域。等中心距离稳定在米级后再次锁定,本次记录为:

text 复制代码
DEPTH_EVIDENCE_LOCK sample=159 centerMm=2906 minMm=1431 maxMm=3104 valid=784 highConfidenceRatio=9

这行日志表示中心 2906 mm,画面范围约 1.43--3.10 m,784 个格子仍全部有效。两次中心距离相差 2115 mm,页面也从暖色为主变成绿色、蓝色为主,数字和颜色变化能够互相验证。

暂停、恢复和销毁

远距离证据锁定后先恢复实时采样,再点击"暂停深度"。页面停在第 274 帧,等待 5 秒后没有出现新的 DEPTH_SAMPLE

点击"恢复深度"后,日志先记录从第 274 帧恢复,随后采样增长到第 280、288、296 帧,页面重新显示"实时深度"。

text 复制代码
DEPTH_SESSION_PAUSE sample=274
DEPTH_SESSION_RESUME sample=274 locked=false
DEPTH_SAMPLE sample=280 image=256x256 format=4 valid=784 highConfidenceRatio=9 centerMm=2906 minMm=1431 maxMm=3104

点击"销毁退出"后,采样停在第 326 帧,错误计数为 0,页面返回实验工作台。

text 复制代码
DEPTH_SESSION_DESTROY sample=326 pending=19 errors=0 locked=false

深度图、置信度图和 AR 帧都在 finally 中释放。即使读取深度时抛出异常,也不会跳过释放路径。

ArkTS/ets 复制代码
} finally {
  if (confidenceImage) {
    await confidenceImage.release();
  }
  if (depthImage) {
    await depthImage.release();
  }
  if (frame) {
    await frame.release();
  }
}

页面退出时销毁 ARViewContext,并清空 Canvas:

ArkTS/ets 复制代码
private async destroyARView(): Promise<void> {
  if (!this.arContext || this.isDestroyed) {
    return;
  }
  this.isDestroyed = true;

  const context: arViewController.ARViewContext = this.arContext;
  this.arContext = undefined;
  await context.destroy();

  if (this.canvasReady) {
    this.heatmapContext.clearRect(
      0,
      0,
      this.heatmapContext.width,
      this.heatmapContext.height
    );
  }
}

构建并确认不是演示数据

静态回归检查覆盖以下内容:

  • DEPTH 能力门禁;
  • 自动深度和自动对焦配置;
  • 深度图、置信度图的 Int32Array 转换;
  • y * width + x 平面索引;
  • 中心区域中位数;
  • Canvas 热力图;
  • 锁定、暂停、恢复和销毁日志;
  • 深度图、置信度图和 AR 帧释放;
  • 随机或模拟深度数据为 0 命中。

执行结果:

text 复制代码
AR Engine 深度估计静态回归检查通过。

随后执行 clean 签名构建:

powershell 复制代码
hvigorw clean assembleHap --no-daemon

本次结果:

text 复制代码
CompileArkTS SUCCESS
PackageHap  SUCCESS
SignHap     SUCCESS
BUILD SUCCESSFUL in 17 s 410 ms

最终签名 HAP 大小为 2,780,410 bytes,覆盖安装和 Ability 启动均成功。

最后得到的结果

验证项 实际结果
CAMERA、SLAM、DEPTH 门禁 通过
深度图与置信度图 成功取得
深度图尺寸 256 × 256
热力图网格 28 × 28,784 个格子
修订版有效抽样 784/784
中近距离中心值 791 mm
远距离中心值 2906 mm
两组中心差值 2115 mm
暂停 第 274 帧停止增长
恢复 从第 274 帧恢复并继续出帧
销毁 第 326 帧销毁,errors=0

这次案例完成的是一条完整的真实深度链路:AR 会话取得 16 位毫米深度和置信度,ArkTS 按官方方式转换数组,固定网格抽样并计算中心中位数,Canvas 把结果画到真实相机画面上,最后用近、远两组数据和完整生命周期做了验证。

官方资料

相关推荐
GKxx23 分钟前
在 HarmonyOS 上给 QEMU 搭一个最小 aarch64 Linux guest(内核 + busybox initramfs)
linux·华为·qemu·harmonyos·鸿蒙·鸿蒙pc
黑臂麒麟30 分钟前
屏幕信息全解析:HarmonyOS的分辨率、刷新率、折叠状态一个API搞定
华为·arkts·鸿蒙
黑臂麒麟32 分钟前
HarmonyOS 网络连接诊断实战:检测网络状态、WiFi 切换、弱网监测一网打尽
网络·华为·arkts·鸿蒙
贾伟康1 小时前
【天体运行模拟|08】HarmonyOS ArkTS 单位换算实战:处理天文尺度、科学计数与精度
harmonyos·arkts·arkui·数值精度·单位换算
lilian2331 小时前
HarmonyOS 7 新特性(十三)|3DGS 模型加载、交互与性能回退
3d·交互·harmonyos
2501_919749032 小时前
华为鸿蒙免费反诈APP—小羊反诈
华为·harmonyos·鸿蒙
2501_919749032 小时前
华为鸿蒙免费视频播放器—小羊免费播放器
华为·harmonyos·鸿蒙
贾伟康3 小时前
【天体运行模拟|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致
harmonyos·arkts·数据持久化·状态管理·preferences
黑臂麒麟3 小时前
Harmony鸿蒙实战应用10:随手账本——发布检查与最终验收
华为·app·arkts·鸿蒙