pub struct SoftCompress { /* private fields */ }Expand description
Soft chroma compression on linear-light RGB strips.
Wraps a GamutBoundaryLut with a knee threshold and exposes the
compression as an explicit per-strip API. Construction performs the
LUT build once; subsequent apply_strip calls
reuse it.
§Pipeline
For each pixel:
- Convert linear RGB → OKLab (via
crate::oklab::rgb_to_oklab). - Compute chroma
c = √(a² + b²)and hueh = atan2(b, a). - Look up max in-gamut chroma
c_maxat(L, h). - If
c > knee · c_max, compress:c' = knee·c_max + range · excess / (excess + range). - Convert OKLab → linear RGB.
Hue and lightness are preserved within float precision; only chroma is
modified. The rational compression curve is C¹-continuous at the knee
(slope 1.0 on the inside, asymptote at the gamut boundary).
§Examples
use zenpixels_convert::hdr::SoftCompress;
use zenpixels_convert::oklab;
use zenpixels::ColorPrimaries;
let m1_inv = oklab::lms_to_rgb_matrix(ColorPrimaries::Bt709).unwrap();
let compress = SoftCompress::new(&m1_inv, 0.96);
let mut pixels = vec![[1.2_f32, 0.05, 0.05]]; // out-of-gamut red
compress.apply_strip(&mut pixels);
for px in &pixels {
for &c in px {
assert!(c <= 1.0 + 1e-2, "expected in-gamut output");
}
}Implementations§
Source§impl SoftCompress
impl SoftCompress
Sourcepub const DEFAULT_KNEE: f32 = 0.96
pub const DEFAULT_KNEE: f32 = 0.96
Production default knee — the fraction of max chroma where the soft
rolloff begins. Empirically calibrated against the 76-sample
imazen-26 gain-mapped HDR corpus on 2026-06-23: 0.96 is the
largest knee value (i.e. the LEAST chroma compression / desaturation)
where the corpus-p90 fraction of pre-clamp out-of-gamut pixels stays
under 0.1 %. Surfaced as a pub const so test fixtures and external
callers can refer to the same anchor as
crate::HdrConfig::default’s gamut_knee field. Matches
HdrConfig::default().gamut_knee byte-for-byte.
Sourcepub fn new(m1_inv: &GamutMatrix, knee: f32) -> Self
pub fn new(m1_inv: &GamutMatrix, knee: f32) -> Self
Construct a SoftCompress for the given primaries (via m1_inv,
the LMS → RGB matrix from
crate::oklab::lms_to_rgb_matrix) and
knee threshold (0.0–1.0; production default 0.96,
corpus-validated 2026-06-23).
The matching forward matrix is derived by inverting m1_inv. If you
already have the forward matrix on hand (the rgb_to_lms_matrix
output), prefer SoftCompress::from_matrices (which takes both
and cannot panic).
§Panics
Panics if m1_inv is singular (non-invertible). Every matrix from
crate::oklab::lms_to_rgb_matrix — the intended input — is
invertible, so the pipeline path never triggers this; it only
fires for a hand-constructed degenerate matrix. Pass matrices from
oklab::lms_to_rgb_matrix / rgb_to_lms_matrix, or use
from_matrices to supply the inverse
directly.
Sourcepub fn from_matrices(m1: &GamutMatrix, m1_inv: &GamutMatrix, knee: f32) -> Self
pub fn from_matrices(m1: &GamutMatrix, m1_inv: &GamutMatrix, knee: f32) -> Self
Construct a SoftCompress from both forward and inverse matrices.
m1 is the linear-RGB → LMS matrix (from oklab::rgb_to_lms_matrix);
m1_inv is the LMS → linear-RGB matrix.
The two matrices must be a matched inverse pair for the same
primaries — this constructor does not verify that (it’s the cheap,
no-panic path). A mismatched pair yields a wrong (but non-panicking)
color transform. Prefer new when you only have the
inverse and want the forward derived consistently.
Sourcepub fn apply_strip(&self, rgb: &mut [[f32; 3]])
pub fn apply_strip(&self, rgb: &mut [[f32; 3]])
Apply soft gamut compression to a strip of linear RGB pixels in place.
Expects finite linear-RGB input; NaN/inf channels pass through
to NaN/inf output (this is an inner strip primitive — the
pipeline scrubs non-finite values before the tone-map chain, so
direct callers own that themselves).
Sourcepub fn lut(&self) -> &GamutBoundaryLut
pub fn lut(&self) -> &GamutBoundaryLut
Borrow the inner GamutBoundaryLut for direct planar use (used by
zenfilters::Pipeline, which keeps its own planar OKLab buffer).
Trait Implementations§
Source§impl Clone for SoftCompress
impl Clone for SoftCompress
Source§fn clone(&self) -> SoftCompress
fn clone(&self) -> SoftCompress
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more