Skip to content

Commit 3d7f618

Browse files
committed
Add the HyAB color difference metric
1 parent a552a3c commit 3d7f618

File tree

5 files changed

+104
-2
lines changed

5 files changed

+104
-2
lines changed

palette/src/color_difference.rs

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
//! Algorithms for calculating the difference between colors.
2+
//!
3+
//! ## Selecting an algorithm
4+
//!
5+
//! Different distance/difference algorithms and formulae are good for different
6+
//! situations. Some are faster but less accurate and some may only be suitable
7+
//! for certain color spaces. This table may help navigating the options a bit
8+
//! by summarizing the difference between the traits in this module.
9+
//!
10+
//! **Disclaimer:** _This is not an actual benchmark! It's always best to test and
11+
//! evaluate the differences in an actual application, when possible._
12+
//!
13+
//! Property explanations:
14+
//! - **Complexity:** Low complexity options are generally faster than high
15+
//! complexity options.
16+
//! - **Accuracy:** How the numerical difference compares to the perceived
17+
//! difference. May differ with the color space.
18+
//!
19+
//! | Trait | Complexity | Accuracy | Notes |
20+
//! |-------|------------|----------|-------|
21+
//! | [`Ciede2000`] | High | High for small differences, lower for large differences | The de-facto standard, but requires complex calculations to compensate for increased errors in certain areas of the CIE L\*a\*b\* (CIELAB) space.
22+
//! | [`EuclideanDistance`] | Low | Medium to high for perceptually uniform spaces, otherwise low | Can be good enough for perceptually uniform spaces or as a "quick and dirty" check.
23+
//! | [`HyAb`] | Low | High accuracy for medium to large differences. Less accurate than CIEDE2000 for small differences, but still performs well and is much less computationally expensive. | Similar to Euclidean distance, but separates lightness and chroma more. Limited to Cartesian spaces with a lightness axis and a chroma plane.
24+
//! | [`Wcag21RelativeContrast`] | Low | Low and only compares lightness | Meant for checking contrasts in computer graphics (such as between text and background colors), assuming sRGB. Mostly useful as a hint or for checking WCAG 2.1 compliance, considering the criticism it has received.
225
326
use core::ops::{Add, BitAnd, BitOr, Div};
427

@@ -384,12 +407,39 @@ pub trait Wcag21RelativeContrast: Sized {
384407
}
385408
}
386409

