├── .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 | ![Imgur](https://i.imgur.com/E3s6RUi.png) 2 | 3 | ![PulseTracker Tests](https://github.com/akilhylton/pulsetracker/workflows/PulseTracker%20Tests/badge.svg) 4 | ![PulseTracker Docs](https://img.shields.io/readthedocs/pulsetracker) 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](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 | ![Imgur](https://i.imgur.com/E3s6RUi.png) 2 | ![ui test](https://github.com/akilhylton/pulsetracker/workflows/ui%20test/badge.svg) 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 | ![Imgur](https://i.imgur.com/E3s6RUi.png) 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 }
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ui/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "storage": { 3 | "rules": "storage.rules" 4 | }, 5 | "hosting": { 6 | "public": "build", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulsetracker", 3 | "version": "0.2", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.10.2", 7 | "@material-ui/icons": "^4.9.1", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.3.2", 10 | "@testing-library/user-event": "^7.1.2", 11 | "firebase": "^7.15.3", 12 | "plotly.js": "^1.58.4", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-plotly.js": "^2.5.1", 16 | "react-router": "^5.2.0", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "3.4.1" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "start-api": "cd api && python3 api.py", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "proxy": "https://pulsetracker-api-v2.herokuapp.com/" 43 | 44 | } 45 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k105la/pulsetracker/648dfee4d6c0185395f52a055a15900f8e30a421/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 24 | 33 | PulseBox 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .capture { 17 | display: none; 18 | } 19 | 20 | .App-header { 21 | background-color: #282c34; 22 | min-height: 100vh; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | justify-content: center; 27 | font-size: calc(10px + 2vmin); 28 | color: white; 29 | } 30 | 31 | .App-link { 32 | color: #61dafb; 33 | } 34 | 35 | @keyframes App-logo-spin { 36 | from { 37 | transform: rotate(0deg); 38 | } 39 | to { 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import firebase from 'firebase'; 3 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 4 | import { AuthProvider } from './components/Auth/Auth'; 5 | import Login from './components/Login/Login'; 6 | import Home from './components/Home/Home'; 7 | import PrivateRoute from './components/PrivateRoute'; 8 | import './App.css'; 9 | 10 | class App extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | user: {}, 15 | }; 16 | } 17 | componentDidMount() { 18 | firebase.auth().onAuthStateChanged((user) => { 19 | if (user) { 20 | this.setState({ user: user }); 21 | document.body.style.background = 'white'; 22 | } else { 23 | this.setState({ user: null }); 24 | document.body.style.background = '#FF75B5'; 25 | } 26 | }); 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 | 33 |
34 | 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /ui/src/components/AlertButton/AlertButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | 9 | export default function AlertButton() { 10 | const [open, setOpen] = React.useState(false); 11 | 12 | const handleClickOpen = () => { 13 | setOpen(true); 14 | }; 15 | 16 | const handleClose = () => { 17 | setOpen(false); 18 | }; 19 | 20 | return ( 21 |
22 | 25 | 31 | 32 | {'How to check your pulse?'} 33 | 34 | 35 | 36 | Place your finger over your camera
for 15-30 seconds. Then 37 | upload your video and wait for results. 38 |
39 |
40 | 41 | 44 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /ui/src/components/Auth/Auth.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k105la/pulsetracker/648dfee4d6c0185395f52a055a15900f8e30a421/ui/src/components/Auth/Auth.css -------------------------------------------------------------------------------- /ui/src/components/Auth/Auth.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { app } from './config/fire'; 3 | 4 | export const AuthContext = React.createContext(); 5 | 6 | export const AuthProvider = ({ children }) => { 7 | const [currentUser, setCurrentUser] = useState(null); 8 | const [pending, setPending] = useState(true); 9 | 10 | useEffect(() => { 11 | app.auth().onAuthStateChanged((user) => { 12 | setCurrentUser(user); 13 | setPending(false); 14 | }); 15 | }, []); 16 | 17 | if (pending) { 18 | return <>Loading...; 19 | } 20 | 21 | return ( 22 | 27 | {children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /ui/src/components/Auth/config/fire.js: -------------------------------------------------------------------------------- 1 | //import firebase from "firebase"; 2 | import firebase from 'firebase/app'; 3 | import 'firebase/auth'; 4 | import 'firebase/storage'; 5 | 6 | export const app = firebase.initializeApp({ 7 | apiKey: 'AIzaSyBoCLNFNU2-_J6NQtbLD7GGy30zRvkzBmk', 8 | authDomain: 'pulse-box.firebaseapp.com', 9 | databaseURL: 'https://pulse-box.firebaseio.com', 10 | projectId: 'pulse-box', 11 | storageBucket: 'pulse-box.appspot.com', 12 | messagingSenderId: '118237267477', 13 | appId: '1:118237267477:web:ccee5e3ab7928a8ce4301c', 14 | measurementId: 'G-0Q1RSLYZVN', 15 | }); 16 | -------------------------------------------------------------------------------- /ui/src/components/Home/Home.css: -------------------------------------------------------------------------------- 1 | .upload-input { 2 | display: none; 3 | } 4 | .top-text { 5 | font-weight: bold; 6 | } 7 | .pulse-data-spinner { 8 | margin: 2.5em auto 2.5em auto; 9 | } 10 | 11 | .pulse { 12 | width: 100%; 13 | margin: 1em auto; 14 | } 15 | .progress-circle { 16 | position: absolute; 17 | left: 50%; 18 | top: 50%; 19 | transform: translate(-50%, -50%); 20 | } 21 | 22 | .camara-button { 23 | margin: 2em auto 1em auto; 24 | } 25 | 26 | .uid { 27 | margin: 1em auto 1em auto; 28 | } 29 | .stick-to-bottom { 30 | width: 100%; 31 | position: fixed; 32 | bottom: 0; 33 | left: 0; 34 | right: 0; 35 | } 36 | #user-uid { 37 | font-size: 12px; 38 | font-weight: bold; 39 | margin-left: 0.5em; 40 | } 41 | 42 | .welcome-text { 43 | font-weight: bold; 44 | } 45 | 46 | @media screen and (min-width: 768px) { 47 | .pulse { 48 | width: 45% !important; 49 | margin: 1.5em auto 1em auto; 50 | } 51 | .camara-button { 52 | margin: 2em auto 2em auto; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { app } from '../Auth/config/fire'; 3 | import firebase from 'firebase'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import PhotoCamera from '@material-ui/icons/PhotoCamera'; 6 | import CloudUploadIcon from '@material-ui/icons/CloudUpload'; 7 | import CircularProgress from '@material-ui/core/CircularProgress'; 8 | import FileCopyIcon from '@material-ui/icons/FileCopy'; 9 | import ExitToAppIcon from '@material-ui/icons/ExitToApp'; 10 | import BottomNavigation from '@material-ui/core/BottomNavigation'; 11 | import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; 12 | // Addded chart for displaying data 13 | import Plot from 'react-plotly.js'; 14 | import { Logo } from '../../utils/imgUrl'; 15 | import AlertButton from '../AlertButton/AlertButton.js'; 16 | import './Home.css'; 17 | 18 | let hr_arr = []; 19 | let count_arr = []; 20 | 21 | const styles = { 22 | largeIcon: { 23 | width: 50, 24 | height: 50, 25 | }, 26 | }; 27 | 28 | class Home extends Component { 29 | constructor(props) { 30 | super(props); 31 | this.state = { 32 | user: {}, 33 | hr: 0, 34 | prog: 0, 35 | count: 0, 36 | }; 37 | this.retrievePulse = this.retrievePulse.bind(this); 38 | this.uploadVideoToFirebase = this.uploadVideoToFirebase.bind(this); 39 | } 40 | 41 | componentDidMount() { 42 | firebase.auth().onAuthStateChanged((user) => { 43 | if (user) { 44 | document.getElementById('user-uid').textContent = user.uid; 45 | this.setState({ user: user }); 46 | } else { 47 | this.setState({ user: null }); 48 | } 49 | }); 50 | } 51 | 52 | uploadVideoToFirebase(event) { 53 | const storage = firebase.storage().ref(); 54 | let file = event.target.files[0]; 55 | let blob = file.slice(0, file.size, 'video/quicktime'); 56 | let newFile = new File([blob], 'hr_test.MOV', { type: 'video/quicktime' }); 57 | 58 | console.log(newFile); 59 | firebase.auth().onAuthStateChanged((user) => { 60 | if (user) { 61 | let pulseBoxStorage = storage.child('data/' + user.uid); 62 | pulseBoxStorage.listAll().then((res) => { 63 | res.prefixes.forEach((folderRef) => {}); 64 | 65 | res.items.forEach((itemRef) => { 66 | let pulseBoxCurrentStorage = storage.child(itemRef.location.path_); 67 | pulseBoxCurrentStorage.delete().then(() => { 68 | console.log('Cleaning ' + user.uid + ' storage box.'); 69 | }); 70 | }); 71 | }); 72 | 73 | let pulseBoxData = storage.child( 74 | 'data/' + user.uid + '/' + newFile.name 75 | ); 76 | pulseBoxData.put(newFile).on('state_changed', (snapshot) => { 77 | let progress = 78 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 79 | this.setState({ prog: progress }); 80 | if (progress === 100) { 81 | this.setState({ prog: 0 }); 82 | } 83 | }); 84 | } 85 | }); 86 | } 87 | 88 | retrievePulse() { 89 | firebase.auth().onAuthStateChanged((user) => { 90 | this.setState({ loading: true }, () => { 91 | if (user) { 92 | fetch(`https://pulsetracker-api.herokuapp.com/${user.uid}`) 93 | .then((res) => res.json()) 94 | .then((result) => { 95 | const heartrate = result.pulse; 96 | this.setState({ hr: heartrate }); 97 | this.setState({ loading: false }); 98 | hr_arr.push(this.state.hr); 99 | console.log(hr_arr); 100 | 101 | this.setState({ count: this.state.count + 1 }); 102 | count_arr.push(this.state.count); 103 | console.log(count_arr); 104 | }); 105 | } else { 106 | this.setState({ hr: 0 }); 107 | } 108 | }); 109 | }); 110 | } 111 | 112 | copyUID() { 113 | let copyText = document.getElementById('user-uid'); 114 | let textArea = document.createElement('textarea'); 115 | textArea.value = copyText.textContent; 116 | console.log(textArea.value); 117 | document.body.appendChild(textArea); 118 | textArea.select(); 119 | document.execCommand('copy'); 120 | textArea.remove(); 121 | } 122 | 123 | render() { 124 | const loading = this.state.loading; 125 | const progessValue = this.state.prog; 126 | return ( 127 |
128 | PulseTracker logo 135 | {progessValue ? ( 136 |
137 | {' '} 138 | {' '} 139 |
140 | ) : ( 141 |
142 |

143 | {' '} 144 | Welcome, {this.state.user.displayName}{' '} 145 |

146 | 147 |
148 | UID 149 | 150 |
151 | 152 | 153 |
154 | {loading ? ( 155 |
156 | {' '} 157 |
158 | ) : ( 159 | 195 | )} 196 |
197 | (this.fileUpload = ref)} 204 | /> 205 | 215 |
216 |
217 | )} 218 | 219 | } 224 | /> 225 | } 230 | /> 231 | app.auth().signOut()} 235 | icon={} 236 | /> 237 | 238 |
239 | ); 240 | } 241 | } 242 | 243 | export default Home; 244 | -------------------------------------------------------------------------------- /ui/src/components/Login/Login.css: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | justify-content: center; 4 | } 5 | .pulse-logo { 6 | width: 100%; 7 | margin: 2.5em auto 1em auto; 8 | } 9 | .pulse-intro-text { 10 | color: white; 11 | font-weight: 400; 12 | font-size: 1.4rem; 13 | line-height: 1.6; 14 | margin-bottom: 0.5em; 15 | } 16 | .sign-in-button { 17 | color: white !important; 18 | border: 1px solid white !important; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/components/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react'; 2 | import { withRouter, Redirect } from 'react-router'; 3 | import { app } from '../Auth/config/fire'; 4 | import { AuthContext } from '../Auth/Auth.js'; 5 | import firebase from 'firebase'; 6 | import Button from '@material-ui/core/Button'; 7 | import { Logo } from '../../utils/imgUrl'; 8 | import './Login.css'; 9 | 10 | const auth = app.auth(); 11 | let provider = new firebase.auth.GoogleAuthProvider(); 12 | 13 | const Login = ({ history }) => { 14 | const handleLogin = useCallback( 15 | async (event) => { 16 | event.preventDefault(); 17 | 18 | try { 19 | await auth.signInWithPopup(provider); 20 | history.push('/'); 21 | } catch (error) { 22 | alert(error); 23 | } 24 | }, 25 | [history] 26 | ); 27 | 28 | const { currentUser } = useContext(AuthContext); 29 | 30 | if (currentUser) { 31 | return ; 32 | } 33 | 34 | return ( 35 |
36 | pulsetracker 37 |
38 | An open source tool built for monitoring heart rate. 39 |
40 |
41 | 49 |
50 | ); 51 | }; 52 | 53 | export default withRouter(Login); 54 | -------------------------------------------------------------------------------- /ui/src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { AuthContext } from './Auth/Auth'; 4 | 5 | const PrivateRoute = ({ component: RouteComponent, ...rest }) => { 6 | const { currentUser } = useContext(AuthContext); 7 | return ( 8 | 11 | !!currentUser ? ( 12 | 13 | ) : ( 14 | 15 | ) 16 | } 17 | /> 18 | ); 19 | }; 20 | 21 | export default PrivateRoute; 22 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #ff75b5; 3 | margin: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /ui/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then((registration) => { 135 | registration.unregister(); 136 | }) 137 | .catch((error) => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /ui/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /ui/src/tests/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../App'; 4 | 5 | test('renders App component', () => { 6 | render(); 7 | }); 8 | -------------------------------------------------------------------------------- /ui/src/tests/Home.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent } from '@testing-library/react'; 3 | import Home from '../components/Home/Home'; 4 | 5 | test('renders Home component', () => { 6 | const { getByText, getByTestId } = render(); 7 | expect(getByText("Let's check your pulse!")).toBeInTheDocument(); 8 | expect( 9 | getByText('Place your finger over your camera for 15-30 seconds') 10 | ).toBeInTheDocument(); 11 | fireEvent.click(getByTestId('camera-button')); 12 | fireEvent.click(getByTestId('upload-button')); 13 | fireEvent.click(getByTestId('sign-out-button')); 14 | }); 15 | -------------------------------------------------------------------------------- /ui/src/utils/imgUrl.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'https://i.imgur.com/'; 2 | 3 | export const Logo = `${baseUrl}ZnWnu8a.png`; 4 | 5 | const imgUrl = { Logo }; 6 | 7 | export default imgUrl; 8 | -------------------------------------------------------------------------------- /ui/storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | --------------------------------------------------------------------------------