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

xyz -> lab conversion #8

Closed
sidred opened this issue Jan 12, 2016 · 8 comments
Closed

xyz -> lab conversion #8

sidred opened this issue Jan 12, 2016 · 8 comments
Labels

Comments

@sidred
Copy link
Contributor

sidred commented Jan 12, 2016

Hey I just started working on a similar library repo.

I noticed your cie -> lab conversion does follow wikipedia or http://www.brucelindbloom.com/index.html?ColorCalculator.html which I used as reference. What is your source for the conversions formulas?

link to the conversion I implemented for reference.

@Ogeon Ogeon added the question label Jan 12, 2016
@Ogeon
Copy link
Owner

Ogeon commented Jan 12, 2016

It's a combination of those two sources and a bunch of other. I haven't really kept record of them, but the formulas are quite well known and easy to find.

You are welcome to join forces, by the way, if you would be interested.

@sidred
Copy link
Contributor Author

sidred commented Jan 12, 2016

I am happy to combine our efforts. I am sharing my thoughts on a few things and would like your input on these as well

  1. f64 support
    ImageMagic uses doubles everywhere, so it seems like a good idea to support f64.
    Since generic float math is not easy in rust, I have created separate structs for the color spaces (Labf32, Labf64, etc..) using macros.

Using generics I had to write equations like (x+16.0)/116.0 as
( x + NumCast::from(16.0).unwrap()) / NumCast::from(116.0).unwrap(). I am not sure if there is a better way.

  1. WhitePoint aware conversions
    Most of the formula's for conversion need the reference white point. Use a default illuminant and observer (maybe make it globally settable like in the log crate?) and derive From and Into traits for the color spaces using these defaults. Provide additional methods for custom white points like the FromColor trait in my library.

I have done some of this work already at least for the cie color spaces. I just have to add the formulas for the some of remaining conversions. I see the api being something like this.

let c = Lab::from(&xyz) // default white points using the rust from trait
let c = Lab::from_white_point(&xyz, illuminant, observer) // custom FromColor Trait 
let c = Lab::from_illuminant&xyz, illuminant) // FromColor Trait 

You can check out my library for how it looks.

  1. alpha
    I have not seen any library have an alpha parameter for Lab, Xyz, Lch etc. color spaces. Any particular reason to have this?

  2. Rgb types
    I was thinking rgb_u8, rgba_u8, rgb_u16, rgba_u16 (for 10 bit color support?) and the f32 and f64 variants. I haven't worked on these. Maybe an overkill?

  3. Rgb color spaces
    Start with some of the popular spaces like sRgb, adobeRgb and appleRgb for now and add the rest later. I had a list of 16 color rgb variants :).

  4. Hue
    Hue is saved -180,180. Photoshop, Gimp, OpenCV have it as 0,360. ImageMagik uses radians. I think this is going to cause a lot of confusion.

@Ogeon
Copy link
Owner

Ogeon commented Jan 12, 2016

  1. It may be worth doing it the "hard way" in the long run, and it will also allow custom types besides f32 and f64. Having multiple copies of each type may end up being even more tedious to maintain.
  2. I don't know how the white points are most commonly used, but the global method may end up being too restrictive. I'm thinking about the case when someone is working with multiple white points simultaneously. An other problem with it and your trait method is that the colors doesn't remember which white point they are adapted to. I wonder if it's feasible to make something at type level...
  3. The alpha component is carried around to prevent it from being lost in conversion. I have considered decoupling it from the color spaces in some way. Maybe as an Alpha<Color> kind of wrapper, but I don't really have any concrete plans there.
  4. That's outside the scope for this library, except for conversion. rgb(a)_u8 exists as a constructor for Rgb, but that's about it at the moment. There's Separation of the RGB types #7, which proposes to separate the pixel representations from the linear RGB type and those may have room for more kinds of representations.
  5. Conversion to and from sRGB is there. I don't know about Apple RGB, but Adobe RGB is quite different, since it covers more colors than sRGB. Adobe RGB is a popular photo editing space, so it would be nice to support it. The question is what the best way of doing it is. This may also be related to Separation of the RGB types #7.

@sidred
Copy link
Contributor Author

sidred commented Jan 12, 2016

  1. I have used macro's to generate the separate types. So any change in one should be reflected in all. I am open to either method though.

I have a working solution for xyY and XYZ using generics, in the generic branch.

  1. I was assuming that the color space is usually at the image level than the pixel level. So the user or image library keeps track of that. I have only seen python color library keep track of the white point. ImageMagic and OpenCV do not store these in the pixel.

Here is my understanding of the white point:

  • XYZ and xyY are white point independent .
  • All conversions usually go through XYZ ex. Lab -> XYZ -> Luv or SRgb -> XYZ -> AdobeRgb.
  • The cie spaces like Lab, Lch and Luv are white point dependent, but do not have a standard white point defined. The conversion formula's for these depend on a white point's xyz values.
  • The rgb spaces usually have a standard white point defined like D65 for sRGB and D50 for adobe rgb. The conversion matices to xyz are given only for their standart illuminants. For those RGB working spaces that are not natively D50, the Bradford-adapted, D50 matrices are given. What this means is that there is no way to convert XYZ to sRGB with illuminant E. So there is a limited set of conversions to support. List of conversion matrices

