☰
HarmonyOS AR Engine深度估计实战——把毫米距离画成实时热力图
2026/10/25 17:50:26 网站建设 项目流程

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

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

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

锁定场景中心距离抽样范围有效抽样高置信度
中近距离木纹地面791 mm0.48–0.80 m784/78411%
远距离区域2.91 m1.43–3.10 m784/7849%

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

先看懂页面

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

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

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

实验准备

本次运行环境如下:

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

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

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

"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" } } ]

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

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 都关闭,本次页面只处理距离数据。

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 最多一次;页面暂停、结果锁定、页面退出或上一轮还没读完时,都不会重复读取。

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()返回同一位置的可信程度。两张图尺寸相同,下标也一一对应。

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时,页面把它显示成“正在准备”,继续等待,而不是立即判定失败:

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当成一个个原始字节,又使用rowStride和pixelStride跳着取值。但 ArkTS 转换完成后,每个数组元素已经是一个完整数值,再按字节步长跳读,就会有一部分位置永远被跳过。

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

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

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,竖向空带消失。rowStride和pixelStride仍保留在日志中作为原始信息,但不再用来计算数组下标。

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

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 个格子。距离决定颜色,可信程度只决定颜色是更实还是更淡:

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 每次清空旧画面,再按行列绘制全部色块:

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 帧结果为:

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. 锁定远距离结果

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

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 帧,页面重新显示“实时深度”。

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,页面返回实验工作台。

DEPTH_SESSION_DESTROY sample=326 pending=19 errors=0 locked=false

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

} finally { if (confidenceImage) { await confidenceImage.release(); } if (depthImage) { await depthImage.release(); } if (frame) { await frame.release(); } }

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

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 命中。

执行结果:

AR Engine 深度估计静态回归检查通过。

随后执行 clean 签名构建:

hvigorw clean assembleHap--no-daemon

本次结果:

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 把结果画到真实相机画面上,最后用近、远两组数据和完整生命周期做了验证。

官方资料

  • AR Engine Kit 指南
  • 获取深度估计信息(ArkTS)
  • ArrayBuffer 数据类型转换说明

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询