Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
absl-py==0.10.0
albumentations==0.5.1
appnope==0.1.0
astunparse==1.6.3
attrs==19.3.0
backcall==0.1.0
bleach==3.1.0
cachetools==4.1.1
certifi==2018.1.18
chardet==3.0.4
cycler==0.10.0
Cython==0.29.15
decorator==4.4.1
defusedxml==0.6.0
dlib==19.21.0
entrypoints==0.3
falcon==2.0.0
ffmpeg==1.4
flatbuffers==1.12
gast==0.3.3
google-auth==1.22.1
google-auth-oauthlib==0.4.1
google-pasta==0.2.0
grpcio==1.33.1
gunicorn==20.0.4
h5py==2.10.0
idna==2.10
imageio==2.9.0
imgaug==0.4.0
importlib-metadata==1.5.0
imutils==0.5.3
ipykernel==5.1.4
ipython==7.12.0
ipython-genutils==0.2.0
ipywidgets==7.5.1
jedi==0.16.0
Jinja2==2.11.1
json5==0.9.1
jsonschema==3.2.0
jupyter==1.0.0
jupyter-client==5.3.4
jupyter-console==6.1.0
jupyter-core==4.6.1
jupyterlab==1.2.6
jupyterlab-server==1.0.6
Keras-Preprocessing==1.1.2
kiwisolver==1.1.0
lab==5.3
Markdown==3.3.3
MarkupSafe==1.1.1
matplotlib==3.1.3
mistune==0.8.4
mkl-fft==1.0.15
mkl-random==1.1.0
mkl-service==2.3.0
nbconvert==5.6.1
nbformat==5.0.4
networkx==2.5
notebook==6.0.3
numpy==1.18.1
oauthlib==3.1.0
opencv-python==4.2.0.32
opencv-python-headless==4.2.0.32
opt-einsum==3.3.0
pandocfilters==1.4.2
parso==0.6.1
PeakUtils==1.3.3
pexpect==4.8.0
pickleshare==0.7.5
Pillow==7.0.0
prometheus-client==0.7.1
prompt-toolkit==3.0.3
protobuf==3.13.0
ptyprocess==0.6.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pyfakewebcam==0.1.0
Pygments==2.5.2
pyparsing==2.4.6
PyQt5==5.15.1
PyQt5-sip==12.8.1
pyqtgraph==0.11.0
pyrsistent==0.15.7
python-dateutil==2.8.1
PyWavelets==1.1.1
PyYAML==5.3
pyzmq==18.1.1
qtconsole==4.6.0
requests==2.24.0
requests-oauthlib==1.3.0
rsa==4.6
scikit-image==0.17.2
scipy==1.5.3
Send2Trash==1.5.0
Shapely==1.7.1
simplejson==3.17.0
six==1.14.0
tensorboard==2.3.0
tensorboard-plugin-wit==1.7.0
tensorflow==2.3.1
tensorflow-estimator==2.3.0
termcolor==1.1.0
terminado==0.8.3
testpath==0.4.4
tflite==2.3.0
tifffile==2020.9.3
torch==0.4.1
torchvision==0.5.0
tornado==6.0.3
traitlets==4.3.3
urllib3==1.25.11
wcwidth==0.1.8
webencodings==0.5.1
Werkzeug==1.0.1
widgetsnbextension==3.5.1
wrapt==1.12.1
yacs==0.1.6
youtube-dl==2020.9.20
zipp==2.2.0
1 change: 1 addition & 0 deletions src/heart_rate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["segmented_average", "heart_rate_controller"]
21 changes: 21 additions & 0 deletions src/heart_rate/heart_rate_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import json

import falcon

from src.heart_rate import segmented_average


class CalculateHeartRate(object):
@classmethod
def on_post(cls, req, resp):
try:
raw_json = req.stream.read()
body = json.loads(raw_json)
data_stream = body["data_stream"]
fps = body["fps"]

if len(data_stream) > 0 and fps > 0:
bpm = segmented_average.calc_bpm(data_stream, fps)
resp.body = json.dumps({"bpm": bpm})
except Exception as ex:
raise falcon.HTTPError(falcon.HTTP_400, 'Error', ex.message)
94 changes: 94 additions & 0 deletions src/heart_rate/segmented_average.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import numpy as np
from scipy.signal import find_peaks
import scipy.fftpack as fftpack
import math


