平面检测与边界获取详解
平面检测的工作机制
ARCore 的平面检测建立在它的 SLAM 特征点地图之上。整个过程:
- 特征点收集:每一帧从摄像头画面提取数千个特征点,跟踪它们的空间位置
- 共面性检测:ARCore 不断检查特征点是否呈现共面趋势——如果一批特征点都大致落在同一个平面上,就会创建一个 Plane 对象
- 边界扩展:随着看到更多特征点,Plane 的 polygon(边界多边形)会逐渐扩大,直到完整覆盖实际物理平面
- 方向判定:ARCore 用表面法线方向把 Plane 分类为水平朝上、水平朝下、垂直、倾斜等类型
这个过程完全是 ARCore 内部自动运行的,开发者只需要在每一帧的 session.update() 之后读取 Plane 列表。
四种 Plane 类型
| 类型 | Plane.Type 常量 | 常见物体 | 表面法线方向 |
|---|---|---|---|
| 水平朝上 | HORIZONTAL_UPWARD_FACING | 桌面、地板、茶几、灶台 | 大致朝上 |
| 水平朝下 | HORIZONTAL_DOWNWARD_FACING | 天花板、吊灯上方 | 大致朝下 |
| 垂直 | VERTICAL | 墙壁、柜门、窗户 | 大致水平 |
| 倾斜 | INCLINED | 楼梯台阶、书架斜角 | 非水平非垂直 |
还有一种特殊的 UNKNOWN 类型——ARCore 在检测初期还无法确定方向时会返回这个类型,通常是暂时的。
用法:根据类型做交互
session.update(frame)
val planes = session.getAllTrackables<Plane>()
planes.forEach { plane ->
if (plane.trackingState != TrackingState.TRACKING) return@forEach
when (plane.type) {
Plane.Type.HORIZONTAL_UPWARD_FACING -> {
// 可以在上面放桌子、地毯等
showFurniturePlacementHint(plane)
}
Plane.Type.VERTICAL -> {
// 可以在上面挂画、镜子等
showWallDecorationHint(plane)
}
Plane.Type.INCLINED -> {
// 楼梯等,可能适合放台阶、滑雪坡道
}
}
}
Plane 的边界 Polygon
Plane 对象的核心是它的 polygon 属性——一个多边形列表,描述这个平面的实际物理边界。
val plane: Plane = session.getAllTrackables<Plane>().first()
// polygon 是 List<List<Pose>>
// 外层 List 是多个边界环(外轮廓 + 可能的内部孔洞)
// 内层 List 是一个环上的所有顶点 Pose(首尾相连形成闭合多边形)
val boundaries = plane.polygon
boundaries.forEach { ring ->
ring.forEach { vertexPose ->
val worldPos = vertexPose.translation
// 可以用这些点在 3D 世界中画出平面边界
}
}
边界的质量受以下因素影响:
- 用户扫描程度:平面的哪些区域被摄像头看到过,ARCore 才能检测到边界
- 特征点密度:边缘位置的特征点越多,边界越精确
- 平面合并:多个小 Plane 合并成一个大 Plane 后,新 Plane 的 polygon 是合并后的完整边界
extents:粗略的近似尺寸
如果不需要精确的多边形边界,可以用 extents 属性快速获取 Plane 的大致宽高:
val extents: FloatArray = plane.extents // [width, height](单位:米)
注意:extents 返回的是 AABB(轴对齐包围盒) 的尺寸,不是实际形状的精确尺寸。对于圆角或斜边的桌子,这个值会偏大。
监听新 Plane 的出现
ARCore 不会主动通知开发者有新 Plane 出现——你需要在每一帧检查。一个高效的做法是用 Plane 的标识符(hashCode 或引用)记录已处理过的 Plane:
private val processedPlaneIds = HashSet<Int>()
session.update(frame)
session.getAllTrackables<Plane>().forEach { plane ->
if (plane.trackingState != TrackingState.TRACKING) return@forEach
if (!processedPlaneIds.add(plane.hashCode())) return@forEach
// 这是新检测到的 Plane
onNewPlaneDetected(plane)
}
平面合并:坑点与处理
ARCore 会在后续帧中合并它判断为同一个物理平面的多个小 Plane。合并后的表现:
- 原来的 Plane 对象变成
TrackingState.STOPPED - 新 Plane 对象有更大的 polygon 和 extents
- 两个 Plane 对象的标识符不同
开发者需要确保在合并时正确处理旧 Plane 上的 Anchor 和虚拟物体:
session.getAllTrackables<Plane>().forEach { plane ->
if (plane.trackingState == TrackingState.STOPPED) {
// 这个 Plane 已停止跟踪(可能被合并)
// 检查是否有 Anchor 还绑定在它上面
findAnchorsOnPlane(plane)?.forEach { anchor ->
anchor.detach()
// 或者尝试把虚拟物体重新放置到新 Plane 上
}
}
}
Plane 合并的时机:ARCore 检测到两个 Plane 的特征点簇有大量重叠时,会在一帧之内完成合并。
影响平面检测的环境条件
| 条件 | 好 | 坏 | 原因 |
|---|---|---|---|
| 纹理 | 木纹地板、花纹地毯、海报贴墙 | 纯白色墙面、光滑玻璃桌面、磨砂金属 | 特征点密度 |
| 光照 | 明亮均匀的室内光 | 昏暗走廊、强逆光、高频闪烁光源 | 摄像头成像质量 |
| 扫描范围 | 缓慢扫过整个房间 | 只盯着桌子一角 | 完整特征点视图 |
| 视野深度 | 同时看到近处和远处 | 只看到 20cm 内的物体 | 视差不足 |
| 遮挡 | 平面完整暴露 | 桌子上堆满书和杯子 | 特征点被遮挡 |
给用户的引导文案
根据环境条件自动判断是否需要引导用户:
"请缓慢移动手机扫一下周围环境" "光照条件不太好,建议移到光线充足的地方" "当前没有检测到平面,请对准桌面、地板或墙壁"