ArCore屏幕四角点平面检测:解决frame.hitTest返回空问题
解决ARCore中frame.hitTest返回空值,实现稳定的屏幕四角平面检测
问题背景
需要在ARCore识别出的平面上绘制拍摄区域边框,但原实现中frame.hitTest偶尔返回空值,导致无法获取四角的Pose。原代码及尝试的数学平面求交实现如下:
原hitTest实现
var lbPose = convertAndroidViewCoords(bounds[0], frame, plane) var rbPose = convertAndroidViewCoords(bounds[1], frame, plane) var rtPose = convertAndroidViewCoords(bounds[2], frame, plane) var ltPose = convertAndroidViewCoords(bounds[3], frame, plane) private fun convertAndroidViewCoords(point: PointF, frame: Frame, plane: Plane): Pose? { convertFloats[0] = point.x convertFloats[1] = point.y frame.transformCoordinates2d( Coordinates2d.IMAGE_PIXELS, convertFloats, Coordinates2d.VIEW, convertFloatsOut ) return hitTestView(convertFloatsOut[0], convertFloatsOut[1], frame, plane) } private fun hitTestView(x: Float, y: Float, frame: Frame, plane: Plane): Pose? { val hits = frame.hitTest(x, y) val result = hits.firstOrNull { it.trackable.equals(plane) } ?: return null return result.hitPose }
尝试的数学平面求交实现
val planePose = plane.centerPose val quaternion = Quaternion(planePose.rotationQuaternion) val normal = Quaternion.rotateVector(quaternion, Vector3.right()) val mathPlane = MathPlane(Vector3(planePose.tx(), planePose.ty(), planePose.tz()), normal) // left-bottom corner val ray = rayHitCalculation.screenPointToRay(0f, view.root.height.toFloat()) val rayHit = RayHit() if (mathPlane.rayIntersection(ray, rayHit)) { val p = rayHit.point lbPose = Pose(floatArrayOf(p.x, p.y, p.z), planePose.rotationQuaternion) } /// etc. for another corners
解决方案
1. 修复hitTest的匹配逻辑
原代码用it.trackable.equals(plane)判断匹配平面存在准确性问题,ARCore的Trackable对象equals方法无法保证精准匹配同一平面,应改用平面ID匹配,同时先筛选所有平面类型的检测结果,避免被锚点等其他Trackable干扰:
private fun hitTestView(x: Float, y: Float, frame: Frame, plane: Plane): Pose? { val hits = frame.hitTest(x, y) // 先筛选平面类型结果,再匹配目标平面ID val result = hits.firstOrNull { it.trackable is Plane && (it.trackable as Plane).id == plane.id } ?: return null return result.hitPose }
2. 修正数学平面的法线计算
原代码用Vector3.right()旋转得到法线是错误的,ARCore平面的法线对应其centerPose的Y轴方向(垂直平面向上),正确的法线构建方式如下:
val planePose = plane.centerPose // 从平面Pose的旋转四元数中提取Y轴作为法线 val quaternion = Quaternion(planePose.rotationQuaternion) val normal = Quaternion.rotateVector(quaternion, Vector3(0f, 1f, 0f)) val mathPlane = MathPlane( Vector3(planePose.tx(), planePose.ty(), planePose.tz()), normal )
同时,确保screenPointToRay使用ARCore相机的投影矩阵和视图矩阵计算射线,避免参数错误导致无交点:
// 基于ARCore Camera构建射线示例 fun screenPointToRay(x: Float, y: Float, camera: Camera, viewWidth: Int, viewHeight: Int): Ray { val normalizedX = (x / viewWidth) * 2f - 1f val normalizedY = -(y / viewHeight) * 2f + 1f val nearPoint = camera.viewProjectionMatrix.inverse().transformPoint(Vector4(normalizedX, normalizedY, -1f, 1f)) val farPoint = camera.viewProjectionMatrix.inverse().transformPoint(Vector4(normalizedX, normalizedY, 1f, 1f)) val direction = farPoint.subtract(nearPoint).normalized() return Ray(Vector3(nearPoint.x, nearPoint.y, nearPoint.z), direction) }
3. 实现双策略 fallback 机制
优先使用frame.hitTest获取Pose,若返回null则自动切换到数学平面求交方法,最大化稳定性:
private fun getCornerPose(point: PointF, frame: Frame, plane: Plane, camera: Camera, viewWidth: Int, viewHeight: Int): Pose? { // 优先用hitTest方法 convertFloats[0] = point.x convertFloats[1] = point.y frame.transformCoordinates2d( Coordinates2d.IMAGE_PIXELS, convertFloats, Coordinates2d.VIEW, convertFloatsOut ) val hitPose = hitTestView(convertFloatsOut[0], convertFloatsOut[1], frame, plane) if (hitPose != null) return hitPose // hitTest失败,使用数学平面求交 val planePose = plane.centerPose val quaternion = Quaternion(planePose.rotationQuaternion) val normal = Quaternion.rotateVector(quaternion, Vector3(0f, 1f, 0f)) val mathPlane = MathPlane(Vector3(planePose.tx(), planePose.ty(), planePose.tz()), normal) val ray = screenPointToRay(point.x, point.y, camera, viewWidth, viewHeight) val rayHit = RayHit() return if (mathPlane.rayIntersection(ray, rayHit)) { val p = rayHit.point Pose(floatArrayOf(p.x, p.y, p.z), planePose.rotationQuaternion) } else { null } }
内容的提问来源于stack exchange,提问作者Michael Budash
相关产品推荐
相关产品推荐