410+
/// Calculate a combination of Euclidean and Manhattan/City-block distance
411+
/// between two colors.
412+
///
413+
/// The HyAB distance was suggested as an alternative to CIEDE2000 for large
414+
/// color differences in [Distance metrics for very large color
415+
/// differences](http://markfairchild.org/PDFs/PAP40.pdf) (in [Color Res Appl.
416+
/// 2019;1–16](https://doi.org/10.1002/col.22451)) by Saeedeh Abasi, Mohammad
417+
/// Amani Tehran and Mark D. Fairchild. It's originally meant for [CIE L\*a\*b\*
418+
/// (CIELAB)][crate::Lab], but this trait is also implemented for other color
419+
/// spaces that have similar semantics, although **without the same quality
420+
/// guarantees**.
421+
///
422+
/// The hybrid distance is the sum of the absolute lightness difference and the
423+
/// distance on the chroma plane. This makes the lightness and chroma
424+
/// differences more independent from each other, which is meant to correspond
425+
/// more to how humans perceive the two qualities.
426+
pub trait HyAb {
427+
/// The type for the distance value.
428+
type Scalar;
429+
430+
/// Calculate the hybrid distance between `self` and `other`.
431+
///
432+
/// This returns the sum of the absolute lightness difference and the
433+
/// distance on the chroma plane.
434+
fn hybrid_distance(self, other: Self) -> Self::Scalar;
435+
}
436+
387437
#[cfg(test)]
388438
mod test {
389439
use core::str::FromStr;
390440

391-
use super::Wcag21RelativeContrast;
392-
use crate::Srgb;
441+
use super::{HyAb, Wcag21RelativeContrast};
442+
use crate::{FromColor, Lab, Srgb};
393443

394444
#[test]
395445
fn relative_contrast() {
@@ -429,4 +479,16 @@ mod test {
429479
assert_relative_eq!(c1.relative_contrast(white), 1.22, epsilon = 0.01);
430480
assert_relative_eq!(c1.relative_contrast(black), 17.11, epsilon = 0.01);
431481
}
482+
483+
#[test]
484+
fn hyab() {
485+
// From https://github.com/Evercoder/culori/blob/cd1fe08a12fa9ddfcf6b2e82914733d23ac117d0/test/difference.test.js#L186
486+
let red = Lab::<_, f64>::from_color(Srgb::from(0xff0000).into_linear());
487+
let green = Lab::<_, f64>::from_color(Srgb::from(0x008000).into_linear());
488+
assert_relative_eq!(
489+
red.hybrid_distance(green),
490+
139.93576718451553,
491+
epsilon = 0.000001
492+
);
493+
}
432494
}

palette/src/lab.rs

+1
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ impl_mix!(Lab<Wp>);
297297
impl_lighten!(Lab<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {a, b} phantom: white_point);
298298
impl_premultiply!(Lab<Wp> {l, a, b} phantom: white_point);
299299
impl_euclidean_distance!(Lab<Wp> {l, a, b});
300+
impl_hyab!(Lab<Wp> {lightness: l, chroma1: a, chroma2: b});
300301

301302
impl<Wp, T> GetHue for Lab<Wp, T>
302303
where

palette/src/luv.rs

+1
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ impl_mix!(Luv<Wp>);
305305
impl_lighten!(Luv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {u, v} phantom: white_point);
306306
impl_premultiply!(Luv<Wp> {l, u, v} phantom: white_point);
307307
impl_euclidean_distance!(Luv<Wp> {l, u, v});
308+
impl_hyab!(Luv<Wp> {lightness: l, chroma1: u, chroma2: v});
308309

309310
impl<Wp, T> GetHue for Luv<Wp, T>
310311
where

palette/src/macros/color_difference.rs

+33
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,36 @@ macro_rules! impl_euclidean_distance {
2929
}
3030
};
3131
}
32+
33+
macro_rules! impl_hyab {
34+
(
35+
$ty: ident
36+
{$($components: tt)+}
37+
$(where $($where: tt)+)?
38+
) => {
39+
// add empty generics brackets
40+
impl_hyab!($ty<> {$($components)+} $(where $($where)+)?);
41+
};
42+
(
43+
$ty: ident <$($ty_param: ident),*>
44+
{lightness: $lightness:ident, chroma1: $chroma1:ident, chroma2: $chroma2:ident $(,)? }
45+
$(where $($where: tt)+)?
46+
) => {
47+
impl<$($ty_param,)* T> crate::color_difference::HyAb for $ty<$($ty_param,)* T>
48+
where
49+
T: self::num::Real + self::num::Abs + self::num::Sqrt + core::ops::Sub<T, Output=T> + core::ops::Add<T, Output=T> + core::ops::Mul<T, Output=T> + Clone,
50+
$($($where)+)?
51+
{
52+
type Scalar = T;
53+
54+
#[inline]
55+
fn hybrid_distance(self, other: Self) -> Self::Scalar {
56+
let lightness = self.$lightness - other.$lightness;
57+
let chroma1 = self.$chroma1 - other.$chroma1;
58+
let chroma2 = self.$chroma2 - other.$chroma2;
59+
60+
lightness.abs() + (chroma1.clone() * chroma1 + chroma2.clone() * chroma2).sqrt()
61+
}
62+
}
63+
};
64+
}

palette/src/oklab/properties.rs

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ impl_mix!(Oklab);
5454
impl_lighten!(Oklab increase {l => [Self::min_l(), Self::max_l()]} other {a, b} where T: One);
5555
impl_premultiply!(Oklab { l, a, b });
5656
impl_euclidean_distance!(Oklab { l, a, b });
57+
impl_hyab!(Oklab {
58+
lightness: l,
59+
chroma1: a,
60+
chroma2: b
61+
});
5762

5863
impl<T> GetHue for Oklab<T>
5964
where

0 commit comments

Comments
 (0)