Определяем пульс по вебкамере в 50 строчек кода

    Привет, Хабр.

    Однажды мне попалось описание приложения для Android, которое определяло пульс по камере телефона, просто по общей картинке. Камера не прикладывалась к пальцу, не просвечивалась светодиодом. Интересный момент был в том, что ревьюеры не поверили в возможность такого определения пульса, и приложение было отклонено. Чем дело кончилось у автора программы, не знаю, но стало интересно проверить, возможно ли это.

    Для тех кому интересно что получилось, продолжение под катом.

    Разумеется, я не буду делать приложение под Android, гораздо проще проверить идею на языке Python.

    Получаем данные с камеры

    Сначала мы должны получить поток с вебкамеры, для чего воспользуемся OpenCV. Код является кроссплатформенным, и может работать как под Windows, так и под Linux/OSX.

    import cv2
    import io
    import time
    
    
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
    cap.set(cv2.CAP_PROP_FPS, 30)
    
    while(True):
        ret, frame = cap.read()
    
        # Our operations on the frame come here
        img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
        # Display the frame
        cv2.imshow('Crop', crop_img)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()

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

    x, y, w, h = 800, 500, 100, 100
    crop_img = img[y:y + h, x:x + w]
    
    cv2.imshow('Crop', crop_img)

    Если все было сделано правильно, при запуске программы мы должны получить примерно такую картинку с камеры (заблюрено из соображений приватности) и кропа:

    Обработка

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

    heartbeat_count = 128
    heartbeat_values = [0]*heartbeat_count
    heartbeat_times = [time.time()]*heartbeat_count
    
    while True:
        ...
        # Update the list
        heartbeat_values = heartbeat_values[1:] + [np.average(crop_img)]
        heartbeat_times = heartbeat_times[1:] + [time.time()]
    

    Функция numpy.average вычисляет среднее из двухмерного массива, на выходе мы получаем число, которое и является усредненной яркостью.

    Остается вывести график на экран в реальном времени:

    fig = plt.figure()
    ax = fig.add_subplot(111)
    while(True):
        ...
    
        ax.plot(heartbeat_times, heartbeat_values)
        fig.canvas.draw()
        plot_img_np = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')
        plot_img_np = plot_img_np.reshape(fig.canvas.get_width_height()[::-1] + (3,))
        plt.cla()
        
        cv2.imshow('Graph', plot_img_np)

    Тут есть небольшая тонкость: OpenCV работает с изображениями в формате numpy, поэтому мы должны получить из matplotlib график в виде массива, для чего используется функция numpy.fromstring.

    Собственно и все.

    Запускаем программу, подбираем такое положение, чтобы в кропе с камеры был только фрагмент кожи, принимаем "позу мыслителя", подперев голову рукой - изображение должно быть максимально неподвижно. И вуаля - это действительно работает!

    Возможно, из заголовка не совсем очевидно, но камера не прикладывается к коже, мы просто анализируем общую картинку с человеком на экране. И удивительно, что даже на таком расстоянии изменение оттенка кожи вполне уверенно фиксируется камерой! Как видно из картинки, реальная разница яркости составляет менее 0.5% и конечно, не видна "невооруженным глазом", но на графике уверенно различима. Разумеется, по клеточкам считать не точно, примерный пульс получился около 75bpm. Для сравнения, результат с поверенного китайскими мастерами пульсоксиметра:

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

    Заключение

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

    И раз уж мы анализируем видеопоток, может возникнуть отдельный вопрос - работает ли это со сжатыми данными, можно ли увидеть пульс у актера кино или диктора на телевидении? Ответа я не знаю, желающие могут попробовать самостоятельно. Для этого достаточно заменить в коде строку cap = cv2.VideoCapture(0) на cap = cv2.VideoCapture("video.mp4"), код программы остается тот же.

    Для желающих поэкспериментировать, исходный код целиком под спойлером.

    Spoiler
    import numpy as np
    from matplotlib import pyplot as plt
    import cv2
    import io
    import time
    
    # Camera stream
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1280)
    cap.set(cv2.CAP_PROP_FPS, 30)
    # Video stream (optional)
    # cap = cv2.VideoCapture("videoplayback.mp4")
    
    # Image crop
    x, y, w, h = 800, 500, 100, 100
    heartbeat_count = 128
    heartbeat_values = [0]*heartbeat_count
    heartbeat_times = [time.time()]*heartbeat_count
    
    # Matplotlib graph surface
    fig = plt.figure()
    ax = fig.add_subplot(111)
    
    while(True):
        # Capture frame-by-frame
        ret, frame = cap.read()
    
        # Our operations on the frame come here
        img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        crop_img = img[y:y + h, x:x + w]
    
        # Update the data
        heartbeat_values = heartbeat_values[1:] + [np.average(crop_img)]
        heartbeat_times = heartbeat_times[1:] + [time.time()]
    
        # Draw matplotlib graph to numpy array
        ax.plot(heartbeat_times, heartbeat_values)
        fig.canvas.draw()
        plot_img_np = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')
        plot_img_np = plot_img_np.reshape(fig.canvas.get_width_height()[::-1] + (3,))
        plt.cla()
    
        # Display the frames
        cv2.imshow('Crop', crop_img)
        cv2.imshow('Graph', plot_img_np)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()

    И как обычно, всем удачных экспериментов

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 69

      +2
      У оптических датчиков (даже у наручных браслетов) есть проблема с данными в движении. Особенно когда каденс более или менее в районе пульса.
        +6

        Хотелось бы Фурье образ увидеть в качестве иллюстрации. И ещё вопрос: при каком освещении Вы снимали?

          0
          Да, была мысль применить к массиву FFT. Освещение обычное, просто вебкамера на мониторе.
            +4

            Искусственное освещение может мерцать, идеально проверить ещё при солнечном освещении.

              0

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

                0
                Попробовать несложно, но мерцание лампы с частотой 1Гц маловероятно.
                  +2

                  А вот 50Гц — самое то.

                    0
                    На графике по горизонтали — секунды а не миллисекунды :)
                      +11
                      Очевидно, чтобы заметить пульсации 50Гц, надо получать картинку с камеры чаще, чем 50 раз в секунду. Из-за разницы частот возникают биения яркости, и они не обязаны быть равны 50 герцам.
                        0
                        Очевидно, чтобы заметить пульсации 50Гц, надо получать картинку с камеры чаще, чем 50 раз в секунду
                        На самом деле не совсем так. Скажем камера с 30 кадрами в секунду тоже даст возможнось заметить пульсации 50гц. Измерить 50гц не сможете, но именно заметить пульсацию — вполне.
                        И еще надо помнить, что частота кадров не однозначно определяет выдержку камеры. Вполне возможна ситуация 30 к/с камеры с выдержкой кадра 1/90.
                          +1
                          На самом деле, все еще сложнее :)
                          ru.dsplib.org/content/discrete_aliasing/discrete_aliasing.html

                          Можно оцифровывать сигналы из зон Найквиста высших порядков (выше частоты дискретизации АЦП), но в случае света непонятно как быть с фильтрацией. В радиотехнике это работает, а вот в оптике хз.
                    0

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

            +3
            Была интересная работа Video Magnification от MIT. Может подтолкнёт к идеям.
              +1

              Чёрт, а я по описанию вначале подумал, что смарт просто держать в руках, не направляя на себя – и пульс ловит по синхронизированным с ним микродвижениям (есть подобная прога, работающая по акселерометру или гироскопу).

                +1
                Гироскоп это тоже интересный кандидат на проверку :)
                +5
                Чем дело кончилось у автора программы, не знаю, но стало интересно проверить, возможно ли это.

                Автор упомянутого приложения переодически читает Хабр и просил передать, что у него все хорошо — приложение пока не удалили.


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


                Было бы очень интересно почитать продолжение статьи с выдергиванием пульса актера из фильма.

                  +1
                  Да там собственно писать нечего.
                  Заменяем строчку:
                  cap = cv2.VideoCapture(0)
                  на
                  cap = cv2.VideoCapture("video.mp4")


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

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


                    Было интересно может у кого-нибудь получится выделить пульс лучше.

                      +1
                      Для начала можно поэкспериментировать с собственным видео, чтобы понять, сохраняет ли вообще MP4 такие малые перепады яркости. Если нет, то дальнейшее бесмысленно :)

                      Так да, нужна привязка к «поверхности», есть программы для программной стабилизации видео с астрономических камер, например astrojolo.com/wp-content/uploads/2019/03/pipp_as_01.jpg Возможно сработает, не знаю.
                        0
                        Прикольно! Надо потестить. Хотя они, наверное, оптимизированы под стабилизацию неизменного изображения.
                          0

                          Зачем стабилизировать? Сегментировать и искать непрерывные участки, похожие на кожу. Заодно выделим области примерно одинаково освещенные, чтобы не возиться с тенями и бликами. Главное смотреть на области. где задеектировано лицо.
                          Усреднять, наверно, имеет смысл, но что если нормировать и считать БПФ от всех точек кожи, а фурье-спектры перемножать. Тогда резонансные участки выделятся, а шум помножится на ноль.



                          Спустя 5 минут. Хз что я тут написал. наверно бред полный.

                            0
                            Если бы кожа имела 100% одинаковый цвет на всей плоскости, то можно не стабилизировать :) В реале малейший сдвиг уводит кривую вверх-вниз, так что привязка нужна.

                            Запустите скрипт и поэкспериментируйте, сразу видно будет.
                              0

                              да запустил. Елозит туда-сюда график, без фурье не удобно. Еще можно поиграться стабилизируя линию на среднем по меньшему окну. Пусть весь график будет выбиваться, но в целом будет понятнее.

                              0

                              Очень здравый подход. Правда, если пытаться найти однотонные участки кожи — будут получатся небольшие плавающие пятна, особенно если тело в движении.


                              Но после небольшого файн-тюнинга, данная идея стабильно улучшает получаемый спектр. Вот строчка, которая работает в подобном пульсометре на проде (после небольшой чистки от ООП):


                              removeOtherColors(maskedBitmap, getAverageRgb(maskedBitmap), 0.5f)

                              Насчет перемножения спектров преобразований Фурье — идея интересная. Вы имеете ввиду неперекрывающиеся отрезки видео?


                              Есть два вида помех:


                              1. Те, которые имеют определенную частоту (шаги человека, если говорить о фитнес-браслетах)
                              2. Случайные шумы

                              Разберем оба случая.


                              1. Те шумы, которые имеют стабильную частоту — останутся во всех спектрах и умножение просто уменьшит модуль.
                              2. По идее, делая преобразование Фурье от всего видео-ряда, шумы, которые не имеют гармоники затухают сами, потому что вместо синфазного накопления

                              cуммы sin(Real_freq x) sin (Assumed_freq x) для всех x, когда real_freq = assumed_freq и сигнал находитсяв фазе с синусойдой assumed_freq.
                              Мы получаем нечто похожее
                              random()
                              sin (Assumed_freq * x), что стремиться к 0 при делении на количество точек в ряде.

                                0

                                Время редактирования истекло. В последнем примере имеются ввиду не только sin, но и cos, конечно.


                                Но даже учитывая то, что шумы гасятся сами самим ПФ, такой подход с умножением может быстрее убрать шумы, чем использование всего ряда. Особенно для гармонических шумов, представленных только на каком-то отрезке измерения. Нужно экспериментировать.


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

                                  0

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


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

                                  Но вернёмся к теме. Попробуйте анализировать пульсации не на ровном участке кожи, а, к примеру, на глазу. Почему-то у меня в таком случае происходит максимально четкая детекция ритма. Может быть это какая-то гармоника от лампы накаливания? Всё бы ничего. но примерно такуой же "пульс" детектится у стены, если вовсе выйти из кадра.

                                    0

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


                                    Тут хитростей особо нет, что-то на изображении пульсирует — осталось понять что. В вашем случае это может быть гармоника лампы накаливания (или баг в алгоритме, но вряд ли).


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

                                      +1

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

                            +3
                            Очевидно же, что в фильмах маэстро Тарковского :)
                          +2
                          Под линкусом запустилось без проблем, крайне полезный для меня фрагмент кода
                          Уже видел что то подобное, но не помню что именно там делалось
                          FFT прикручу на досуге
                            +6
                            Физзарядку надо было сделать, чтобы показать как пульс изменился. А так непонятно, что вы намеряли, может у вас освещение пульсирет с таким периодом, типа дерево перед окном качается.
                              +1
                              Проверял конечно. Просто по клеточкам считать неточно, а выкладывать некачественные результаты не хочется.

                              Если наложить два графика, «на глаз» видно что пульс выше после зарядки:



                              Кстати данный скриншот сделан при солнечном освещении, это к вопросу выше о лампочках.
                                0
                                Сюда прямо просится фильтр высоких частот :)
                              0
                              Для контроля нужно замерить пульс после нагрузки. Неужели Вы не проверили?
                                0

                                Для проверки нужно снять ЭКГ и график наложить на видео. Сколько стоит сделать ЭКГ на камеру? Скажите. что вы блогер=)

                                0

                                То есть вы берёте цвет кожи, переводите в оттенки серого, и в таком сигнале видите пульсации? Как-то это слишком просто. Удивительно, что это работает. Но, как пишут комментаторы выше, лучше бы в этом убедиться более надёжно.

                                  0
                                  На самом деле колебания яркости всего лишь 0.3% от диапазона 0-255, так что неудивительно что глазом мы этого не видим. Но да, я думал что будет сложнее, придется отдельные каналы выделять, смотреть где больше влияние и пр.
                                  +4

                                  Камера снимает, наверное, 30 кадров в секунду, лампочка на потолке мерцает 50 раз в секунду, монитор 60 раз. Какие тут получатся результирующие биения, предсказать не берусь. Но всё же предложу померять пульс у обычной фотографии зафиксированной неподвижно. Вполне возможно получите такой же пульс как у живого человека

                                    0

                                    Очевидно. что наименьшее общее кратное (НОК) периодов это и будет совокупный период колебаний системы.

                                    0
                                    Скажите, а что за приложение на андроид вы видели?

                                    Просто я писал точно такое же приложение для одного нашего заказчика в пору работы в аутсорс. Наше приложение называлось PulseR :)
                                    Как я понял, тот чувак так и не смог найти инвестиций, точность измерений была крайне сомнительной — адекватный результат выдавался 1 раз из 10
                                      0
                                      Не помню, читал наверно год назад, но не было времени проверить.

                                      Со смартфоном действительно сложнее, если его в руках держать, тряска будет сильная. С вебкамерой проще. Тут 95% точности, это фиксация камеры и пациента :) График на самом деле сильно колбасит если хоть чуть пошевелиться, а если еще и камера трясется, то наверно 1 к 10 и будет хоть какой-то результат.
                                        0

                                        Вообще-то имеет место быть вот этот эффект.
                                        Главную роль играет не цвет, а свет, поэтому можно видеть пульс даже у рубашки.
                                        И эффективней будет брать local binary pattern, нормировать и прогонять через FFT фильтр, который будет отсекать все частоты, кроме диапазона 40-90 герц.

                                          +1
                                          Слишком малый диапазон — у меня нередко бывает пульс за 120 (для выявления чего, собственно, и нужно измерять пульс при тахикардии)
                                            0
                                            0.5-3Hz
                                        +2

                                        Вероятно речь шла об этой статье:


                                        https://m.habr.com/ru/post/463281/


                                        За полгода после публикации я немного доработал алгоритм, так что адекватный результат получается немного почаще, чем в 10% случаев. Вот ссылка, если захотите протестировать


                                        https://play.google.com/store/apps/details?id=ki.facehrm

                                          0
                                          Спасибо, наверно эту статью я и читал.
                                        +1
                                        У меня знакомый делал замер пульса по пьезодатчиам. Ставил пьезодатчики под ножки кровати и когда человек спит, то в фазе глубокого сна можно снимать биение сердца.
                                          0
                                          Интересно, неужели матрас не сглаживает столь малые колебания?

                                          Хотя такой способ еще может быть удобен для автоматического взвешивания человека :) Тут алгоритмически может быть много тонкостей, как учитывать вес простыней когда человека нет, как учитывать если спят двое, и пр.
                                            +1
                                            как-то получалось…

                                            Даже на графике было видно вторую гармонику — вариабельность ритма в зависимости от фазы дыхания (heart rate variability). По этой гармонике можно анализировать некоторые параметры здоровья.
                                          +1
                                          хм, а если двое спят? хотя тогда можно анализировать не только сон
                                            +1

                                            Анализируя ваш сон
                                            Андроид задетектил стон
                                            И после электронный мозг
                                            Жить без Гиноида не мог.

                                          +2
                                          интересно было бы анализировать некоторых телевизионных персонажей, насколько у них учащается пульс, когда доносят до зрителей определенную информацию
                                            +1
                                            Интересная работа, сам я и не задумывался, чтобы вебкамерой пульс определять
                                              0
                                              Какое значение выдает программа если в кадре нет одушевленных или подвижных предметов?
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                                0
                                                Есть такие приложения для мобильного, на iOS точно.
                                                0

                                                Вот не пойму, если тупо с веб-камеры так надежно виден ритм в реальном времени, какого черта спец-датчики во всяких мифитах сяомевских и прочих смарт-часах так четко не работают? там же и подсветка специальная, и матрица однопиксельная… казалось бы фильтруй и показывай в реальном времени, ан нет, ждать приходится усреднения, натягивать на руку туго и все равно через раз фиксит. Может это у меня такая шерсть густая на руке?

                                                  0
                                                  Ритм виден надежно только для неподвижной картинки, алгоритм сильно чувствителен к горизонтальному смещению. Пульсоксиметр с прищепкой ритм видит четко, но спортивные часы так никто носить не будет :)
                                                    0

                                                    Вживить бы пульсоксиметр под кожу. Например в составе кохлеарного импланта с bluetooth

                                                    +1

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


                                                    Мы воспринимаем как данность стабильный уровень осещения. На самом деле, если мы не можем обеспечить константное освещение — модель деградирует гораздо быстрее, чем от смещения обьекта (при условии треккинга) вдоль линии постоянной осещенности.


                                                    Почему же инжнеры не могут сделать "нормальные" фитнес-брасслеты? Освещенность на Солнце 30 — 100 тысяч люкс, что очень много. Кожа сама по себе имеет небольшую прозрачность для света, а если браслет еще и установлен неплотно, кожа под браслетом может "немного" подсвечиваться солнцем, что само по себе и неплохо. Если бы не два фактора:


                                                    1. Мощность диода весьма ограничена и он не может тягаться с Солнцем, если браслет надет неплотно
                                                    2. При беге или ходьбе изменения освещенности от Солнца еще РИТМИЧНО, что делает задачу распознавания пульса в таких условиях очень сложной

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

                                                    0
                                                    Китайские браслеты настолько продвинуты, что видят пульс даже у колбасы)
                                                    Скрытый текст
                                                    image
                                                      0

                                                      Нормальный пульс. Колбаса вас не боится. Видимо для вас она опаснее, чем вы для нее.

                                                    +1
                                                    Вот подобный прроект за которым уже давно слежу.
                                                    github.com/thearn/webcam-pulse-detector.git
                                                      0
                                                      Я чего-то не понимаю. Каким образом камера с 30 фпс может выдать картинку, показанную на графике? Если «синус» на нем соответствует 75 герцам, а в одном периоде порядка 15 отсчетов, то частота дискретизации около/больше 1 кГц. Или действительно наблюдается картина с другой зоны Найквиста?
                                                        0

                                                        "Синус" соотвествует 75/60 гц, т.е. чуть больше одного герца. Эта "ошибка" часто возникает при переходе от привычных нам удар/мин в систему си.

                                                          0
                                                          Спасибо, только прочитал и понял свою ошибку) Какие еще 75 Гц, о чем я думал) Действительно, 30 фпс без проблем позволят оценивать пульс и другие периодичности вплоть до 15 Гц.

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое