Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the HyAB color difference metric #328

Merged
merged 1 commit into from
May 7, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions palette/src/color_difference.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
//! Algorithms for calculating the difference between colors.
//!
//! ## Selecting an algorithm
//!
//! Different distance/difference algorithms and formulae are good for different
//! situations. Some are faster but less accurate and some may only be suitable
//! for certain color spaces. This table may help navigating the options a bit
//! by summarizing the difference between the traits in this module.
//!
//! **Disclaimer:** _This is not an actual benchmark! It's always best to test and
//! evaluate the differences in an actual application, when possible._
//!
//! Property explanations:
//! - **Complexity:** Low complexity options are generally faster than high
//! complexity options.
//! - **Accuracy:** How the numerical difference compares to the perceived
//! difference. May differ with the color space.
//!
//! | Trait | Complexity | Accuracy | Notes |
//! |-------|------------|----------|-------|
//! | [`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.
//! | [`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.
//! | [`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.
//! | [`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.

use core::ops::{Add, BitAnd, BitOr, Div};

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

/// Calculate a combination of Euclidean and Manhattan/City-block distance
/// between two colors.
///
/// The HyAB distance was suggested as an alternative to CIEDE2000 for large
/// color differences in [Distance metrics for very large color
/// differences](http://markfairchild.org/PDFs/PAP40.pdf) (in [Color Res Appl.
/// 2019;1–16](https://doi.org/10.1002/col.22451)) by Saeedeh Abasi, Mohammad
/// Amani Tehran and Mark D. Fairchild. It's originally meant for [CIE L\*a\*b\*
/// (CIELAB)][crate::Lab], but this trait is also implemented for other color
/// spaces that have similar semantics, although **without the same quality
/// guarantees**.
///
/// The hybrid distance is the sum of the absolute lightness difference and the
/// distance on the chroma plane. This makes the lightness and chroma
/// differences more independent from each other, which is meant to correspond
/// more to how humans perceive the two qualities.
pub trait HyAb {
/// The type for the distance value.
type Scalar;

/// Calculate the hybrid distance between `self` and `other`.
///
/// This returns the sum of the absolute lightness difference and the
/// distance on the chroma plane.
fn hybrid_distance(self, other: Self) -> Self::Scalar;
}

#[cfg(test)]
mod test {
use core::str::FromStr;

use super::Wcag21RelativeContrast;
use crate::Srgb;
use super::{HyAb, Wcag21RelativeContrast};
use crate::{FromColor, Lab, Srgb};

#[test]
fn relative_contrast() {
@@ -429,4 +479,16 @@ mod test {
assert_relative_eq!(c1.relative_contrast(white), 1.22, epsilon = 0.01);
assert_relative_eq!(c1.relative_contrast(black), 17.11, epsilon = 0.01);
}

#[test]
fn hyab() {
// From https://github.com/Evercoder/culori/blob/cd1fe08a12fa9ddfcf6b2e82914733d23ac117d0/test/difference.test.js#L186
let red = Lab::<_, f64>::from_color(Srgb::from(0xff0000).into_linear());
let green = Lab::<_, f64>::from_color(Srgb::from(0x008000).into_linear());
assert_relative_eq!(
red.hybrid_distance(green),
139.93576718451553,
epsilon = 0.000001
);
}
}
1 change: 1 addition & 0 deletions palette/src/lab.rs
Original file line number Diff line number Diff line change
@@ -297,6 +297,7 @@ impl_mix!(Lab<Wp>);
impl_lighten!(Lab<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {a, b} phantom: white_point);
impl_premultiply!(Lab<Wp> {l, a, b} phantom: white_point);
impl_euclidean_distance!(Lab<Wp> {l, a, b});
impl_hyab!(Lab<Wp> {lightness: l, chroma1: a, chroma2: b});

impl<Wp, T> GetHue for Lab<Wp, T>
where
1 change: 1 addition & 0 deletions palette/src/luv.rs
Original file line number Diff line number Diff line change
@@ -305,6 +305,7 @@ impl_mix!(Luv<Wp>);
impl_lighten!(Luv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {u, v} phantom: white_point);
impl_premultiply!(Luv<Wp> {l, u, v} phantom: white_point);
impl_euclidean_distance!(Luv<Wp> {l, u, v});
impl_hyab!(Luv<Wp> {lightness: l, chroma1: u, chroma2: v});

impl<Wp, T> GetHue for Luv<Wp, T>
where
33 changes: 33 additions & 0 deletions palette/src/macros/color_difference.rs
Original file line number Diff line number Diff line change
@@ -29,3 +29,36 @@ macro_rules! impl_euclidean_distance {
}
};
}

macro_rules! impl_hyab {
(
$ty: ident
{$($components: tt)+}
$(where $($where: tt)+)?
) => {
// add empty generics brackets
impl_hyab!($ty<> {$($components)+} $(where $($where)+)?);
};
(
$ty: ident <$($ty_param: ident),*>
{lightness: $lightness:ident, chroma1: $chroma1:ident, chroma2: $chroma2:ident $(,)? }
$(where $($where: tt)+)?
) => {
impl<$($ty_param,)* T> crate::color_difference::HyAb for $ty<$($ty_param,)* T>
where
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,
$($($where)+)?
{
type Scalar = T;

#[inline]
fn hybrid_distance(self, other: Self) -> Self::Scalar {
let lightness = self.$lightness - other.$lightness;
let chroma1 = self.$chroma1 - other.$chroma1;
let chroma2 = self.$chroma2 - other.$chroma2;

lightness.abs() + (chroma1.clone() * chroma1 + chroma2.clone() * chroma2).sqrt()
}
}
};
}
5 changes: 5 additions & 0 deletions palette/src/oklab/properties.rs
Original file line number Diff line number Diff line change
@@ -54,6 +54,11 @@ impl_mix!(Oklab);
impl_lighten!(Oklab increase {l => [Self::min_l(), Self::max_l()]} other {a, b} where T: One);
impl_premultiply!(Oklab { l, a, b });
impl_euclidean_distance!(Oklab { l, a, b });
impl_hyab!(Oklab {
lightness: l,
chroma1: a,
chroma2: b
});

impl<T> GetHue for Oklab<T>
where