├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ ├── python-publish.yml
│ └── test.yml
├── .gitignore
├── .readthedocs.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docs
├── Makefile
├── api.rst
├── conf.py
├── index.rst
├── install.rst
├── intro.rst
└── make.bat
├── examples
├── signal_diff.py
└── time_series.py
├── install.sh
├── requirements.txt
├── run_docker_test.sh
├── setup.py
├── src
├── __init__.py
└── pulse.py
├── tests
├── test_bpm.py
├── test_gray.py
├── test_peaks.py
├── test_sigdiff.py
├── test_time.py
└── test_variances.py
└── ui
├── README.md
├── api
├── .flaskenv
├── Dockerfile
├── README.md
├── api.py
├── requirements.txt
├── static
│ ├── PULSETRACKER-LOGO.png
│ └── style.css
└── templates
│ └── index.html
├── firebase.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.js
├── components
│ ├── AlertButton
│ │ └── AlertButton.js
│ ├── Auth
│ │ ├── Auth.css
│ │ ├── Auth.js
│ │ └── config
│ │ │ └── fire.js
│ ├── Home
│ │ ├── Home.css
│ │ └── Home.js
│ ├── Login
│ │ ├── Login.css
│ │ └── Login.js
│ └── PrivateRoute.js
├── index.css
├── index.js
├── serviceWorker.js
├── setupTests.js
├── tests
│ ├── App.test.js
│ └── Home.test.js
└── utils
│ └── imgUrl.js
├── storage.rules
└── yarn.lock
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Finding
11 | A clear and concise description of what the bug is.
12 |
13 | ### Solution
14 |
15 | **Additional information**
16 | Add any other context about the problem here.
17 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package to PyPi
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Set up Python
13 | uses: actions/setup-python@v2
14 | with:
15 | python-version: '3.x'
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install setuptools wheel twine
20 | - name: Build and publish python package
21 | env:
22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
24 | run: |
25 | python setup.py sdist bdist_wheel
26 | twine upload -u ${{env.TWINE_USERNAME}} -p ${{env.TWINE_PASSWORD}} -r pulsetracker --repository-url https://upload.pypi.org/legacy/ dist/*
27 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: PulseTracker Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - devel
8 | pull_request:
9 | branches:
10 | - master
11 | - devel
12 |
13 | jobs:
14 | tests:
15 | name: Unit Tests
16 | runs-on: ubuntu-18.04
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Run Unit Test
20 | run: sh run_docker_test.sh
21 |
22 |
23 | api-test:
24 | name: API Test
25 | runs-on: ubuntu-18.04
26 | env:
27 | TEST_UID: 1kzd0DmeunLGEeB0nWLFFaIfuFZn
28 | steps:
29 | - run: curl https://pulsetracker-api-v2.herokuapp.com/${{env.TEST_UID}}
30 |
31 |
32 | prettier:
33 | name: Code Formatting
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v2
37 | with:
38 | # Make sure the actual branch is checked out when running on pull requests
39 | ref: ${{ github.head_ref }}
40 | - name: Prettify code
41 | uses: creyD/prettier_action@v2.2
42 | with:
43 | # This part is also where you can pass other options, for example:
44 | prettier_options: --write --single-quote ui/src
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /images
3 | .*.swp
4 | */*.egg-info/
5 | __pycache__/
6 |
7 | # Dependency directories
8 | node_modules/
9 |
10 | # logs
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Build documentation in the docs/ directory with Sphinx
9 | sphinx:
10 | configuration: docs/conf.py
11 |
12 | # Optionally set the version of Python and requirements required to build your docs
13 | python:
14 | version: 3.7
15 | install:
16 | - requirements: requirements.txt
17 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at hyltonakil@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Our software is open source so you can solve your own problems without needing help from others.
4 | And if you solve a problem and are so kind, you can upstream it for the rest of the world to use.
5 |
6 | ## Getting Started
7 | * Make sure you have a [GitHub account](https://github.com/signup/free)
8 | * Fork [our repository](https://github.com/akilhylton/pulsetracker) on GitHub
9 |
10 | ## Testing
11 | ### Local Testing
12 | You can test your changes on your machine by running `run_docker_tests.sh`.
13 | This will run some automated tests in docker against your code.
14 |
15 | ### Automated Testing
16 | All PRs and commits are automatically checked by Github Actions. Check out .github/workflows/ for what Github Actions runs. Any new tests sould be added to Github Actions.
17 |
18 | ## Pull Requests
19 | Pull requests should be against the master branch.
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8
2 | COPY . ./pulsetracker
3 | WORKDIR /pulsetracker
4 | ENV PYTHONPATH /pulse/src
5 | RUN apt-get update
6 | RUN apt-get install ffmpeg libsm6 libxext6 -y
7 | RUN make install
8 | RUN make
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Akil M Hylton
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SRC_CODE='./src/pulse.py'
2 | TEST_TIME='./tests/test_time.py'
3 | TEST_GRAY='./tests/test_gray.py'
4 | TEST_PEAKS='./tests/test_peaks.py'
5 | TEST_SIGNAL='./tests/test_sigdiff.py'
6 | TEST_VARI='./tests/test_variances.py'
7 | TEST_BPM='./tests/test_bpm.py'
8 |
9 |
10 | all:
11 | pip3 install -e .
12 |
13 | install:
14 | pip3 install -r requirements.txt
15 |
16 | test:
17 | python3 ${TEST_TIME}
18 | python3 ${TEST_GRAY}
19 | python3 ${TEST_SIGNAL}
20 | python3 ${TEST_VARI}
21 | python3 ${TEST_VARI}
22 | python3 ${TEST_BPM}
23 | rm -rf ./images/
24 |
25 | lint:
26 | flake8 --ignore=E722,E501,W291,W293 src/pulse.py
27 | pylint --disable=trailing-whitespace,superfluous-parens,missing-docstring,bare-except,len-as-condition --extension-pkg-whitelist=cv2 --max-line-length=120 src/
28 |
29 | clean:
30 | rm -rf ./images/
31 | rm -rf ./*/images/
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 | 
5 | [](https://github.com/prettier/prettier)
6 |
7 | ## 💭 Background
8 | An open source tool built for monitoring heart rate. The goal here is to have a low cost and widely accessible way to measure someones heart rate. It uses a touch-based system for generating heart rate values. In contrast to it's counterpart(touchless-based systems) it is a far more accurate and less sensitive to enviornmental conditions.
9 |
10 |
11 | Directory Structure
12 | ------
13 | .
14 | ├── docs # Sphinx documentation folder
15 | ├── examples # The example code
16 | ├── src # The source code for the library
17 | ├── tests # Unit tests and system tests
18 | └── ui # The UI
19 |
20 | ## Branches
21 | * devel -> PR this branch for everything
22 | * master -> DO NOT TOUCH, this is what's running in production
23 |
24 |
25 | ## How to run locally
26 | After cloning this repository and changing directories to it.
27 |
28 | #### 1. Install the dependencies
29 | ```
30 | $ make install
31 | ```
32 | #### 2. Install library
33 | ```
34 | $ make
35 | ```
36 | See example of usage in examples folder.
37 |
38 | ## Testing
39 | #### Local Testing
40 | ```
41 | $ sudo sh install.sh && sudo make test
42 | ```
43 | #### Testing with Docker
44 | ```
45 | $ sudo sh run_docker_test.sh
46 | ```
47 |
48 | ## Documentation
49 |
50 | We use [sphinx](https://www.sphinx-doc.org/en/master/) to build our documentation based on rST files and comments in the code, below is a quick guide to getting started.
51 | ```
52 | cd docs
53 | make html
54 | ```
55 |
56 | This will output the documentation to `docs/_build/html`.
57 | Now to view built documentation run `open _build/html/index.html`.
58 |
59 | [pulsetracker on readthedocs](https://pulsetracker.readthedocs.io/en/latest/)
60 |
61 | ## Contributing
62 |
63 | Contributions are welcome! Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and [how to contribute](CONTRIBUTING.md) before contributing to help this project stay welcoming.
64 |
65 | To understand how the library works see [`pulse.py`](src/pulse.py)
66 |
67 | ## License
68 |
69 | MIT License
70 |
71 | Copyright (c) 2020 Akil M Hylton
72 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API Reference
2 | =====================
3 |
4 | This part of the documentation covers all the interfaces of PulseTracker.
5 |
6 | .. automodule:: pulse
7 | :members:
8 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 | sys.path.insert(0, os.path.abspath('../src/'))
16 |
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = 'pulsetracker'
21 | copyright = '2020, Akil M Hylton'
22 | author = 'Akil M Hylton'
23 |
24 |
25 | # -- General configuration ---------------------------------------------------
26 |
27 | # Add any Sphinx extension module names here, as strings. They can be
28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
29 | # ones.
30 | master_doc = 'index'
31 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon' ]
32 |
33 | # Add any paths that contain templates here, relative to this directory.
34 | templates_path = ['_templates']
35 |
36 | # List of patterns, relative to source directory, that match files and
37 | # directories to ignore when looking for source files.
38 | # This pattern also affects html_static_path and html_extra_path.
39 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
40 |
41 | # -- Options for HTML output -------------------------------------------------
42 |
43 | # The theme to use for HTML and HTML Help pages. See the documentation for
44 | # a list of builtin themes.
45 | #
46 | html_theme = 'sphinx_rtd_theme'
47 |
48 | # Add any paths that contain custom static files (such as style sheets) here,
49 | # relative to this directory. They are copied after the builtin static files,
50 | # so a file named "default.css" will overwrite the builtin "default.css".
51 | html_static_path = ['_static']
52 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. heartrate documentation master file, created by
2 | sphinx-quickstart on Mon May 11 05:15:24 2020.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. image:: https://i.imgur.com/ZnWnu8a.png
7 | :width: 600
8 |
9 | ==================================
10 |
11 | **PulseTracker** is a simple Python library used to monitor heart rate using video.
12 |
13 | -----
14 |
15 | **Basic usage of PulseTracker:**
16 |
17 | .. code-block:: python
18 |
19 | import pulse as p
20 | pulse = p.Pulse()
21 | pulse.video_to_frames("./path/to/video.mp4")
22 | pulse.bpm()
23 |
24 |
25 | -----
26 |
27 | **Usage of PulseTracker with PulseBox:**
28 |
29 | .. code-block:: python
30 |
31 | import pulse as p
32 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
33 | pulse = p.Pulse()
34 | pulse.pulsebox_to_frames(testing_uid)
35 | pulse.bpm()
36 |
37 |
38 | The User Guide
39 | """"""""""""""
40 | This part of the documentation, which is mostly prose, begins with some background information about PulseTracker, then focuses on step-by-step instructions for getting the most out of this library.
41 |
42 | .. toctree::
43 | :maxdepth: 2
44 |
45 | intro
46 | install
47 | api
48 |
--------------------------------------------------------------------------------
/docs/install.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Installation
3 | ============
4 |
5 | Installing with Docker:
6 |
7 | .. code-block:: bash
8 |
9 | docker pull docker.pkg.github.com/akilhylton/pulsetracker/pulsetracker:0.4
10 | docker run -it docker.pkg.github.com/akilhylton/pulsetracker/pulsetracker:0.4
11 |
12 |
13 | Installing the development version
14 |
15 | .. code-block:: bash
16 |
17 | git clone https://github.com/akilhylton/pulsetracker.git
18 | cd pulsetracker
19 | pip3 install -r requirements.txt
20 | sudo pip3 install -e .
21 |
--------------------------------------------------------------------------------
/docs/intro.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Introduction
3 | ============
4 |
5 | **pulsetracker** is an open source tool built for monitoring heart rate.
6 | The goal here is to have a low cost and widely accessible way to measure someones heart rate.
7 | It uses a touch-based system for generating heart rate values.
8 | In contrast to it's counterpart(touchless-based systems) it is
9 | a far more accurate and less sensitive to enviornmental conditions.
10 |
11 | Why was pulsetracker developed?
12 | -------------------------------
13 |
14 | After an exhaustive search for a simple library to do touch based
15 | heart rate analysis. We found that the majority of code was writing
16 | for touchless based system and some touch based systems as well.
17 | However, a well documented and simple to use API was not found.
18 | So we decided to develop pulsetracker to fill this void.
19 |
20 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/examples/signal_diff.py:
--------------------------------------------------------------------------------
1 | import pulse as p
2 | import matplotlib.pyplot as plt
3 |
4 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
5 | pulse = p.Pulse()
6 | pulse.pulsebox_to_frames(testing_uid)
7 |
8 | new_signal = pulse.frames_to_gray()
9 | plt.imshow(new_signal[0], cmap = plt.cm.gray)
10 | plt.show()
11 |
--------------------------------------------------------------------------------
/examples/time_series.py:
--------------------------------------------------------------------------------
1 | import pulse as p
2 | import matplotlib.pyplot as plt
3 |
4 | pulse = p.Pulse()
5 |
6 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
7 | pulse.pulsebox_to_frames(testing_uid)
8 |
9 | peaks = pulse.get_peaks()
10 | reds = pulse.signal_diff()
11 |
12 | plt.figure(figsize=(15,5))
13 | plt.title("Signal Differentiation")
14 | plt.ylabel("average red constant value")
15 | plt.xlabel("frame number")
16 | plt.plot(reds)
17 | plt.plot(peaks[:200], reds[peaks[:200]], 'x')
18 | plt.show()
19 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | sudo apt-get install flake8 pylint
3 | echo -e "\nTools for testing has been installed!"
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | opencv-python
3 | scipy
4 | pyrebase
5 | matplotlib
6 | Flask
7 | urllib3
8 | sphinx_rtd_theme
9 |
--------------------------------------------------------------------------------
/run_docker_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | docker build -t pulsetracker:latest .
3 | docker run --rm -i pulsetracker:latest sh -c "make test"
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | def readme():
4 | with open('README.md') as f:
5 | return f.read()
6 |
7 | setup(
8 | name='pulsetracker',
9 | version='0.5',
10 | description='A simple algorithm to monitor heartrate using any mobile phone with a camera and flash.',
11 | long_description=readme(),
12 | package_dir={"": "src"},
13 | author='Akil M Hylton',
14 | author_email='hyltonakil@gmail.com',
15 | url='pulsetrackerapp.com',
16 | project_urls={
17 | 'CoronaTracker': 'https://coronatrackerbeta.com',
18 | 'Source Code': 'https://github.com/akilhylton/pulsetracker',
19 | },
20 | python_requires='>=3',
21 | install_requires=[
22 | 'numpy',
23 | 'opencv-python',
24 | 'scipy',
25 | 'pyrebase',
26 | 'setuptools'
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k105la/pulsetracker/648dfee4d6c0185395f52a055a15900f8e30a421/src/__init__.py
--------------------------------------------------------------------------------
/src/pulse.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import os
3 | import glob
4 | import pyrebase
5 | import numpy as np
6 | import cv2 as cv
7 | from scipy.signal import find_peaks, argrelmin
8 |
9 |
10 | class Pulse(object):
11 | """
12 | This is the main class of the pulsetracker module.
13 | """
14 |
15 | def __init__(self, frame_rate=30):
16 | """
17 | Initialise the Pulse class. It takes one argument fr(frame rate)
18 | which defaults to 30.
19 | """
20 | self.avg_red = []
21 | self.rgb_imgs = []
22 | self.distance = []
23 | self.sigmoids = []
24 | self.frame_rate = frame_rate
25 | self.dim = (320, 240)
26 |
27 | @staticmethod
28 | def remove_frames():
29 | """
30 | Removes all frames from images folder.
31 | """
32 | img_path = glob.glob("./images/*.jpg")
33 | for img in img_path:
34 | os.remove(img)
35 |
36 | def pulsebox_to_frames(self, uid):
37 | """
38 | Converts the a video stored in a users
39 | pulsebox storage to frames.
40 | """
41 | config = {
42 | "apiKey": "AIzaSyBoCLNFNU2-_J6NQtbLD7GGy30zRvkzBmk",
43 | "authDomain": "pulse-box.firebaseapp.com",
44 | "databaseURL": "https://pulse-box.firebaseio.com",
45 | "storageBucket": "pulse-box.appspot.com",
46 | }
47 |
48 | firebase = pyrebase.initialize_app(config)
49 |
50 | storage = firebase.storage()
51 | pulsebox_video = storage.child(f"data/{uid}/hr_test.MOV").get_url(1)
52 |
53 | capture = cv.VideoCapture(pulsebox_video)
54 | ret, frame = capture.read()
55 |
56 | if not os.path.exists("./images/"):
57 | os.makedirs("./images/")
58 | else:
59 | self.remove_frames()
60 |
61 | for count in range(300):
62 | try:
63 | cv.imwrite(f"./images/frame{count}.jpg", frame)
64 | except cv.error as e:
65 | print(
66 | "Error: Must provide a UID from \x1b]8;;https://pulse-box.firebaseapp.com/\apulsebox\x1b]8;;\a,",
67 | e,
68 | )
69 | break
70 |
71 | ret, frame = capture.read()
72 |
73 | capture.release()
74 | cv.destroyAllWindows()
75 |
76 | def video_to_frames(self, video_path):
77 | """
78 | Converts input videoPath to frames.
79 | """
80 | capture = cv.VideoCapture(video_path)
81 | ret, frame = capture.read()
82 |
83 | if not os.path.exists("./images/"):
84 | os.makedirs("./images/")
85 | else:
86 | self.remove_frames()
87 |
88 | for count in range(300):
89 | try:
90 | cv.imwrite("./images/frame{}.jpg".format(count), frame)
91 | except cv.error as e:
92 | print("Error: Must provide a video to this function,", e)
93 | break
94 |
95 | ret, frame = capture.read()
96 | if ret is not True:
97 | if count / self.frame_rate < 10:
98 | self.remove_frames()
99 | print(
100 | "Your video was {} seconds but video length must be 10 seconds long.".format(
101 | count / self.frame_rate
102 | )
103 | )
104 | print("Try Again. Please use a video that is 10 seconds or longer.")
105 | break
106 |
107 | capture.release()
108 | cv.destroyAllWindows()
109 |
110 | def frames_to_gray(self):
111 | """
112 | Converts RGB input frames to grayscale.
113 | """
114 | try:
115 | img_path = glob.glob("./images/*.jpg")
116 | cv_img = []
117 | for img in img_path:
118 | gray_image = cv.imread(img, 0)
119 | gray_array = cv.resize(gray_image, self.dim)
120 | cv_img.append(gray_array)
121 | grayscale_imgs = np.asarray(cv_img)
122 | return grayscale_imgs
123 |
124 | except:
125 | print("The images directory is empty, you must first run video_to_frames()")
126 |
127 | def signal_diff(self):
128 | """
129 | Subtracts one frame from the subsequent frame.
130 | """
131 | try:
132 | gray = self.frames_to_gray()
133 |
134 | if len(self.avg_red) == 0: # Will only run loop if avg_red list is empty
135 | for i in range(200):
136 | self.avg_red.append(int(abs(np.mean(gray[i] - gray[i + 1]))))
137 | red = np.asarray(self.avg_red)
138 | return red
139 |
140 | except:
141 | pass
142 |
143 | def get_peaks(self, dist=5):
144 | """
145 | Takes a one-dimensional array and finds all local maxima
146 | by simple comparison of neighbouring values.
147 | """
148 | try:
149 | red = self.signal_diff()
150 | peaks, _ = find_peaks(red, distance=dist)
151 | return peaks.reshape(-1, 1)
152 |
153 | except:
154 | pass
155 |
156 | def variances(self):
157 | """
158 | Calculates the how far the peaks are
159 | spread out from their average value.
160 | """
161 | try:
162 | peaks = self.get_peaks()
163 | if len(self.sigmoids) == 0: # Will only run loop if sigmoids list is empty
164 | for i in range(len(peaks) - 1):
165 | self.distance.append(peaks[i + 1] - peaks[i])
166 | dist = np.asarray(self.distance)
167 | self.sigmoids.append(abs(np.square(abs(dist[i] - np.mean(dist)))))
168 | sig = np.asarray(self.sigmoids)
169 | return sig.reshape(-1, 1)
170 |
171 | except:
172 | pass
173 |
174 | def bpm(self):
175 | """
176 | Calculates the heart rate value.
177 | """
178 | try:
179 | variance = self.variances()
180 | minima = argrelmin(variance)
181 | minima = np.asarray(minima)
182 | final_minima = minima[
183 | (minima > self.frame_rate * 60 / 200)
184 | ] # Filters values less than 9
185 | minima_mean = np.mean(final_minima)
186 | heart_rate = self.frame_rate * 60 / minima_mean
187 | return int(heart_rate)
188 |
189 | except:
190 | pass
191 |
--------------------------------------------------------------------------------
/tests/test_bpm.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import pulse as p
3 |
4 |
5 | class TestBPM(unittest.TestCase):
6 | """This test Class is for variances()"""
7 |
8 | def test_range_of_bpm(self):
9 | """
10 | This function assures that variances()
11 | returns a one dimensional matrix.
12 | """
13 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
14 | pulse = p.Pulse()
15 | pulse.pulsebox_to_frames(testing_uid)
16 | hr = pulse.bpm()
17 | self.assertTrue(int(hr) > 0 and int(hr) <= 220)
18 |
19 |
20 | unittest.main()
21 |
--------------------------------------------------------------------------------
/tests/test_gray.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import pulse as p
3 |
4 |
5 | class GrayTest(unittest.TestCase):
6 | """This test Class is variances()"""
7 |
8 | def testFramesToGray(self):
9 | """
10 | This function assures that variances()
11 | returns a one dimensional matrix.
12 | """
13 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
14 | pulse = p.Pulse()
15 | pulse.pulsebox_to_frames(testing_uid)
16 | gray = pulse.frames_to_gray()
17 | self.assertEqual(len(gray), 300)
18 |
19 |
20 | unittest.main()
21 |
--------------------------------------------------------------------------------
/tests/test_peaks.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import pulse as p
3 |
4 |
5 | class TestPeaks(unittest.TestCase):
6 | """Class for testing get_peaks()"""
7 |
8 | def test_get_peaks(self):
9 | """
10 | This function assures that get_peaks()
11 | returns a one dimentional matrix.
12 | """
13 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
14 | pulse = p.Pulse()
15 | pulse.pulsebox_to_frames(testing_uid)
16 | peaks = pulse.get_peaks()
17 | peaks_dim = peaks.shape[1]
18 | self.assertEqual(peaks_dim, 1)
19 |
20 |
21 | unittest.main()
22 |
--------------------------------------------------------------------------------
/tests/test_sigdiff.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import pulse as p
3 |
4 |
5 | class SignalTest(unittest.TestCase):
6 | """This test Class is for signal_diff()"""
7 |
8 | def test_signal_diff(self):
9 | """
10 | This function test to assure that
11 | signal_diff() returns a size of 200.
12 | """
13 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
14 | pulse = p.Pulse()
15 | pulse.pulsebox_to_frames(testing_uid)
16 | red = pulse.signal_diff()
17 | self.assertEqual(len(red), 200)
18 |
19 |
20 | unittest.main()
21 |
--------------------------------------------------------------------------------
/tests/test_time.py:
--------------------------------------------------------------------------------
1 | import time
2 | import unittest
3 | import pulse as p
4 |
5 |
6 | class TimeTest(unittest.TestCase):
7 | """This Class tests the whole systems time speed."""
8 |
9 | def setUp(self):
10 | """Setup function"""
11 | self.startTime = time.time()
12 |
13 | def tearDown(self):
14 | """This function gets called after setUp() succeeds."""
15 | t = time.time() - self.startTime
16 | print("{}: {}".format(self.id(), t))
17 |
18 | def testSystemSpeed(self):
19 | """This function runs the entire heart rate system."""
20 | testing_video = "https://firebasestorage.googleapis.com/v0/b/pulse-box.appspot.com/o/data%2F1kzd0DmeunLGEeB0nWLFFaIfuFZn%2Fhr_test.MOV?alt=media&token=221ee115-5fb1-4c38-8264-5b1f8a859fda"
21 | pulse = p.Pulse()
22 | pulse.video_to_frames(testing_video)
23 | pulse.bpm()
24 |
25 |
26 | unittest.main()
27 |
--------------------------------------------------------------------------------
/tests/test_variances.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import pulse as p
3 |
4 |
5 | class TestVariances(unittest.TestCase):
6 | """This test Class is variances()"""
7 |
8 | def test_variances(self):
9 | """
10 | This function assures that variances()
11 | returns a one dimensional matrix.
12 | """
13 | testing_uid = "1kzd0DmeunLGEeB0nWLFFaIfuFZn"
14 | pulse = p.Pulse()
15 | pulse.pulsebox_to_frames(testing_uid)
16 | v = pulse.variances()
17 | v_dim = v.shape[1]
18 | self.assertEqual(v_dim, 1)
19 |
20 | unittest.main()
21 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 | An open source tool built for monitoring heart rate and this is the user interface code, a simple web application built with ReactJS, Material UI and Firebase.
5 |
6 | ## Front-End Overview
7 |
8 | Technology Stack: React, Material UI, Firebase
9 |
10 | - Frontend: [React](https://github.com/facebook/react)
11 | - Backend: [Firebase](https://firebase.google.com/)
12 |
13 | ### Material UI
14 |
15 | - Material UI: [Installation](https://material-ui.com/getting-started/installation/) and [Usage](https://material-ui.com/getting-started/usage/)
16 | - Material Design: [Color System](https://material.io/design/color/)
17 | - Material Design: [Accessibility](https://material.io/design/usability/accessibility.html)
18 | - Udemy: [Material UI Courses](https://www.udemy.com/topic/material-design/)
19 |
20 | ### React
21 |
22 | - Tutorial: [Intro to React](https://reactjs.org/tutorial/tutorial.html)
23 | - Udemy: [React Courses](https://www.udemy.com/topic/react/)
24 | - PluralSight: [React Paths and Courses](https://www.pluralsight.com/search?q=react)
25 |
26 | ## Available Scripts
27 | In the project directory, you can run:
28 |
29 | ### `yarn start`
30 |
31 | Runs the app in the development mode.
32 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
33 |
34 | The page will reload if you make edits.
35 | You will also see any lint errors in the console.
36 |
37 |
38 | ### `yarn start-api`
39 | Runs the server with the heart rate algorithm on it.
40 | Request retrieved from http://127.0.0.1:5000/{user_uid}
41 |
42 | ### `yarn test`
43 |
44 | Launches the test runner in the interactive watch mode.
45 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
46 |
47 | ### `yarn build`
48 |
49 | Builds the app for production to the `build` folder.
50 | It correctly bundles React in production mode and optimizes the build for the best performance.
51 |
52 | The build is minified and the filenames include the hashes.
53 | Your app is ready to be deployed!
54 |
55 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
56 |
57 | ### `yarn eject`
58 |
59 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
60 |
61 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
62 |
63 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
64 |
65 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
66 |
67 | ## Learn More
68 |
69 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
70 |
71 | To learn React, check out the [React documentation](https://reactjs.org/).
72 |
73 | ### Code Splitting
74 |
75 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
76 |
77 | ### Analyzing the Bundle Size
78 |
79 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
80 |
81 | ### Making a Progressive Web App
82 |
83 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
84 |
85 | ### Advanced Configuration
86 |
87 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
88 |
89 | ### Deployment
90 |
91 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
92 |
93 | ### `yarn build` fails to minify
94 |
95 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
96 |
--------------------------------------------------------------------------------
/ui/api/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=api.py
2 | FlASK_ENV=development
3 |
--------------------------------------------------------------------------------
/ui/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/k105la/pulsetracker/pulsetracker:latest
2 | COPY . ./
3 | RUN pip3 install -r requirements.txt
4 | CMD python3 api.py
5 |
--------------------------------------------------------------------------------
/ui/api/README.md:
--------------------------------------------------------------------------------
1 | 
2 | PulseTracker's API Service: https://pulsetracker-api.herokuapp.com/
3 |
4 | ### Deploying Service (must have admin privileges)
5 | Inside this directory run:
6 |
7 |
8 | ```console
9 | akilhylton:~$ heroku container:login
10 | akilhylton:~$ heroku git:remote -a pulsetracker-api
11 | akilhylton:~$ heroku container:push web -a pulsetracker-api
12 | akilhylton:~$ heroku container:release web -a pulsetracker-api
13 | ```
14 |
--------------------------------------------------------------------------------
/ui/api/api.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, jsonify, render_template
3 | from flask_cors import CORS
4 | import pulse as p
5 |
6 | app = Flask(__name__)
7 | CORS(app)
8 |
9 | @app.route('/')
10 | def welcome_screen():
11 | return render_template('index.html')
12 |
13 | @app.route('/', methods=['GET'])
14 | def view_heartrate(uid):
15 | pulse = p.Pulse()
16 | pulse.pulsebox_to_frames(uid)
17 | hr = pulse.bpm()
18 | os.system('rm -rf images')
19 | return jsonify({'pulse': hr})
20 |
21 |
22 | if __name__ == "__main__":
23 | app.run(host="0.0.0.0", port=int(os.environ.get('PORT', 8080)))
24 |
--------------------------------------------------------------------------------
/ui/api/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | flask-cors
3 | python-dotenv
4 |
--------------------------------------------------------------------------------
/ui/api/static/PULSETRACKER-LOGO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k105la/pulsetracker/648dfee4d6c0185395f52a055a15900f8e30a421/ui/api/static/PULSETRACKER-LOGO.png
--------------------------------------------------------------------------------
/ui/api/static/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | text-align: center;
3 | }
4 |
5 | .logo {
6 | margin: 1em auto;
7 | }
8 | .intro-text {
9 | font-size: 30px;
10 | font-family: 'Roboto', sans-serif;
11 | font-weight: normal;
12 | }
13 |
14 | .usage-text {
15 | font-size: 20px;
16 | font-family: 'Roboto', sans-serif;
17 | line-height: 2em;
18 | font-weight: normal;
19 | }
20 |
--------------------------------------------------------------------------------
/ui/api/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PulseTracker API
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
This is the PulseTracker API
17 |
Usage: https://pulsetracker.herokuapp.com/{user_uid} returns a json response { "pulse": int }