Unlocker 3D. Особенности разработки
Жесты — неотъемлемая часть нашей жизни. Даже при работе с нашими мобильными устройствами мы постоянно ими пользуемся: касаемся экрана, вызываем те или иные функции, листаем страницы. Что и говорить, это одна из причин, по которым мы без ума от сенсорного функционала. Все это смело можно назвать 2D-жестами.
Но однажды мы задались вопросом, существует ли возможность «удивить» Android-устройство 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; } }
На выходе имеем только то, что необходимо для сравнения.
Сравнение данных
Переходим к самому интересному: сравнение двух наборов данных с учетом масштабирования, сдвига по индексам и т.д. Тут нам пригодятся знания (кто бы мог подумать) статистики. Для сравнения графиков мы будем использовать корреляционный анализ, а именно корреляцию Пирсона.
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 (данные идентичны).
Конечно, на этом работа не заканчивается, нужно подобрать правильные коэффициенты для фильтрования и сравнения, сохранять данные, подтверждать сохранение данных, но это уже совсем другая история...