9
175
Fork
You've already forked wallust
21

WIP: Redesign of color gatherings (fixes #189) #195

Closed
explosion-mental wants to merge 60 commits from master into v3
pull from: master
merge into: explosion-mental:v3
explosion-mental:flake-lock-update
explosion-mental:v3
explosion-mental:pages
explosion-mental:ui

The goal of this PR is to remove obstacles for the sampling and better gathering of the colors, making a much more pleasant and "readable" (see #188) colorscheme palettes. This change, requires a lot of internals changes, API breaks, and so on. This is why, it marks a tremendous change code wise, but also in the results perceived as a user.

The goal of this PR is to remove obstacles for the sampling and better gathering of the colors, making a much more pleasant and "readable" (see #188) colorscheme palettes. This change, requires a lot of internals changes, API breaks, and so on. This is why, it marks a tremendous change code wise, but also in the results perceived as a user.
modify the current one to fit the new structure
impl the rest of salience
Some checks failed
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
a502278949
Author
Owner
Copy link

ping @usaradark , given your interested, would like your feedback of the current design. I didn't quite had much time, but I come with this for now.

I now imagine the palettes as functions that continue changing the "histogram" according to some ord (dark/light) and it's own goal (maybe is an ANSI palette, "soft" dark or the like, but this is for the end)

ping @usaradark , given your interested, would like your feedback of the current design. I didn't quite had much time, but I come with this for now. I now imagine the palettes as functions that continue changing the "histogram" according to some ord (dark/light) and it's own goal (maybe is an ANSI palette, "soft" dark or the like, but this is for the end)
Main skeleton for how a Palette would work
Some checks failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
1e83c214ac
Contributor
Copy link

I will take a deeper look into it with the coming days. It's kind of late for me right now. But here's a few comments. Also heads up, I'm still very much on the early stages of my Rust journey so I can't help too much on Rust-specific nuances.

Not too sure what the full scope, and very much could be thinking out of scope, but...


Lines 251 to 259 in 1e83c21
/// TODO how effective is this approach? I've tested this with lab previously, see
/// colorspaces::dedup_cols
fn dedup(&mutself){
// self.histo.sort_by_key(|a| a.color.chroma as i32);
self.histo
.iter_mut()
.dedup_by_with_count(|a,b|a.color.diff(&b.color,self.threshold,&self.mode))
.for_each(|x|x.1.count+=x.0);
}


I never really thought about about it, but could you explain why there is a gather-merge step AND a dedup step? Isn't the merge step the dedup step? If colors have been merged with threshold 10, then wouldn't a dedup step with the same threshold be redundant? They also use the same comparison color.diff.

Like say we only had brightness on a range from 0 to 9 and a threshold of 2.

The image has the following colors.

0 1 2 3 5 7 9

Bucketing would look like this.

hist: [bucket[color, count]]
histo = [[0, 3], [3, 2], [7, 2]]

Then we have the dedup step. With the current histo being as follows, how would it dedup? It wouldn't do anything as far as I'm aware. Maybe it's more important when the mix flag is used?


So the more I think about sampling, the more I think the original-original design doesn't support it. Truncating kills the pool of still perfectly-good colors.

Let's assume a monochromatic image that only had brightness values from 0 to 9.

If we truncate to only 3 colors, depending on the theme (say dark), we might end up with the following.

7 8 9

Now if we sample this, we don't have much to sample from. We've truncated the pool of colors to sample from, but those colors we truncated are actually still perfectly usable (assuming the histogram generated colors correctly).

So a more useful pool of colors to sample from, generated from the histogram, might look something like this, and should NOT need any truncating at all.

3 4 5 6 7 8 9

And we'll say the background is around 1, and that 3 is enough contrast for SampleLow to work.

Which leads into the next point. Histogram generation should generate colors that, when sampled, guarantees a minimum legible contrast from the background. Again, this allows SampleLow to work, or whatever the heck sampling method we decide on (i.e. completely user defined dynamic).

All of this is to suggest that, unless we have a very specific reason that we NEED to throw away those lower colors, I think we should keep them i.e. maybe remove truncation all together... though maybe truncation in a different context...

I know in #189 I brought up the problem of the histogram keeping colors that only had a very low counts (e.g. <10 on a large image). Obviously these colors aren't really a major part of the image and might be considered noise. This possibly might be another threshold we or the user defines, on how "prominent" does a color have to be for it to be accepted into the final histogram. This could be a better trunc() step, as in truncate colors that aren't important to the image i.e. have very low counts.

...

Why do we limit to only 16 colors...?


If I were to sum up those initial thoughts...

  • Sampling requires a substantial pool of colors to sample from, generated by the histogram.
  • Sampling should not need to further process the colors (except to generate bg and maybe col0, col7, col8, and col15).
  • All color transformations and filtering should be done at the histogram step.
  • Thoughts suggest the following pipeline:
    • gather -> merge -> dedup? -> filter
      • The filter step does the following:
        • Filter colors that would be too close to the background given the ColorOrder
        • Filter colors that have rather low counts (might be noise or unimportant parts of the image)
        • Maybe not truncate only to 16 colors, keep all colors so sampling has a good pool to choose from

I also noticed you have a SchemeSort. I'm not too sure where this is going, but sorting based on salience != luminance of course... is it going to make use of the Difference trait? I vaguely remember calling for sort over and over again when I wrote Salience. This might be some Rust thing I'm not familiar with right now.

Alright, it's late, brain fried. Until next time.

I will take a deeper look into it with the coming days. It's kind of late for me right now. But here's a few comments. Also heads up, I'm still very much on the early stages of my Rust journey so I can't help too much on Rust-specific nuances. Not too sure what the full scope, and very much could be thinking out of scope, but... --- https://codeberg.org/explosion-mental/wallust/src/commit/1e83c214ac454e34b46efde4d1c01c79820e0a4a/src/histogram/salience.rs#L251-L259 I never really thought about about it, but could you explain why there is a gather-merge step AND a dedup step? Isn't the merge step _the dedup step_? If colors have been merged with threshold `10`, then wouldn't a `dedup` step with the same threshold be redundant? They also use the same comparison `color.diff`. Like say we only had brightness on a range from 0 to 9 and a threshold of 2. The image has the following colors. ``` 0 1 2 3 5 7 9 ``` Bucketing would look like this. ``` hist: [bucket[color, count]] histo = [[0, 3], [3, 2], [7, 2]] ``` Then we have the `dedup` step. With the current histo being as follows, how would it dedup? It wouldn't do anything as far as I'm aware. Maybe it's more important when the `mix` flag is used? --- So the more I think about sampling, the more I think the original-original design doesn't support it. Truncating kills the pool of still perfectly-good colors. Let's assume a monochromatic image that only had brightness values from 0 to 9. If we truncate to only 3 colors, depending on the theme (say dark), we might end up with the following. ``` 7 8 9 ``` Now if we sample this, we don't have much to sample from. We've truncated the pool of colors to sample from, but those colors we truncated are actually still perfectly usable (assuming the histogram generated colors correctly). So a more useful pool of colors to sample from, generated from the histogram, might look something like this, and should NOT need any truncating at all. ``` 3 4 5 6 7 8 9 ``` And we'll say the background is around 1, and that 3 is enough contrast for `SampleLow` to work. Which leads into the next point. Histogram generation should generate colors that, when sampled, guarantees a minimum legible contrast from the background. Again, this allows `SampleLow` to work, or whatever the heck sampling method we decide on (i.e. completely user defined dynamic). **All of this is to suggest that, unless we have a very specific reason that we NEED to throw away those lower colors, I think we should keep them** i.e. maybe remove truncation all together... though maybe truncation in a different context... I know in #189 I brought up the problem of the histogram keeping colors that only had a very low counts (e.g. <10 on a large image). Obviously these colors aren't really a major part of the image and might be considered noise. This possibly might be another threshold we or the user defines, on how "prominent" does a color have to be for it to be accepted into the final histogram. This could be a better `trunc()` step, as in _truncate colors that aren't important to the image i.e. have very low counts_. ... Why do we limit to only 16 colors...? --- If I were to sum up those initial thoughts... - Sampling requires a substantial pool of colors to sample from, generated by the histogram. - Sampling should not need to further process the colors (except to generate `bg` and maybe `col0`, `col7`, `col8`, and `col15`). - All color transformations and filtering should be done at the histogram step. - Thoughts suggest the following pipeline: - `gather` -> `merge` -> `dedup?` -> `filter` - The filter step does the following: - Filter colors that would be too close to the background given the `ColorOrder` - Filter colors that have rather low counts (might be noise or unimportant parts of the image) - Maybe not truncate only to 16 colors, keep all colors so sampling has a good pool to choose from --- I also noticed you have a `SchemeSort`. I'm not too sure where this is going, but sorting based on salience != luminance of course... is it going to make use of the `Difference` trait? I vaguely remember calling for sort over and over again when I wrote Salience. This might be some Rust thing I'm not familiar with right now. Alright, it's late, brain fried. Until next time.
Author
Owner
Copy link

Why do we limit to only 16 colors...?

Yes, truncation can be omitted. I liked that idea.

gather -> merge -> dedup? -> filter

What's merge? I've combined read_bytes and gather since there is no point in being divided (also bc of the type system :P). So the pipeline looks like:

Convert bytes to a histogram -> Post processing, probably filter out really annoying colors like pitch black and white and other colors, unless it's monochrome -> dedup (?, can be discussed later) -> sorting the colors in a way that makes sense (dark/light/ansidark , etc) 

My current question is, do you require the histogram to sort the colors? as in, is the .score or .count information useful when organizing the color palette? In my previous implementations no, I've tried to divide those steps. However, it does kinda make sense in scenario in which there are multiple palettes. Like, what if I want some kind of "Nord" like theme? That could be done artificially, maintaining some histo information.

In that sense, I'm thinking more that the palettes become modifiers. Or rather, there could be 2 palettes: dark and light, and modifiers that, maybe, can be combined. Like palette = dark and modifier = SampleLow or modifier = ansi and so on. This would depend on the internals, since there would probably exist a case that couldn't be combined. js throwing out some ideas..

> Why do we limit to only 16 colors...? Yes, truncation can be omitted. I liked that idea. > gather -> merge -> dedup? -> filter What's merge? I've combined `read_bytes` and `gather` since there is no point in being divided (also bc of the type system :P). So the pipeline looks like: ``` Convert bytes to a histogram -> Post processing, probably filter out really annoying colors like pitch black and white and other colors, unless it's monochrome -> dedup (?, can be discussed later) -> sorting the colors in a way that makes sense (dark/light/ansidark , etc) ``` My current question is, do you require the histogram to sort the colors? as in, is the `.score` or `.count` information useful when organizing the color palette? In my previous implementations no, I've tried to divide those steps. However, it does kinda make sense in scenario in which there are multiple palettes. Like, what if I want some kind of "Nord" like theme? That could be done artificially, maintaining some histo information. In that sense, I'm thinking more that the palettes become modifiers. Or rather, there could be 2 palettes: dark and light, and modifiers that, maybe, can be combined. Like `palette = dark` and `modifier = SampleLow` or `modifier = ansi` and so on. This would depend on the internals, since there would probably exist a case that couldn't be combined. js throwing out some ideas..
Switching to the histogram method, tho the palette process is missing.
Some checks failed
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
49e0d28f36
Contributor
Copy link

gather -> merge -> dedup? -> filter

What's merge? I've combined read_bytes and gather since there is no point in being divided (also bc of the type system :P). So the pipeline looks like:

Sorry I didn't make it clear. merge maybe wasn't the right term here. I kind of meant the initial bucketing. The initial bucketing in a way "merges" the close colors together. Dedup does that too, does it? In that cause, wouldn't dedup be redundant? Not too sure why we have two "merges".

My current question is, do you require the histogram to sort the colors? as in, is the .score or .count information useful when organizing the color palette? In my previous implementations no, I've tried to divide those steps. However, it does kinda make sense in scenario in which there are multiple palettes. Like, what if I want some kind of "Nord" like theme? That could be done artificially, maintaining some histo information.

So the reasoning why I had the weird sorting during the histo step was actually a workaround for the 16 color truncation step.

.score is a measure of how "prominent" a color is to an image. It was created to solve #189 (drop low prominent colors), but if I developed any further it would I would be overstepping into the underlying infrastructure, hence we are here. It was mainly an issue when the total buckets was less than 16, hence those lower counts would be let through.

So it was required NOT to sort the colors, but it IS required to filter low prominent colors.

From our discussions, I think we're leaning towards a direction where histo's responsibility is to return colors that are significant (prominent) to the image, bucketed appropriately to get some pool of colors to sample from.

I'm also not too sure how maintaining histo information would be required for generating a Nord-like theme. I actually have no idea what you mean by Nord. Like arctic cool-blue colors?

However, there is a very big question on where, and if we do, what we filter. See below.

In that sense, I'm thinking more that the palettes become modifiers. Or rather, there could be 2 palettes: dark and light, and modifiers that, maybe, can be combined. Like palette = dark and modifier = SampleLow or modifier = ansi and so on. This would depend on the internals, since there would probably exist a case that couldn't be combined. js throwing out some ideas..

I think you make a very, very important point.

So let's say the user wants to generate a dark theme. Right now, colors from a pool (P) that clash with the dark background get removed during the histo phase. If we wanted to re-use (P) to generate a light theme, it would not be sufficient because during the histo step, we filtered out the darkest colors. We would have to regenerate the histo to get those dark colors. This suggests that the histo phase should not filter colors to allow palette to modify and sample appropriately.

This also allows caching to work rather well. If we cache a good pool of colors generated from the histo, all sampling types that we have (or maybe the user defines...) just sample from that pool. Currently, for all the sampling methods for Salience, it redoes it from the very beginning; a lot of wasted compute.

If the histo step IS to filter any colors, it would probably very similar to the filter you defined, removing colors of very low chroma or of very extreme whiteness or blackness.


All of this is to say that filtering and sorting might actually be responsible for the palette stage, or perhaps somewhere in-between histo and palette.

If we think ahead...

  • histo is to return all "significant" bucketed colors of the image <--- maybe cache here
  • minimally filter for egregious (very bad) colors <--- maybe cache here too?
  • filter colors that clash against user theme (too dark if dark theme, too light if light theme.
  • sorting depends on the theme (light or dark), but also the sorting method (luminance or salience) <--- maybe cache here as well
  • modify colors (i.e. ansi, nord, soft, hard) <--- maybe cache too?
  • sample colors <-- maybe cache here if computations from previous cache is rather large

When I think of caching, I think of the operations that take the longest. Dynamic thresholding is probably what it is, and if all steps after generating the histo is computationally trivial, then maybe that's all that needs to be cached. Like we don't really need to cache sample high and sample low, just cache the pool that it samples from. idk... maybe just cache at the steps where the user can make a decision.

edit - rewording, clarifying why salience histo sorts the way it does

> > gather -> merge -> dedup? -> filter > > What's merge? I've combined `read_bytes` and `gather` since there is no point in being divided (also bc of the type system :P). So the pipeline looks like: Sorry I didn't make it clear. `merge` maybe wasn't the right term here. I kind of meant the initial bucketing. The initial bucketing in a way "merges" the close colors together. Dedup does that too, does it? In that cause, wouldn't dedup be redundant? Not too sure why we have two "merges". > My current question is, do you require the histogram to sort the colors? as in, is the .score or .count information useful when organizing the color palette? In my previous implementations no, I've tried to divide those steps. However, it does kinda make sense in scenario in which there are multiple palettes. Like, what if I want some kind of "Nord" like theme? That could be done artificially, maintaining some histo information. So the reasoning why I had the weird sorting during the histo step was actually a workaround for the 16 color truncation step. `.score` is a measure of how "prominent" a color is to an image. It was created to solve #189 (drop low prominent colors), but if I developed any further it would I would be overstepping into the underlying infrastructure, hence we are here. It was mainly an issue when the total buckets was less than 16, hence those lower counts would be let through. **So it was required NOT to sort the colors, but it IS required to filter low prominent colors.** From our discussions, I think we're leaning towards a direction where `histo`'s responsibility is to return colors that are significant (prominent) to the image, bucketed appropriately to get some pool of colors to sample from. I'm also not too sure how maintaining histo information would be required for generating a Nord-like theme. I actually have no idea what you mean by Nord. Like arctic cool-blue colors? However, there is a very big question on where, and if we do, what we filter. See below. > In that sense, I'm thinking more that the palettes become modifiers. Or rather, there could be 2 palettes: dark and light, and modifiers that, maybe, can be combined. Like palette = dark and modifier = SampleLow or modifier = ansi and so on. This would depend on the internals, since there would probably exist a case that couldn't be combined. js throwing out some ideas.. I think you make a very, very important point. So let's say the user wants to generate a dark theme. Right now, colors from a pool (P) that clash with the dark background get removed during the histo phase. If we wanted to re-use (P) to generate a light theme, it would not be sufficient because during the histo step, we filtered out the darkest colors. We would have to regenerate the histo to get those dark colors. This suggests that the histo phase should not filter colors to allow palette to modify and sample appropriately. This also allows caching to work rather well. If we cache a good pool of colors generated from the histo, all sampling types that we have (or maybe the user defines...) just sample from that pool. Currently, for all the sampling methods for Salience, it redoes it from the very beginning; a lot of wasted compute. If the histo step IS to filter any colors, it would probably very similar to the filter you defined, removing colors of very low chroma or of very extreme whiteness or blackness. --- All of this is to say that filtering and sorting might actually be responsible for the palette stage, or perhaps somewhere in-between histo and palette. If we think ahead... - histo is to return all "significant" bucketed colors of the image <--- maybe cache here - minimally filter for egregious (very bad) colors <--- maybe cache here too? - filter colors that clash against user theme (too dark if dark theme, too light if light theme. - sorting depends on the theme (light or dark), but also the sorting method (luminance or salience) <--- maybe cache here as well - modify colors (i.e. ansi, nord, soft, hard) <--- maybe cache too? - sample colors <-- maybe cache here if computations from previous cache is rather large When I think of caching, I think of the operations that take the longest. Dynamic thresholding is probably what it is, and if all steps after generating the histo is computationally trivial, then maybe that's all that needs to be cached. Like we don't _really_ need to cache sample high and sample low, just cache the pool that it samples from. idk... maybe just cache at the steps where the user can make a decision. edit - rewording, clarifying why salience histo sorts the way it does
First working state, only dark Salience impl
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
c600170062
You can run it with
	cargo run -- run "$(readlink -f $WALLPAPER)" -wuT -t 17
And change the threshold, dynamic one is left at the end.
Author
Owner
Copy link

Got the first salience version working. Think I kinda like where it goes, but would like it to be a bit more different. However, the approach is clean enough. Is either a dark or light palette, that both should impl. Now, the palettes could have modifiers, which change the pipeline.

Since this is Rust, I'm very attracted to the

histogram.with_threshold(30).with_scheme(Dark).with_modifier(Ansi).with...

And so on. I'm leaving this to the end. Don't think much about cache btw, at least not now. I think storing the first histogram is enough. Even on large images, the file is relatively short. So, I imagine, with even more histogram process, the elements would be minimal. I prefer that recompute than to lose precision.

I'm also not too sure how maintaining histo information would be required for generating a Nord-like theme. I actually have no idea what you mean by Nord. Like arctic cool-blue colors?
Prominent colors, intermediate colors, either based on count or score, can be placed to simulate a theme. Rather than, "just the brightess at 15th position", just place them following an order, and then manually tweak them. Hence, a modifier for original colors. 1

Anyway, let's not get into modifiers right now, your pipeline help me get this right. For consistency sake, I've implmemented this into one function. I think we can treat the threshold problem now, since that is a requirement for the dynamic implementation.

Edit: showoff ur salience work
salience-working

Got the first salience version working. Think I kinda like where it goes, but would like it to be a bit more different. However, the approach is clean enough. Is either a dark or light palette, that both should impl. Now, the palettes could have modifiers, which change the pipeline. Since this is Rust, I'm very attracted to the ```rs histogram .with_threshold(30) .with_scheme(Dark) .with_modifier(Ansi) .with ... ``` And so on. I'm leaving this to the end. Don't think much about cache btw, at least not now. I think storing the first histogram is enough. Even on large images, the file is relatively short. So, I imagine, with even more histogram process, the elements would be minimal. I prefer that recompute than to lose precision. > I'm also not too sure how maintaining histo information would be required for generating a Nord-like theme. I actually have no idea what you mean by Nord. Like arctic cool-blue colors? Prominent colors, intermediate colors, either based on count or score, can be placed to simulate a theme. Rather than, "just the brightess at 15th position", just place them following an order, and then manually tweak them. Hence, a _modifier_ for original colors. [^1] Anyway, let's not get into modifiers right now, your pipeline help me get this right. For consistency sake, I've implmemented this into one function. I think we can treat the threshold problem now, since that is a requirement for the dynamic implementation. Edit: showoff ur salience work ![salience-working](/attachments/5532e04e-a5a2-4452-8f0c-cec0afadf4b4) [^1]: https://www.nordtheme.com/docs/colors-and-palettes I like this descriptions.
Contributor
Copy link

Threshold problem, I'm going to assume it's what I was talking earlier about thresholding against DeltaE vs ImprovedDeltaE and how it can be... odd. I'm going to reiterate some of the stuff I said in other threads to try to centralize the points.


DeltaE is the real distance between a two colors. The scale is rather linear.
https://docs.rs/palette/0.7.6/src/palette/cam16/ucs_jab.rs.html#201-212

ImprovedDeltaE is the perceptual distance between two colors. The scale is non-linear, using power-law scaling. This is to say that at some point, "more different" is meaningless. It's like you're rich, but then you get richer; doesn't matter, you're rich.

The official crate implementation for Cam16 raises it to a power of 0.63 and multiplies the entire thing by 1.41 such that a value of 1.0 describes the point of Just Noticeable Difference (JND)... or at least that's my understanding of it.
https://docs.rs/palette/0.7.6/src/palette/cam16/ucs_jab.rs.html#201-212

This means that the perceptual delta of each color difference method is the same at 1.0, but values beyond that quickly diverge due to their computation.

Now the problem comes with how we a user would "naturally" threshold. For numerical thresholding, I would say most humans do it linearly. So while the user (and dynamic) sets linear threshold values, the implementation of color differences actually uses non-linear values.

This results in a case where the user sets values around >28, but seemingly has no affect because ImprovedDeltaE saturates at that point. But also, because the range of ImprovedDeltaE is [0, 28](?) compared to DeltaE's range of [0, 100](?) means that threshold values are rather sensitive, especially given the former's non-linear scaling nature.

This can also be problematic for dynamic thresholding because it searches by a linear binary search, then once a floor is found, it linearly increases threshold until some max value(?). The starting value is also rather high for ImprovedDeltaE as the "middle" is more like 7(?), while for DeltaE the "middle" is more like 40(?).

Lines 196 to 198 in b528453
// start from the middle, and then search either upper or lower values, as a simple bin tree
letidx=[14,16,13,17,12,18,11,19,10,20,9,21,8,22,7,23,6,24,5,25,4,
26,3,27,2,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44];

So the main questions to answer are the following.

  • Do we use linear scaling? Non-linear scaling?
  • Only one or the other?
  • If we use both, when should we use one or the other?

On the main branch, I believe thresholding is put up against ImprovedDeltaE. There are a few solutions we might be able to take...

  • Have scaling be linear for everything
    • This means differences are no longer perceptually accurate at large differences.
    • This does NOT affect sorting.
    • Can make constraints for color mods a bit more difficult as it's no longer mapped to perception (e.g constrain bg to have DeltaE of 20). But this can be solved by using ImprovedDeltaE and DeltaE appropriately...?
  • Let the user do linear scaling, but map their linear-scaling to match the non-linear scaling
Threshold problem, I'm going to assume it's what I was talking earlier about thresholding against `DeltaE` vs `ImprovedDeltaE` and how it can be... odd. I'm going to reiterate some of the stuff I said in other threads to try to centralize the points. --- `DeltaE` is the real distance between a two colors. The scale is rather linear. https://docs.rs/palette/0.7.6/src/palette/cam16/ucs_jab.rs.html#201-212 `ImprovedDeltaE` is the perceptual distance between two colors. The scale is non-linear, using power-law scaling. This is to say that at some point, "more different" is meaningless. It's like you're rich, but then you get richer; doesn't matter, you're rich. The official crate implementation for Cam16 raises it to a power of `0.63` and multiplies the entire thing by `1.41` such that a value of `1.0` describes the point of Just Noticeable Difference (JND)... or at least that's my understanding of it. https://docs.rs/palette/0.7.6/src/palette/cam16/ucs_jab.rs.html#201-212 This means that the perceptual delta of each color difference method is the same at `1.0`, but values beyond that quickly diverge due to their computation. Now the problem comes with how we a user would "naturally" threshold. For numerical thresholding, I would say most humans do it linearly. So while the user (and dynamic) sets linear threshold values, the implementation of color differences actually uses non-linear values. This results in a case where the user sets values around >28, but seemingly has no affect because `ImprovedDeltaE` saturates at that point. But also, because the range of `ImprovedDeltaE` is `[0, 28]`(?) compared to `DeltaE`'s range of `[0, 100]`(?) means that threshold values are rather sensitive, especially given the former's non-linear scaling nature. This can also be problematic for dynamic thresholding because it searches by a linear binary search, then once a floor is found, it linearly increases threshold until some max value(?). The starting value is also rather high for `ImprovedDeltaE` as the "middle" is more like 7(?), while for `DeltaE` the "middle" is more like 40(?). https://codeberg.org/explosion-mental/wallust/src/commit/b528453fc11b5a48b457b0e04c9487e804bf0bfa/src/colorspaces/mod.rs#L196-L198 --- So the main questions to answer are the following. - Do we use linear scaling? Non-linear scaling? - Only one or the other? - If we use both, when should we use one or the other? On the main branch, I believe thresholding is put up against `ImprovedDeltaE`. There are a few solutions we might be able to take... - Have scaling be linear for everything - This means differences are no longer perceptually accurate at large differences. - This does NOT affect sorting. - Can make constraints for color mods a bit more difficult as it's no longer mapped to perception (e.g constrain `bg` to have `DeltaE` of `20`). But this can be solved by using `ImprovedDeltaE` and `DeltaE` appropriately...? - Let the user do linear scaling, but map their linear-scaling to match the non-linear scaling - This map will vary from colorspace to colorspace, as the scaling is not the same across all colorpalettes (e.g. Lch uses `powf(0.55)` and a multiplier of `1.26` to bring the JND to `1` https://docs.rs/palette/0.7.6/src/palette/lab.rs.html#266-274)
Contributor
Copy link

Edit: showoff ur salience work

Ayo not gonna lie, that image is a really good demo of Salience. You got the muted colors which should be sorted lower and the and the vibrant eye-catching lights in the front. I'm curious what the colors in the histogram looks like and how it sorted. Did you ever get a debug print working? I'll post my debug print in the end and you can work with that if you want.

  1. https://www.nordtheme.com/docs/colors-and-palettes I like this descriptions. ↩︎

That's very interesting... I think it's certainly possible to have something like this.

If you do not care about the original Nord colors and only want to follow the structure/method of it, I'd imagine that after a low-threshold histogram of colors, the user defines an array of their target salience values or modifiers. Something like...

# define the nord "sampling method" template
# values the percentage-index from left-to-right of a sorted array of colors
nord = [
	5, 6, 7, 8,			# polar night
	30, 32, 34,			# snow storm
	40, 42, 44, 46,		# frost
	70, 74, 76, 78, 80	# aurora
]

Or, with built-in color filtering/modding to better match Nord's inner workings (i.e. nord1 is a brigher shade of nord0)

# define the nord "sampling method" template
# values the percentage-index from left-to-right of a sorted array of colors
nord = [
 5, 5:brighten(0.05), 5:brighten(0.10), 5:brighten(0.15), # polar night
 ...
]

However, if you DO care about the original nord colors, you run into a similar issue to ANSI, how you have to mix but somewhat retain the original hue of the original. That's a can of worms I'm not ready for yet. I haven't read how you managed to do it.


edit

Print colors with their index, 10 per row

Lines 1007 to 1035 in a30af2c
pubfn print_visual(cols: &[Spec]){
useowo_colors::OwoColorize;
letcols: Vec<Myrgb>=cols.iter().map(|&c|c.into()).collect();
println!();
letchunk_size=10;
for(chunk_index,chunk)incols.chunks(chunk_size).enumerate(){
letstart=chunk_index*chunk_size;
// Print indices
foriin0..chunk.len(){
ifi==0{
print!("{:^3}",(chunk_index)%10);
}
else{
print!("{:^3}",(start+i)%10);
}
}
println!();
// Print values
forvalinchunk{
print!("{}","".on_color(val.owo_col()));
}
println!();
println!();
}
}

As for the table... I'm sure you know I disgustingly hacked that thing together.

// This giant block is sort_histo_by_score, but with a debug table to
// manually validate calculated score values. You can delete this later
// once score_histogram_by_score is finalized. Though any changes
// here and there are not mirrored, you will have to manually do clone
// the changes. I'm sure there's a more elegant way to do this...
letmuthistoscore: Vec<HistoScore<Spec>>=histo.into_iter().map(|h|h.into()).collect();
letcnt_sclr=4.0;
letsal_factor=1.0/20.0;
histoscore.iter_mut().for_each(|hs|{
letscore_log_scaled=(hs.histo.countasf32).powf(1.0/cnt_sclr);
letcol=hs.histo.color;
hs.score=score_log_scaled*(1.0+col.improved_salience(&bg)*sal_factor);
});
histoscore.sort_by(|a,b|b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Greater));
println!();
// histoscore.insert(0, bg_histo.into());
histoscore.insert(0,max_histo_og.into());
//
letcols: Vec<Spec>=histoscore.iter().map(|hs|hs.histo.color).collect();
print_visual(&cols);
println!();
//
println!("HISTOSCORE");
// println!("{:>2} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7} {:>7}", "i", "l", "c", "h", "cnt", "cnt_log", "base", "c_sal", "h_sal", "sal_n", "sal", "scr");
println!("{:>2}{:>7}{:>7}{:>7}{:>7}{:>7}{:>7}{:>7}{:>7}{:>7}{:>7}","i","l","c","h","cnt","cnt_log","base","c_sal","h_sal","sal_n","scr");
histoscore.iter().enumerate().for_each(|(i,hs)|{
letcnt=hs.histo.count;
letcnt_log=(hs.histo.countasf32).powf(1.0/cnt_sclr);
letcol=hs.histo.color;
letl=col.lightness;
letc=col.colorfulness;
leth=(360.0+col.hue.into_raw_degrees())%360.0;
letcsal=c_sal(&col,&bg);
lethsal=h_sal(&col,&bg);
letsal=col.improved_salience(&bg);
letsal_naive=col.improved_salience_naive(ord);
letbase=col.improved_salience(&bg);
letscore=hs.score;
// println!("{i:>2} {l:>7.2} {c:>7.2} {h:>7.2} {cnt:>7} {cnt_log:>7.2} {base:>7.2} {csal:>7.2} {hsal:>7.2} {sal_naive:>7.2} {sal:>7.4} {score:>7.4}");
println!("{i:>2}{l:>7.2}{c:>7.2}{h:>7.2}{cnt:>7}{cnt_log:>7.2}{base:>7.2}{csal:>7.2}{hsal:>7.2}{sal_naive:>7.2}{score:>7.4}");
});
println!();
histoscore.remove(0);
letmuthisto: Vec<Histo<Spec>>=histoscore.into_iter().map(Histo::from).collect();
// END DEBUG PRINT
> Edit: showoff ur salience work Ayo not gonna lie, that image is a really good demo of Salience. You got the muted colors which should be sorted lower and the and the vibrant eye-catching lights in the front. I'm curious what the colors in the histogram looks like and how it sorted. Did you ever get a debug print working? I'll post my debug print in the end and you can work with that if you want. > 1. https://www.nordtheme.com/docs/colors-and-palettes I like this descriptions. ↩︎ That's very interesting... I think it's certainly possible to have something like this. If you do not care about the original Nord colors and only want to follow the structure/method of it, I'd imagine that after a low-threshold histogram of colors, the user defines an array of their target salience values or modifiers. Something like... ``` # define the nord "sampling method" template # values the percentage-index from left-to-right of a sorted array of colors nord = [ 5, 6, 7, 8, # polar night 30, 32, 34, # snow storm 40, 42, 44, 46, # frost 70, 74, 76, 78, 80 # aurora ] ``` Or, with built-in color filtering/modding to better match Nord's inner workings (i.e. `nord1` is a brigher shade of `nord0`) ``` # define the nord "sampling method" template # values the percentage-index from left-to-right of a sorted array of colors nord = [ 5, 5:brighten(0.05), 5:brighten(0.10), 5:brighten(0.15), # polar night ... ] ``` However, if you DO care about the original nord colors, you run into a similar issue to ANSI, how you have to mix but somewhat retain the original hue of the original. That's a can of worms I'm not ready for yet. I haven't read how you managed to do it. --- edit Print colors with their index, 10 per row https://codeberg.org/explosion-mental/wallust/src/commit/a30af2c72bebeb19b6bd736a91204a9864b3b3b7/src/colorspaces/salience.rs#L1007-L1035 As for the table... I'm sure you know I disgustingly hacked that thing together. https://codeberg.org/explosion-mental/wallust/src/commit/a30af2c72bebeb19b6bd736a91204a9864b3b3b7/src/colorspaces/salience.rs#L412-L456
between methods, temporarily
naive impl of multithreaded dynamic threshold
Some checks failed
ci/woodpecker/pr/docs Pipeline failed
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
b70077c127
Author
Owner
Copy link

Print colors with their index, 10 per row

Done. I'm leaving it out behind a debug compiler flag

But this can be solved by using ImprovedDeltaE and DeltaE appropriately...?

Yes. from your comment I got so much insight on it. While on a dynamic search for a 'perfect' or 'best' threshold, use linear, to 'allow' more precision or possibilities of 'finding' the right spot. Just define a common function and give a parameter to change to choose between improved or default impls.

Let the user do linear scaling, but map their linear-scaling to match the non-linear scaling

I think this is also correct, but would be perceive as "nothing happens" by the users. Instead, we can just declare that the threshold is limited by an interval, [0,28> roughtly.
Now, another attempt would be to scale it to 0-100%, and let the decimals make the difference.
This requires testing to determine a good enough approach.
Edit: Or if decimals matter as the overall "difference" in between colors (probably in very large images) or if they matter in the question I had below, to determine a perfect threshold

I think the codebase is much more cleanear now, you really did a good job on getting this to work in the past implementation. If you do get the urge to tweak, use the config to change mode = "salience" or mode = "luminance". This is here for testing only. There is a missing piece, which is determining when to use salience or luminance. This is very important, since this allowed me to make the Build trait infallible or 'pure'. My initial thought on this previous method was to determine if it's monochromatic or not. Even then, hell you could probably make a palette with a single color pixel, and just interpolate it with either a predetermined white or black color constant. These scenarios could make unnecessary a dynamic threshold and a salience histogram. The other use of luminance, in that sense, it just to sort based on, luminance. I haven't tweak luminance yet, more excited about these other stuff rn, but soon will rebuild it like the colorspace one.

Question I have now is... How to determine a good threshold on the salience approach? With my previous impl it was simple, since it gather into histos and then dedup it, with diff methods, most colors just disappear and I could simple just do until MIN_COLS or whatever has the most colors.

I really invite you to test this one out, the print_values + dynamic threshold searching is kinda cool. Pushing the limits of saliency

Edit2: Maybe I should js rayon all the iter()s kek..

> Print colors with their index, 10 per row Done. I'm leaving it out behind a debug compiler flag > But this can be solved by using ImprovedDeltaE and DeltaE appropriately...? Yes. from your comment I got so much insight on it. While on a dynamic search for a 'perfect' or 'best' threshold, use linear, to 'allow' more precision or possibilities of 'finding' the right spot. Just define a common function and give a parameter to change to choose between improved or default impls. > Let the user do linear scaling, but map their linear-scaling to match the non-linear scaling I think this is also correct, but would be perceive as "nothing happens" by the users. Instead, we can just declare that the threshold is limited by an interval, [0,28> roughtly. Now, another attempt would be to scale it to 0-100%, and let the decimals make the difference. This requires testing to determine a good enough approach. Edit: Or if decimals matter as the overall "difference" in between colors (probably in very large images) or if they matter in the question I had below, to determine a _perfect threshold_ I think the codebase is much more cleanear now, you really did a good job on getting this to work in the past implementation. If you do get the urge to tweak, use the config to change `mode = "salience"` or `mode = "luminance"`. This is here for testing only. There is a missing piece, which is *determining when to use salience or luminance*. This is very important, since this allowed me to make the Build trait infallible or 'pure'. My initial thought on this previous method was to determine if it's monochromatic or not. Even then, hell you could probably make a palette with a single color pixel, and just interpolate it with either a predetermined white or black color constant. These scenarios could make unnecessary a dynamic threshold and a salience histogram. The other use of luminance, in that sense, it just to sort based on, luminance. I haven't tweak luminance yet, more excited about these other stuff rn, but soon will rebuild it like the colorspace one. Question I have now is... How to determine a good threshold on the salience approach? With my previous impl it was simple, since it gather into histos and then dedup it, with diff methods, most colors just disappear and I could simple just do until `MIN_COLS` or whatever has the most colors. I really invite you to test this one out, the `print_values` + `dynamic` threshold searching is kinda cool. Pushing the limits of _saliency_ Edit2: Maybe I should js [rayon](https://github.com/rayon-rs/rayon) all the `iter()`s kek..
Contributor
Copy link

I will have more time in the coming days to try it out. What I really wanted to do is to create a tool to quickly test constants for salience calculations and sorting. This way I can get some more datasets on what might be the right constants. The constants used for salience calculations are a very coarse "binary search" that I did.

Question I have now is... How to determine a good threshold on the salience approach? With my previous impl it was simple, since it gather into histos and then dedup it, with diff methods, most colors just disappear and I could simple just do until MIN_COLS or whatever has the most colors.

Because the original implementation had 16 as MAX_COLS, that was what I went with; the threshold that creates a histogram with about 16 buckets. If 32 colors generated, they get clipped to 16, meaning only 50% of the histogram-image representation could be used. If only 8 colors were generated, while the histogram fully represents the image, sampling is limited and narrow. But, because we are now removing this constraint (because it had no real reason to exist), this best-threshold approach is no longer valid.

Threshold, at least for the initial bucketing, is to merge similar colors together. This is to say that at low thresholds, the perceptual difference between two colors are low, while at high thresholds, the individual colors get more and more distinct.

My understanding is that all contrast calculations that at least follow some standardized method (most known contrast methods) follow the rule that 1.0 stands for Just Noticeable Difference (JND), with further elaboration on the color difference wikipedia. However, beyond that 1.0 JND, the scale is NOT standardized in any way. ImprovedDeltaE might be standardized across implementations, though this needs confirmation.

When considering for sampling, if we SampleHigh, we don't want the top 8 colors to all be some variation of neon pink. A best-threshold would have it such that colors are distinct enough from each other, but not so high that actual distinct colors become merged. I haven't tested using DeltaE, but at least for ImprovedDeltaE, the best value to get 16 colors is about 11. That threshold was before the removal of MAX_COLS, so... maybe try 4 and move up from there? idk I'll maybe try some testing on my end as well for that.

I may have also manually adjusted the value returned from diff_col or w/e that function was to get a reasonable amount of colors returned given the default dynamic-threshold search values.

I think this is also correct, but would be perceive as "nothing happens" by the users. Instead, we can just declare that the threshold is limited by an interval, [0,28> roughtly.

What I meant by this is like...

letrescaled_threshold=1.41*threshold.powf(0.63)

This lets the user use threshold linearly as if it were DeltaE, but it's actually scaled to perceptual difference ImprovedDeltaE. Previously, without this scaling and when using ImprovedDeltaE as difference logic, "nothing happens". This rescale would fix it, but then you might as well just be using DeltaE on difference logic.

There is a missing piece, which is determining when to use salience or luminance. This is very important, since this allowed me to make the Build trait infallible or 'pure'.

I didn't get the chance to look at the fallback logic for monochromatic images. But do remember that salience on Cam16 is quite literally a weighted average of Jab. Which is to say that if you took away weights for ab, you get Lightness. While not exactly the same thing as luminance, it might be more perceptually accurate over just bucketing with luminance.

These scenarios could make unnecessary a dynamic threshold and a salience histogram. The other use of luminance, in that sense, it just to sort based on, luminance. I haven't tweak luminance yet, more excited about these other stuff rn, but soon will rebuild it like the colorspace one.

That brings up a very good question. What did dynamic thresholding solve, and do we need it now if we're doing a Sample step at the end of the pipeline to generate the palette? Because with a sane image and a sane default threshold, the generated histogram should be robust enough to sample from. You might not even need to deal with multithreading anymore.

I would imagine that if less than... idk 24 (8*3) colors are generated then a monochromatic fallback may be considered so that sampling has some range to work with.

edit - brain tired but can actually do some more work on this in the coming days.

I will have more time in the coming days to try it out. What I really wanted to do is to create a tool to quickly test constants for salience calculations and sorting. This way I can get some more datasets on what might be the right constants. The constants used for salience calculations are a very coarse "binary search" that I did. > Question I have now is... How to determine a good threshold on the salience approach? With my previous impl it was simple, since it gather into histos and then dedup it, with diff methods, most colors just disappear and I could simple just do until MIN_COLS or whatever has the most colors. Because the original implementation had 16 as `MAX_COLS`, that was what I went with; the threshold that creates a histogram with about 16 buckets. If 32 colors generated, they get clipped to 16, meaning only 50% of the histogram-image representation could be used. If only 8 colors were generated, while the histogram fully represents the image, sampling is limited and narrow. But, because we are now removing this constraint (because it had no real reason to exist), this best-threshold approach is no longer valid. Threshold, at least for the initial bucketing, is to merge similar colors together. This is to say that at low thresholds, the perceptual difference between two colors are low, while at high thresholds, the individual colors get more and more distinct. My understanding is that all contrast calculations that at least follow some standardized method (most known contrast methods) follow the rule that `1.0` stands for [Just Noticeable Difference (JND)](https://en.wikipedia.org/wiki/Just-noticeable_difference), with further elaboration on the [color difference wikipedia](https://en.wikipedia.org/wiki/Color_difference). However, beyond that `1.0` JND, the scale is NOT standardized in any way. `ImprovedDeltaE` might be standardized across implementations, though this needs confirmation. **When considering for sampling, if we `SampleHigh`, we don't want the top 8 colors to all be some variation of neon pink. A best-threshold would have it such that colors are distinct _enough_ from each other, but not so high that actual distinct colors become merged.** I haven't tested using `DeltaE`, but at least for `ImprovedDeltaE`, the best value to get 16 colors is about `11`. That threshold was before the removal of `MAX_COLS`, so... maybe try `4` and move up from there? idk I'll maybe try some testing on my end as well for that. I may have also manually adjusted the value returned from `diff_col` or w/e that function was to get a reasonable amount of colors returned given the default dynamic-threshold search values. > I think this is also correct, but would be perceive as "nothing happens" by the users. Instead, we can just declare that the threshold is limited by an interval, [0,28> roughtly. What I meant by this is like... ```rust let rescaled_threshold = 1.41 * threshold.powf(0.63) ``` This lets the user use threshold linearly as if it were `DeltaE`, but it's actually scaled to perceptual difference `ImprovedDeltaE`. Previously, without this scaling and when using `ImprovedDeltaE` as difference logic, "nothing happens". This rescale would fix it, but then you might as well just be using `DeltaE` on difference logic. > There is a missing piece, which is determining when to use salience or luminance. This is very important, since this allowed me to make the Build trait infallible or 'pure'. I didn't get the chance to look at the fallback logic for monochromatic images. But do remember that salience on `Cam16` is quite literally a weighted average of `Jab`. Which is to say that if you took away weights for `ab`, you get `Lightness`. While not exactly the same thing as `luminance`, it might be more perceptually accurate over just bucketing with `luminance`. > These scenarios could make unnecessary a dynamic threshold and a salience histogram. The other use of luminance, in that sense, it just to sort based on, luminance. I haven't tweak luminance yet, more excited about these other stuff rn, but soon will rebuild it like the colorspace one. That brings up a very good question. What did dynamic thresholding solve, and **do we need it now if we're doing a `Sample` step at the end of the pipeline to generate the palette?** Because with a sane image and a sane default threshold, the generated histogram should be robust enough to sample from. You might not even need to deal with multithreading anymore. I would imagine that if less than... idk 24 (8*3) colors are generated then a monochromatic fallback may be considered so that sampling has some range to work with. edit - brain tired but can actually do some more work on this in the coming days.
Author
Owner
Copy link

Because the original implementation had 16 as MAX_COLS, that was what I went with; the threshold that creates a histogram with about 16 buckets. If 32 colors generated, they get clipped to 16, meaning only 50% of the histogram-image representation could be used. If only 8 colors were generated, while the histogram fully represents the image, sampling is limited and narrow. But, because we are now removing this constraint (because it had no real reason to exist), this best-threshold approach is no longer valid.

I think you choose your colors based on index? Not sure, but that was the straightfowards approach to choose 6 colors. With salience there are the modifiers, so we can get rid of the truncation overall.

That brings up a very good question. What did dynamic thresholding solve, and do we need it now if we're doing a Sample step at the end of the pipeline to generate the palette? Because with a sane image and a sane default threshold, the generated histogram should be robust enough to sample from. You might not even need to deal with multithreading anymore.

To just run with the best option, assume an uninterested user about this color theory stuff, as this is how many of us start the "just works" stage. While, initially I did implement a dynamic threshold, first "automatic" as I called it, to just hardcode a threshold to each colorspace. Still, raises the question to... which palette is actually the "better" one?

muh-salience

> Because the original implementation had 16 as MAX_COLS, that was what I went with; the threshold that creates a histogram with about 16 buckets. If 32 colors generated, they get clipped to 16, meaning only 50% of the histogram-image representation could be used. If only 8 colors were generated, while the histogram fully represents the image, sampling is limited and narrow. But, because we are now removing this constraint (because it had no real reason to exist), this best-threshold approach is no longer valid. I think you choose your colors based on index? Not sure, but that was the straightfowards approach to choose 6 colors. With salience there are the modifiers, so we can get rid of the truncation overall. > That brings up a very good question. What did dynamic thresholding solve, and do we need it now if we're doing a Sample step at the end of the pipeline to generate the palette? Because with a sane image and a sane default threshold, the generated histogram should be robust enough to sample from. You might not even need to deal with multithreading anymore. To just run with the best option, assume an uninterested user about this color theory stuff, as this is how many of us start the "just works" stage. While, initially I did implement a dynamic threshold, first "automatic" as I called it, to just hardcode a threshold to each colorspace. Still, raises the question to... which palette is actually the "better" one? ![muh-salience](/attachments/e7ec9cec-0ce2-483f-b7c8-1f8bd68ffd84)
Contributor
Copy link

I have time now and for the next two days (hopefully) to work on this so I will look into some method of finding this.

I think you choose your colors based on index? Not sure, but that was the straightfowards approach to choose 6 colors. With salience there are the modifiers, so we can get rid of the truncation overall.

By index as in the upper 6, lower 6, center 6, or distributed 6, yes by index.

Still, raises the question to... which palette is actually the "better" one?

Assuming what's printed on the console is a single histogram... that's probably way too many buckets. I don't know what you mean by better. What am I comparing this to?

What I'm going to do, once I finish some other stuff, is to create a few tools to help us better understand what thresholds we need. Something like "create a demo palette where for each palette, it is at least X threshold away from the previous color". It should help us understand more about how threshold (ImprovedDeltaE and DeltaE) actually contribute to the visual differences.

What I'm predicting is that there's a strange issue where hue-shifts dominate lightness/colorfulness-shifts. In other words, colors of similar hue do not get bucketed due to differing lightness and colorfuless when instead they should. If this is so, this might suggest the need for a new heuristic for bucketing colors. Maybe like bucket much more aggressively on lightness and colorfulness.

I have time now and for the next two days (hopefully) to work on this so I will look into some method of finding this. > I think you choose your colors based on index? Not sure, but that was the straightfowards approach to choose 6 colors. With salience there are the modifiers, so we can get rid of the truncation overall. By index as in the upper 6, lower 6, center 6, or distributed 6, yes by index. > Still, raises the question to... which palette is actually the "better" one? Assuming what's printed on the console is a single histogram... that's probably way too many buckets. I don't know what you mean by better. What am I comparing this to? What I'm going to do, once I finish some other stuff, is to create a few tools to help us better understand what thresholds we need. Something like "create a demo palette where for each palette, it is at least X threshold away from the previous color". It should help us understand more about how threshold (`ImprovedDeltaE` and `DeltaE`) actually contribute to the visual differences. What I'm predicting is that there's a strange issue where hue-shifts dominate lightness/colorfulness-shifts. In other words, colors of similar hue do not get bucketed due to differing lightness and colorfuless when instead they should. If this is so, this might suggest the need for a new heuristic for bucketing colors. Maybe like bucket much more aggressively on lightness and colorfulness.
explosion-mental changed title from (削除) Redesign of color gatherings (fixes #189) (削除ここまで) to WIP: Redesign of color gatherings (fixes #189) 2025年12月09日 02:00:23 +01:00
Author
Owner
Copy link

Assuming what's printed on the console is a single histogram... that's probably way too many buckets. I don't know what you mean by better. What am I comparing this to?

No, it's all the threshold table from the previous dynamic multithreadead implementation, so there are multiple histograms. There is a lot more not shown in the screen, but, like I said, which one is the "good" one?

What I'm predicting is that there's a strange issue where hue-shifts dominate lightness/colorfulness-shifts. In other words, colors of similar hue do not get bucketed due to differing lightness and colorfuless when instead they should. If this is so, this might suggest the need for a new heuristic for bucketing colors. Maybe like bucket much more aggressively on lightness and colorfulness.

So another filter? sounds good, that's what I wanted to do in the old lch colorspace impl, but there wasn't a concrete type to work on and store constant data, problem you've experience with implementing salience with a Lock and a global var. With the new impl, ofc, this is no longer an issue, so you have way more 'space' to work on specific ideas.

> Assuming what's printed on the console is a single histogram... that's probably way too many buckets. I don't know what you mean by better. What am I comparing this to? No, it's all the threshold table from the previous dynamic multithreadead implementation, so there are multiple histograms. There is a lot more not shown in the screen, but, like I said, which one is the "good" one? > What I'm predicting is that there's a strange issue where hue-shifts dominate lightness/colorfulness-shifts. In other words, colors of similar hue do not get bucketed due to differing lightness and colorfuless when instead they should. If this is so, this might suggest the need for a new heuristic for bucketing colors. Maybe like bucket much more aggressively on lightness and colorfulness. So another filter? sounds good, that's what I wanted to do in the old `lch` colorspace impl, but there wasn't a concrete type to work on and store constant data, problem you've experience with implementing salience with a `Lock` and a global var. With the new impl, ofc, this is no longer an issue, so you have way more 'space' to work on specific ideas.
Author
Owner
Copy link

Btw, I was trying to reach a "complete" state on the structural rewrite and realize that, there is no need of a dark/light implementation for salience, it's just based on ColorOrder, am I right? In that case, it could even make things more simpler, I was working with a new enum type with Dark and Light variants, but seems unnecessary.

Btw, I was trying to reach a "complete" state on the structural rewrite and realize that, there is no need of a dark/light implementation for salience, it's just based on `ColorOrder`, am I right? In that case, it could even make things more simpler, I was working with a new enum type with `Dark` and `Light` variants, but seems unnecessary.
Contributor
Copy link

Something I've come to understand as I kept on tinkering with salience on Lch is that when considering human perception of color, Lch kind of sucks for the perceptual accuracy I needed to get salience working. Cam16 is nearly better in always in that all components are much more, is not are, perceptually linear compared to Lch. Lch also struggles in low light, underestimates blue hues, and overestimates yellow hues.

Me knowing that Cam16 fixes all of that, I find it hard to recommend continuing with Lch. I cannot think of a scenario where Lch would perform better than Cam16 unless computation is a big issue, which Cam16 most definitely has more computation. But even when testing lch vs salience on the master branch, there is little-to-no difference computation times of trials I ran; they are neck-and-neck.

If we wish to change the sorting method, then we just change the heuristic on how sorting is used, no need to change the entire colorspace.

I bring this up because the way salience buckets is DeltaE or ImprovedDeltaE, NOT with salience. The latter is exclusively used for palette sorting. luminance, or rather lch, also uses the same bucketing method. This is redundancy, and their implementation only really diverges on the sorting step, as early-sorting due to trunc() is no longer needed. See the next section on that.


Because we are no longer truncating, sort_by_score() for salience.post_dedup() actually serves no purpose anymore. Honestly I think the entirety of post_dedup() can be removed... at least for salience. Again, this was here to prevent colors from being truncated during the trunc() step by moving more interesting colors upwards on the sort.

We still need to sort the colors somewhere, either by salience or luminance as you mentioned earlier. I want to say use salience first as it includes lightness, colorfulness, AND hue, while luminance is, well, just luminance. Sure you could exclusively use luminance for greyscale, but it will not work for monochromatic as chroma and luminance varies there.

The only place where luminance is sound (valid) is greyscale... unless the user explicitly wants to sort by luminance and not salience, I think salience sort should cover most user expectations... but then again, this whole salience thing is somewhat novel(?) in the palette generation space. That said, I still think this better matches the intention of what users actually want rather than what they think they would want. Hopefully that makes sense.

Something I've come to understand as I kept on tinkering with salience on Lch is that when considering human perception of color, Lch kind of sucks for the perceptual accuracy I needed to get salience working. Cam16 is nearly better in always in that all components are much more, is not are, perceptually linear compared to Lch. Lch also struggles in low light, underestimates blue hues, and overestimates yellow hues. Me knowing that Cam16 fixes all of that, I find it hard to recommend continuing with Lch. I cannot think of a scenario where Lch would perform better than Cam16 unless computation is a big issue, which Cam16 most definitely has more computation. But even when testing `lch` vs `salience` on the master branch, there is little-to-no difference computation times of trials I ran; they are neck-and-neck. If we wish to change the sorting method, then we just change the heuristic on how sorting is used, no need to change the entire colorspace. I bring this up because the way salience buckets is `DeltaE` or `ImprovedDeltaE`, NOT with `salience`. The latter is exclusively used for palette sorting. `luminance`, or rather `lch`, also uses the same bucketing method. This is redundancy, and their implementation only really diverges on the sorting step, as early-sorting due to `trunc()` is no longer needed. See the next section on that. --- Because we are no longer truncating, `sort_by_score()` for `salience.post_dedup()` actually serves no purpose anymore. Honestly I think the entirety of `post_dedup()` can be removed... at least for `salience`. Again, this was here to prevent colors from being truncated during the `trunc()` step by moving more interesting colors upwards on the sort. We still need to sort the colors somewhere, either by `salience` or `luminance` as you mentioned earlier. I want to say use `salience` first as it includes lightness, colorfulness, AND hue, while `luminance` is, well, just luminance. Sure you could exclusively use `luminance` for greyscale, but it will not work for monochromatic as chroma and luminance varies there. The only place where luminance is sound (valid) is greyscale... unless the user explicitly wants to sort by luminance and not salience, I think salience sort should cover most user expectations... but then again, this whole salience thing is somewhat novel(?) in the palette generation space. That said, I still think this better matches the intention of what users actually want rather than what they think they would want. Hopefully that makes sense.
Contributor
Copy link

@explosion-mental wrote in #195 (comment):

Btw, I was trying to reach a "complete" state on the structural rewrite and realize that, there is no need of a dark/light implementation for salience, it's just based on ColorOrder, am I right? In that case, it could even make things more simpler, I was working with a new enum type with Dark and Light variants, but seems unnecessary.

This is correct... to an extent. ColorOrder is used to determine what the initial background is (light/dark).

(削除) There is a pre-sort to determine which color is closest to that generic safe initial background, then we choose the closest color, constrain it as a background, then do another sort to finalized the palette. (削除ここまで)

So it IS used, but only once and then never again.

edit - which color is closest, not which background is closest, typooo


edit - correction

Actually, the MOST PROMINENT COLOR is chosen as the background. Then, depending on if dark or light, it constrains that color accordingly, then sorts. After that, ColorOrder is never used.

@explosion-mental wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8818404: > Btw, I was trying to reach a "complete" state on the structural rewrite and realize that, there is no need of a dark/light implementation for salience, it's just based on `ColorOrder`, am I right? In that case, it could even make things more simpler, I was working with a new enum type with `Dark` and `Light` variants, but seems unnecessary. This is correct... to an extent. `ColorOrder` is used to determine what the initial background is (light/dark). ~~There is a pre-sort to determine which color is closest to that generic safe initial background, then we choose the closest color, constrain it as a background, then do another sort to finalized the palette.~~ So it IS used, but only once and then never again. edit - which color is closest, not which background is closest, typooo --- edit - correction Actually, the MOST PROMINENT COLOR is chosen as the background. Then, depending on if `dark` or `light`, it constrains that color accordingly, then sorts. After that, `ColorOrder` is never used.
Contributor
Copy link

@explosion-mental wrote in #195 (comment):

No, it's all the threshold table from the previous dynamic multithreadead implementation, so there are multiple histograms. There is a lot more not shown in the screen, but, like I said, which one is the "good" one?

You need to label which palettes belong to which thresholds. There's no way for me to determine which row belongs to which threshold.

The way I wrote the print function, how you're supposed to read it as is as follows.

ten's digit place
 | single's digit place
 | |
 v v
 0 1 2 3 4 5 6 7 8 9 <-- 0 to 9
00 01 02 03 04 05 06 07 08 09 <-- read like
colors here
 1 1 2 3 4 5 6 7 8 9 <-- 10 to 19
10 11 12 13 14 15 16 17 18 19 <-- read like
colors here

I am SO sorry I did not make that very clear. I don't know if it works properly beyond 100 colors... lol


edit addition

So I don't know what threshold you're using, but it goes all the way to the 90's digit, and possibly loops and goes beyond 100. What threshold are you using here? I would imagine you get that many colors in the histogram if you used a threshold like 5 or something.

@explosion-mental wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8818359: > No, it's all the threshold table from the previous dynamic multithreadead implementation, so there are multiple histograms. There is a lot more not shown in the screen, but, like I said, which one is the "good" one? You need to label which palettes belong to which thresholds. There's no way for me to determine which row belongs to which threshold. The way I wrote the print function, how you're supposed to read it as is as follows. ``` ten's digit place | single's digit place | | v v 0 1 2 3 4 5 6 7 8 9 <-- 0 to 9 00 01 02 03 04 05 06 07 08 09 <-- read like colors here 1 1 2 3 4 5 6 7 8 9 <-- 10 to 19 10 11 12 13 14 15 16 17 18 19 <-- read like colors here ``` I am SO sorry I did not make that very clear. I don't know if it works properly beyond 100 colors... lol --- edit addition So I don't know what threshold you're using, but it goes all the way to the 90's digit, and possibly loops and goes beyond 100. What threshold are you using here? I would imagine you get that many colors in the histogram if you used a threshold like 5 or something.
Contributor
Copy link

Here is a more useful post_dedup() for salience that properly does a pre-sort. Replace at line 242 on histogram/salience.rs.

fn post_dedup(&mutself){// Make the most prominent (most count) color as the background
// i.e. if blue themed wallpaper, then blue theme!
let(idx,_)=self.histo.iter().enumerate().max_by_key(|(_,item)|item.count).unwrap();letmax_histo=self.histo[idx];letbg=max_histo.color;letcams: Vec<Spec>=self.histo.iter().map(|h|h.color).collect();letbg=self.constrain_col_as_bg(bg);letres=constrain_col_against_cols(bg,&cams,&self.ord,&[self.bg_min_sal()]);letbg=res[0];self.histo.sort_by(|a,b|improved_salience(&a.color,&bg).partial_cmp(&improved_salience(&b.color,&bg)).unwrap_or(Ordering::Equal));#[cfg(debug_assertions)]println!("salience-sorted in post_dedup()");#[cfg(debug_assertions)]self.print_visual();}

At threshold 17, generated 20 colors (index 0-19).
image

edit addition:
Also, the second print_visual() was placed post-histo_sort(). Because it was a histo-sort and not a salience-sort, it is difficult to tell which palette is actually the better one, because it isn't actually intuitively sorted by visual color alone (as an entire table of color count, score, and salience is missing).

Try it with this and you should be able to better grasp what would be a good threshold.

Here is a more useful `post_dedup()` for `salience` that properly does a pre-sort. Replace at line 242 on `histogram/salience.rs`. ```rust fn post_dedup(&mut self) { // Make the most prominent (most count) color as the background // i.e. if blue themed wallpaper, then blue theme! let (idx, _) = self.histo.iter().enumerate().max_by_key(|(_, item)| item.count).unwrap(); let max_histo = self.histo[idx]; let bg = max_histo.color; let cams: Vec<Spec> = self.histo.iter().map(|h| h.color).collect(); let bg = self.constrain_col_as_bg(bg); let res = constrain_col_against_cols(bg, &cams, &self.ord, &[self.bg_min_sal()]); let bg = res[0]; self.histo.sort_by(|a, b| improved_salience(&a.color, &bg).partial_cmp(&improved_salience(&b.color, &bg)).unwrap_or(Ordering::Equal)); #[cfg(debug_assertions)] println!("salience-sorted in post_dedup()"); #[cfg(debug_assertions)] self.print_visual(); } ``` At threshold 17, generated 20 colors (index 0-19). ![image](/attachments/ac3921a8-83af-414f-a0e7-154c70e1d19c) edit addition: Also, the second `print_visual()` was placed post-`histo_sort()`. Because it was a histo-sort and not a salience-sort, it is difficult to tell which palette is actually the better one, because it isn't actually intuitively sorted by visual color alone (as an entire table of color count, score, and salience is missing). Try it with this and you should be able to better grasp what would be a good threshold.
105 KiB
Author
Owner
Copy link

@usaradark wrote in #195 (comment):

@explosion-mental wrote in #195 (comment):

No, it's all the threshold table from the previous dynamic multithreadead implementation, so there are multiple histograms. There is a lot more not shown in the screen, but, like I said, which one is the "good" one?

You need to label which palettes belong to which thresholds. There's no way for me to determine which row belongs to which threshold.

The way I wrote the print function, how you're supposed to read it as is as follows.

ten's digit place
 | single's digit place
 | |
 v v
 0 1 2 3 4 5 6 7 8 9 <-- 0 to 9
00 01 02 03 04 05 06 07 08 09 <-- read like
colors here
 1 1 2 3 4 5 6 7 8 9 <-- 10 to 19
10 11 12 13 14 15 16 17 18 19 <-- read like
colors here

I am SO sorry I did not make that very clear. I don't know if it works properly beyond 100 colors... lol

edit addition

So I don't know what threshold you're using, but it goes all the way to the 90's digit, and possibly loops and goes beyond 100. What threshold are you using here? I would imagine you get that many colors in the histogram if you used a threshold like 5 or something.

Let me add the threshold, it was just a demostration that, there is, in fact, a lot of "good enough" palettes created by salience, which increases the difficulty of looking for the "best one" (whatever that means).

Maybe, a mega resize to the image to get the most prominent pixels? This fails because prominent != salient necessarily. So, this question is up to test, once again.

@usaradark wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8818452: > @explosion-mental wrote in #195 (comment): > > > No, it's all the threshold table from the previous dynamic multithreadead implementation, so there are multiple histograms. There is a lot more not shown in the screen, but, like I said, which one is the "good" one? > > You need to label which palettes belong to which thresholds. There's no way for me to determine which row belongs to which threshold. > > The way I wrote the print function, how you're supposed to read it as is as follows. > > ```text > ten's digit place > | single's digit place > | | > v v > 0 1 2 3 4 5 6 7 8 9 <-- 0 to 9 > 00 01 02 03 04 05 06 07 08 09 <-- read like > colors here > 1 1 2 3 4 5 6 7 8 9 <-- 10 to 19 > 10 11 12 13 14 15 16 17 18 19 <-- read like > colors here > ``` > > I am SO sorry I did not make that very clear. I don't know if it works properly beyond 100 colors... lol > > edit addition > > So I don't know what threshold you're using, but it goes all the way to the 90's digit, and possibly loops and goes beyond 100. What threshold are you using here? I would imagine you get that many colors in the histogram if you used a threshold like 5 or something. Let me add the threshold, it was just a demostration that, there is, in fact, a lot of "good enough" palettes created by salience, which increases the difficulty of looking for the "best one" (whatever that means). Maybe, a mega resize to the image to get the most prominent pixels? This fails because prominent != salient necessarily. So, this question is up to test, once again.
Author
Owner
Copy link

@usaradark wrote in #195 (comment):

Something I've come to understand as I kept on tinkering with salience on Lch is that when considering human perception of color, Lch kind of sucks for the perceptual accuracy I needed to get salience working. Cam16 is nearly better in always in that all components are much more, is not are, perceptually linear compared to Lch. Lch also struggles in low light, underestimates blue hues, and overestimates yellow hues.

Me knowing that Cam16 fixes all of that, I find it hard to recommend continuing with Lch. I cannot think of a scenario where Lch would perform better than Cam16 unless computation is a big issue, which Cam16 most definitely has more computation. But even when testing lch vs salience on the master branch, there is little-to-no difference computation times of trials I ran; they are neck-and-neck.

If we wish to change the sorting method, then we just change the heuristic on how sorting is used, no need to change the entire colorspace.

I bring this up because the way salience buckets is DeltaE or ImprovedDeltaE, NOT with salience. The latter is exclusively used for palette sorting. luminance, or rather lch, also uses the same bucketing method. This is redundancy, and their implementation only really diverges on the sorting step, as early-sorting due to trunc() is no longer needed. See the next section on that.

Because we are no longer truncating, sort_by_score() for salience.post_dedup() actually serves no purpose anymore. Honestly I think the entirety of post_dedup() can be removed... at least for salience. Again, this was here to prevent colors from being truncated during the trunc() step by moving more interesting colors upwards on the sort.

We still need to sort the colors somewhere, either by salience or luminance as you mentioned earlier. I want to say use salience first as it includes lightness, colorfulness, AND hue, while luminance is, well, just luminance. Sure you could exclusively use luminance for greyscale, but it will not work for monochromatic as chroma and luminance varies there.

The only place where luminance is sound (valid) is greyscale... unless the user explicitly wants to sort by luminance and not salience, I think salience sort should cover most user expectations... but then again, this whole salience thing is somewhat novel(?) in the palette generation space. That said, I still think this better matches the intention of what users actually want rather than what they think they would want. Hopefully that makes sense.

That is true, and, now that I think about it, if you really want to, you could just sort Salience by lightness. Let's leave the monochromatic edge case to the end. With this conclusion, traits can be removed, and implementations can be done directly to Salience type.

@usaradark wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8818410: > Something I've come to understand as I kept on tinkering with salience on Lch is that when considering human perception of color, Lch kind of sucks for the perceptual accuracy I needed to get salience working. Cam16 is nearly better in always in that all components are much more, is not are, perceptually linear compared to Lch. Lch also struggles in low light, underestimates blue hues, and overestimates yellow hues. > > Me knowing that Cam16 fixes all of that, I find it hard to recommend continuing with Lch. I cannot think of a scenario where Lch would perform better than Cam16 unless computation is a big issue, which Cam16 most definitely has more computation. But even when testing `lch` vs `salience` on the master branch, there is little-to-no difference computation times of trials I ran; they are neck-and-neck. > > If we wish to change the sorting method, then we just change the heuristic on how sorting is used, no need to change the entire colorspace. > > I bring this up because the way salience buckets is `DeltaE` or `ImprovedDeltaE`, NOT with `salience`. The latter is exclusively used for palette sorting. `luminance`, or rather `lch`, also uses the same bucketing method. This is redundancy, and their implementation only really diverges on the sorting step, as early-sorting due to `trunc()` is no longer needed. See the next section on that. > > Because we are no longer truncating, `sort_by_score()` for `salience.post_dedup()` actually serves no purpose anymore. Honestly I think the entirety of `post_dedup()` can be removed... at least for `salience`. Again, this was here to prevent colors from being truncated during the `trunc()` step by moving more interesting colors upwards on the sort. > > We still need to sort the colors somewhere, either by `salience` or `luminance` as you mentioned earlier. I want to say use `salience` first as it includes lightness, colorfulness, AND hue, while `luminance` is, well, just luminance. Sure you could exclusively use `luminance` for greyscale, but it will not work for monochromatic as chroma and luminance varies there. > > The only place where luminance is sound (valid) is greyscale... unless the user explicitly wants to sort by luminance and not salience, I think salience sort should cover most user expectations... but then again, this whole salience thing is somewhat novel(?) in the palette generation space. That said, I still think this better matches the intention of what users actually want rather than what they think they would want. Hopefully that makes sense. That is true, and, now that I think about it, if you really want to, you could just sort Salience by lightness. Let's leave the monochromatic edge case to the end. With this conclusion, traits can be removed, and implementations can be done directly to Salience type.
add suggested change by usaradark
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
f58832dbe8
Contributor
Copy link

@explosion-mental wrote in #195 (comment):

Let me add the threshold, it was just a demostration that, there is, in fact, a lot of "good enough" palettes created by salience, which increases the difficulty of looking for the "best one" (whatever that means).

Maybe, a mega resize to the image to get the most prominent pixels? This fails because prominent != salient necessarily. So, this question is up to test, once again.

That's actually kind of crazy now that I tried it myself.

(top-half-ish)
image

This is with a threshold of 5. Wherever we sample, each color is still rather distinct from each other EXCEPT the very end where there's a lot of consecutive similar colors.

(scroll down)
image

Threshold 9 seems to have them merged, at least for this background image I'm using.
image

After testing it on several other images, 9~12 seems to be the sweet spot, with 9 being on the riskier side of not merging colors that should be merged because they visually appear similar.

I do wonder if bucketing by salience rather than DeltaE would be a good idea... initially I thought it wasn't, hence why I never did it, but I never tried...

edit - Hm... this "best threshold" is harder as it seems, as you said...

@explosion-mental wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8818569: > Let me add the threshold, it was just a demostration that, there is, in fact, a lot of "good enough" palettes created by salience, which increases the difficulty of looking for the "best one" (whatever that means). > > Maybe, a mega resize to the image to get the most prominent pixels? This fails because prominent != salient necessarily. So, this question is up to test, once again. That's actually kind of crazy now that I tried it myself. (top-half-ish) ![image](/attachments/3db1d124-87ae-4bef-bb4e-f2d038c60ba9) This is with a threshold of 5. Wherever we sample, each color is still rather distinct from each other EXCEPT the very end where there's a lot of consecutive similar colors. (scroll down) ![image](/attachments/c7bdf1e7-8cdb-486f-81e6-ac45ca31ec7f) Threshold 9 seems to have them merged, at least for this background image I'm using. ![image](/attachments/f2b397ee-34e3-49c7-9c8d-b5cd53c76e9e) After testing it on several other images, 9~12 seems to be the sweet spot, with 9 being on the riskier side of not merging colors that should be merged because they visually appear similar. I do wonder if bucketing by salience rather than `DeltaE` would be a good idea... initially I thought it wasn't, hence why I never did it, but I never tried... edit - Hm... this "best threshold" is harder as it seems, as you said...
dynamic and 'static' threshold respects cli args
Some checks failed
ci/woodpecker/push/check Pipeline failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
aeed684e52
use -k for dynamic or -t 17 or whatever threshold you want
Author
Owner
Copy link

I do wonder if bucketing by salience rather than DeltaE would be a good idea... initially I thought it wasn't, hence why I never did it, but I never tried...

Yeah, curiosity! :]

I think it can be tweaked if the changes are not "good" overall.

> I do wonder if bucketing by salience rather than DeltaE would be a good idea... initially I thought it wasn't, hence why I never did it, but I never tried... Yeah, curiosity! :] I think it can be tweaked if the changes are not "good" overall.
Contributor
Copy link

Here's an interesting experiment I just ran. Threshold for this one is 18 to try to fit it on one screen.

  • Initial bucketing
  • Lightness-sorted
  • Colorfulness-sorted
  • Hue-sorted
  • Salience-sorted

2025年12月08日T18:35:19,506923300-08:00

Of these sorting methods, which one would you best be able to tell if colors are too similar from each other and need to be merged?

For me, it's when it's hue-sorted.

But is this discovery useful? Well, this is to suggest that we more easily group (and merge) colors that are closer in hue rather than lightness or chroma. If you ask someone to group colors together, they would 99% probably group them by hue, not by lightness or colorfulness.

This may also inversely suggest hues dominate contrast more than I initially thought. This will probably be important in the next salience-calculation implementation. It's possible I may need to do similar math as I did on Lch, though using a simpler model.

I don't think this is too useful in determining the "best" threshold, as what really matters at the end is salience-sorting.

...

Actually... this is a very kind of big. For bucketing, hue is VERY important, then lightness, then colorfulness. If we do a new heuristic to improve bucketing, we would need to merge colors where hue is similar, which means a custom diff function will need to be written.

I can look to do this after this redesign gets finalized. For now, just use ImprovedDeltaE. It will work good enough.

As for the best threshold, I think the best we can do right now is to play it safe. A threshold too low can result in colors too-similar neighboring each other. This causes problems when, of course, sampling colors adjacent to each other, which is how Low, Balanced, and High works. Maybe a value of 12 is decent, but a value of 15 is probably extra safe.

Here's an interesting experiment I just ran. Threshold for this one is 18 to try to fit it on one screen. - Initial bucketing - Lightness-sorted - Colorfulness-sorted - Hue-sorted - Salience-sorted ![2025年12月08日T18:35:19,506923300-08:00](/attachments/543bd421-2a46-454c-a297-ea0563f48541) Of these sorting methods, which one would you best be able to tell if colors are too similar from each other and need to be merged? For me, it's when it's hue-sorted. But is this discovery useful? Well, this is to suggest that we more easily group (and merge) colors that are closer in hue rather than lightness or chroma. If you ask someone to group colors together, they would 99% probably group them by hue, not by lightness or colorfulness. This may also inversely suggest hues dominate contrast more than I initially thought. This will probably be important in the next salience-calculation implementation. It's possible I may need to do similar math as I did on Lch, though using a simpler model. I don't think this is too useful in determining the "best" threshold, as what really matters at the end is salience-sorting. ... Actually... this is a very kind of big. For bucketing, hue is VERY important, then lightness, then colorfulness. If we do a new heuristic to improve bucketing, we would need to merge colors where hue is similar, which means a custom `diff` function will need to be written. I can look to do this after this redesign gets finalized. For now, just use `ImprovedDeltaE`. It will work good enough. As for the best threshold, I think the best we can do right now is to play it safe. A threshold too low can result in colors too-similar neighboring each other. This causes problems when, of course, sampling colors adjacent to each other, which is how `Low`, `Balanced`, and `High` works. Maybe a value of `12` is decent, but a value of `15` is probably extra safe.
Contributor
Copy link

@explosion-mental wrote in #195 (comment):

I do wonder if bucketing by salience rather than DeltaE would be a good idea... initially I thought it wasn't, hence why I never did it, but I never tried...

Yeah, curiosity! :]

I think it can be tweaked if the changes are not "good" overall.

Bucketing by salience doesn't sit right with my gut. Salience is meant to describe how much "attention grabbing" a color is compared to another color (background). For bucketing which compares a color to all other colors and groups them, it doesn't make sense.

Histogram bucketing is essentially grouping similar colors together, and from the earlier experiment, humans intuitively group colors MUCH MORE by hue than lightness or colorfulness. I think we need a very specific function, like is_similar_to() that puts much more weight on hue than lightness or colorfulness. DeltaE and ImprovedDeltaE do a good job at mathematically and realistically providing a value of difference, and even though Cam16Ucs has components that are perceptually linear, its DeltaE weights all components equally, which may not be applicable to what we're doing.

The scope grows...

edit - added context by quote

@explosion-mental wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8818761: > > I do wonder if bucketing by salience rather than DeltaE would be a good idea... initially I thought it wasn't, hence why I never did it, but I never tried... > > Yeah, curiosity! :] > > I think it can be tweaked if the changes are not "good" overall. Bucketing by salience doesn't sit right with my gut. Salience is meant to describe how much "attention grabbing" a color is compared to another color (`background`). For bucketing which compares a color to all other colors and groups them, it doesn't make sense. Histogram bucketing is essentially grouping similar colors together, and from the earlier experiment, humans intuitively group colors MUCH MORE by hue than lightness or colorfulness. I think we need a very specific function, like `is_similar_to()` that puts much more weight on hue than lightness or colorfulness. `DeltaE` and `ImprovedDeltaE` do a good job at mathematically and realistically providing a value of difference, and even though `Cam16Ucs` has components that are perceptually linear, its `DeltaE` weights all components equally, which may not be applicable to what we're doing. The scope grows... edit - added context by quote
Author
Owner
Copy link

@usaradark wrote in #195 (comment):

Here's an interesting experiment I just ran. Threshold for this one is 18 to try to fit it on one screen.

* Initial bucketing
* Lightness-sorted
* Colorfulness-sorted
* Hue-sorted
* Salience-sorted

2025年12月08日T18:35:19,506923300-08:00

Of these sorting methods, which one would you best be able to tell if colors are too similar from each other and need to be merged?

For me, it's when it's hue-sorted.

But is this discovery useful? Well, this is to suggest that we more easily group (and merge) colors that are closer in hue rather than lightness or chroma. If you ask someone to group colors together, they would 99% probably group them by hue, not by lightness or colorfulness.

This may also inversely suggest hues dominate contrast more than I initially thought. This will probably be important in the next salience-calculation implementation. It's possible I may need to do similar math as I did on Lch, though using a simpler model.

I don't think this is too useful in determining the "best" threshold, as what really matters at the end is salience-sorting.

...

Actually... this is a very kind of big. For bucketing, hue is VERY important, then lightness, then colorfulness. If we do a new heuristic to improve bucketing, we would need to merge colors where hue is similar, which means a custom diff function will need to be written.

I can look to do this after this redesign gets finalized. For now, just use ImprovedDeltaE. It will work good enough.

As for the best threshold, I think the best we can do right now is to play it safe. A threshold too low can result in colors too-similar neighboring each other. This causes problems when, of course, sampling colors adjacent to each other, which is how Low, Balanced, and High works. Maybe a value of 12 is decent, but a value of 15 is probably extra safe.

I kind of got this result with the lightness one, as my testing increased with changing from LAB to LCH. I like the salience sort btw. These algos can have an interesting use cases, maybe use darker color for a variant with dark colors, like my harddark old impl.

@usaradark wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8819223: > Here's an interesting experiment I just ran. Threshold for this one is 18 to try to fit it on one screen. > > * Initial bucketing > > * Lightness-sorted > > * Colorfulness-sorted > > * Hue-sorted > > * Salience-sorted > > > [![2025年12月08日T18:35:19,506923300-08:00](/attachments/543bd421-2a46-454c-a297-ea0563f48541)](/explosion-mental/wallust/attachments/543bd421-2a46-454c-a297-ea0563f48541) > > Of these sorting methods, which one would you best be able to tell if colors are too similar from each other and need to be merged? > > For me, it's when it's hue-sorted. > > But is this discovery useful? Well, this is to suggest that we more easily group (and merge) colors that are closer in hue rather than lightness or chroma. If you ask someone to group colors together, they would 99% probably group them by hue, not by lightness or colorfulness. > > This may also inversely suggest hues dominate contrast more than I initially thought. This will probably be important in the next salience-calculation implementation. It's possible I may need to do similar math as I did on Lch, though using a simpler model. > > I don't think this is too useful in determining the "best" threshold, as what really matters at the end is salience-sorting. > > ... > > Actually... this is a very kind of big. For bucketing, hue is VERY important, then lightness, then colorfulness. If we do a new heuristic to improve bucketing, we would need to merge colors where hue is similar, which means a custom `diff` function will need to be written. > > I can look to do this after this redesign gets finalized. For now, just use `ImprovedDeltaE`. It will work good enough. > > As for the best threshold, I think the best we can do right now is to play it safe. A threshold too low can result in colors too-similar neighboring each other. This causes problems when, of course, sampling colors adjacent to each other, which is how `Low`, `Balanced`, and `High` works. Maybe a value of `12` is decent, but a value of `15` is probably extra safe. I kind of got this result with the lightness one, as my testing increased with changing from LAB to LCH. I like the salience sort btw. These algos can have an interesting use cases, maybe use darker color for a variant with dark colors, like my `harddark` old impl.
Author
Owner
Copy link

I can look to do this after this redesign gets finalized. For now, just use ImprovedDeltaE. It will work good enough.

This will sit no worries. You can PR to the new-histo branch and continue. There are some caveats I need to address as well

> I can look to do this after this redesign gets finalized. For now, just use ImprovedDeltaE. It will work good enough. This will sit no worries. You can PR to the `new-histo` branch and continue. There are some caveats I need to address as well
Author
Owner
Copy link

BTW, since there are many colors, would be interesting to get different 16 colors. Not all can be different, like the 16 colors filter, they could simple be a variation of the latter. Just a thought. Right now, I'm not sure if I should remove dedup and/or truncate overall ? Given that you are editing it

BTW, since there are many colors, would be interesting to get different 16 colors. Not all can be different, like the 16 colors filter, they could simple be a variation of the latter. Just a thought. Right now, I'm not sure if I should remove dedup and/or truncate overall ? Given that you are editing it
Contributor
Copy link

@explosion-mental wrote in #195 (comment):

BTW, since there are many colors, would be interesting to get different 16 colors. Not all can be different, like the 16 colors filter, they could simple be a variation of the latter. Just a thought. Right now, I'm not sure if I should remove dedup and/or truncate overall ? Given that you are editing it

I won't be touching that tonight. I'll be working on a better bucketing heuristic, so feel free to continue to reorganize things. I will be making an entirely new function, like Spec.similar_to() for bucketing purposes.

I think it's good that we have many colors. Especially since they are all actually somewhat distinct; salience-sort is doing it's job. It's just that the merge should be better. Merge is not the same as salience sort.

I like the idea of having slightly different variations of colors for an image. If someone really doesn't like the colors samples, easy, they just need to sample very slightly differently to get a similar effect. I know I've had a similar issue where the colors picked are like "bleh."

@explosion-mental wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8819343: > BTW, since there are many colors, would be interesting to get different 16 colors. Not all can be different, like the 16 colors filter, they could simple be a variation of the latter. Just a thought. Right now, I'm not sure if I should remove dedup and/or truncate overall ? Given that you are editing it I won't be touching that tonight. I'll be working on a better bucketing heuristic, so feel free to continue to reorganize things. I will be making an entirely new function, like `Spec.similar_to()` for bucketing purposes. I think it's good that we have many colors. Especially since they are all actually somewhat distinct; salience-sort is doing it's job. It's just that the merge should be better. Merge is not the same as salience sort. I like the idea of having slightly different variations of colors for an image. If someone really doesn't like the colors samples, easy, they just need to sample very slightly differently to get a similar effect. I know I've had a similar issue where the colors picked are like "bleh."
clean up: remove luminance impl, salience can do lightness by itself
Some checks failed
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
de7bf6c7ef
order differences colors into a trait
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
c4ac151b17
to linear or not..
PR automatically created by Woodpecker CI
Co-authored-by: WoodpeckerCI <woodpecker@codeberg.org>
Reviewed-on: #193 
flake.nix fix #196
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
ba2a0bdc2e
Update to use the monthly updated rust toolchain
https://mynixos.com/fenix 
Contributor
Copy link

Because we're using DeltaE rather than Improved, make sure you are using a threshold higher than you would usually use. If you usually use 15, try bumping that up to 30+.

ImprovedDeltaE soft-caps around 20. An Improved value of 20 in DeltaE might be around 50. iirc black vs white results in a DeltaE of close to 100. I would have to validate this, it's based on rough memory and a one-off experiment.

Because we're using `DeltaE` rather than `Improved`, make sure you are using a threshold higher than you would usually use. If you usually use 15, try bumping that up to 30+. `ImprovedDeltaE` soft-caps around 20. An `Improved` value of 20 in `DeltaE` might be around 50. iirc black vs white results in a `DeltaE` of close to 100. I would have to validate this, it's based on rough memory and a one-off experiment.
clean ups and move Sampling to palettes
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
2de7d59046
Specialized heuristic for bucketing colors and filter pre-bucketing ( #197 )
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/docs Pipeline failed
ci/woodpecker/pr/nix-build Pipeline was successful
f8e49ec211
This pull request serves to bucket colors more intuitively to how humans perceive the difference in colors.
Currently, colors are bucketed via `DeltaE` or its `Improved` variant. `DeltaE` describes the mathematical difference between a color's components with another color. While this is mathematically accurate, it isn't perceptually accurate or intuitive to how humans would bucket colors. This results in bucketing where neighboring similar-appearing colors of the same hue are not bucketed due to differing lightness or colorfuless.
This PR proposes a specialized heuristic for bucketing colors. The general idea is is that humans tend to bucket (group) colors based on hue first, then lightness, then colorfulness. The latter two components may be swapped depending on results. The actual implementation idea is to put less weight on hue differences when doing `diff()`, though when walking through the code, `diff()` might actually make more sense to be named `similar()` based on what it is calculating. I will elaborate more on this once I better understand what the actual logic and intentions are.
Reviewed-on: #197
Co-authored-by: uwidev <btyang24@gmail.com>
Co-committed-by: uwidev <btyang24@gmail.com>
Contributor
Copy link

Was there a specific reason why print_visual() was implemented specifically on Salience and not as a generic pub fn? I'm having some issues utilizing it to validate that salience values are correct outside of the Salience.histo context.

Was there a specific reason why `print_visual()` was implemented specifically on `Salience` and not as a generic `pub fn`? I'm having some issues utilizing it to validate that salience values are correct outside of the `Salience.histo` context.
Author
Owner
Copy link

@usaradark wrote in #195 (comment):

Was there a specific reason why print_visual() was implemented specifically on Salience and not as a generic pub fn? I'm having some issues utilizing it to validate that salience values are correct outside of the Salience.histo context.

Nope, easy of use really.

@usaradark wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8865516: > Was there a specific reason why `print_visual()` was implemented specifically on `Salience` and not as a generic `pub fn`? I'm having some issues utilizing it to validate that salience values are correct outside of the `Salience.histo` context. Nope, easy of use really.
fix sadness
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
8d40bd2e3d
chore: simple refactors
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
dcfed22843
TODO new name
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
7850f33d39
while it acts for the salience methods, it's thought as the intermediate
represenattion of colors in between raw bytes and a whole palette
remove all unnused stuff
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
558f9d862f
implement variants: new variant ansi-dark
All checks were successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline was successful
5b5aae92b9
add config parameters to interact with Salience
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
5fd91428ac
and change salience with ansi
essentials into the mod.rs, show important stuff at the top, and other
refactors. No behaviour change
-1 TODO, remove clone, use retain. Remove post_read()
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
b4d450bc78
This allows a much simpler control with adding stuff and handling the
configurations of the internals. Also, it's more clearer what is used
where (in the histogram module, specifically).
fix up multithreading approach. But requires more investigation
Some checks failed
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/pr/check Pipeline was successful
ci/woodpecker/pr/nix-build Pipeline failed
5d3b7dfa95
fix up caching with the new implementations
Some checks failed
ci/woodpecker/push/check Pipeline failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
488b845acb
remove colorspace module completely
Some checks failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/push/check Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
74c3d4137f
versions. Temporarily allow unused
Older wallust version used the hard/soft terms. Now, "soft" is pastel
and "hard" is vibrant.
remove truncation
Some checks failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/push/check Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
0a3c453598
Given the nature of how salience chooses colors, with the Sampling enum,
it doesn't require to return exactly 16 colors, or even 6, since they
are choosen by centroids/median or whatever the Sampling is.
the palette itself
with salience config, I've reached every case of the old Palettes
Some checks failed
ci/woodpecker/push/check Pipeline failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
d163e111a7
It isn't entirely correct because the usual Dark (also meaning SoftDark,
HardDark) has been replaced by salience approach. So the `Salience` +
something variants are the default ones. The `Ansi` variant is now
another mode, since the palette keyword in the config has either salience or
ansi.
Some examples:
With
```toml
style = "dark"
salience = { sampling = "balanced", intensity = "normal" }
```
have SalienceDark
If I use
```toml
style = "dark"
salience = { sampling = "distributed", intensity = "pastel" }
use16cols = true
```
it results in SalienceDarkDistributed16 + SoftDark
and so on. We can finally 'let this be'
Author
Owner
Copy link

Now that I think about it, I might push this into another branch instead of master, and have something like v4-alpha, given that config and cli flags are still subject to change. If I find more "modes" or more "palettes" (in the new definition), like the "nord" like them I mentioned, it might require more tweaking, meaning more specialized internals or exposure etc etc.

tldr; probably doing an alpha testing for tinkerers since user configs might change.

Now that I think about it, I might push this into another branch instead of master, and have something like v4-alpha, given that config and cli flags are still subject to change. If I find more "modes" or more "palettes" (in the new definition), like the "nord" like them I mentioned, it might require more tweaking, meaning more specialized internals or exposure etc etc. tldr; probably doing an alpha testing for tinkerers since user configs might change.
the struct has potential to be used outside of simple salience
gatherings
move some comments
Some checks failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/pr/docs Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed
bcd520df19
Author
Owner
Copy link

@explosion-mental wrote in #195 (comment):

The goal of this PR is to remove obstacles for the sampling and better gathering of the colors, making a much more pleasant and "readable" (see #188) colorscheme palettes. This change, requires a lot of internals changes, API breaks, and so on. This is why, it marks a tremendous change code wise, but also in the results perceived as a user.

I've achieve this goal basically for histogram. I'm reusing this branch for a "v4-alpha pre-release", given that it's basically complete. This is because there are other stuff to clean up given the removal of colorspaces and the replacement of palettes. Also, some changes could affect the histogram, so the branch should cover the changes.

Some comments to save for future work:

For a "Nord like" theme: #195 (comment)

About the threshold scaling: #195 (comment)

@explosion-mental wrote in https://codeberg.org/explosion-mental/wallust/pulls/195#issue-2789661: > The goal of this PR is to remove obstacles for the sampling and better gathering of the colors, making a much more pleasant and "readable" (see #188) colorscheme palettes. This change, requires a lot of internals changes, API breaks, and so on. This is why, it marks a tremendous change code wise, but also in the results perceived as a user. I've achieve this goal basically for histogram. I'm reusing this branch for a "v4-alpha pre-release", given that it's basically complete. This is because there are other stuff to clean up given the removal of colorspaces and the replacement of palettes. Also, some changes could affect the histogram, so the branch should cover the changes. Some comments to save for future work: For a "Nord like" theme: https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8680299 About the threshold scaling: https://codeberg.org/explosion-mental/wallust/pulls/195#issuecomment-8786838
Some checks failed
ci/woodpecker/pr/check Pipeline failed
ci/woodpecker/pr/docs Pipeline failed
ci/woodpecker/pr/nix-build Pipeline failed

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No reviewers
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
explosion-mental/wallust!195
Reference in a new issue
explosion-mental/wallust
No description provided.
Delete branch "master"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?