Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jxl-color: Try better PQ to HLG #348

Merged
merged 3 commits into from
Sep 17, 2024
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
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