Перейти к содержимому
Compvision.ru
Smorodov

Измеритель пульса по изображению участка кожи.

Recommended Posts

Реализовал измеритель пульса по изображению с камеры (pdf-ка в архиве с исходниками).

Работает не очень устойчиво (думаю из-за того что переключаются каналы разделенные при помощи ICA), но при удачном исходе на разложении Фурье виден острый четкий пик.

Предлагаю желающим поэксперименировать :)

Там встроен детектор лица, но и так работает :)

post-1-0-50096100-1388331327_thumb.png

Исходники (используются: OpenCV и Eigen):

HeartRateMeasure.rar

UPD: добавил комментариев и немного подправил исходники.

Только что откопал интересный ресурс по теме:

http://people.csail.mit.edu/mrub/vidmag/

с исходниками на MATLAB и видеороликами (обязательно посмотрите ).

И еще исходники на питоне:

https://github.com/thearn/webcam-pulse-detector

  • Like 2

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

*Выдох* Круууто!

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Статья на хабре:

http://habrahabr.ru/post/232515/

 

Появились также сишные исходники на github-е.

Ссылки не привожу, т.к. гуглятся на раз.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Привет всем умным людям!

Пришло время и добрался до измерителя пульса с веб-камеры. Как и раньше прогнал на Матлабе = все ок. Перевожу на VS+OpenCV.

Прошу помощи по 2-м вопросам:

1. Что посмотреть/почитать по C++ такого, чтобы чуть легче стало переводить Матлабовские алгоритмы в VS+OpenCV.

2. Пока не могу понять почему получаю разные результаты в коде:

 for( int n=0; n<ires->height; n++ ) { 
        uchar* ptr = (uchar*) ( ires->imageData) + n * ires->widthStep; 
        
       cvInRangeS(rim, cvScalar(n*8), cvScalar(n*8+8), hv); // creating mask for Rim pixels
          cvMul(gim, hv, mres, 0.00390625); // implementing Rim-mask for Gim pixels 0.00390625 (нормировка=1/256)
            
        for( int m=0; m<ires->width; m++ ) { 

           cvInRangeS(mres, cvScalar(m*8), cvScalar(m*8+8), mv); // creating mask for Gim pixels

           //    ptr[m] = cv::sum(cv::Mat(mv)).val[0]; // sum of sorted pixels
                 ptr[m] = cvCountNonZero(mv);

         }
  }

Имея 2 изображения rim и gim, хотелось бы получить третье ires, которое является распределением амплитуд по диапазонам: в цикле выделяю узкий диапазон амплитуд в первом (rim), делаю для выделенных значений маску (mres), которую применяю ко второму(gim). В получившемся втором (gim) аналогично выделяю узкий диапазон пикселей и хочу подсчитать их количество для каждого комплексного диапазона. В случае sum вроде близко к нужному результату, но все-равно не так, как в Матлабе получается, а в случае CountNonZero вообще какая-то лажа = почти все смещено в один узкий диапазон (никакого распределения).

Может кто подскажет, почему CountNonZero как-то странно считает кол-во ненулевых пикселей для каждого заданного диапазона?

CountNonZero.bmp

Sum.bmp

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Какая-то мешанина из разных версий API и мне как-то не очевидна логика. Но да ладно.

Я когда-то интересовался и исходной работой, и исходниками Smorodov'а. Немного их отрефакторил под себя, добавил стороннее motion magnification. Получился очень сырой проект HeartRateMeasure.

В планах на правах хобби:

1. Разобраться со скачками, о которых писал выше Smorodov.

2. Добавить вычисление пульса не только по цвету, но и по движению.

3. Сделать нормальный трекинг лица по face features. Для этого можно взять dlib, можно OpenFace. После этого можно будет уже для измерения брать не средний цвет лица или анализировать optical flow, а смотреть на участки кожи, близко к которым расположены крупные артерии.

Вот.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Там, вроде, пульс не считают. Но да, видел. Факт заброшенности настораживает.

Большое обсуждение на Reddit. Есть уже приложения и под Андроид с открытыми исходниками, но всё это на уровне баловства. И точность страдает при высокой вычислительной нагрузке. Так что копать есть куда.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Появилось немного времени и обновил проект. Пытался разобраться с непонятными скачками пульса.

