Skip to content

Using CIECAM to bring out-of-gamut RGB into gamut #91

@faern

Description

@faern

Back in comment #32 (comment) you wrote about three ways to bring a WideRgb value back into the valid RGB range (bring it into the RGB gamut):

  • The simplest would be to clamp the out-of-range R, G, or B value, either on <0.0 or >1.0 values.
  • The second approach would be to transform all R, G, and B values by using an offset and scaling, effectively desaturating the colors and scaling the luminance.
  • A third one would be to convert an XYZ or RGB value to a CIELAB or CIECAM value, and scale the chroma value down, keeping the hue value the same, until the RGB value is within gamut. Hue lines are not straight lines in the CIE xy color diagrams...

The first two options are covered by WideRgb::clamp and WideRgb::compress respectively. I'm now looking into the third option as a way to improve the rendering of my chromaticity diagram. I would love to add a method for this third option onto WideRgb so it becomes part of the library.

I'm toying with this at the moment to try to understand more about CIECAM. I'm rendering the "hue lines" to try to understand them. But I get weird results. I'm probably just doing something fundamentally wrong. I'm opening this issue to try to understand what, in order to be able to implement and contribute something like WideRgb::constrain_keeping_hue() -> Rgb.

Here is my code for taking an XYZ value and lowering the chroma until no RGB value is negative. And drawing a line along the chromaticity coordinates visited along the way. I then run this for a set of XYZ values along the observer's spectral locus.

The abort condition or the step size is not important right now, but rather the fact that it starts out far outside the chromaticity diagram, and then when passing the boundary it does so in a very different spot than the original XYZ value. This means that if I convert an XYZ value to CIECAM16 and then back to XYZ again, I never get the same result back, no matter the chroma used (even the original chroma). Why not?

fn compute_ciecam_hue_line(xyz: XYZ, observer: Observer) -> Vec<Vertex> {
    let xyzn = observer.xyz_d65();
    let viewconditions = ViewConditions::default();

    let xyz_to_vertex = |xyz: XYZ| {
        let chromaticity = xyz.chromaticity();
        Vertex {
            position: [chromaticity.x() as f32, chromaticity.y() as f32],
            color: [0.5, 0.5, 0.5],
        }
    };

    let mut vertices = vec![];
    // Start with the original XYZ value
    vertices.push(xyz_to_vertex(xyz));

    // The lightness and hue stay fixed. We only lower the chroma.
    let [lightness, mut chroma, hue] =
        CieCam16::from_xyz(xyz, xyzn, viewconditions).unwrap().jch();
    loop {
        let cam = CieCam16::new([lightness, chroma, hue], xyzn, viewconditions);
        let xyz = cam.xyz(None, None).unwrap();
        vertices.push(xyz_to_vertex(xyz));

        // When there are no negative RGB values, we stop.
        if xyz.rgb(None).values().iter().all(|&v| v >= 0.0) {
            break;
        }
        chroma *= 0.95;
    }
    vertices
}

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions