Unlocker 3D. Особенности разработки

Unlocker 3D. Особенности разработки

Жесты — неотъемлемая часть нашей жизни. Даже при работе с нашими мобильными устройствами мы постоянно ими пользуемся: касаемся экрана, вызываем те или иные функции, листаем страницы. Что и говорить, это одна из причин, по которым мы без ума от сенсорного функционала. Все это смело можно назвать 2D-жестами.

Но однажды мы задались вопросом, существует ли возможность «удивить» Android-устройство 3D-жестом? Для реализации этой идеи гаджеты уже оснащены всем необходимым, осталось только выработать подход.

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

Unlocker 3D. Особенности разработки

Работу с сенсорами можно разбить на такие задачи:

  • Получение данных сенсоров.
  • Фильтрование данных.
  • Подготовка данных.
  • Сравнение данных.

Получение данных сенсоров

Для начала необходимо разобраться, какие сенсоры нам нужно использовать. Датчики ОС Android делятся на три категории: движения, положения и окружающей среды. Датчики могут быть самыми разными:

  • акселерометр;
  • гироскоп;
  • датчик освещения;
  • датчик магнитных полей;
  • барометр;
  • датчик поднесения телефона к голове;
  • датчик температуры аппарата;
  • датчик температуры окружающей среды;
  • измеритель относительной влажности и т. д.

Для наших целей нам понадобиться гироскоп и акселерометр. Первым делом активность должна реализовать интерфейс SensorEventListener:

public class MainActivity extends Activity implements SensorEventListener

Теперь нужен доступный для реализации метод onSensorChanged. Получаем доступ к менеджеру сенсоров:

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

Регистрируем слушатели:

sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_GAME);

sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);

Теперь можем получать данные:

public void onSensorChanged(final SensorEvent event) {
        synchronized (this) {
            {
                if (isSensorOn) {
                    switch (event.sensor.getType()) {
                        case Sensor.TYPE_ACCELEROMETER:
                            double[] accData = Comparison.lowFilter(event.values[0], event.values[1], event.values[2]);
                            accDataList.add(accData);
                            break;
                        case Sensor.TYPE_GYROSCOPE:
                            double[] gyrData = Comparison.lowFilter(event.values[0], event.values[1], event.values[2]);
                            gyrDataList.add(gyrData);
                            getPoint(event.values[0], event.values[1], event.values[2]);
                            break;
                    }
                }
            }
        }
    }

Фильтрование данных

Eсли выводить линейный график после получения данных, сразу видно, что не все так гладко: все датчики очень сильно «шумят». Чтобы исправить ситуацию, пропускаем данные через фильтр нижних частот (low-pass filter):

public static double[] lowFilter(double x, double y, double z) {
        double[] acceleration = new double[3];
        acceleration[0] = x * kFilteringFactor + acceleration[0] * (1.0 - kFilteringFactor);
        x = x - acceleration[0];
        acceleration[0] = x;

        acceleration[1] = y * kFilteringFactor + acceleration[1] * (1.0 - kFilteringFactor);
        y = y - acceleration[1];
        acceleration[1] = y;

        acceleration[2] = z * kFilteringFactor + acceleration[2] * (1.0 - kFilteringFactor);
        z = z - acceleration[2];
        acceleration[2] = z;

        return acceleration;
    }

Теперь графики стали более плавными, но работать с ними все еще не получится. Нам понадобится фильтр посложнее. После недолгих поисков оказалось, что нам подойдет либо фильтр Калмана, либо альфа-бета фильтр (Complementary filter). Для наших целей больше подходит второй вариант. Его реализация достаточно проста:

public static double[] complementaryFilter(double accData[], double gyrData[]) {
        double pitchAcc, rollAcc;
        double pitch = 0;
        double roll = 0;
        double[] result = new double[2];

        // Integrate the gyroscope data -> int(angularSpeed) = angle
        pitch += ((float) gyrData[0] / GYROSCOPE_SENSITIVITY) * dt; // Angle around the X-axis
        roll -= ((float) gyrData[1] / GYROSCOPE_SENSITIVITY) * dt;    // Angle around the Y-axis

        // Compensate for drift with accelerometer data if !bullshit
        // Sensitivity = -2 to 2 G at 16Bit -> 2G = 32768 && 0.5G = 8192
        double forceMagnitudeApprox = Math.abs(accData[0]) + Math.abs(accData[1]) + Math.abs(accData[2]);
        if (forceMagnitudeApprox > 8192 && forceMagnitudeApprox < 32768) {
            // Turning around the X axis results in a vector on the Y-axis
            pitchAcc = Math.atan2((float) accData[1], (float) accData[2]) * 180 / Math.PI;
            pitch = pitch * 0.98 + pitchAcc * 0.02;
            // Turning around the Y axis results in a vector on the X-axis
            rollAcc = Math.atan2((float) accData[0], (float) accData[2]) * 180 / Math.PI;
            roll = roll * 0.98 + rollAcc * 0.02;
        }
        result[0] = pitch;
        result[1] = roll;
        return result;
    }

На выходе получаем pitch-roll (наклон-крен). Ну вот, наши графики достаточно красивые и гладкие :).

Подготовка данных

Далее нам нужно подготовить данные для их последующего сравнения. Например, в большинстве случаев перед началом и в конце жеста получаются почти нулевые графики — это бездействие пользователя, потому эти данные нам стоит опустить. Применяем такой метод:

public static List<double[]> prepareArrays(double[] arrayX, double[] arrayY) {
        try {
            double offset = 0.00009;
            int len;
            if (arrayX.length > arrayY.length) {
                len = arrayY.length;
            } else {
                len = arrayX.length;
            }
            int i;
            for (i = 0; i < len; i++) {
                if ((arrayX[i] < offset && arrayX[i] > (-offset)) && (arrayY[i] < offset && arrayY[i] > (-offset))) {
                } else {
                    break;
                }
            }
            double[] pArrayX = Arrays.copyOfRange(arrayX, i, arrayX.length);
            double[] pArrayY = Arrays.copyOfRange(arrayY, i, arrayY.length);
            for (i = len - 1; i >= 0; i--) {
                if ((arrayX[i] < offset && arrayX[i] > (-offset)) && (arrayY[i] < offset && arrayY[i] > (-offset))) {
                } else {
                    break;
                }
            }
            pArrayX = Arrays.copyOfRange(pArrayX, 0, i);
            pArrayY = Arrays.copyOfRange(pArrayY, 0, i);
            ArrayList<double[]> doubles = new ArrayList<double[]>();
            doubles.add(pArrayX);
            doubles.add(pArrayY);
            return doubles;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

На выходе имеем только то, что необходимо для сравнения.

Unlocker 3D. Особенности разработки

Сравнение данных

Переходим к самому интересному: сравнение двух наборов данных с учетом масштабирования, сдвига по индексам и т.д. Тут нам пригодятся знания (кто бы мог подумать) статистики. Для сравнения графиков мы будем использовать корреляционный анализ, а именно корреляцию Пирсона.

public static double pirsonCompare(double[] x, double[] y) {
        int len;
        if (x.length > y.length) {
            len = y.length;
        } else {
            if (((y.length * 0.6) > x.length)) {
                return -1;
            }
            len = x.length;
        }
        double xs = 0;
        for (int i = 0; i < len; i++) {
            xs += x[i];
        }
        xs = xs / x.length;
        double ys = 0;
        for (int i = 0; i < len; i++) {
            ys += y[i];
        }
        ys = ys / y.length;
        double dxy = 0;
        for (int i = 0; i < len; i++) {
            dxy += (x[i] - xs) * (y[i] - ys);
        }

        double mqx = 0;
        for (int i = 0; i < len; i++) {
            mqx += Math.pow(x[i] - xs, 2);
        }
        mqx = Math.sqrt(mqx);

        double mqy = 0;
        for (int i = 0; i < len; i++) {
            mqy += Math.pow(y[i] - ys, 2);
        }
        mqy = Math.sqrt(mqy);

        double rxy = dxy / (mqx * mqy);
        return rxy;
    }

Результат - коэффициент корреляции Пирсона, который может принимать значения от -1 (данные противоположны) до 1 (данные идентичны).

Unlocker 3D. Особенности разработки

Конечно, на этом работа не заканчивается, нужно подобрать правильные коэффициенты для фильтрования и сравнения, сохранять данные, подтверждать сохранение данных, но это уже совсем другая история...

Приложение в Google Play

Репозиторий на GitHub