Для этого я считал не одно значение с экспоненциальным сглаживанием, а добавил микстуру из 6 Гауссианов. Почему 6?

1. 3 штуки на ошибку с выбором независимой компоненты, выбираем мы 1 из 3-х.

2. В выбранной независимой компоненте я считаю 2 максимума.

В итоге 3 * 2 = 6.

В результате в одном из Гауссианов получается достаточно точное значение пульса и вес у него неплохой. Вот.

 

  • Like 1

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

А гауссианы жестко сидят на каналах, или они просто собирают все максимумы подряд и затем сортируется кто куда идет ?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Последнее. Там всё по классической схеме: есть один Гауссиан, которому скармливаются значения. Как только они выходят за рамки нормального распределения для первого Гауссиана, создаётся для них второй и т.д. пока не получится 6 штук (может вообще не получиться). Чем чаще значения выпадают на конкретный гауссиан, тем больше его вес.

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Добавил вычисление цвета только по коже. За детектор кожи взял Decision tree от mrgloom.

Вопрос к нему: почему именно Decision tree? Мне бы что-нибудь самое быстрое и не сильно лажающее. Можно, конечно, самому опытов напроводить, но интересно, почему первоначально был выбран именно этот вариант. EM будет не точнее и быстрее?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Изначально decision tree, т.к. этот метод интерпретируемый, т.е. там дерево из threshold'ов которые выучены из данных, чтобы не было таких вот хардкодов threshold'ов или in range как тут:

http://bytefish.de/blog/opencv/skin_color_thresholding/

https://github.com/WillBrennan/SkinDetector

И да их сначала можно выучить, а потом для скорости захардкодить)

 

Такой же вопрос почему в grabcut используется GMM? по сути нам там без разницы какой метод будет выдавать probability пикселя?

https://github.com/opencv/opencv/blob/c8783f3e235f81edb97279fafabcf12ec43ccc9f/modules/imgproc/src/grabcut.cpp

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
3 hours ago, mrgloom said:

Изначально decision tree, т.к. этот метод интерпретируемый, т.е. там дерево из threshold'ов которые выучены из данных, чтобы не было таких вот хардкодов threshold'ов или in range как тут:

http://bytefish.de/blog/opencv/skin_color_thresholding/

И да их сначала можно выучить, а потом для скорости захардкодить)

Такой же вопрос почему в grabcut используется GMM? по сути нам там без разницы какой метод будет выдавать probability пикселя?

https://github.com/opencv/opencv/blob/c8783f3e235f81edb97279fafabcf12ec43ccc9f/modules/imgproc/src/grabcut.cpp

Один раз обучить и сохранить - да.

Почему EM, например? Или GMM - это похожая степь. На первый взгляд кажется, что области цвета кожи должны быть достаточно локальными в цветовом пространстве. Почему бы не описать их эллипсоидами? Кажется, что так и должно быть, при этом мы как раз и получим самые настоящие вероятности. Разумеется, в HSV области будут более компактными, чем в RGB, а в Lab ещё лучше. Но для скорости лучше остаться в RGB.

Собственно, так оно и есть:

blah.png

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Собственно есть изящный метод как раз с эллипсом:

 

	cv::Mat Src = imread("image.jpg", 1);	
	cv::Mat mask = Mat::zeros(Src.size(), CV_8UC1);
	
    Mat crcb;
	skinCrCbHist = Mat::zeros(Size(256, 256), CV_8UC1);
	cvtColor(Src, crcb, CV_BGR2YCrCb);

	ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);

	double t = (double)getTickCount();

	for (size_t i = 0; i < Src.rows; i++)
	{
		for (size_t j = 0; j < Src.cols; j++)
		{
			Scalar v;
			v = crcb.at<Vec3b>(i, j);
			int s=skinCrCbHist.at<uchar>(v[1], v[2]); // проверяем попали ли в эллипс, если да, то кожа
			if (s > 0)
			{
				mask.at<uchar>(i, j) = 255;
			}
		}
	}

и довольно неплохо работает.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
12 minutes ago, Smorodov said:

Собственно есть изящный метод как раз с эллипсом:

Сдаётся мне, что код у тебя довольно расисткий, на первом попавшемся негре не отработал. Ай-ай-ай!

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Ну, можно для негров отдельный эллипс организовать :)

Распределения можно взять здесь: https://www.cs.rutgers.edu/~elgammal/pub/skin.pdf

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Гауссианы это хорошо, но одной отделаться не получится.

Может попробую потом что то такое:

http://scikit-learn.org/stable/modules/mixture.html

 

Насчёт вебки скорее всего надо переучить, не знаю насколько репрезентативен этот датасет:

https://archive.ics.uci.edu/ml/datasets/Skin+Segmentation

 

И еще кстати как минус decision tree я не знаю как можно выкрутить recall на полную, т.е. это может быть полезно в зачах типа детектирования лиц, как первый дешевый шаг который снижается пространство для поиска с recall=1.0.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
5 minutes ago, mrgloom said:

Гауссианы это хорошо, но одной отделаться не получится

Это да, особенно если брать цвета разных рас. Но тот же стандартный EM из OpenCV умеет несколько эллипсоидов.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Нашёл чувака, который занимается пульсом. Его работы впечатляют больше, чем успехи из MIT. К сожалению, исходники являются собственностью Phillips и их не потестить. Вот.

Насколько я понимаю, главным его нововведением является замена PCA (PCA у MIT, ICA у Smorodov) на POS (Plane Orthogonal to the Skin-tone direction). Что и дало такую точность.

  • Like 2

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Практика.

Молодцы, вывели свою работу на рынок. Эх! Хочу безусловный доход, чтобы была возможность самому больше времени уделять таким интересным штукам.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Какого качества камеру надо чтобы нормально 'увидеть эффект'?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Не знаю. У меня пока получается, что на видео с неплохого фотоаппарата с 60 fps пульс считается хуже, чем при съёмке обычным китайским телефоном.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Подумалось, что для вычисления пульса по лицу надо не искать лицо Хааром и вычислять участки с кожей, а делать что-то посовременнее. К тому же пульс можно считать двумя способами: как по частоте изменения цвета кожи, так и по микродвижениям головы в такт сердечным сокращениям. Для последнего в статье из MIT используют просто оптический поток для произвольных точек.

Сразу напрашивается мысль, как можно убить двух зайцев одним ударом: и более точно определять лицо с кожей и находить движение точек. Для этого искать face landmarks - ключевых точек на лице. По ним можно определить и область лица (кожа) с углом поворота, а также, сравнивая их на соседних кадрах, находить микрокачания головы. И на основании этих двух измерений уже выдавать пульс.

Как считаете, подход рабочий?

 

И второй вопрос, технический. Соответственно нужен быстрый алгоритм поиска лица и ключевых точек на нём. Желательно, чтобы он работал точнее древнего Хаара и не требовал установки монструозных нейросетевых фреймворков, был кроссплатформенным типа OpenCV (тот же TensorFlow слабо кроссплатформенный, CUDA версия под Windows не заводится), всё работало без Питона и на С++. Если удастся ограничиться только OpenCV и остаться в его рамках (это я о dlib), то будет здорово. Модуль opencv_dnn работает здорово, OpenCL версия тоже подтягивается. Вот.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Так это, MTCNN уже всюду портировали и на чистом OpenCV + BLAS и еще один видел, по детекту лиц и 5 ключевых точек, в маленьком разрешении работает близко к реалтайм. 

Еще есть face parsing, там же и датасет можно найти: https://www.sifeiliu.net/face-parsing в реалтайме вряд ли-пойдет, но для записанных видео, вполне (не пробовал, могу ошибаться). 

 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Создайте учётную запись или войдите для комментирования

Вы должны быть пользователем, чтобы оставить комментарий

Создать учётную запись

Зарегистрируйтесь для создания учётной записи. Это просто!

Зарегистрировать учётную запись

Войти

Уже зарегистрированы? Войдите здесь.

Войти сейчас


  • Сейчас на странице   0 пользователей

    Нет пользователей, просматривающих эту страницу

×