上图根据本次真机实验的书籍、开口盒子和地面锚定关系制作,用于展示最终效果。后文保留未经生成式处理的真机截图和原生日志作为验证证据。
这次实操在 Mate 60 Pro 上完成了两项可以直接看到结果的 AR 操作:
- AR Engine 的
ARTarget把一本平放的实体书自动分类为RECTANGLE; - 在真实水平平面上创建锚点,显示一个顶部敞开的半透明立体盒子,把实体书放进盒子后确认匹配完成。
真机在第 766、780 和 810 帧三次取得有效矩形结果。第一次成功时,书本的包围盒尺寸为0.2219 × 0 × 0.2976 m,包围球半径为0.1856 m:
OBJECT_SNAPSHOT sample=766 camera=0 total=3 tracking=1 paused=2 stopped=0 acquireErrors=0 stateErrors=0 classified=1 unknown=0 cube=0 circle=0 rectangle=1 shape=3 radius=0.1856 size=0.2219,0.0000,0.2976 center=-0.0727,-0.8548,-0.4059 shapeStatus=0 geometryStatus=0本机 SDK 中shape=3对应ARENGINE_TARGET_SHAPE_RECTANGLE。shapeStatus=0和geometryStatus=0表示形状、包围盒、半径及中心位姿均读取成功。
最初版本使用扁平书籍模型,地面命中距离为0.6598 m。为了让放入动作更直观,后续把锚点上的几何体改为28 × 21 × 6 cm开口盒子,内部保留24 × 17 cm的书籍框。开口盒子复测的命中距离为0.7255 m,锚点状态为TRACKING:
BOOK_GUIDE_PLACE model=OPEN_BOX_GUIDE placed=1 hitStatus=0 hitCount=2 anchorTracking=0 distance=0.7255 center=-0.3791,-0.8131,0.0138实体书放入开口盒子后,本轮确认记录写在第 6946 帧:
BOOK_MATCH_CONFIRM confirmed=1 manual=true sample=6946 camera=0 anchor=0 status=0这里有两个独立结果:RECTANGLE是单独扫描实体书时由 AR Engine 自动返回的形状分类;“书籍摆放匹配成功”是锚点稳定时,由操作者确认实体书已经放入盒子。人工确认不会修改ARTarget的 shape、计数或几何数据。
本次实验的完整过程
整个过程分为四步:扫描水平平面、创建盒子锚点、放入实体书、读取官方形状结果。
应用中同时保留两条数据链路:
地面扫描 → HitTest → ARAnchor → OpenGL 开口盒子 → 人工确认 相机帧 → ARTarget → TrackingState → ShapeType → AABB / Radius / CenterPose第一条链路负责可见的 AR 摆放效果;第二条链路负责官方物体形状分类。把它们放在同一个实验页里,既能看到 AR 模型,也能判断ARTarget到底有没有返回有效形状。
实操环境与准备
| 项目 | 本次实操环境 |
|---|---|
| 开发工具 | DevEco Studio |
| 开发语言 | ArkTS + C++ |
| SDK | HarmonyOS API 26 |
| 测试设备 | HUAWEI Mate 60 Pro |
| 设备系统 | HarmonyOS 7.0 |
| AR 会话类型 | ARENGINE_TYPE_WORLD |
| 平面模式 | 水平与垂直平面 |
| 语义模式 | ARENGINE_SEMANTIC_MODE_TARGET |
| 深度模式 | ARENGINE_DEPTH_MODE_AUTOMATIC |
| 对焦模式 | ARENGINE_FOCUS_MODE_AUTO |
| 必需权限 | CAMERA、ACCELEROMETER、GYROSCOPE |
| 必需能力 | SLAM、SEMANTIC |
测试物体选择了一本封面颜色鲜明、边缘完整的平装书。书本平放后轮廓接近矩形,封面有足够纹理,移动手机时也容易保持完整取景。
入口页先完成相机权限和设备能力检测。CAMERA、SLAM 或 SEMANTIC 条件不满足时,不进入原生 AR 会话。
权限声明如下:
"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" } } ]AR Engine 物体形状使用 C API。本次通过 Native XComponent 显示真实相机画面和 OpenGL 模型,再通过 NAPI 把形状、几何与锚点状态传给 ArkTS 页面。
CMake 配置如下:
cmake_minimum_required(VERSION 3.5.0) project(ObjectSemanticLab) set(CMAKE_CXX_STANDARD 17) find_library(ARENGINE_LIB libarengine_ndk.z.so) add_library(objectsemantic SHARED object_semantic.cpp) target_link_libraries(objectsemantic PUBLIC EGL GLESv3 hilog_ndk.z ace_ndk.z ace_napi.z ${ARENGINE_LIB} )创建 TARGET 语义会话
会话使用 WORLD 类型,同时开启平面检测、TARGET 语义、自动深度和自动对焦。
AREngine_ARConfig*config=nullptr;HMS_AREngine_ARConfig_Create(session,&config);HMS_AREngine_ARConfig_SetPreviewSize(session,config,1440,1080);HMS_AREngine_ARConfig_SetUpdateMode(session,config,ARENGINE_UPDATE_MODE_LATEST);HMS_AREngine_ARConfig_SetFocusMode(session,config,ARENGINE_FOCUS_MODE_AUTO);HMS_AREngine_ARConfig_SetARType(session,config,ARENGINE_TYPE_WORLD);HMS_AREngine_ARConfig_SetPlaneFindingMode(session,config,ARENGINE_PLANE_FINDING_MODE_HORIZONTAL_AND_VERTICAL);HMS_AREngine_ARConfig_SetSemanticMode(session,config,ARENGINE_SEMANTIC_MODE_TARGET);HMS_AREngine_ARConfig_SetDepthMode(session,config,ARENGINE_DEPTH_MODE_AUTOMATIC);AREngine_ARStatus configureStatus=HMS_AREngine_ARSession_Configure(session,config);HMS_AREngine_ARConfig_Destroy(config);配置完成后回读语义和对焦模式,并把每个调用的状态写入日志。本次所有状态均为 0:
OBJECT_CONFIG preview=0 update=0 focusSet=0 focusGet=0 focusMode=1 type=0 plane=0 semanticSet=0 semanticGet=0 semanticMode=2 depth=0 configure=0 frame=0 texture=0 geometry=0 resume=0本机 SDK 中semanticMode=2为 TARGET,focusMode=1为 AUTO。
查询 ARTarget,先看跟踪状态
每一帧先查询ARENGINE_TRACKABLE_TARGET,再读取每个目标的跟踪状态。
AREngine_ARTrackableList*targetList=nullptr;HMS_AREngine_ARTrackableList_Create(session,&targetList);HMS_AREngine_ARSession_GetAllTrackables(session,ARENGINE_TRACKABLE_TARGET,targetList);int32_ttargetSize=0;HMS_AREngine_ARTrackableList_GetSize(session,targetList,&targetSize);for(int32_tindex=0;index<targetSize;++index){AREngine_ARTrackable*trackable=nullptr;AREngine_ARStatus acquireStatus=HMS_AREngine_ARTrackableList_AcquireItem(session,targetList,index,&trackable);if(acquireStatus!=ARENGINE_SUCCESS||trackable==nullptr){acquireErrors+=1;continue;}AREngine_ARTrackingState state=ARENGINE_TRACKING_STATE_STOPPED;AREngine_ARStatus stateStatus=HMS_AREngine_ARTrackable_GetTrackingState(session,trackable,&state);if(stateStatus!=ARENGINE_SUCCESS){stateErrors+=1;HMS_AREngine_ARTrackable_Release(trackable);continue;}if(state==ARENGINE_TRACKING_STATE_PAUSED){paused+=1;HMS_AREngine_ARTrackable_Release(trackable);continue;}if(state==ARENGINE_TRACKING_STATE_STOPPED){stopped+=1;HMS_AREngine_ARTrackable_Release(trackable);continue;}// 只有 TRACKING 目标才进入形状与几何读取。}首轮扫描时已经发现两个候选目标,但两者一直处于PAUSED:
OBJECT_SNAPSHOT sample=82 camera=0 total=2 tracking=0 paused=2 stopped=0 acquireErrors=0 stateErrors=0 classified=0 unknown=0 cube=0 circle=0 rectangle=0 shape=0这时相机已经是TRACKING,目标列表也能取得数据,但还不能读取有效形状。页面因此显示“候选目标未进入跟踪”,没有把total=2写成识别成功。
读取形状、包围盒、半径和中心位姿
目标进入TRACKING后,将通用 Trackable 转为AREngine_ARTarget,读取形状类型。
auto*target=reinterpret_cast<AREngine_ARTarget*>(trackable);AREngine_ARTargetShapeLabel shape=ARENGINE_TARGET_SHAPE_UNKNOWN;AREngine_ARStatus shapeStatus=HMS_AREngine_ARTarget_GetShapeType(session,target,&shape);if(shapeStatus==ARENGINE_SUCCESS&&shape!=ARENGINE_TARGET_SHAPE_UNKNOWN){classified+=1;}else{unknown+=1;}if(shape==ARENGINE_TARGET_SHAPE_CUBE){cube+=1;}elseif(shape==ARENGINE_TARGET_SHAPE_CIRCLE){circle+=1;}elseif(shape==ARENGINE_TARGET_SHAPE_RECTANGLE){rectangle+=1;}几何数据使用三个接口分别读取:
floataabb[6]={};floatradius=0.0f;floatposeRaw[7]={};AREngine_ARPose*pose=nullptr;AREngine_ARStatus aabbStatus=HMS_AREngine_ARTarget_GetAxisAlignedBoundingBox(session,target,aabb,6);AREngine_ARStatus radiusStatus=HMS_AREngine_ARTarget_GetRadius(session,target,&radius);HMS_AREngine_ARPose_Create(session,nullptr,0,&pose);AREngine_ARStatus centerStatus=HMS_AREngine_ARTarget_GetCenterPose(session,target,pose);AREngine_ARStatus rawStatus=HMS_AREngine_ARPose_GetPoseRaw(session,pose,poseRaw,7);HMS_AREngine_ARPose_Destroy(pose);floatsizeX=std::abs(aabb[3]-aabb[0]);floatsizeY=std::abs(aabb[4]-aabb[1]);floatsizeZ=std::abs(aabb[5]-aabb[2]);书本平放在水平面上,因此本次 AABB 的一个轴接近 0,另外两个轴约为 22 cm 和 30 cm,与画面中的实体书尺寸吻合。
在地面创建盒子锚点
页面中央显示十字准星。点击“在这里放置开口盒子”时,原生层在准星附近执行 HitTest。
AREngine_ARHitResultList*hitList=nullptr;HMS_AREngine_ARHitResultList_Create(session,&hitList);floatpixelX=static_cast<float>(width)*0.5f;floatpixelY=static_cast<float>(height)*0.48f;AREngine_ARStatus hitStatus=HMS_AREngine_ARFrame_HitTest(session,frame,pixelX,pixelY,hitList);命中结果不会直接使用。候选对象还要同时满足四个条件:
- Trackable 类型为
ARENGINE_TRACKABLE_PLANE; - Trackable 状态为
ARENGINE_TRACKING_STATE_TRACKING; - 平面方向为
ARENGINE_PLANE_FACING_HORIZONTAL_UPWARD; - 命中 Pose 位于平面多边形内部。
boolvalidPlane=trackableType==ARENGINE_TRACKABLE_PLANE&&trackableState==ARENGINE_TRACKING_STATE_TRACKING&&planeType==ARENGINE_PLANE_FACING_HORIZONTAL_UPWARD&&poseStatus==ARENGINE_SUCCESS&&polygonStatus==ARENGINE_SUCCESS&&inPolygon!=0;if(validPlane){AREngine_ARAnchor*anchor=nullptr;AREngine_ARStatus anchorStatus=HMS_AREngine_ARHitResult_AcquireNewAnchor(session,hit,&anchor);AREngine_ARTrackingState anchorState=ARENGINE_TRACKING_STATE_STOPPED;HMS_AREngine_ARAnchor_GetTrackingState(session,anchor,&anchorState);}正式放置前,对着有纹理的地面缓慢移动手机,让十字落在无遮挡区域。
成功会话返回两个命中候选,最终使用的锚点处于TRACKING,距离相机0.6598 m。
用 OpenGL 绘制顶部敞开的盒子
盒子不是图片覆盖,而是原生 OpenGL 几何体。外框尺寸为0.28 × 0.21 × 0.06 m,内部书籍框为0.24 × 0.17 m。顶部不绘制面,因此可以清楚看到实体书位于盒子内部。
constexprfloatBOX_OUTER_WIDTH_METERS=0.28f;constexprfloatBOX_OUTER_LENGTH_METERS=0.21f;constexprfloatBOX_WALL_HEIGHT_METERS=0.06f;constexprfloatBOOK_SLOT_WIDTH_METERS=0.24f;constexprfloatBOOK_SLOT_LENGTH_METERS=0.17f;每帧从锚点取得模型矩阵,再读取相机 View 和 Projection 矩阵:
floatmodel[16]={};floatview[16]={};floatprojection[16]={};HMS_AREngine_ARAnchor_GetPose(session,guideAnchor,pose);HMS_AREngine_ARPose_GetMatrix(session,pose,model,16);HMS_AREngine_ARCamera_GetViewMatrix(session,camera,view,16);AREngine_ClipPlaneDistance clip{0.05f,20.0f};HMS_AREngine_ARCamera_GetProjectionMatrix(session,camera,clip,projection,16);autoviewModel=MultiplyMatrix(view,model);automvp=MultiplyMatrix(projection,viewModel.data());盒子使用一块半透明底板和四面竖直侧壁,共 30 个三角形顶点;顶部边框、底部边框、四条竖边、内部书籍框和中心十字使用不透明线段。等待放书时为青色,确认完成后改成绿色:
glEnable(GL_DEPTH_TEST);glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);glUseProgram(guideProgram);glUniformMatrix4fv(mvpUniform,1,GL_FALSE,mvp.data());if(matched){glUniform4f(colorUniform,0.20f,0.94f,0.65f,0.32f);}else{glUniform4f(colorUniform,0.16f,0.74f,1.0f,0.24f);}glDrawArrays(GL_TRIANGLES,0,30);glVertexAttribPointer(positionAttribute,3,GL_FLOAT,GL_FALSE,0,lineVertices);glDrawArrays(GL_LINES,0,36);盒子出现后,移动手机观察它是否仍留在原地。锚点继续TRACKING时,盒子不会跟着屏幕移动。
在 ArkTS 页面完成三步交互
Native 模块向 ArkTS 暴露启动、快照、放置、确认、重置和生命周期动作:
declare const objectSemantic: { start: (id: string, rotation: number) => void; getSnapshot: (id: string) => ObjectSemanticSnapshot; placeGuide: (id: string) => void; confirmGuide: (id: string) => void; resetGuide: (id: string) => void; pause: (id: string) => void; resume: (id: string) => void; stop: (id: string) => void; };页面根据guideState只保留一个主动作:
private runPrimaryAction(): void { if (this.snapshot.guideState === 2) { objectSemantic.confirmGuide(this.componentId); hilog.info(DOMAIN, TAG, 'BOOK_UI_CONFIRM sample=%{public}d manual=true', this.snapshot.sample); return; } if (this.snapshot.guideState === 3) { objectSemantic.resetGuide(this.componentId); return; } objectSemantic.placeGuide(this.componentId); }三个阶段对应的按钮文字为:
未放置:在这里放置开口盒子 已锚定:确认实体书已放入盒子 已完成:重新开始匹配确认动作不调用图像分类,也不写入官方 TARGET 结果。它只在相机和书籍锚点都处于TRACKING时记录人工完成状态:
boolconfirmed=anchorStatus==ARENGINE_SUCCESS&&anchorState==ARENGINE_TRACKING_STATE_TRACKING&&cameraState==ARENGINE_TRACKING_STATE_TRACKING;if(confirmed){snapshot.guideState=GUIDE_MATCHED;snapshot.guideManualConfirmed=1;snapshot.guideConfirmedSample=sample;}真机怎么操作
1. 先让书完整进入画面
把书平放在有纹理的地面上,手机距离书大约 60~90 cm。取景时保留书本四周的一圈地面,不要让封面边缘贴住画面边界。
首轮近距离扫描时,两个候选目标持续PAUSED,没有有效形状。
2. 缓慢移动手机,不在屏幕上滑动
保持书本完整入镜,手机向左、右和斜上方缓慢移动,让 AR Engine 取得不同视角。屏幕滑动不会改变相机位姿,对物体跟踪没有帮助。
本次移动到第 766 帧时,第 3 个候选目标进入TRACKING,形状变为RECTANGLE;第 780 帧仍然保持相同尺寸,第 810 帧再次取得矩形结果。
sample=766 tracking=1 paused=2 classified=1 rectangle=1 shape=3 radius=0.1856 size=0.2219,0.0000,0.2976 sample=780 tracking=1 paused=2 classified=1 rectangle=1 shape=3 radius=0.1856 size=0.2219,0.0000,0.2976 sample=810 tracking=1 paused=2 classified=1 rectangle=1 shape=3 radius=0.1841 size=0.2240,0.0000,0.29213. 扫描地面并放置盒子
让中心十字对准书本旁边的空地,点击“在这里放置开口盒子”。成功时页面显示盒子已锚定,原生日志同时给出命中候选数、距离、锚点状态和世界坐标。
4. 把实体书放进盒子
把书移入半透明盒子,调整位置和方向,让书位于内部24 × 17 cm落位框附近。确认实体书已经进入盒子后,点击“确认实体书已放入盒子”。
放入时页面显示锚点TRACKING · 稳定跟踪、命中距离0.73 m和盒子世界坐标:
点击确认后,页面显示“书籍摆放匹配成功”,盒子描边变为绿色,并保留“人工确认”标记:
BOOK_GUIDE_PLACE model=OPEN_BOX_GUIDE placed=1 hitStatus=0 hitCount=2 anchorTracking=0 distance=0.7255 center=-0.3791,-0.8131,0.0138 BOOK_MATCH_CONFIRM confirmed=1 manual=true sample=6946 camera=0 anchor=0 status=0这一轮书和盒子组合进入 TARGET 视野后,官方结果稳定返回CUBE,AABB 为0.2081 × 0.0281 × 0.2726 m。它没有覆盖前面单独扫描书籍取得的RECTANGLE,而是作为本轮组合外形的独立记录:
下面的图只用来区分两轮扫描对象:左侧是单独扫描书籍,右侧是书籍放入盒子后的组合目标。分类结论仍以真机截图和紧随其后的原生日志为准。
OBJECT_SNAPSHOT sample=5940 camera=0 total=4 tracking=1 paused=3 stopped=0 classified=1 cube=1 rectangle=0 shape=1 radius=0.1720 size=0.2081,0.0281,0.2726 shapeStatus=0 geometryStatus=0 OBJECT_SNAPSHOT sample=6960 camera=0 total=4 tracking=1 paused=3 stopped=0 classified=1 cube=1 rectangle=0 shape=1 radius=0.1720 size=0.2081,0.0281,0.2726 shapeStatus=0 geometryStatus=0遇到的情况与处理
情况一:目标数量增加,但一直没有形状
最初页面能看到两个 TARGET,却一直是PAUSED 2 / TRACKING 0。这说明目标候选已经产生,形状读取接口也没有报错,但目标尚未进入可读取形状的跟踪状态。
处理时依次核对了三项配置:
- 平面检测不能关闭,改为
HORIZONTAL_AND_VERTICAL; - 相机对焦从默认固定模式改为
AUTO; - 书本四条边不能贴住相机画面边缘,扫描时保留周围地面。
系统日志曾反复出现:
Invalid 2D box. (2D box lies along the image border)把整本书完整放进画面并缓慢移动后,目标进入TRACKING,随后取得RECTANGLE。
情况二:模型已经出现,证据卡仍显示初始值
第一版页面顶部已经显示“模型已锚定”,相机画面也能看到立体模型,但底部锚点卡仍显示“尚未创建”。原生日志中的锚点状态和距离是正确的,问题出在 ArkUI 自定义 Builder。
错误写法把动态值作为 Builder 参数传入:
this.evidenceRow('锚点状态', this.guideTrackingText())修改后只传固定标签,在 Builder 内直接读取@State:
this.evidenceRow('锚点状态') @Builder private evidenceRow(label: string): void { Row() { Text(label) Text(this.evidenceValue(label)) } }修复后的待放置、命中失败和按钮状态均能随快照变化。
情况三:点击放置后命中结果为 0
地面看起来平整,不代表 AR Engine 已经建立可命中的平面。重新进入会话后立即点击,原生返回:
BOOK_GUIDE_PLACE placed=0 hitStatus=-2001 hitCount=0 anchorTracking=-1 distance=0.0000 center=0.0000,0.0000,0.0000页面按失败分支显示“这次没有命中地面”,不创建锚点,也不显示模型。
处理动作只有两个:继续缓慢移动手机,让画面看到更多地面纹理;再让十字落在无遮挡区域重新放置。不能在hitCount=0时直接生成一个假锚点。
构建与验证结果
静态回归脚本检查以下内容:
- TARGET 会话保留水平与垂直平面检测;
- 自动对焦已经设置并回读;
- PAUSED、STOPPED、Acquire 错误和状态错误均有统计;
- HitTest、水平向上平面、锚点、盒子渲染和释放接口均存在;
- 开口盒子的外框、墙高、内部书籍框和
OPEN_BOX_GUIDE日志标识均存在; - 人工确认日志固定包含
manual=true; - ArkTS 页面不会用人工确认覆盖官方 TARGET 结果。
回归结果:
物体语义状态观测回归检查通过。最终执行 clean 签名构建:
hvigorw clean assembleHap--no-daemon构建结果:
BuildNativeWithCmake SUCCESS BuildNativeWithNinja SUCCESS CompileArkTS SUCCESS PackageHap SUCCESS SignHap SUCCESS BUILD SUCCESSFUL本次结果
这次真机实验得到的结果如下:
| 验证项 | 真机结果 |
|---|---|
| TARGET 配置 | 成功,semanticMode=2 |
| 官方形状分类:单独扫描实体书 | 成功,返回RECTANGLE |
| 目标几何 | 成功,约0.222 × 0.298 m,半径约0.186 m |
| 地面开口盒子 | 成功,28 × 21 × 6 cm,命中距离0.7255 m,锚点TRACKING |
| 实体书放入盒子 | 成功,真机画面可见书位于四面侧壁内部 |
| 摆放确认 | 成功,第 6946 帧记录,manual=true |
| 官方形状分类:书与盒子组合 | 返回CUBE,AABB0.2081 × 0.0281 × 0.2726 m |
自动分类与人工摆放确认是两条独立证据。单独扫描书籍得到RECTANGLE,放入盒子后的组合外形得到CUBE;人工确认不会决定或修改这两个枚举结果。目标在部分帧会从TRACKING回到PAUSED,因此页面保留最后一次有效自动分类结果,实时 TARGET 计数仍继续更新。
退出页面时先 detach 并 release 书籍锚点,再停止 Session,销毁 Frame、两个 OpenGL Program、相机纹理、EGL Surface、EGL Context 和 Native XComponent 实例,避免相机与原生资源留在后台。
官方资料
- AR Engine Kit 指南
- 识别目标形状(C/C++)
- HarmonyOS AR Engine C++ 示例