def calc_bpm(a, fps):
bucket_size_secs = 10
max_bpm_diff_for_mean = 20
freq_limits = (50.0 / 60, 180.0 / 60)

y2 = np.array(a) # np.array(rolling_average(a, rolling_average_n)).flatten()

sum_max_freq = 0
sum_avg_bps = 0
max_bps = -1
min_bps = 10
bucket_i = 0
num_non_empty_buckets = 0
while True:
bucket = y2[int(bucket_i * bucket_size_secs * fps): int(
(bucket_i + 1) * bucket_size_secs * fps)] # use decay weights
if bucket.shape[0] <= 0: break
bucket_i += 1
sig_fft, power, sample_freq = get_fft_sig(bucket, 1 / fps)
max_fq_arr = find_max_frequencies_within_range(np.square(power), sample_freq, freq_limits, 2)
if (max_fq_arr.shape[0] == 0):
continue
num_non_empty_buckets += 1
max_fq, min_fq = max(max_fq_arr), min(max_fq_arr)
max_fq_mean = np.mean(max_fq_arr) if abs(max_fq - min_fq) * 60 <= max_bpm_diff_for_mean else max_fq
if not math.isnan(max_fq_mean):
sum_max_freq += max_fq_mean

peaks2, _ = find_peaks(bucket, distance=7.5, prominence=2) # BEST!
max_bps = max(max_bps, peaks2.shape[0] * fps / bucket.shape[0])
min_bps = min(min_bps, peaks2.shape[0] * fps / bucket.shape[0])
bucket_bps = peaks2.shape[0] * fps / (bucket.shape[0])
print(f'{bucket_i} {bucket_bps * 60} {max_fq_arr * 60} {sum_max_freq * 60}')
sum_avg_bps += bucket_bps
if (num_non_empty_buckets > 0):
avg_max_freq = sum_max_freq / num_non_empty_buckets
return avg_max_freq * 60
return -1


def get_max_power_frequencies(bucket, fps, freq_limits):
sig_fft, power, sample_freq = get_fft_sig(bucket, 1 / fps)
return find_max_frequencies_within_range(power, sample_freq, freq_limits, 2)


# returns freq for n_max power values in the req_range and in decreasing order
def find_max_frequencies_within_range(powers, frequencies, req_range, n_max):
# filtered based on sig range
filtered_freq, filtered_power = filter_on_frequency_range(frequencies, powers, req_range)

# indices for n_max values
n_max_power_indices = filter_on_power_k_order_statistic(filtered_power, n_max)

# indices order required to sort filtered_n_max_sig_vals (sort order is increasing)
increasing_filtered_sig_val_indices = np.argsort(filtered_power[n_max_power_indices])

# frequencies for the n_max power within range
filtered_n_max_frequency = filtered_freq[n_max_power_indices]

# return in decreasing order of power
return np.flip(filtered_n_max_frequency[increasing_filtered_sig_val_indices])


# returns filtered_sig, and filtered_sig_val based on the sig_val range
def filter_on_frequency_range(frequencies, powers, req_range):
filtered_freq_indices = np.logical_and(frequencies >= req_range[0], frequencies <= req_range[1])
return frequencies[filtered_freq_indices], powers[filtered_freq_indices]


def filter_on_power_k_order_statistic(power, n_max):
safe_n_max = min(n_max, power.shape[0])
return np.argpartition(power, -safe_n_max)[-safe_n_max:]


def get_min_max(dim, max_limit, min_limit):
return max(min_limit, dim[0] - dim[1]), min(max_limit, dim[0] + dim[1])


def get_fft_sig(sig, time_step):
# The FFT of the signal
sig_fft = fftpack.fft(sig)

# And the power (sig_fft is of complex dtype)
power = np.abs(sig_fft) ** 2

# The corresponding frequencies
sample_freq = fftpack.fftfreq(sig.size, d=time_step)
return sig_fft, power, sample_freq
2 changes: 2 additions & 0 deletions src/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import falcon
import json
from src.logger import logging
from .heart_rate import *
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -58,5 +59,6 @@ def get_app():
api = falcon.API()
api.add_route('/', StaticResource())
api.add_route('/update', UpdateResource())
api.add_route('/calc_bpm', heart_rate_controller.CalculateHeartRate())
api.add_sink(handle_404, '')
return api