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

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 probes the first video stream using ffprobe to check the color_transfer characteristic. This prevents unnecessary processing on already-SDR footage.

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), 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 (lines 111–115) constructs a linear workflow that preserves color fidelity during conversion:

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 (lines 79–86). The code builds a filter list dynamically, inserting HDR conversion before any scaling or color grading operations.

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:


# 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:

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 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →