Разумеется, я не буду делать приложение под 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.
Собственно и все.
Запускаем программу, подбираем такое положение, чтобы в кропе с камеры был только фрагмент кожи, принимаем «позу мыслителя», подперев голову рукой — изображение должно быть максимально неподвижно. И вуаля — это действительно работает!
Разумеется, по клеточкам считать не точно, примерный пульс получился около 75bpm. Для сравнения, результат с поверенного китайскими мастерами пульсоксиметра:
Заключение
Как ни странно, но это действительно работает. Если честно, в результате я был не уверен. Разумеется, для реального использования нужно сначала найти лицо на изображении, но встроенная функция поиска лиц в OpenCV также есть. И конечно, нужна несложная математика для выделения периода из достаточно шумных данных.
Для желающих поэкспериментировать самостоятельно, исходный код целиком под спойлером.
Spoiler
import numpy as np
from matplotlib import pyplot as plt
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, 1280)
cap.set(cv2.CAP_PROP_FPS, 30)
# 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()
И как обычно, всем удачных экспериментов