# How video-use Performs HDR to SDR Tone Mapping with zscale and Hable

> Explore how video-use performs HDR to SDR tone mapping using zscale and Hable tonemap for automatic color space conversion and perceptual roll-off in your video projects.

- Repository: [Browser Use/video-use](https://github.com/browser-use/video-use)
- Tags: performance
- Published: 2026-07-03

---

**The video-use repository automatically converts HDR10 and HLG sources to Rec. 709 SDR using a FFmpeg filter chain that combines zscale for color space conversion and the Hable tonemap operator for perceptual roll-off.**

The `browser-use/video-use` project handles modern video workflows where source footage may arrive in HDR formats (PQ/Rec. 2020 or HLG) but final delivery requires standard SDR. Understanding how this HDR to SDR tone mapping is implemented helps you customize the rendering pipeline or debug color issues in your exported videos.

## HDR Detection in video-use

Before applying any conversion filters, the system must identify whether the source material contains HDR metadata.

### How HDR Sources Are Identified

The `is_hdr_source()` function in [`helpers/render.py`](https://github.com/browser-use/video-use/blob/main/helpers/render.py) probes the first video stream using `ffprobe` to check the `color_transfer` characteristic. This prevents unnecessary processing on already-SDR footage.

```python
HDR_TRANSFERS = {"smpte2084", "arib-std-b67"}          # PQ (HDR10) and HLG

def is_hdr_source(video: Path) -> bool:
    out = subprocess.run(
        ["ffprobe", "-v", "error", "-select_streams", "v:0",
         "-show_entries", "stream=color_transfer",
         "-of", "default=noprint_wrappers=1:nokey=1", str(video)],
        capture_output=True, text=True, check=True,
    )
    return out.stdout.strip() in HDR_TRANSFERS

```

This detection runs automatically during segment extraction (lines 108–130 in [`helpers/render.py`](https://github.com/browser-use/video-use/blob/main/helpers/render.py)), ensuring that only HDR inputs trigger the tone-mapping pipeline.

## The zscale and Hable Tonemap Filter Chain

Once HDR is detected, video-use prepends a specific `TONEMAP_CHAIN` to the FFmpeg video filter graph. This chain handles the complete transformation from HDR to SDR in five stages.

### Breaking Down the TONEMAP_CHAIN

The constant defined in [`helpers/render.py`](https://github.com/browser-use/video-use/blob/main/helpers/render.py) (lines 111–115) constructs a linear workflow that preserves color fidelity during conversion:

```python
TONEMAP_CHAIN = (
    "zscale=t=linear:npl=100,"          # linearise 10‑bit Rec.2020

    "format=gbrpf32le,"                # work in high‑precision float

    "zscale=p=bt709,"                  # chroma‑convert to BT.709 primaries

    "tonemap=tonemap=hable:desat=0,"   # Hable curve → SDR

    "zscale=t=bt709:m=bt709:r=tv,"    # final 8‑bit BT.709 transfer

    "format=yuv420p"                   # FFmpeg‑compatible pixel format

)

```

The process works as follows:

- **Linearisation** (`zscale=t=linear:npl=100`): Converts the HDR gamma curve (PQ or HLG) to linear light with a nominal peak luminance of 100 nits, preparing the data for mathematical tone mapping.
- **High-precision format** (`format=gbrpf32le`): Switches to 32-bit floating point to prevent banding during the aggressive tonal compression.
- **Gamut conversion** (`zscale=p=bt709`): Transforms the wide Rec. 2020 color primaries to the narrower Rec. 709 standard.
- **Hable tonemap** (`tonemap=tonemap=hable:desat=0`): Applies the Hable filmic curve to compress highlights smoothly into SDR range without desaturating colors.
- **Output normalization** (`zscale=t=bt709:m=bt709:r=tv,format=yuv420p`): Applies the BT.709 transfer curve and converts to 8-bit `yuv420p` for final encoding.

## Integration into the Rendering Pipeline

The tone-mapping chain is injected during segment extraction in [`helpers/render.py`](https://github.com/browser-use/video-use/blob/main/helpers/render.py) (lines 79–86). The code builds a filter list dynamically, inserting HDR conversion before any scaling or color grading operations.

```python
vf_parts: list[str] = []
if is_hdr_source(source):
    vf_parts.append(TONEMAP_CHAIN)   # ← HDR → SDR conversion

vf_parts.append(scale)                # 1080p/720p scaling

if grade_filter:
    vf_parts.append(grade_filter)    # optional colour‑grade

vf = ",".join(vf_parts)

```

This ordering ensures that **scaling and grading operate on the SDR signal**, preventing the washed-out or clipped appearance that occurs when filters process HDR luminance values directly.

## Code Examples

### CLI Usage

Render a project with automatic HDR handling:

```bash

# Assuming you have an EDL JSON at ./edl.json

python helpers/render.py edl.json -o final.mp4

```

The script will:
- Probe each source video for `color_transfer` metadata
- Detect HDR via `is_hdr_source()`
- Prepend the `TONEMAP_CHAIN` to the FFmpeg `-vf` argument
- Produce an SDR `final.mp4` compatible with all platforms

### Programmatic Usage

Import the helpers directly for custom workflows:

```python
from pathlib import Path
from helpers.render import extract_segment, is_hdr_source, TONEMAP_CHAIN

src = Path("raw/iphone_clip.mov")
if is_hdr_source(src):
    print("HDR detected – applying tone‑map")
else:
    print("SDR source – no tone‑map needed")

# Extract a 5‑second clip with automatic HDR handling

extract_segment(
    source=src,
    seg_start=10.0,
    duration=5.0,
    grade_filter="",                # no extra grade

    out_path=Path("tmp/clip.mp4"),
    preview=False,
    draft=False,
)

```

## Summary

- **HDR Detection**: The `is_hdr_source()` function in [`helpers/render.py`](https://github.com/browser-use/video-use/blob/main/helpers/render.py) identifies PQ (smpte2084) and HLG (arib-std-b67) content via `ffprobe`.
- **zscale Pipeline**: The linearisation and gamut conversion use `zscale` filters to move from Rec. 2020 to Rec. 709 before tone mapping.
- **Hable Tonemap**: The `tonemap=hable` operator provides perceptual highlight compression that mimics professional display processing.
- **Automatic Injection**: The `TONEMAP_CHAIN` is prepended to the filter graph only when HDR is detected, ensuring SDR sources remain untouched.

## Frequently Asked Questions

### What is the Hable tonemap operator and why does video-use use it?

The Hable tonemap is a filmic curve designed to compress high-dynamic-range luminance into standard dynamic range while preserving perceptual contrast. According to the video-use source code, the `tonemap=hable:desat=0` setting applies this curve without desaturating colors, avoiding the flat, oversaturated look that occurs when HDR metadata is simply stripped or ignored.

### How does video-use detect HDR content automatically?

The repository uses the `is_hdr_source()` function to run `ffprobe` against the first video stream and read the `color_transfer` tag. If the value matches `smpte2084` (HDR10/PQ) or `arib-std-b67` (HLG), the function returns `True` and triggers the HDR to SDR conversion pipeline.

### Can I modify the TONEMAP_CHAIN parameters for custom conversion?

Yes. The `TONEMAP_CHAIN` is defined as a module-level constant in [`helpers/render.py`](https://github.com/browser-use/video-use/blob/main/helpers/render.py) (lines 111–115). You can edit the string to adjust parameters like `npl` (nominal peak luminance) or swap `hable` for other operators like `mobius` or `reinhard`, though this requires direct modification of the source file as the current implementation does not expose these parameters via the CLI.

### Does the tone mapping process affect extraction performance?

The HDR to SDR conversion adds computational overhead due to the 32-bit floating-point processing (`gbrpf32le`) and multiple `zscale` operations. However, the filter chain is only applied when `is_hdr_source()` returns `True`, so SDR sources render at full speed without the penalty of these additional filter stages.