Преобразовать значения магнитного поля X, Y, Z из устройства в глобальную систему отсчета

При использовании датчика TYPE_MAGNETOMETER вы получаете значения напряженности магнитного поля X, Y, Z относительно ориентации устройства. То, что я хочу получить, это преобразовать эти значения в глобальную систему отсчета, пояснив: пользователь берет устройство, измеряет эти значения, затем поворачивает устройство на несколько градусов вокруг любой оси и получает ~ те же самые значения. Ниже приведены похожие вопросы: Получение значений магнитного поля в глобальных координатах. Как получить вектор магнитного поля, не зависящий от вращения устройства? В этом ответе описывается пример решения (оно предназначено для линейного ускорения, но я думаю, что это не имеет значения): /questions/13211552/uskorenie-iz-sistemyi-koordinat-ustrojstva-v-absolyutnuyu-sistemu-koordinat/13211567#13211567 Я использовал его и получил 3 значения, X всегда очень маленький (не думаю, что это правильно), Y и Z в порядке, но они все же немного изменились, когда я поворачиваю устройство. Как это можно отрегулировать? И можно ли все это решить? Я использую простой фильтр Калмана для аппроксимации измеренных значений, потому что без него я получаю различные значения, даже если устройство вообще не движется / не вращается. Пожалуйста, найдите мой код ниже:

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.Matrix;
import android.os.Bundle;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import com.test.statistics.filter.kalman.KalmanState;
import com.example.R;

/**
 * Activity for gathering magnetic field statistics.
 */
public class MagneticFieldStatisticsGatheringActivity extends Activity implements SensorEventListener {

    public static final int KALMAN_STATE_MAX_SIZE = 80;
    public static final double MEASUREMENT_NOISE = 5;

    /** Sensor manager. */
    private SensorManager mSensorManager;
    /** Magnetometer spec. */
    private TextView vendor;
    private TextView resolution;
    private TextView maximumRange;

    /** Magnetic field coordinates measurements. */
    private TextView magneticXTextView;
    private TextView magneticYTextView;
    private TextView magneticZTextView;

    /** Sensors. */
    private Sensor mAccelerometer;
    private Sensor mGeomagnetic;
    private float[] accelerometerValues;
    private float[] geomagneticValues;

    /** Flags. */
    private boolean specDefined = false;
    private boolean kalmanFiletring = false;

    /** Rates. */
    private float nanoTtoGRate = 0.00001f;
    private final int gToCountRate = 1000000;

    /** Kalman vars. */
    private KalmanState previousKalmanStateX;
    private KalmanState previousKalmanStateY;
    private KalmanState previousKalmanStateZ;
    private int previousKalmanStateCounter = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main2);
        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

        mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mGeomagnetic = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

        vendor = (TextView) findViewById(R.id.vendor);
        resolution = (TextView) findViewById(R.id.resolution);
        maximumRange = (TextView) findViewById(R.id.maximumRange);

        magneticXTextView = (TextView) findViewById(R.id.magneticX);
        magneticYTextView = (TextView) findViewById(R.id.magneticY);
        magneticZTextView = (TextView) findViewById(R.id.magneticZ);

        mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
        mSensorManager.registerListener(this, mGeomagnetic, SensorManager.SENSOR_DELAY_FASTEST);
    }

    /**
     * Refresh statistics.
     *
     * @param view - refresh button view.
     */
    public void onClickRefreshMagneticButton(View view) {
        resetKalmanFilter();
    }

    /**
     * Switch Kalman filtering on/off
     *
     * @param view - Klaman filetring switcher (checkbox)
     */
    public void onClickKalmanFilteringCheckBox(View view) {
        CheckBox kalmanFiltering = (CheckBox) view;
        this.kalmanFiletring = kalmanFiltering.isChecked();
    }

    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        if (sensorEvent.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
            return;
        }
        synchronized (this) {
            switch(sensorEvent.sensor.getType()){
                case Sensor.TYPE_ACCELEROMETER:
                    accelerometerValues = sensorEvent.values.clone();
                    break;
                case Sensor.TYPE_MAGNETIC_FIELD:
                    if (!specDefined) {
                        vendor.setText("Vendor: " + sensorEvent.sensor.getVendor() + " " + sensorEvent.sensor.getName());
                        float resolutionValue = sensorEvent.sensor.getResolution() * nanoTtoGRate;
                        resolution.setText("Resolution: " + resolutionValue);
                        float maximumRangeValue = sensorEvent.sensor.getMaximumRange() * nanoTtoGRate;
                        maximumRange.setText("Maximum range: " + maximumRangeValue);
                    }
                    geomagneticValues = sensorEvent.values.clone();
                    break;
            }
            if (accelerometerValues != null && geomagneticValues != null) {
                float[] Rs = new float[16];
                float[] I = new float[16];

                if (SensorManager.getRotationMatrix(Rs, I, accelerometerValues, geomagneticValues)) {

                    float[] RsInv = new float[16];
                    Matrix.invertM(RsInv, 0, Rs, 0);

                    float resultVec[] = new float[4];
                    float[] geomagneticValuesAdjusted = new float[4];
                    geomagneticValuesAdjusted[0] = geomagneticValues[0];
                    geomagneticValuesAdjusted[1] = geomagneticValues[1];
                    geomagneticValuesAdjusted[2] = geomagneticValues[2];
                    geomagneticValuesAdjusted[3] = 0;
                    Matrix.multiplyMV(resultVec, 0, RsInv, 0, geomagneticValuesAdjusted, 0);

                    for (int i = 0; i < resultVec.length; i++) {
                        resultVec[i] = resultVec[i] * nanoTtoGRate * gToCountRate;
                    }

                    if (kalmanFiletring) {

                        KalmanState currentKalmanStateX = new KalmanState(MEASUREMENT_NOISE, accelerometerValues[0], (double)resultVec[0], previousKalmanStateX);
                        previousKalmanStateX = currentKalmanStateX;

                        KalmanState currentKalmanStateY = new KalmanState(MEASUREMENT_NOISE, accelerometerValues[1], (double)resultVec[1], previousKalmanStateY);
                        previousKalmanStateY = currentKalmanStateY;

                        KalmanState currentKalmanStateZ = new KalmanState(MEASUREMENT_NOISE, accelerometerValues[2], (double)resultVec[2], previousKalmanStateZ);
                        previousKalmanStateZ = currentKalmanStateZ;

                        if (previousKalmanStateCounter == KALMAN_STATE_MAX_SIZE) {
                            magneticXTextView.setText("x: " + previousKalmanStateX.getX_estimate());
                            magneticYTextView.setText("y: " + previousKalmanStateY.getX_estimate());
                            magneticZTextView.setText("z: " + previousKalmanStateZ.getX_estimate());

                            resetKalmanFilter();
                        } else {
                            previousKalmanStateCounter++;
                        }

                    } else {
                        magneticXTextView.setText("x: " + resultVec[0]);
                        magneticYTextView.setText("y: " + resultVec[1]);
                        magneticZTextView.setText("z: " + resultVec[2]);
                    }
                }
            }
        }
    }

    private void resetKalmanFilter() {
        previousKalmanStateX = null;
        previousKalmanStateY = null;
        previousKalmanStateZ = null;
        previousKalmanStateCounter = 0;
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
    }
}

