Skip to content

gamut-icc: the BT.2100 PQ built-in profile is peak-referred, so diffuse white renders near black #557

Description

@justin13888

Filed from the review of #542, which added IccProfile::builtin(BuiltinProfile::Bt2020Pq). Recorded there and in crates/gamut-icc/STATUS.md as a known limit; not changed silently, because the alternative is a colour-appearance decision with its own consequences.

The behaviour

gamut-icc encodes the BT.2100 PQ tone curve as a sampled curveType (ICC.1:2022 §10.6, 1024 uInt16 points) of gamut_color::transfer::pq_eotf normalized to the transfer's own peak: pq_eotf returns absolute luminance in cd/m², and the samples are divided by pq_eotf(1.0) = 10 000 cd/m² to land in the [0, 1] range the encoding allows.

That is what "peak-referred" means here, and the consequence is arithmetic:

  • The BT.2408 reference (diffuse) white for HDR production is ~203 cd/m².
  • 203 / 10 000 = 0.0203 media-relative.

So a CMM applying this profile maps a diffuse-white signal to ~2 % of media white, and content graded against diffuse white renders near black. Nothing is wrong with the encoding — for a profile that declares its media white to be the 10 000 cd/m² peak, that is the correct media-relative value — but it is unlikely to be what a caller who embeds the profile next to SDR-referred content expects.

The alternative, and why it is not obviously right

Normalizing to diffuse white instead (dividing by pq_eotf(V) at the ~203 cd/m² signal, or applying a scale in the chad/colorant matrix) makes diffuse white land on media white, at the cost of clipping every specular highlight above it — the top ~5.7 stops of the PQ range — unless a tone curve is applied. That trades one wrong answer for another, and which one is wanted depends on what the profile is for:

  • Embedding beside HDR content for an HDR-aware pipeline: peak-referred is right.
  • Embedding so an SDR CMM produces something viewable: neither is right; that wants tone mapping (gamut-tonemap already owns the curves — Reinhard/ACES/Hable/Drago).

What this issue should decide

  1. Whether BuiltinProfile::Bt2020Pq keeps the peak-referred normalization as its only behaviour, gains a sibling variant, or is parameterized.
  2. What a caller is told to do when the goal is an SDR rendering — likely: do not embed a PQ profile, tone-map through gamut-tonemap and embed an SDR profile.
  3. Whether the limit belongs in validate() output in any form (probably not: the profile is conforming).

Any change here is breaking for the bytes builtin(Bt2020Pq) emits, so it should be decided before the constructor has downstream consumers.

Acceptance

Whatever is decided, it is decided against the Little-CMS oracle: a transform of a known diffuse-white PQ signal through the chosen profile, compared with what lcms2 produces through its own equivalent, with the rendering intent stated. A decision to keep the current behaviour closes this issue, provided the reasoning and the measurement are recorded.

Refs #424, #542.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions