You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 22:28:18