Спасибо всем, кто прочитал этот пост и кто заранее опубликовал некоторые мысли о проблеме.

2 ответа

Решение

В моем комментарии к проверенному ответу по приведенной выше ссылке я сослался на свой простой ответ при расчете ускорения относительно истинного севера.

Позвольте мне ответить здесь снова с дополнительными разъяснениями. Ответ - произведение матрицы вращения на значения магнитного поля. Если вы читаете далее "X всегда очень маленький", это правильное значение.

Акселерометр и датчики магнитного поля измеряют ускорение устройства и магнитное поле земли в месте расположения устройства соответственно. Они являются векторами в трехмерном пространстве, назовем их a и m соответственно.
Если вы стоите и вращаете свое устройство, теоретически m не изменяется, если не существует магнитных помех от окружающих объектов (на самом деле m должно мало измениться, если вы перемещаетесь, так как магнитное поле Земли на небольшом расстоянии должно измениться незначительно). Но a действительно меняется, хотя это не должно быть решительным в большинстве ситуаций.

Теперь вектор v в трехмерном пространстве может быть представлен 3-мя кортежами (v_1, v_2, v_3) относительно некоторого базиса (e_1, e_2, e_3), то есть v = v_1 e_1 + v_2 e_2 + v_3 e_3. (v_1, v_2, v_3) называются координатами v относительно базиса (e_1, e_2, e_3).

В устройствах Android основанием является (x, y, z), где для большинства телефонов x располагается вдоль более короткой стороны и направлен вправо, y - вдоль более длинной стороны и направлен вверх, а z - перпендикулярно экрану и указывает на него.
Теперь эта база меняется по мере изменения положения устройства. Можно считать эти базисы функцией времени (x (t), y (t), z (t)), в математике это движущаяся система координат.

Таким образом, несмотря на то, что m не изменяется, но возвращаемые датчиками значения event.values различаются, поскольку основа различна (позже я расскажу о флуктуациях). Как и так, event.values бесполезны, потому что он дает нам координаты, но мы не знаем, что это за основа, то есть относительно какой-то базы, которую мы знаем.

Теперь возникает вопрос: возможно ли найти координаты a и m относительно основы фиксированного мира (w_1, w_2, w_3), где w_1 указывает на восток, w_2 указывает на магнитный север, а w_3 указывает на небо?

Ответ положительный при условии соблюдения двух важных предположений.
С этими двумя допущениями легко вычислить (всего несколько перекрестных произведений) изменение базисной матрицы R от базиса (x, y, z) к базису (w_1, w_2, w_3), который в Android называется вращением матрица Тогда координаты вектора v относительно базиса (w_1, w_2, w_3) получаются умножением R на координаты v относительно (x, y, z). Таким образом, координаты m относительно мировой системы координат являются просто произведением матрицы вращения и значений event.values, возвращаемых датчиком TYPE_MAGNETIC_FIELD и аналогично для a.

В Android матрица вращения получается путем вызова getRotationMatrix (float[] R, float[] I, float[] gravity, float[] geomagnet), и мы обычно передаем возвращенные значения акселерометра для параметра гравитации и значения магнитного поля для геомагнитный.

2 важных предположения:
1- Параметр гравитации представляет вектор, лежащий в w_3, более конкретно, это минус вектора, на который влияет только гравитация.
Таким образом, если вы передадите значения акселерометра без фильтрации, матрица вращения будет слегка отключена. Вот почему вам нужно отфильтровать акселерометр так, чтобы значения фильтра составляли только минус вектор силы тяжести. Поскольку гравитационное ускорение является доминирующим фактором в векторе акселерометра, обычно достаточно фильтра низких частот.
2- Геомагнитный параметр представляет вектор, лежащий в плоскости, охватываемой векторами w_2 и w_3. То есть он лежит в плоскости North-Sky. Таким образом, в терминах (w_1, w_2, w_3) первая координата должна быть 0. Следовательно, "X всегда очень маленький", как вы указали выше, является правильным значением, в идеале это должно быть 0. Теперь магнитный Значения полей будут колебаться совсем немного. Это отчасти ожидаемо, так же как обычная стрелка компаса не будет стоять на месте, если вы держите ее в руке, и ваша рука слегка дрожит. Кроме того, вы можете получить помехи от окружающих вас предметов, и в этом случае значения магнитного поля непредсказуемы. Однажды я тестировал свое приложение компаса, сидящее возле "каменного" стола, и мой компас был отключен более чем на 90 градусов, только с помощью настоящего компаса, который я обнаружил, что в моем приложении нет ничего плохого, и таблица "камень" выдает действительно сильное магнитное поле.
С гравитацией как доминирующим фактором вы можете фильтровать значения акселерометра, но без каких-либо других знаний, как вы подходите магнитные значения? Как узнать, есть ли помехи от окружающих предметов или нет?

Вы можете сделать гораздо больше, например, получить полное представление о пространственном положении вашего устройства и т. Д. С пониманием матрицы вращения.

Согласно приведенному выше объяснению, сделайте это

private static final int TEST_GRAV = Sensor.TYPE_ACCELEROMETER;
private static final int TEST_MAG = Sensor.TYPE_MAGNETIC_FIELD;
private final float alpha = (float) 0.8;
private float gravity[] = new float[3];
private float magnetic[] = new float[3];

public void onSensorChanged(SensorEvent event) {
    Sensor sensor = event.sensor;
    if (sensor.getType() == TEST_GRAV) {
            // Isolate the force of gravity with the low-pass filter.
              gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0];
              gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1];
              gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2];
    } else if (sensor.getType() == TEST_MAG) {

            magnetic[0] = event.values[0];
            magnetic[1] = event.values[1];
            magnetic[2] = event.values[2];

            float[] R = new float[9];
            float[] I = new float[9];
            SensorManager.getRotationMatrix(R, I, gravity, magnetic);
            float [] A_D = event.values.clone();
            float [] A_W = new float[3];
            A_W[0] = R[0] * A_D[0] + R[1] * A_D[1] + R[2] * A_D[2];
            A_W[1] = R[3] * A_D[0] + R[4] * A_D[1] + R[5] * A_D[2];
            A_W[2] = R[6] * A_D[0] + R[7] * A_D[1] + R[8] * A_D[2];

            Log.d("Field","\nX :"+A_W[0]+"\nY :"+A_W[1]+"\nZ :"+A_W[2]);

        }
    }

Stochastically собрал ответ, который я считаю лучше того, что публикуется здесь.

/questions/40680004/nesovmestimyie-znacheniya-datchika-orientatsii-na-android-dlya-azimuta-ryiskaniya-i-krena/40680012#40680012

Это выглядит SensorManager.getOrientation() не может правильно перевести в мировую рамку.

Правильный код будет:

SensorManager.getRotationMatrix(gravityCompassRotationMatrix, inclinationValues, gravityValues, magnitudeValues);
SensorManager.remapCoordinateSystem(currentOrientationRotationMatrix.matrix, worldAxisX, worldAxisY, adjustedRotationMatrix);
float sin = adjustedRotationMatrix[1] - adjustedRotationMatrix[4];
float cos = adjustedRotationMatrix[0] + adjustedRotationMatrix[5];
float m_azimuth_radians = (float) (sin != 0 && cos != 0 ? Math.atan2(sin, cos) : 0);

Другие вопросы по тегам