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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
<!-- mkdocs-end -->
Expand Down
110 changes: 26 additions & 84 deletions docs/results.md
Original file line number Diff line number Diff line change
@@ -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)
```
4 changes: 2 additions & 2 deletions examples/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
2 changes: 1 addition & 1 deletion vitallens/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion vitallens/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading