Skip to content

Commit

Permalink
Implemented tests; Debugging
Browse files Browse the repository at this point in the history
  • Loading branch information
prouast committed May 20, 2024
1 parent ddcf48f commit ec07bac
Show file tree
Hide file tree
Showing 15 changed files with 733 additions and 48 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ jobs:
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Test with pytest
env:
VITALLENS_DEV_API_KEY: ${{ secrets.VITALLENS_DEV_API_KEY }}
run: |
pytest
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ This can also be configured using the following parameters:

| Parameter | Description | Default |
|---------------------|---------------------------------------------------------------------------------------|---------|
| video | The video to analyze. Either a path to a video file or `np.ndarray`. [More info here.](https://github.com/Rouast-Labs/vitallens-python/blob/2a674a22e910c432a7c9c135d5f7cc9f2cdb566c/vitallens/client.py#L99) | |
| faces | Face detections. Ignored unless `detect_faces=False`. [More info here.](https://github.com/Rouast-Labs/vitallens-python/blob/2a674a22e910c432a7c9c135d5f7cc9f2cdb566c/vitallens/client.py#L102) | `None` |
| video | The video to analyze. Either a path to a video file or `np.ndarray`. [More info here.](https://github.com/Rouast-Labs/vitallens-python/blob/ddcf48f29a2765fd98a7029c0f10075a33e44247/vitallens/client.py#L98) | |
| faces | Face detections. Ignored unless `detect_faces=False`. [More info here.](https://github.com/Rouast-Labs/vitallens-python/blob/ddcf48f29a2765fd98a7029c0f10075a33e44247/vitallens/client.py#L101) | `None` |
| fps | Sampling frequency of the input video. Required if video is `np.ndarray`. | `None` |
| override_fps_target | Target frequency for inference (optional - use methods's default otherwise). | `None` |

Expand Down Expand Up @@ -120,3 +120,21 @@ my_video_fps = 30
vl = VitalLens(method=Method.POS)
result = vl(my_video_arr, fps=my_video_fps)
```

## Linting and tests

Before running tests, please make sure that you have an environment variable `VITALLENS_DEV_API_KEY` set to a valid API Key.
To lint and run tests:

```
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
pytest
```

## Build

To build:

```
python -m build
```
Binary file added examples/test.mp4
Binary file not shown.
7 changes: 4 additions & 3 deletions examples/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from prpy.ffmpeg.readwrite import read_video_from_path

input_str = True
# TODO: Select a better test video
video_path = "examples/test.mp4"

if input_str:
Expand All @@ -20,7 +21,7 @@
print("Video shape: {}".format(video.shape))

vl = vitallens.VitalLens(
method=vitallens.Method.POS,
method=vitallens.Method.VITALLENS,
api_key="INSERT_API_KEY_HERE")
start = timeit.default_timer()
result = vl(video=video, fps=fps, override_fps_target=30.0)
Expand All @@ -29,6 +30,6 @@
print("Inference time: {:.2f} ms".format((stop-start)*1000))

import matplotlib.pyplot as plt
if 'pulse' in result[0]: plt.plot(result[0]['pulse']['sig'])
if 'resp' in result[0]: plt.plot(result[0]['resp']['sig'])
if 'pulse' in result[0]: plt.plot(result[0]['pulse']['val'])
if 'resp' in result[0]: plt.plot(result[0]['resp']['val'])
plt.show()
67 changes: 67 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (c) 2024 Philipp Rouast
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import os
from prpy.ffmpeg.probe import probe_video
from prpy.ffmpeg.readwrite import read_video_from_path
import pytest

import sys
sys.path.append('../vitallens-python')

from vitallens.ssd import FaceDetector

# TODO: Select a better test video
TEST_VIDEO_PATH = "examples/test.mp4"

@pytest.fixture(scope='session')
def test_video_path():
return TEST_VIDEO_PATH

@pytest.fixture(scope='session')
def test_video_ndarray():
video, _ = read_video_from_path(path=TEST_VIDEO_PATH, pix_fmt='rgb24')
return video

@pytest.fixture(scope='session')
def test_video_fps():
fps, *_ = probe_video(TEST_VIDEO_PATH)
return fps

@pytest.fixture(scope='session')
def test_video_faces(request):
det = FaceDetector(
max_faces=1, fs=1.0, iou_threshold=0.45, score_threshold=0.9)
test_video_ndarray = request.getfixturevalue('test_video_ndarray')
test_video_fps = request.getfixturevalue('test_video_fps')
boxes, _ = det(test_video_ndarray, fps=test_video_fps)
boxes = (boxes * [test_video_ndarray.shape[2], test_video_ndarray.shape[1], test_video_ndarray.shape[2], test_video_ndarray.shape[1]]).astype(int)
return boxes[:,0]

@pytest.fixture(scope='session')
def test_dev_api_key():
api_key = os.getenv('VITALLENS_DEV_API_KEY')
if not api_key:
raise pytest.UsageError(
"VITALLENS_DEV_API_KEY environment variable is not set. Please set this variable "
"to a valid VitalLens API Key to run the tests. You can do this by exporting the "
"variable in your shell or adding it to your conda environment configuration."
)
return api_key
59 changes: 59 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) 2024 Philipp Rouast
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import numpy as np
import pytest

import sys
sys.path.append('../vitallens-python')

from vitallens.client import VitalLens, Method

@pytest.mark.parametrize("method", [Method.G, Method.CHROM, Method.POS])
@pytest.mark.parametrize("detect_faces", [True, False])
@pytest.mark.parametrize("file", [True, False])
def test_VitalLens(request, method, detect_faces, file):
vl = VitalLens(method=method, detect_faces=detect_faces)
if file:
test_video_path = request.getfixturevalue('test_video_path')
result = vl(test_video_path, faces = None if detect_faces else [425, 116, 671, 433])
else:
test_video_ndarray = request.getfixturevalue('test_video_ndarray')
test_video_fps = request.getfixturevalue('test_video_fps')
result = vl(test_video_ndarray, fps=test_video_fps, faces = None if detect_faces else [425, 116, 671, 433])
assert len(result) == 1
assert result[0]['face'].shape == (139, 4)
assert result[0]['pulse']['val'].shape == (139,)
np.testing.assert_allclose(result[0]['hr']['val'], 71.5, atol=2)

def test_VitalLens_API(request):
api_key = request.getfixturevalue('test_dev_api_key')
vl = VitalLens(method=Method.VITALLENS, api_key=api_key, detect_faces=True)
test_video_ndarray = request.getfixturevalue('test_video_ndarray')
test_video_fps = request.getfixturevalue('test_video_fps')
result = vl(test_video_ndarray, fps=test_video_fps, faces=None)
assert len(result) == 1
assert result[0]['face'].shape == (139, 4)
assert result[0]['pulse']['val'].shape == (139,)
assert result[0]['pulse']['conf'].shape == (139,)
assert result[0]['resp']['val'].shape == (139,)
assert result[0]['resp']['conf'].shape == (139,)
np.testing.assert_allclose(result[0]['hr']['val'], 73, atol=0.5)
np.testing.assert_allclose(result[0]['rr']['val'], 15, atol=0.5)
84 changes: 84 additions & 0 deletions tests/test_simple_rppg_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright (c) 2024 Philipp Rouast
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import numpy as np
import pytest

import sys
sys.path.append('../vitallens-python')

from vitallens.methods.chrom import CHROMRPPGMethod
from vitallens.methods.g import GRPPGMethod
from vitallens.methods.pos import POSRPPGMethod
from vitallens.utils import load_config

@pytest.mark.parametrize("override_fps_target", [None, 15])
def test_CHROMRPPGMethod(request, override_fps_target):
config = load_config("chrom.yaml")
method = CHROMRPPGMethod(config)
res = method.algorithm(np.random.rand(100, 3), fps=30.)
assert res.shape == (100,)
res = method.pulse_filter(res, fps=30.)
assert res.shape == (100,)
test_video_ndarray = request.getfixturevalue('test_video_ndarray')
test_video_fps = request.getfixturevalue('test_video_fps')
test_video_faces = request.getfixturevalue('test_video_faces')
sig, conf, live = method(
frames=test_video_ndarray, faces=test_video_faces,
fps=test_video_fps, override_fps_target=override_fps_target)
assert sig.shape == (1, test_video_ndarray.shape[0])
np.testing.assert_equal(conf, np.ones((1, test_video_ndarray.shape[0]), np.float32))
np.testing.assert_equal(live, np.ones((test_video_ndarray.shape[0],), np.float32))

@pytest.mark.parametrize("override_fps_target", [None, 15])
def test_GRPPGMethod(request, override_fps_target):
config = load_config("g.yaml")
method = GRPPGMethod(config)
res = method.algorithm(np.random.rand(100, 3), fps=30.)
assert res.shape == (100,)
res = method.pulse_filter(res, fps=30.)
assert res.shape == (100,)
test_video_ndarray = request.getfixturevalue('test_video_ndarray')
test_video_fps = request.getfixturevalue('test_video_fps')
test_video_faces = request.getfixturevalue('test_video_faces')
sig, conf, live = method(
frames=test_video_ndarray, faces=test_video_faces,
fps=test_video_fps, override_fps_target=override_fps_target)
assert sig.shape == (1, test_video_ndarray.shape[0])
np.testing.assert_equal(conf, np.ones((1, test_video_ndarray.shape[0]), np.float32))
np.testing.assert_equal(live, np.ones((test_video_ndarray.shape[0],), np.float32))

@pytest.mark.parametrize("override_fps_target", [None, 15])
def test_POSRPPGMethod(request, override_fps_target):
config = load_config("pos.yaml")
method = POSRPPGMethod(config)
res = method.algorithm(np.random.rand(100, 3), fps=30.)
assert res.shape == (100,)
res = method.pulse_filter(res, fps=30.)
assert res.shape == (100,)
test_video_ndarray = request.getfixturevalue('test_video_ndarray')
test_video_fps = request.getfixturevalue('test_video_fps')
test_video_faces = request.getfixturevalue('test_video_faces')
sig, conf, live = method(
frames=test_video_ndarray, faces=test_video_faces,
fps=test_video_fps, override_fps_target=override_fps_target)
assert sig.shape == (1, test_video_ndarray.shape[0])
np.testing.assert_equal(conf, np.ones((1, test_video_ndarray.shape[0]), np.float32))
np.testing.assert_equal(live, np.ones((test_video_ndarray.shape[0],), np.float32))
Loading

0 comments on commit ec07bac

Please sign in to comment.