From c2c14481c48fd9a2b0ebd831ae4d8d26141af98e Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Sat, 31 Oct 2026 13:27:10 +1100 Subject: [PATCH 1/3] Update vitallens-core to 0.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c34ba90..aed82c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "python-dotenv>=1.0", "pyyaml>=6.0.1", "requests>=2.32.0", - "vitallens-core==0.3.4", + "vitallens-core==0.4.0", ] dynamic = ["version"] From bc8b449f10a88a0e95abcd1b90d48666b96bd4af Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Sat, 31 Oct 2026 13:27:10 +1100 Subject: [PATCH 2/3] Use time.monotonic() for stream timestamps in the examples, clarify push() --- README.md | 2 +- examples/live.py | 4 ++-- vitallens/stream.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2ad1bb9..811938f 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ vl = VitalLens(method="vitallens", api_key="YOUR_API_KEY") with vl.stream() as session: # In your capture loop (e.g., OpenCV) - session.push(frame, timestamp=time.time()) + session.push(frame, timestamp=time.monotonic()) results = session.get_result(block=False) ``` diff --git a/examples/live.py b/examples/live.py index 2fdb145..60a2caf 100644 --- a/examples/live.py +++ b/examples/live.py @@ -55,7 +55,7 @@ def main(): resp_conf = deque(maxlen=150) vital_conf_thresh = 0.8 hrv_conf_thresh = 0.7 - start_time = time.time() + start_time = time.monotonic() ppg_meta = vc.get_vital_info("ppg_waveform") hr_meta = vc.get_vital_info("heart_rate") @@ -70,7 +70,7 @@ def main(): if not ret: break - timestamp = time.time() - start_time + timestamp = time.monotonic() - start_time rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) session.push(rgb_frame, timestamp) diff --git a/vitallens/stream.py b/vitallens/stream.py index 6f480ab..1ad1d0a 100644 --- a/vitallens/stream.py +++ b/vitallens/stream.py @@ -258,7 +258,8 @@ def push(self, frame: np.ndarray, timestamp: float, face: np.ndarray = None): Args: frame: The input video frame of shape (h, w, 3) in RGB format. - timestamp: The absolute timestamp of the frame in seconds. + timestamp: The capture time of the frame in seconds, increasing (e.g. time.monotonic()). + If the camera API does not provide a capture time, take it right after reading the frame. face: Optional pre-detected face box [x0, y0, x1, y1]. """ if not self.running: return From 4310763a68d31eb5dee24b461adb80532116b978 Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Sat, 31 Oct 2026 13:27:10 +1100 Subject: [PATCH 3/3] Docs: results page covers only what is specific to Python, fix rolling vitals --- docs/results.md | 110 +++++++++++--------------------------------- vitallens/client.py | 2 +- 2 files changed, 27 insertions(+), 85 deletions(-) diff --git a/docs/results.md b/docs/results.md index 3634b82..c4eb0fd 100644 --- a/docs/results.md +++ b/docs/results.md @@ -1,104 +1,46 @@ # Understanding the Results -When you process a video using the `vitallens` client, the results are returned as a standard Python **list**. +The structure of a result, the vital keys, when each value is available and how to use the confidence scores are the same for all VitalLens clients and described in [Understanding Results](https://docs.rouast.com/guides/results/). This page covers what is specific to the Python client. + +## One Result per Face + +Processing a video returns a Python **list** with one result per detected face. If two people are visible, the list has two entries. ```python results = vl(video) # Type: List[dict] +hr = results[0]['vitals']['heart_rate']['value'] ``` -## Result Structure - -The list contains one dictionary **per detected face**. If two people are visible in the video, the list will have two entries. - -### JSON Schema - -Each face entry follows this structure. Note that strict types (like `np.ndarray`) are used for raw data, while scalar values are Python floats. +Waveform data, confidences and timestamps are returned as lists; scalar values are Python floats. -``` -[ - { - "face": { - "coordinates": [[247, 52, 444, 332], ...], - "confidence": [0.6115, 0.9207, 0.9183, ...], - "note": "Face detection coordinates..." - }, - "vitals": { - "heart_rate": { - "value": 60.5, - "unit": "bpm", - "confidence": 0.9242, - "note": "Global estimate of Heart Rate..." - }, - "respiratory_rate": { - "value": 12.0, - "unit": "bpm", - "confidence": 0.9976, - "note": "Global estimate of Respiratory Rate..." - }, - "hrv_sdnn": { - "value": 53.79, - "unit": "ms", - "confidence": 0.8712, - "note": "Global estimate of Heart Rate Variability (SDNN)..." - } - }, - "waveforms": { - "ppg_waveform": { - "data": [0.1, -0.2, ...], - "unit": "unitless", - "confidence": [0.9, 0.9, ...], - "note": "..." - } - }, - "message": "The provided values are estimates..." - } -] -``` +Besides `face`, `vitals`, `waveforms` and `message`, each result contains: -## Output & File Export +| Key | Contents | +| --- | --- | +| `rolling_vitals` | Vitals over time, see below. | +| `fps` | The frame rate the video was processed at. | +| `n` | The number of frames. | +| `time` | The timestamp of each frame in seconds. | -The client returns results as a Python list and, by default, also saves them to a timestamped JSON file (e.g., `vitallens_20260203_120642.json`) in your current directory. +## Rolling Vitals -To disable file export: +When processing a video file, the client also estimates each vital over time, as one value per frame from a trailing window (the window is listed under [Availability](https://docs.rouast.com/guides/results/#availability)). They are under `rolling_vitals`, with the same keys as `vitals`: ```python -vl = VitalLens(export_to_json=False) +rolling_hr = results[0]['rolling_vitals']['heart_rate']['data'] # one value per frame ``` -## Data Availability - -You might notice that not all keys (like `hrv_sdnn`) are present in every result. The availability of specific vital signs depends on **two factors**: - -1. **Duration:** Physiological signals require a minimum window of time to measure accurately. -2. **Method:** Simple methods (like `pos`) only measure HR, while `vitallens` measures respiration and HRV. +Rolling vitals are on by default. To skip them: -| Vital Sign | Key | Type | Required Duration | Supported Methods | -| --- | --- | --- | --- | --- | -| **PPG Waveform** | `ppg_waveform` | Continuous waveform | N/A (Always) | All | -| **Heart Rate** | `heart_rate` | Global value | ≥ 5s | All | -| **Rolling HR** | `rolling_heart_rate` | Continuous values | ≥ 10s | All | -| **Respiratory Waveform** | `respiratory_waveform` | Continuous waveform | N/A (Always) | `vitallens` | -| **Respiratory Rate** | `respiratory_rate` | Global value | ≥ 10s | `vitallens` | -| **Rolling RR** | `rolling_respiratory_rate` | Continuous values | ≥ 30s | `vitallens` | -| **HRV (SDNN)** | `hrv_sdnn` | Global value | ≥ 20s | `vitallens` | -| **HRV (RMSSD)** | `hrv_rmssd` | Global value | ≥ 20s | `vitallens` | -| **HRV (LF/HF)** | `hrv_lfhf` | Global | ≥ 55s | `vitallens` | - -> **Note:** Rolling metrics (continuous estimates over time) are only computed if you initialize the client with `estimate_rolling_vitals=True`. - -> **Note:** HRV vitals are only available on `vitallens` version 2.0 or greater. - -## Getting Accurate Results - -Estimates are only as good as the video they come from. For the best accuracy and confidence: - -- **Light the face well, with steady light.** Avoid flickering lights, changing screen colors and strong backlight. -- **Keep the subject within 0.5 m of the camera** and facing it, with the whole face in the frame (add the upper chest for respiratory rate). -- **Keep the subject and the camera still.** -- **Avoid heavy video compression.** It removes the subtle color changes the estimation relies on. +```python +vl = VitalLens(estimate_rolling_vitals=False) +``` -See the [Accuracy & Guidelines](https://docs.rouast.com/guides/accuracy/) page for the details. +## File Export -Every value comes with a `confidence` between 0 and 1. As a guide, the VitalLens apps show heart rate and respiratory rate from a confidence of 0.8, and show HRV only when it and the heart rate both reach 0.8. Consider applying a similar rule: an HRV value next to a heart rate with low confidence is not reliable. +The client also saves the results to a timestamped JSON file (e.g., `vitallens_20260203_120642.json`) in your current directory. To disable this: +```python +vl = VitalLens(export_to_json=False) +``` diff --git a/vitallens/client.py b/vitallens/client.py index 64ca072..94ca557 100644 --- a/vitallens/client.py +++ b/vitallens/client.py @@ -69,7 +69,7 @@ def __init__( api_key: Usage key for the VitalLens API (required for vitallens methods, unless using proxy) proxies: Dictionary mapping protocol to the URL of the proxy. detect_faces: `True` if faces need to be detected, otherwise `False`. - estimate_rolling_vitals: Set `True` to compute rolling vitals (e.g., `rolling_heart_rate`). + estimate_rolling_vitals: Set `True` to also estimate each vital over time (under `rolling_vitals`). fdet_max_faces: The maximum number of faces to detect (if necessary). fdet_fs: Frequency [Hz] at which faces should be scanned. Detections are linearly interpolated for remaining frames.