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

Understanding when to use Srgba vs LinSrgba #160

Closed
ojensen5115 opened this issue Jan 12, 2020 · 4 comments · Fixed by #188
Closed

Understanding when to use Srgba vs LinSrgba #160

ojensen5115 opened this issue Jan 12, 2020 · 4 comments · Fixed by #188

Comments

@ojensen5115
Copy link

ojensen5115 commented Jan 12, 2020

Hello!

I am having trouble understanding a particular situation is it pertains to Srgba vs LinSrgba. I've read through #133 and believe that I understand the general distinction between linear and perceptual RGB, but the following micro-example leaves me confused. I apologize if this is the wrong place to surface this.

This came about because I noticed that images I generated using code very similar to the hue shift example did not match reference images created using image editing software such as GIMP and Photoshop.


I have a source SRGB image, and I experiment by shifting its hue by 180 degrees in the HCL and LCH colorspaces, much like this example. However, comparing the results to reference images I find that when dealing in HCL I need to treat the source image as though it were Srgba (expected), but when dealing in LCH I apparently need to treat the source image as though it were LinSrgb (unexpected). I am struggling with understanding the why of the difference.

In the source below, I'm using Hsla and Srgba to match the example. I swap these out for Lcha and/or LinSrgba as needed to generate the images below:

extern crate image;
extern crate palette;

use crate::palette::Saturate;
use palette::{Hue, Hsla, Lcha, Pixel, LinSrgba, Srgba};

fn main() {
    let mut image = image::open("colors.png").expect("Could not open image").to_rgba();

    for pixel in image.pixels_mut() {
        let color = Srgba::from_raw(&pixel.0).into_format();
        let adjusted = Hsla::from(color).shift_hue(180.0);
        pixel.0 = Srgba::from_linear(adjusted.into()).into_format().into_raw()
    }

    image.save("out.png");
}

These are the reference images: the original, and hue shifted using GIMP (Original / HSL / LCH):
Original colors-180-hsl-reference colors-180-lch-reference

These images are HSL shifted. The left is using LinSrgba, the middle is the GIMP reference image, and the right one using Srgba. In this case, the left image appears to match the reference:
colors-180-hsl-linear colors-180-hsl-reference colors-180-hsl-srgb

These images are LCH shifted. The left is using LinSrgba, the middle is the GIMP reference image, and the right one using Srgba. In this case, the right image appears to match the reference:
colors-180-lch-linear colors-180-lch-reference colors-180-lch-srgb

This surprises me, because it seems to me that the source image is either linear or it is not, and I don't understand why the intermediary color space I work through would matter.

@Ogeon
Copy link
Owner

Ogeon commented Jan 12, 2020

I apologize if this is the wrong place to surface this.

No, this is probably the best one at the moment. 🙂

This is going to be a bit of a non-answer, because this can be legitimately confusing and sometimes hard to say what's "right" (I'll get back to that). It's common for software to get this wrong (including Photoshop, apparently, but I think it's a setting there), or just convert differently, so I can't say how GIMP does it in this case.

With that said, you seem to have the example code right. You want to read and write in sRGB, because that's what's commonly expected, but do changes on linear RGB. Think of sRGB as an encoding, similar to compressed information. You don't want to manipulate that without decoding it first.

RGB, HSL and HSV are cousins. They are based on the same color space (RGB), but described in different ways. Mixing that with not always taking (non)linearity into account makes it hard to compare different software. Are they based on the linear or nonlinear RGB? What's the correct way? Is there a correct way? I can't 100% swear on having gotten it right myself. I chose to convert via linear RGB, and I can only rely on having passing tests to check if it behaves as the test data source expects.

So, Palette is hard wired to always convert sRGB -> linear RGB -> HSV/HSL -> linear RGB -> sRGB, as long as the usual conversion methods are used. Even when using Hsv::from(mySrgbColor). Edit: you can see the code for that here.

When comparing, you want to find software that explicitly uses a "linear workflow", to match what Palette is doing. I think Blender is one of them, off the top of my head. The next best is to at least have something where you know exactly how they do it. It does seem like GIMP should be able to use linear RGB, but I don't know to what extent and if it has to be enabled through some "gamma" setting for the image.

@ojensen5115
Copy link
Author

ojensen5115 commented Jan 12, 2020

Interesting, thank you.

For a little more context: I'm rewriting an image processing stack which previously used ImageMagick, and I'm doing so using Image and Palette (side note: thank you for this library, it's beautiful to use). The stack performs hue and saturation manipulations, and I noticed that images generated using HSL yielded results that didn't match the old output. I performed similar hue and saturation shifts in GIMP, Photoshop, and SAI to compare, and found that they all resulted in images that matched my ImageMagick stack and not the new one, so assumed I was misunderstanding something.

I suppose the solution then is simple enough for my purposes: if I want HSL hue or saturation shifts to match popular image editors such as GIMP, Photoshop, Sai, or ImageMagick, just intentionally presume that the file is linearly encoded (even when in fact it isn't). I was hesitant to try this approach in case I fundamentally miunderstood what I was doing, but since that doesn't seem to be the case, it should be fine. Something like:

pixel_bg.0 = match colorspace {
    Colorspace::Lch => {
        let color = Srgba::from_raw(&pixel_bg.0).into_format();
        let shifted = Lcha::from(color).shift_hue(hue_deg).desaturate(desat);
        Srgba::from_linear(shifted.into()).into_format().into_raw()
    },
    Colorspace::Hsl => {
        let color = LinSrgba::from_raw(&pixel_bg.0).into_format();
        let shifted = Hsla::from(color).shift_hue(hue_deg).desaturate(desat);
        LinSrgba::from_linear(shifted.into()).into_format().into_raw()
    }
}

It's not particularly satisfying, but the world is messy ;)

@Ogeon
Copy link
Owner

Ogeon commented Jan 12, 2020

That's very interesting. I realize I misread your original message. Lch, based on Lab, would more likely be converted to via linear RGB, through XYZ, while HSL is as ambiguous as mentioned. Maybe it's simply based on sRGB in all those applications. I will leave this open as something to investigate. Maybe I need to make it more complex...

But it's good to see that you found a workaround for your case. And it sort of proves my theory that they are not making them linear for HSL.

@Ogeon
Copy link
Owner

Ogeon commented Apr 18, 2020

I think it would be possible to make linear and non-linear versions of HSL, HSV and HWB, using the exact same system as for RGB. The missing piece was the new conversion traits, but now it shouldn't be as complicated to extend the spaces. They are unfortunately not as clear cut as RGB itself, so I believe people should be allowed to use sRGB encoded versions of them if that's how they are generally expected to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants