Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

When when working in cylindrical color spaces like hsl, hsluv, lch, etc (essentially anything with a "hue" component that goes from 0 → 360) the default "euclidian" distance $\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2 + (z_1 - z_2)^2}$ doesn't make much sense. For example, the euclidian distance between hsl(0, 50%, 50%) and hsl(360, 50%, 50%) is calculated as $\sqrt{(0 - 360)^2 + (0.5 - 0.5)^2 + (0.5 - 0.5)^2} = 360$ , even though hsl(0, 50%, 50%) and hsl(360, 50%, 50%) are the same color!

Instead, I think we should first convert the polar coordinates to cartesian ones before calculating the euclidean distance:

$$\displaylines{x=r*cos(\Theta) \\ y=r*sin(\Theta) \\ z=z}$$

where r and z are the normal/orthogonal coordinates — in hsl, for example, they would be s and l — and θ is the radial coordinate, in radians — in hsl, this would be $h*\frac{\pi}{180}$.

This would mean that the euclidian distance between hsl(0, 50%, 50%) and hsl(360, 50%, 50%) would be correctly calculated as 0.

I think the key concern here is that for each cylindrical space, we'd need to indicate which coordinate is the radial one and which are the normal ones. But with that information, euclidian distances would be much more logical.

You must be logged in to vote

Replies: 3 comments · 20 replies

Comment options

I'm not sure I understand the concern. The default ∆E is 76, and it uses Lab, not a cylindrical space, to perform the distance calculation. https://github.com/color-js/color.js/blob/main/src/deltaE/deltaE76.js#L6.

> new Color('red').deltaE(new Color('blue'))
184.01904862099684
> new Color('red').to('hsluv').deltaE(new Color('blue').to('hsluv'))
184.0190486209965
You must be logged in to vote
10 replies
@facelessuser
Comment options

Do you have a particular use case where you feel it is more helpful to use euclidean distance in an unsupported cylindrical space instead of just using a more appropriate cartesian coordinate system? Or are you mainly suggesting that if people do so, we should try to provide more sane defaults?

Normally, if I want distance in HSL, I would use sRGB which represents its gamut, I wouldn't use HSL directly. I could see value in maybe throwing a warning or error if a cylindrical space is used. I guess the alternative would be to convert to a cartesian system to try and return more sane results.

In order to do so, we'd need a way to determine in every color space where hue and some colorfulness/saturation/chroma component lies. Not all LCh spaces place hue at the end (HCT as an example), etc. Is it worth all the effort to provide such behaviour, or is it better to just guide users to not try and do distance in cylindrical spaces?

@ilikescience
Comment options

There's a few cases where I like working in a cylindrical space — specifically, hsluv, though I've also been using okhsl a bit lately too. Those spaces hit a sweet spot between computational simplicity and approximate perceptual uniformity; I can go into more detail if you'd like.

In hsluv, euclidian distance agrees pretty closely to deltaE2000, so you can use it as a heuristic instead of converting to another color space (ie converting to lab to do deltaE2000).

Today, if I'm working in a cylindrical space and want to avoid doing conversions, the distance() method will give me the incorrect euclidian distance.

As for implementation: each color space declares the type for each of its coords; could we check that to determine if we're in a cylindrical space, and if so, which coordinate is angular?

@facelessuser
Comment options

@ilikescience, I understand that working in hsluv, and okhsl can be great, I am not trying to discourage that, but why not use the appropriate space for distancing, Luv and Oklab respectively?

> let c1 = new Color('hsluv', [0, 0, 0])
undefined
> let c2 = new Color('hsluv', [360, 0, 0])
undefined
> c1.distance(c2, 'luv')
0
> let c3 = new Color('okhsl', [0, 0, 0])
undefined
> let c4 = new Color('okhsl', [360, 0, 0])
undefined
> c3.distance(c4, 'oklab')
0

Hopefully, this makes things a bit more clear at what I'm driving at. Is it more helpful to you to get distancing within the actual HSL cylindrical space? Or do you actually want the Luv or Oklab distancing which actually gives you the results closer to ∆E2000? Distancing in the distorted HSL space will not give you what you want I imagine.

@ilikescience
Comment options

I don't think it's correct to say that oklab is more appropriate for making distance measurements than okhsl; they're useful in their own rights. In my case, okhsl has some really useful properties. For example, I can calculate a "maximally different color," because there's an easily derived "furthest point" in okhsl space, which is guaranteed to be in the srgb gamut.

Before okhsl and hsluv were added to colorjs.io, I was supplying my own method for doing these kinds of distance calculations in those spaces. That's ok, and I'm sure folks have opinions on whether or not colorjs.io should support this use case. For now, maybe it's sufficient to focus on the fact that if the distance() function is meant to be a euclidian distance it's currently not behaving correctly for cylindrical spaces. If we don't want to support euclidian distances for cylindrical spaces, maybe the library should throw an error?

@facelessuser
Comment options

I don't think it's correct to say that oklab is more appropriate for making distance measurements than okhsl; they're useful in their own rights. In my case, okhsl has some really useful properties. For example, I can calculate a "maximally different color," because there's an easily derived "furthest point" in okhsl space, which is guaranteed to be in the srgb gamut.

So we aren't talking about perceptual distance, that is what I was getting at, and it is fine if that is not what you are looking for. You specifically want distancing in okhsl and hsluv. That clears that up, and that is okay.

How would you propose HWB would get handled? I think spaces like this may still be an exception, but that is probably okay.

As it seems you truly do want distancing in these cylindrical spaces, I will defer to others for an actual decision on the matter.

I think the problem could be solved if a decision is made to do so, but there may or may not need to be a little framework put in place to generically identify saturation/chroma/colorfulness and hue in spaces. Hue is easy enough as I believe they identify as angles. Maybe you can safely assume the center component is always saturation/chroma/colorfulness. I'm not aware of a specific space that doesn't follow this convention, but I can confidently say that currently supported spaces follow this convention. So maybe no additional infrastructure is needed?

Comment options

So we aren't talking about perceptual distance, that is what I was getting at, and it is fine if that is not what you are looking for. You specifically want distancing in okhsl and hsluv. That clears that up, and that is okay.

Yup, I'm just talking about the geometric distance in the color space. To be pedantic, this is a kind of perceptual distance, in the same way that geometric distance in jzazbz or lab is meant to be perceptually meaningful.

How would you propose HWB would get handled?

Since it's cylindrical, it should be handled the same way; distance should be calculated using cylindrical coordinates.

but there may or may not need to be a little framework put in place to generically identify saturation/chroma/colorfulness and hue in spaces

It looks like there's already a method on ColorSpaces that identifies spaces as being polar or not! eg,

> Color.space.srgb.isPolar
false
> Color.space.hsluv.isPolar
true

Behind the scenes it's just checking if a given coord has is of type angle:

this.coords[id].type === "angle"

So, the distance method could just sort coords into angular and cartesian, then do the distance calculation appropriately.

The "right" way to do this would be to also allow for spherical color spaces, ie spaces where two out of the three coordinates are angular — while I don't think any color spaces fit this description today, it's not outside the realm of possibility. It'd be extremely right to also allow for arbitrary numbers of coordinates (2, 3, 4, ...n), and I'm sure there's a generalized formula for distance in n-dimensions (where some of them can be angular), but I'm not a mathematician so I think we'll have to punt on that one :)

You must be logged in to vote
9 replies
@facelessuser
Comment options

it doesn't matter which cartesian coordinate is which ... like you mentioned with w/b in hwb, you can swap the two and get the same distance.

Let's not confuse things, it does matter whether you take W or B when calculating cartesian coordinates for HWB.

Example.

Screenshot 2024-03-25 at 9 48 57 AM
@ilikescience
Comment options

Yes, sorry, you're absolutely right, these things are pretty unintuitive!

Ok, given that the choice of radial dimension is meaningful, you would need a way of correctly assigning the coordinate. That does seem more involved, as it requires adding extra information to each color space.

@facelessuser
Comment options

Yep, hence my slight concern about locating chroma. Assuming such things were in place, and it was decided that this project wanted to support polar Euclidean distance, I think it would be fine. HWB handling, which I imagine is the only "special" case that would need consideration, would have to be decided then: pick a convention, or throw an exception.

@ilikescience
Comment options

I was just doing some research into HWB and it looks like in the original proposal, W is akin to Saturation and B is akin to Value. So, W would be the radial dimension.

http://alvyray.com/Papers/CG/HWB_JGTv208.pdf

Screenshot 2024-03-25 at 12 17 21@2x

@facelessuser
Comment options

That was my memory, or at least what I settled on. That's how I plot them:

Example.

Screenshot 2024-03-25 at 10 35 38 AM
Comment options

@LeaVerou @svgeesus what do y'all think? Should the .distance() method consider whether a space is polar or not? Is it worth the additional effort to indicate how polar spaces are oriented?

You must be logged in to vote
1 reply
@LeaVerou
Comment options

Absolutely!! The question is who will implement it 😅 Any chance you may be up for it? 🙏🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
💡
Ideas
Labels
None yet
3 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.