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

Когда вы используете датчик TYPE_MAGNETOMETER, вы получаете значения X, Y, Z напряженности магнитного поля в зависимости от ориентации устройства. Я хочу преобразовать эти значения в глобальную систему отсчета, поясняя: пользователь берет устройство, измеряет эти значения, затем поворачивает устройство на несколько градусов вокруг любой оси и получает ~ те же значения. Пожалуйста, найдите похожие вопросы ниже: Получение значений магнитного поля в глобальных координатах Как получить магнитный вектор поля, не зависящий от вращения устройства? В этом ответе описан пример решения (он предназначен для линейного ускорения, но я думаю, что это не имеет значения): https://stackoverflow.com/a/11614404/2152255 Я использовал его и получил 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) {
    }
}

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


person Yahor    schedule 09.03.2013    source источник
comment
Можете ли вы поделиться кодом com.test.statistics.filter.kalman.KalmanState? Мне нужно отфильтровать выходной сигнал датчика с помощью фильтра Калмана.   -  person Thracian    schedule 06.06.2018


Ответы (3)


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

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

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

Теперь вектор v в трехмерном пространстве может быть представлен кортежами из трех элементов (v_1, v_2, v_3) относительно некоторого базиса (e_1, e_2 < / strong>, 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 указывает вверх к небу?

Ответ - да, если выполняются 2 важных допущения.
С этими 2 допущениями легко вычислить (всего несколько перекрестных произведений) изменение базовой матрицы 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 [] geomromagnetic), и мы обычно переходим в возвращенные значения акселерометра для параметра силы тяжести и значения магнитного поля для геомагнитного поля.

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

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

person Hoan Nguyen    schedule 10.03.2013
comment
Большое спасибо за ваш ответ, он прояснил многие детали, которые я раньше не понимал. Учту и поправлю свой код. - person Yahor; 10.03.2013
comment
Вы можете проверить, есть ли у устройства TYPE_GRAVITY, и использовать его вместо TYPE_ACCELEROMETER, что избавит вас от обработки фильтра Калмана. - person Hoan Nguyen; 10.03.2013
comment
Да, я хотел попробовать, но, к сожалению, на моем устройстве нет доступного TYPE_GRAVITY. Планирую чуть позже попробовать другое устройство с ним. - person Yahor; 10.03.2013
comment
В зависимости от того, что вы делаете, фильтр Калмана может оказаться излишним. Для приложений компаса или дополненной реальности фильтра нижних частот должно быть достаточно. Я не знаю, что такое фильтр Калмана, но слышал, что он требует много ресурсов процессора. - person Hoan Nguyen; 10.03.2013
comment
У меня есть еще один вопрос, TYPE_ACCELEROMETER, насколько я понимаю, на самом деле что-то вроде TYPE_LINEAR_ACCELERATION + TYPE_GRAVITY, не так ли? Можно ли переходить к значениям getRotationMatrix из TYPE_GRAVITY? Будет ли он более точным, когда устройство движется (я читал, что если устройство ускоряется, вывод getRotationMatrix неточен). - person Yahor; 10.03.2013
comment
Да, вы можете передавать значения из TYPE_GRAVITY. Я думаю, что TYPE_GRAVITY - это просто TYPE_ACCELEROMETER с каким-то фильтром. Я не знаю, как это реализует Google, поэтому не знаю, будет ли это более точным. Когда устройство ускоряется, передаваемые значения параметра силы тяжести не дают точного приблизительного значения силы тяжести даже при фильтрации. Следовательно, предположение 1 не совсем выполнено, и выход будет отключен. GetRotationMatrix использует параметр силы тяжести в качестве координаты неба. - person Hoan Nguyen; 10.03.2013
comment
Когда устройство ускоряется, значения, передаваемые для параметра гравитации, не будут точными приблизительными значениями силы тяжести, даже если фильтр - абсолютно правильный, значения TYPE_ACCELEROMETER просто непригодны для получения матрицы вращения, когда устройство ускоряется. Я думаю, что значения TYPE_GRAVITY не меняются или меняются не так сильно - person Yahor; 10.03.2013
comment
Я сравниваю TYPE_GRAVITY и фильтр нижних частот TYPE_ACCELEROMETER, значения одинаковы, когда устройство не движется, но как только я быстро повернул устройство, разница может достигать 10 градусов. - person Hoan Nguyen; 11.03.2013
comment
Спасибо за ваш ответ! Сегодня я получу устройство с датчиком TYPE_GRAVITY и попробую его также использовать, а затем вернусь с результатами. - person Yahor; 11.03.2013
comment
Я думаю, что любое устройство будет иметь TYPE_GRAVITY, если API Android 9 или выше. В своем проекте вы можете установить последнюю версию и проверить версию на телефоне, чтобы включить TYPE_GRAVITY. - person Hoan Nguyen; 11.03.2013
comment
@HoanNguyen спасибо за подробный ответ. Однако, когда я удаляю влияние силы тяжести из ускорения (возвращается из TYPE_ACCELEROMETER) getRotationMatrix не может вычислить R, потому что устройство находится в свободном падении? Что мне здесь не хватает? - person Shai; 23.09.2014
comment
Вы должны проходить под действием силы тяжести, а не снимать ее. Таким образом, если вы используете TYPE_ACCELEROMETER, то отфильтруйте ускорение вместо силы тяжести. - person Hoan Nguyen; 24.09.2014
comment
@HoanNguyen, спасибо за ваше объяснение. Можете ли вы помочь мне в этом вопросе? stackoverflow.com/questions/27137239/ - person Arash; 26.11.2014

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

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]);

        }
    }
person vineetv2821993    schedule 13.08.2015

Стохастически собрал воедино ответ, который, на мой взгляд, превосходит то, что публикуется здесь.

https://stackoverflow.com/a/16418016/4033525

Кажется, 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);

person Sameer J    schedule 25.11.2020