2014-12-02

Измерение ритма сердца с помощью камеры на Android устройстве. Получение данных с камеры смартфона.

В процессе разработки Hypoxic у меня возникло желание измерять пульс с помощью встроенной камеры. Изначально в программе была поддержка фитнесс-пояса Zephyr, по блютузу передающего всю интересующую меня информацию в смартфон. Однако, далеко не у всех есть такие пояса, а вот камера в смартфоне есть у многих. Как мне казалось, снять пульс (да что там, межпульсовые интервалы!) с камеры, не составит особого труда, тем паче быстрое гугление показало, что тема довольно таки заезженная. Поэтому недолго думая я решил реализовать эту фишку и в Hypoxic. 


 Рекогносцировка

В идеально-сферическом мире поставленная задача решается по следующим этапам:
  1. Получение картинки с камеры и ее обработка
  2. Фильтрация полученных данных
  3. Извлечение пульсовых данных

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

Работа с камерой

Идея детектирования пульса с помощью камеры напрямую исходит из плетизмографии, в которой изучаются процессы, связанные с кровенаполнением органов и сосудов тела. В нашем случае объектами исследования служат конечности, прикладываемые к объективу камеры, а по изменению яркости света при прохождении тканей могут мы получаем информацию о кровенаполнении и, следовательно, о ритме сердца. По вполне очевидным причинам красный канал проходящего через ткани конечностей света будет наиболее информативным. Таким образом, основная задача при работе с камерой сводится к тому, чтобы получить динамику изменения уровня яркости канала красного света.

Получение изображения в динамике с камеры Android смартфона можно осуществить с помощью preview камеры. Однако следует учесть, что этот инструмент не позволит получать никакие данные с камеры, если это preview не будет отображаться на экране. Всяческие ухищрения вроде установки нулевых размеров этого самого preview или его вынос за пределы экрана к желаемому эффекту не приводят, хотя находятся отдельные индивидуумы, у которых, по их же словам, это получается и все работает. Насколько мне удалось понять, даже размеры этого preview в 1px*1px могут приводить к краху на некоторых устройствах, поэтому preview должно быть иметь размеры сторон кратные двойке. Я смирился с тем, что незаметно работать с камерой не удастся и разместил preview размером 32px * 32px в левом верхнем углу экрана. 

Готовим SurfaceView для preview камеры
// Создаем SurfaceView, устанавливаем его размер 32x32, помещаем его в левый верхний угол
WindowManager windowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
SurfaceView surfaceView = new SurfaceView(this);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
  32, 32,
  WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
  WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
  PixelFormat.TRANSLUCENT
    );
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
windowManager.addView(surfaceView, layoutParams);
surfaceView.getHolder().addCallback(this);


Второй серьезной проблемой стала борьба за повышение fps, которое играет ключевую роль в решении нашей задачи. Дело в том, что ритм сердца здорового человека изменяется со временем и эта изменчивость получила название вариабельность ритма сердца (ВРС). Для анализа этой вариабельности используется ряд статистических характеристик, одна из которых pNN50, или, если по человечески, доля межпульсовых временных интервалов (NN), длительность которых превышает 50 мс. Для здорового молодого человека pNN50 порядка 30-40% и медленно растет с возрастом. Из вышесказанного довольно очевидно следует, что при таких условиях fps камеры было бы желательно иметь не ниже 20. В связи с чем крайне рекомендуется выставить самое низкое разрешение для preview камеры и задать границы допустимых fps для камеры на максимальные значения. Кроме того, мне удалось найти сообщения о том, что снижение разрешения через setPictureSize также помогает повысить fps, но в моем случае это видимого эффекта не принесло. Далее, настоятельно рекомендую использовать setPreviewCallbackWithBuffer с заранее выделенным static буфером под картинку с камеры, поскольку это позволит избежать постоянной инициализации массива под буфер и вмешательства сборщика мусора (которое может стоить вам порядка 80мс!!!). Тем не менее, если на смартфоне стоит не очень хорошая камера, все вышеперечисленное может оказаться не очень действенным способом для решения проблемы повышения fps. На самых слабых устройствах мне удалось добиться производительности в 10 fps, чего хватает впритык для примерной оценки средней ЧСС, но ни о каких точных измерениях речи, разумеется, не идет.

Наличие вспышки, которую можно включить в режиме фонарика, очень способствует повышению качества получаемых данных, поэтому ее желательно включать при осуществлении замеров пульса.

И последнее, что касается работы с камерой из под Android'a — это формат видеозаписи. В документации сказано, что формат NV21 поддерживается практически всеми устройствами, поэтому имеет смысл использовать именно его. Как уже было сказано выше, прежде всего нас интересует интенсивность красного цвета I[R] в видеопотоке с preview


Установка параметров камеры
Camera.Parameters parameters = camera.getParameters();
//Получаем список поддерживаемых разрешений превью и выбираем наименьшее
List localSizes = parameters.getSupportedPreviewSizes();
Camera.Size bestSize = localSizes.get(0);
for (int i = 1; i < localSizes.size(); i++)
 if (localSizes.get(i).width * localSizes.get(i).height < bestSize.width * bestSize.height)
  bestSize = localSizes.get(i);
parameters.setPreviewSize(bestSize.width, bestSize.height);

//Включаем вспышку
if(getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH))
 parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);

//Выбираем список поддерживаемых разрешений фотографии с камеры и устанавливаем минимальное
localSizes = camera.getParameters().getSupportedPictureSizes();
Camera.Size bestPicSize = localSizes.get(0);

for (Camera.Size s : localSizes)
 if (s.width * s.height < bestPicSize.width * bestPicSize.height)
  bestPicSize = s;
parameters.setPictureSize(bestPicSize.width, bestPicSize.height);

//Отключаем автоэкспозицию на современных устройствах
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH)
{
 if (parameters.isAutoExposureLockSupported())
  parameters.setAutoExposureLock(true);
}
//Получаем список поддерживаемых диапазонов фпс и задаем тот режим, у которого минимальный фпс больше остальных
List fps = parameters.getSupportedPreviewFpsRange();
int[] bestfps=new int[]{0,0};
for (int[] s : fps)
 if (s[0]>bestfps[0])
  bestfps = s;

parameters.setPreviewFpsRange(bestfps[0],bestfps[1]);
///Задаем формат видеопотока с камеры. NV21 значение по умолчанию, но все же лучше подстраховаться.
parameters.setPreviewFormat(ImageFormat.NV21);

camera.setParameters(parameters);    
...........
//Вычисляем размер буфера для фреймов с камеры
int yStride = (int) Math.ceil(bestSize.width / 16.0) * 16;
int uvStride = (int) Math.ceil((yStride / 2) / 16.0) * 16;
int ySize = yStride * bestSize.height;
int uvSize = uvStride * bestSize.height / 2;
int bufferSize = ySize + uvSize * 2;
byte[] buffer = new byte[bufferSize];
camera.addCallbackBuffer(buffer);

//Указываем камере на SurfaceView на котором будет производиться предпросмотр
camera.setPreviewDisplay(surfaceHolder);
///Задаем обработчик, который вызывается на каждом заполнении буфера
camera.setPreviewCallbackWithBuffer(previewCallback);            

Извлечение суммарной интенсивности I[R] из одного фрейма preview
private static int decodeRedSum(byte[] data, int width, int height){
        if (data == null)
            return 0;

        final int size = width * height;
        int v,sum = 0;
        int[] y = new int[4];
        
        for (int i = 0, k = 0; i < size; i += 2, k += 2)
        {
            y[0] = data[i] & 0xff;
            y[1] = data[i + 1] & 0xff;
            y[2] = data[width + i] & 0xff;
            y[3] = data[width + i + 1] & 0xff;

            v = data[size + k] & 0xff;
            v = v - 128;

            for (int y1 : y)
            {
                int r = (y1 + (1772 * v)) / 1000;
                sum += r > 255 ? 255 : r < 0 ? 0 : r
            }

            if ((i + 2) % width == 0)
                i += width;
        }

        return sum;
    }
Вот такая динамика I[R] по фремам наблюдается с моей камеры, если приложить к ней палец на 10 секунд.

Глядя на график выше первое, что приходит на ум, это вычислять скользящее среднее и фиксировать сердцебиение как пересечение среднего уровня кривой I[R]. Если вам не критична точность определения частоты сердечных сокращений (ЧСС), то можете на этом остановиться, однако этот подход нестабильный и приводит к большому количеству ошибок из-за артефактов и выбросов.

На этом первая часть окончена. В следующих частях расскажу о том, расскажу о том, какие методы обработки I[R] для извлечения ЧСС существуют и о своих экспериментах со всем этим делом.

Комментариев нет :

Отправить комментарий