Based on this, the global defaults only apply to the cie colorspaces, the rgb spaces have their own standard white point defined.

Having a global default is necessary, your formula for xyz to lab assumes a default value for the white point even though you did not specify it. D65 illumiant with cie1931 2 degree observer is the most commonly recommended one. (sRGB uses this, adobe rgb uses D50 illuminant).

So when the user is aware of the multiple colorspaces, we can use it in the FromColor trait to convert the color_space

let xyz = XYZ::from(&srgb); // Uses default D65 for srgb
let argb = ARGB::from(&xyz); // Uses default D50 for adobe rgb

let xyz = XYZ::from_color(&srgb, D50); //Uses the bradford correction matrix from link above
let xyz = XYZ::from_color(&srgb, E); // not possible

let xyz = XYZ::from(&lab); // uses global default
let xyz = XYZ::from_color(&lab, E); // uses illuminant E

May be start off with separate struct for each variant like sRGB, LinearSRGB, AdobeRGB etc. I am not sure if an enum vesion for all the rgb spaces will be ergonomic.

  1. Looking at existing libraries, Image Magic or OpenCV do not store alpha. Maybe have a variant without alpha to reduce memory usage when not needed? An HD image will use around 8MB for alpha, when not needed.

  2. I was going to have a separate struct for each type. An enum based solution might not be ergonomic.

  3. Hue
    Hue is saved -180,180. Photoshop, Gimp, OpenCV have it as 0,360. ImageMagik uses radians.
    I think this is going to cause a lot of confusion. Are you open to changing to the 0,360?

@Ogeon
Copy link
Owner

Ogeon commented Jan 12, 2016

  1. I would still prefer a solution with generics if the type is the only difference. It gives the users more freedom and makes it easier for other libraries to extend them. We can set either f32 or f64 as the default type to keep it as ergonomic as possible. The messy calculations will not affect the users unless they make their code generic as well, but that's expected.

  2. The illuminants could perhaps be types that implements some Illuminant trait. That would allow compile time rejection of some illuminants. It would become more verbose, on the other hand. I think it may have to be prototyped to really know.

  3. I'm, once again, reluctant when it comes to having two subtle variants of the same type. That's why I included alpha in the first place. The alpha component is usually not used in calculations, except blending, so bolting it on with a wrapper type doesn't sound too bad, after all.

  4. That is the thought with Separation of the RGB types #7. An enum isn't really appropriate in this case, since the spaces should be allowed to have different properties.

  5. Yes, that's a good idea. Note that the hue types behaves in a cyclic way, so -180 is treated as equal to 180. Only the external representation needs to change. E.g. when converted to f32 or when printed through Display.

@sidred
Copy link
Contributor Author

sidred commented Jan 12, 2016

I just read the what it isn't section regarding not a compact format. Part of my motivation to write a color library is that it can be a base for other libraries. If all it takes is adding a variant without alpha for this library to be useful to other libraries, I am inclined to do it.

Most of the hard work in this library is figuring the structure of the code and all the conversions. Supporting a non alpha version should be fairly straightforward.

All the libraries I have looked at definitely support variants without alpha. I've looked at OpenCV, ImageMagic, Haskell, Elm and Python. Even css supports rgb.

Just wanted to let you know my thoughts regarding this.

Let me go ahead and implement the generic version for a few color spaces and lets take it from there.

@Ogeon
Copy link
Owner

Ogeon commented Jan 12, 2016

The thing is that the processing representation and the storage representation doesn't have to be the same. Storing RGBf32 is expensive, but working in RGB8 won't be as easy. That's why I want to separate the two concepts and have these linear intermediate types for operations and conversions, and then encode them back into a more compact format.

The current implementation makes use of the RgbPixel trait to convert to and from pixel representations. This system can easily be extended to include more formats and bit depths, including ones where the alpha is dropped. This is the work flow I had in mind:

pixel format (e.x. RGB8) -> Rgb type -> do stuff -> Rgb type -> pixel format
|--      storage     --|    |--     linear processing    --|    |--storage--|

The processing phase is where the Rgb, Lch, Hsv, etc. types belongs. They are normalized to [0.0, 1.0] to allow things like easy multiplication and should be as lossless as possible within that constraint. A potential non-linear sRGB or AdobeRGB type could belong to an additional encoding phase between storage and processing. These could even allow discrete types, like u8, as components.

I'm not trying to say that you are wrong. I'm just looking at it from different angles. I definitely think you are on to something good, if we can make it work well, but our mental models of it seem to be a bit different. I'm trying to keep the above mentioned work flow in focus, where the processing format is the same, regardless of how the image is stored.

It's getting very late over here and I think I have to sleep on this. I'm really looking forwards to where this will lead us. 😄

@Ogeon
Copy link
Owner

Ogeon commented Jan 19, 2016

I think I have managed to extract the things to do from here into separate small issues, so I'll go ahead and close this.

@Ogeon Ogeon closed this as completed Jan 19, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants