Skip to content

Commit

Permalink
jxl-color: Try better PQ to HLG (#348)
Browse files Browse the repository at this point in the history
* jxl-oxide: Detect HDR transfer type

* jxl-color: Handle PQ to HLG

* Update CHANGELOG.md
  • Loading branch information
tirr-c authored Sep 17, 2024
1 parent dfc2073 commit 918829a
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 23 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- `jxl-color`: Use better PQ to HLG method (#348).

### Fixed
- `jxl-color`: Fix generated `mluc` tag in ICC profile (#347).

Expand Down
62 changes: 61 additions & 1 deletion crates/jxl-color/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ impl ColorEncodingWithProfile {
pub struct ColorTransformBuilder {
detect_peak: bool,
srgb_icc: bool,
from_pq: bool,
}

impl Default for ColorTransformBuilder {
Expand All @@ -127,6 +128,7 @@ impl ColorTransformBuilder {
Self {
detect_peak: false,
srgb_icc: false,
from_pq: false,
}
}

Expand All @@ -140,6 +142,11 @@ impl ColorTransformBuilder {
self
}

pub fn from_pq(&mut self, from_pq: bool) -> &mut Self {
self.from_pq = from_pq;
self
}

pub fn build(
self,
from: &ColorEncodingWithProfile,
Expand Down Expand Up @@ -193,6 +200,7 @@ impl ColorTransform {
let ColorTransformBuilder {
detect_peak,
srgb_icc,
from_pq,
} = builder;
let connecting_tf = if srgb_icc {
TransferFunction::Srgb
Expand Down Expand Up @@ -456,7 +464,7 @@ impl ColorTransform {
);
let luminances = [mat[3], mat[4], mat[5]];

let hdr_params = HdrParams {
let mut hdr_params = HdrParams {
luminances,
intensity_target,
min_nits,
Expand Down Expand Up @@ -485,6 +493,40 @@ impl ColorTransform {
}
}

// PQ to HLG: tonemap to peak 1000 nits before applying inverse OOTF.
if from_pq && target_encoding.tf == TransferFunction::Hlg {
if !(999.0..=1001.0).contains(&hdr_params.intensity_target) {
if current_encoding.colour_space == ColourSpace::Grey {
ops.push(ColorTransformOp::ToneMapLumaRec2408 {
hdr_params,
target_display_luminance: 1000.0,
detect_peak: false,
});
} else {
ops.push(ColorTransformOp::ToneMapRec2408 {
hdr_params,
target_display_luminance: 1000.0,
detect_peak: false,
});
}

hdr_params.intensity_target = 1000.0;
ops.push(ColorTransformOp::HlgInverseOotf(hdr_params));
}

if current_encoding.colour_space != ColourSpace::Grey
&& current_encoding.rendering_intent == RenderingIntent::Perceptual
{
ops.push(ColorTransformOp::GamutMap {
luminances,
saturation_factor: 0.1,
});
}

// Skip inverse OOTF in transfer funcion conversion.
hdr_params.intensity_target = 300.0;
}

if target_encoding.tf != TransferFunction::Linear {
ops.push(ColorTransformOp::TransferFunction {
tf: target_encoding.tf,
Expand Down Expand Up @@ -663,6 +705,7 @@ enum ColorTransformOp {
hdr_params: HdrParams,
inverse: bool,
},
HlgInverseOotf(HdrParams),
ToneMapRec2408 {
hdr_params: HdrParams,
target_display_luminance: f32,
Expand Down Expand Up @@ -734,6 +777,9 @@ impl std::fmt::Debug for ColorTransformOp {
.field("target_display_luminance", target_display_luminance)
.field("detect_peak", detect_peak)
.finish(),
Self::HlgInverseOotf(hdr_params) => {
f.debug_tuple("HlgInverseOotf").field(hdr_params).finish()
}
Self::GamutMap {
luminances,
saturation_factor,
Expand Down Expand Up @@ -773,6 +819,7 @@ impl ColorTransformOp {
..
} => Some(3),
ColorTransformOp::TransferFunction { .. } => None,
ColorTransformOp::HlgInverseOotf(_) => Some(3),
ColorTransformOp::ToneMapRec2408 { .. } => Some(3),
ColorTransformOp::ToneMapLumaRec2408 { .. } => Some(1),
ColorTransformOp::GamutMap { .. } => Some(3),
Expand All @@ -793,6 +840,7 @@ impl ColorTransformOp {
..
} => Some(3),
ColorTransformOp::TransferFunction { .. } => None,
ColorTransformOp::HlgInverseOotf(_) => Some(3),
ColorTransformOp::ToneMapRec2408 { .. } => Some(3),
ColorTransformOp::ToneMapLumaRec2408 { .. } => Some(1),
ColorTransformOp::GamutMap { .. } => Some(3),
Expand Down Expand Up @@ -880,6 +928,18 @@ impl ColorTransformOp {
);
num_input_channels
}
Self::HlgInverseOotf(HdrParams {
luminances,
intensity_target,
..
}) => {
// FIXME: allow grayscale images.
let [r, g, b, ..] = channels else {
unreachable!()
};
tf::hlg_inverse_oo([r, g, b], *luminances, *intensity_target);
3
}
Self::ToneMapRec2408 {
hdr_params,
target_display_luminance,
Expand Down
2 changes: 1 addition & 1 deletion crates/jxl-color/src/icc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod parse;
mod synthesize;

pub use decode::{decode_icc, read_icc};
pub use parse::is_hdr;
pub use parse::icc_tf;
pub(crate) use parse::parse_icc;
pub(crate) use parse::parse_icc_raw;
pub use synthesize::colour_encoding_to_icc;
Expand Down
12 changes: 3 additions & 9 deletions crates/jxl-color/src/icc/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,15 +555,9 @@ pub(crate) fn parse_icc(profile: &[u8]) -> Result<EnumColourEncoding> {
}
}

/// Checks whether a ICC profile has CICP tag with HDR transfer function.
pub fn is_hdr(profile: &[u8]) -> Result<bool> {
let profile = parse_icc_raw(profile)?;
for tag in profile.tags {
if &tag.tag == b"cicp" && matches!(tag.data.get(1), Some(16 | 18)) {
return Ok(true);
}
}
Ok(false)
#[inline]
pub fn icc_tf(profile: &[u8]) -> Option<TransferFunction> {
parse_icc(profile).ok().map(|profile| profile.tf)
}

#[cfg(test)]
Expand Down
5 changes: 5 additions & 0 deletions crates/jxl-color/src/tf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ pub fn hlg_inverse_oo(
[lr, lg, lb]: [f32; 3],
intensity_target: f32,
) {
// System gamma results to ~1 in this range.
if (295.0..=305.0).contains(&intensity_target) {
return;
}

let gamma = 1.2f32 * 1.111f32.powf((intensity_target / 1e3).log2());
// 1/g - 1
let exp = (1.0 - gamma) / gamma;
Expand Down
27 changes: 15 additions & 12 deletions crates/jxl-oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,18 +622,12 @@ impl JxlImage {
}
}

pub fn is_hdr(&self) -> bool {
use jxl_color::TransferFunction;

match &self.image_header.metadata.colour_encoding {
color::ColourEncoding::Enum(e) => {
matches!(e.tf, TransferFunction::Pq | TransferFunction::Hlg)
}
color::ColourEncoding::IccProfile(_) => {
let icc = self.ctx.embedded_icc().unwrap();
jxl_color::icc::is_hdr(icc).unwrap_or(false)
}
}
pub fn hdr_type(&self) -> Option<HdrType> {
self.ctx.suggested_hdr_tf().and_then(|tf| match tf {
jxl_color::TransferFunction::Pq => Some(HdrType::Pq),
jxl_color::TransferFunction::Hlg => Some(HdrType::Hlg),
_ => None,
})
}

/// Requests the decoder to render in specific color encoding, described by an ICC profile.
Expand Down Expand Up @@ -876,6 +870,15 @@ impl PixelFormat {
}
}

/// HDR transfer function type, returned by [`JxlImage::hdr_type`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HdrType {
/// Perceptual quantizer.
Pq,
/// Hybrid log-gamma.
Hlg,
}

/// The result of loading the keyframe.
#[derive(Debug)]
pub enum LoadResult {
Expand Down
16 changes: 16 additions & 0 deletions crates/jxl-render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ impl RenderContext {
self.cms = Box::new(cms);
}

pub fn suggested_hdr_tf(&self) -> Option<jxl_color::TransferFunction> {
let tf = match &self.image_header.metadata.colour_encoding {
jxl_color::ColourEncoding::Enum(e) => e.tf,
jxl_color::ColourEncoding::IccProfile(_) => {
let icc = self.embedded_icc().unwrap();
jxl_color::icc::icc_tf(icc)?
}
};

match tf {
jxl_color::TransferFunction::Pq | jxl_color::TransferFunction::Hlg => Some(tf),
_ => None,
}
}

#[inline]
pub fn request_color_encoding(&mut self, encoding: ColorEncodingWithProfile) {
self.requested_color_encoding = encoding;
Expand Down Expand Up @@ -795,6 +810,7 @@ impl RenderContext {

let mut transform = jxl_color::ColorTransform::builder();
transform.set_srgb_icc(!self.cms.supports_linear_tf());
transform.from_pq(self.suggested_hdr_tf() == Some(jxl_color::TransferFunction::Pq));
let transform = transform.build(
&frame_color_encoding,
&self.requested_color_encoding,
Expand Down

0 comments on commit 918829a

Please sign in to comment.