从法线到智能手机背面的 AbsoluteOrientationSensor 四元数的 2D 罗盘航向。云台锁定问题

2D Compass heading from AbsoluteOrientationSensor quaternion for the normal to the back of the smartphone. Gimbal lock issue

提问人:WSA 提问时间:10/6/2023 最后编辑:WSA 更新时间:10/6/2023 访问量:78

问:

我需要获取智能手机(设备)相对于北方的旋转角度(0 - 360 度) 我对法线的角度感兴趣,从设备的后盖(相机眼睛所在的位置)。无论设备如何不旋转,我都对后盖相对于 0 度的确切外观感兴趣。 让我们想象一下,手机是一个窗口。我想知道我透过这扇窗户看哪里,是北还是南。也就是说,我可以自由地向各个方向旋转我的智能手机。

我觉得返回 AbsoluteOrientationSensor 的四元数具有我需要的所有信息,但是我发现转换为指南针方向的公式容易出现云台锁定问题。 以下是使用此公式(javascript)的重新计算:

 const quaternionToHeading = function(q) {
        let [x, y, z, w] = q;
        let a = Math.atan2(2*x*y + 2*z*w, 1 - 2*y*y - 2*z*z)*(180/Math.PI);
        if(a < 0) a = 360 + a;
        return (360 - a).toFixed(1);
    }

因此,我有正确的指南针值,但并非在所有情况下都如此。

  1. 例如,当我将屏幕放在正前方的智能手机并旋转(偏航)时,我可以看到正确的指南针值。

enter image description here

  1. 当我将智能手机平行于地面转动并在这个位置围绕我旋转(偏航)时,我也看到了正确的值。

enter image description here

  1. 但是,如果我从纵向位置转动(滚动)智能手机,但在后盖正常的情况下以相同的方式向北握住它,指南针值会发生不可预测的变化。

enter image description here

请帮我找到解决方案。

演示应用程序(滚动测试需要从智能手机运行,PC浏览器不支持方向锁定):https://www.arkon.solutions/static/quaternioncompass/index.html

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.board {
    color: white;
    width: 5em;
    height: 5em;
    align-items: center;
    justify-content: center;
    position: absolute;
    display: flex;
    flex-direction: column;
    top: 50%;
    left: 50%;
    transform: translateY(-50%) translateX(-50%);
    font-size: 3em;
}

.control-block {
    padding: 1em;
    position: absolute;
    bottom:0;
    left: 50%;
    transform: translateX(-50%);
    text-align: center;
}

.control-block__message {
    color: orange;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css"/>
    <title>Document</title>
</head>
<script type="module">

    const board = document.querySelector(".board");
    const lblHeading = document.querySelector("#lblHeading");
    const lblSide = document.querySelector("#lblSide");
    
    const options = { frequency: 60, referenceFrame: "device" };
    const sensor = new AbsoluteOrientationSensor(options);
    sensor.addEventListener("reading", () => {
        let heading = quaternionToHeading(sensor.quaternion);
        let side = headingToSide(heading);
        let color = headingToColor(heading);
        lblHeading.innerHTML = Math.round(heading) + '°';
        lblSide.innerHTML = side;
        board.style.backgroundColor = color;
    });
    sensor.addEventListener("error", (error) => {
    if (event.error.name === "NotReadableError") {
        console.log("Sensor is not available.");
    }
    });
    sensor.start();


    //Magic here
    const quaternionToHeading = function(q) {
        let [x, y, z, w] = q;
        let a = Math.atan2(2*x*y + 2*z*w, 1 - 2*y*y - 2*z*z)*(180/Math.PI);
        if(a < 0) a = 360 + a;
        return (360 - a).toFixed(1);
    }

    const headingToSide = function(heading) {
        //N 
        //E 45 - 145
        //S 135 - 225
        //W 225 - 315

        //NE 22.5 - 67,5
        //E 67,5 - 112,5
        //ES 112,5 - 157,5
        //S 157,5 - 202,5
        //WS 202,5 - 247,5
        //W 247,5 - 292,5
        //WN 292,5 - 337,5
        //N - rest
        let result = 'N';
        if (heading > 22.5 && heading <= 67.5) {
            result = 'NE';
        } else if (heading > 67.5 && heading <= 112.5) {
            result = 'E'
        } else if (heading > 112.5 && heading <= 157.5) {
            result = 'SE';
        } else if (heading > 157.5 && heading <= 202.5) {
            result = 'S';
        } else if (heading > 202.5 && heading <= 247.5) {
            result = 'SW';
        } else if (heading > 247.5 && heading <= 292.5) {
            result = 'W';
        } else if (heading > 292.5 && heading <= 337.5) {
            result = 'NW';
        }
        return result;
    }

    const headingToColor = function(heading) {
        let southLevel = heading;
        if (heading > 180) {
            southLevel = 360 - heading;
        }
        
        let r = 0,
            g = 0,
            b = 0;
        r = Math.round(255 * southLevel / 180);
        b = 255 - r;
        
        return `rgb(${r},${g},${b})`;
    }

</script>
<script>


    function showMessage(msg) {
        document.querySelector('.control-block__message').innerHTML = msg;
    }
    
    function switchFullScreen(elem = document.body) {

        if (!!document.fullscreenElement) {
            let fnRequestExitFullScreen = null;
            if (document.exitFullscreen) {
                fnRequestExitFullScreen = document.exitFullscreen();
            } else if (document.webkitExitFullscreen) { /* Safari */
                fnRequestExitFullScreen = document.webkitExitFullscreen();
            } else if (document.msExitFullscreen) { /* IE11 */
                fnRequestExitFullScreen = document.msExitFullscreen();
            }
            fnRequestExitFullScreen.then(() => {
                screen.orientation.unlock();
                this.showMessage("Please, switch app to fullscreen mode");
            }).catch(ex => {
                alert(ex);
            });
        } else {
            let fnRequestFullscreen = null;
            if (elem.requestFullscreen) {
                fnRequestFullscreen = elem.requestFullscreen();
            } else if (elem.webkitRequestFullscreen) { /* Safari */
                fnRequestFullscreen = elem.webkitRequestFullscreen();
            } else if (elem.msRequestFullscreen) { /* IE11 */
                fnRequestFullscreen = elem.msRequestFullscreen();
            }
            fnRequestFullscreen.then(() => {
                return screen.orientation
                    .lock("portrait")
                    .then(() => {
                        this.showMessage("Ok, app is ready to test now");
                    }).catch((ex) => {
                        alert(ex);
                    });
            }).catch(ex => {
                alert(JSON.stringify(ex));
            });
        }
        
    }

    function onFullscreenClick() {
        this.switchFullScreen(document.body);
    }

</script>
<body>
    <div class="board">
        <span id="lblHeading"></span>
        <span id="lblSide"></span>
    </div>
    <div class="control-block">
        <div class="control-block__message">Please, switch app to fullscreen mode</div>
        <button onclick="onFullscreenClick()">Full screen + Lock orientation</button>
    </div>
</body>
</html>

故障项目:https://glitch.com/edit/#!/frequent-parallel-antimatter

JavaScript 数学 传感器 webapi 四元数

评论

1赞 Mike 'Pomax' Kamermans 10/6/2023
请记住,您的帖子应包含所有详细信息,包括所有(最小可重现示例)代码。指向外部网站的链接是可以的,但前提是除了在您的帖子中包含相关代码之外。Stackoverflow 有一个内置的“可运行片段”功能,用于使用“运行”按钮将纯 html/js/css 代码直接放入您的帖子中。
1赞 WSA 10/6/2023
你是对的,我把代码粘贴到这里,让任何人都愿意将测试复制到他们的编辑器中(我希望如此)。
1赞 Kasper Kamperman 11/21/2023
如果我将它与 iOS 指南针进行比较,它仍然有 20 度的差异。我研究了一段时间,但我始终无法在浏览器中获得 Android 上的可靠读数。不要明白,在 AR 时代,这不是一个像 iOS 提供的 webkitCompassHeading 那样的简单 API。

答: 暂无答案