use palette::{Srgb, Xyz, convert::IntoColor, white_point::D65};
use crate::constants::*;
use crate::error::{MunsellError, Result};
const THRESHOLD_INTEGER: f64 = 1e-3; #[allow(dead_code)]
const TOLERANCE_ABSOLUTE_DEFAULT: f64 = 1e-8;
const MAX_OUTER_ITERATIONS: usize = 64;
const MAX_INNER_ITERATIONS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Illuminant {
A,
C,
E,
D50,
D55,
D65,
D75,
F2,
F7,
F11,
}
impl Illuminant {
pub fn white_point(&self) -> [f64; 3] {
match self {
Illuminant::A => ILLUMINANT_A_XYZ,
Illuminant::C => ILLUMINANT_C_XYZ,
Illuminant::E => ILLUMINANT_E_XYZ,
Illuminant::D50 => ILLUMINANT_D50_XYZ,
Illuminant::D55 => ILLUMINANT_D55_XYZ,
Illuminant::D65 => ILLUMINANT_D65_XYZ,
Illuminant::D75 => ILLUMINANT_D75_XYZ,
Illuminant::F2 => ILLUMINANT_F2_XYZ,
Illuminant::F7 => ILLUMINANT_F7_XYZ,
Illuminant::F11 => ILLUMINANT_F11_XYZ,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChromaticAdaptation {
Bradford,
XYZScaling,
CAT02,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MunsellSpecification {
pub hue: f64,
pub family: String,
pub value: f64,
pub chroma: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CieXyY {
pub x: f64,
pub y: f64,
pub y_luminance: f64,
}
mod coordinate_transforms {
#[inline]
pub fn cartesian_to_polar(x: f64, y: f64) -> (f64, f64) {
let rho = x.hypot(y); let phi = y.atan2(x); (rho, phi)
}
#[inline]
pub fn polar_to_cartesian(rho: f64, phi: f64) -> (f64, f64) {
let x = rho * phi.cos();
let y = rho * phi.sin();
(x, y)
}
#[inline]
pub fn cartesian_to_cylindrical(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
let (rho, phi) = cartesian_to_polar(x, y);
(rho, phi, z)
}
#[inline]
#[allow(dead_code)]
pub fn cylindrical_to_cartesian(rho: f64, phi: f64, z: f64) -> (f64, f64, f64) {
let (x, y) = polar_to_cartesian(rho, phi);
(x, y, z)
}
}
mod hue_conversions {
#[allow(dead_code)]
const HUE_FAMILY_CODES: [(u8, &str); 10] = [
(1, "BG"), (2, "G"), (3, "GY"), (4, "Y"), (5, "YR"),
(6, "R"), (7, "RP"), (8, "P"), (9, "PB"), (10, "B")
];
#[inline]
pub fn hue_to_astm_hue(hue: f64, code: u8) -> f64 {
let raw = (17.0 - code as f64) % 10.0 + (hue * 0.1) - 0.5;
let single_hue = ((raw % 10.0) + 10.0) % 10.0;
linear_interpolate_hue_angle(single_hue)
}
#[inline]
pub fn hue_angle_to_hue(hue_angle: f64) -> (f64, u8) {
let single_hue = reverse_interpolate_hue_angle(hue_angle);
let code = if single_hue <= 0.5 { 7 } else if single_hue <= 1.5 { 6 } else if single_hue <= 2.5 { 5 } else if single_hue <= 3.5 { 4 } else if single_hue <= 4.5 { 3 } else if single_hue <= 5.5 { 2 } else if single_hue <= 6.5 { 1 } else if single_hue <= 7.5 { 10 } else if single_hue <= 8.5 { 9 } else if single_hue <= 9.5 { 8 } else { 7 };
let hue = (10.0 * (single_hue % 1.0) + 5.0) % 10.0;
let final_hue = if hue == 0.0 { 10.0 } else { hue };
(final_hue, code)
}
#[inline]
fn linear_interpolate_hue_angle(single_hue: f64) -> f64 {
let breakpoints = [0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0, 10.0];
let angles = [0.0, 45.0, 70.0, 135.0, 160.0, 225.0, 255.0, 315.0, 360.0];
for i in 0..breakpoints.len()-1 {
if single_hue >= breakpoints[i] && single_hue <= breakpoints[i+1] {
let t = (single_hue - breakpoints[i]) / (breakpoints[i+1] - breakpoints[i]);
return angles[i] + t * (angles[i+1] - angles[i]);
}
}
360.0
}
#[inline]
fn reverse_interpolate_hue_angle(hue_angle: f64) -> f64 {
let angles = [0.0, 45.0, 70.0, 135.0, 160.0, 225.0, 255.0, 315.0, 360.0];
let breakpoints = [0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0, 10.0];
for i in 0..angles.len()-1 {
if hue_angle >= angles[i] && hue_angle <= angles[i+1] {
let t = (hue_angle - angles[i]) / (angles[i+1] - angles[i]);
return breakpoints[i] + t * (breakpoints[i+1] - breakpoints[i]);
}
}
0.0
}
pub fn bounding_hues_from_renotation(hue: f64, code: u8) -> ((f64, u8), (f64, u8)) {
let mut hue_cw: f64;
let code_cw: u8;
let mut hue_ccw: f64;
let code_ccw: u8;
if (hue % 2.5 - 0.0).abs() < 1e-10 {
if (hue - 0.0).abs() < 1e-10 {
hue_cw = 10.0;
code_cw = match code {
1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 8, 8 => 9, 9 => 10, 10 => 1, _ => code };
} else {
hue_cw = hue;
code_cw = code;
}
hue_ccw = hue_cw;
code_ccw = code_cw;
} else {
hue_cw = 2.5 * (hue / 2.5).floor();
hue_ccw = (hue_cw + 2.5) % 10.0;
if (hue_ccw - 0.0).abs() < 1e-10 {
hue_ccw = 10.0;
}
if (hue_cw - 0.0).abs() < 1e-10 {
hue_cw = 10.0;
code_cw = match code {
1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 8, 8 => 9, 9 => 10, 10 => 1, _ => code };
} else {
code_cw = code;
}
code_ccw = code;
}
((hue_cw, code_cw), (hue_ccw, code_ccw))
}
pub fn code_to_family(code: u8) -> &'static str {
match code {
1 => "B",
2 => "BG",
3 => "G",
4 => "GY",
5 => "Y",
6 => "YR",
7 => "R",
8 => "RP",
9 => "P",
10 => "PB",
_ => "N" }
}
#[allow(dead_code)]
pub fn family_to_code(family: &str) -> u8 {
match family {
"B" => 1,
"BG" => 2,
"G" => 3,
"GY" => 4,
"Y" => 5,
"YR" => 6,
"R" => 7,
"RP" => 8,
"P" => 9,
"PB" => 10,
_ => 0 }
}
pub fn hue_to_hue_angle(hue: f64, code: u8) -> f64 {
let single_hue = ((18.0 - code as f64) % 10.0 + (hue / 10.0) - 0.5) % 10.0;
linear_interpolate_hue_angle(single_hue)
}
}
#[allow(dead_code)]
mod interpolation_methods {
use super::hue_conversions::*;
#[derive(Debug, Clone, PartialEq)]
pub enum InterpolationMethod {
None, Linear, Radial, }
pub fn interpolation_method_from_renotation_ovoid(hue: f64, value: f64, chroma: f64, code: u8) -> InterpolationMethod {
if chroma == 0.0 {
return InterpolationMethod::None;
}
let value = value.round() as i32;
let chroma = (2.0 * (chroma / 2.0).round()) as i32;
if (hue % 2.5 - 0.0).abs() < 1e-10 {
return InterpolationMethod::None;
}
let astm_hue = hue_to_astm_hue(hue, code);
match value {
1 => {
match chroma {
2 => {
if (15.0 < astm_hue && astm_hue < 30.0) || (60.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
4 => {
if (12.5 < astm_hue && astm_hue < 27.5) || (57.5 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
6 => {
if 55.0 < astm_hue && astm_hue < 80.0 {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
8 => {
if 67.5 < astm_hue && astm_hue < 77.5 {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 10 => {
if 72.5 < astm_hue && astm_hue < 77.5 {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
2 => {
match chroma {
2 => {
if (15.0 < astm_hue && astm_hue < 27.5) || (77.5 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
4 => {
if (12.5 < astm_hue && astm_hue < 30.0) || (62.5 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
6 => {
if (7.5 < astm_hue && astm_hue < 22.5) || (62.5 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
8 => {
if (7.5 < astm_hue && astm_hue < 15.0) || (60.0 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 10 => {
if 65.0 < astm_hue && astm_hue < 77.5 {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
3 => {
match chroma {
2 => {
if (10.0 < astm_hue && astm_hue < 37.5) || (65.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
4 => {
if (5.0 < astm_hue && astm_hue < 37.5) || (55.0 < astm_hue && astm_hue < 72.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
6 | 8 | 10 => {
if (7.5 < astm_hue && astm_hue < 37.5) || (57.5 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 12 => {
if (7.5 < astm_hue && astm_hue < 42.5) || (57.5 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
4 => {
match chroma {
2 | 4 => {
if (7.5 < astm_hue && astm_hue < 42.5) || (57.5 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
6 | 8 => {
if (7.5 < astm_hue && astm_hue < 40.0) || (57.5 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 10 => {
if (7.5 < astm_hue && astm_hue < 40.0) || (57.5 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
5 => {
match chroma {
2 => {
if (5.0 < astm_hue && astm_hue < 37.5) || (55.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
4 | 6 | 8 => {
if (2.5 < astm_hue && astm_hue < 42.5) || (55.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 10 => {
if (2.5 < astm_hue && astm_hue < 42.5) || (55.0 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
6 => {
match chroma {
2 | 4 => {
if (5.0 < astm_hue && astm_hue < 37.5) || (55.0 < astm_hue && astm_hue < 87.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
6 => {
if (5.0 < astm_hue && astm_hue < 42.5) || (57.5 < astm_hue && astm_hue < 87.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
8 | 10 => {
if (5.0 < astm_hue && astm_hue < 42.5) || (60.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
12 | 14 => {
if (5.0 < astm_hue && astm_hue < 42.5) || (60.0 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 16 => {
if (5.0 < astm_hue && astm_hue < 42.5) || (60.0 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
7 => {
match chroma {
2 | 4 | 6 => {
if (5.0 < astm_hue && astm_hue < 42.5) || (60.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
8 => {
if (5.0 < astm_hue && astm_hue < 42.5) || (60.0 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
10 => {
if (30.0 < astm_hue && astm_hue < 42.5) || (5.0 < astm_hue && astm_hue < 25.0) || (60.0 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
12 => {
if (30.0 < astm_hue && astm_hue < 42.5) || (7.5 < astm_hue && astm_hue < 27.5) || (80.0 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 14 => {
if (32.5 < astm_hue && astm_hue < 40.0) || (7.5 < astm_hue && astm_hue < 15.0) || (80.0 < astm_hue && astm_hue < 82.5) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
8 => {
match chroma {
2 | 4 | 6 | 8 | 10 | 12 => {
if (5.0 < astm_hue && astm_hue < 40.0) || (60.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 14 => {
if (32.5 < astm_hue && astm_hue < 40.0) || (5.0 < astm_hue && astm_hue < 15.0) || (60.0 < astm_hue && astm_hue < 85.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
9 => {
match chroma {
2 | 4 => {
if (5.0 < astm_hue && astm_hue < 40.0) || (55.0 < astm_hue && astm_hue < 80.0) {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
6 | 8 | 10 | 12 | 14 => {
if 5.0 < astm_hue && astm_hue < 42.5 {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ if chroma >= 16 => {
if 35.0 < astm_hue && astm_hue < 42.5 {
InterpolationMethod::Radial
} else {
InterpolationMethod::Linear
}
}
_ => InterpolationMethod::Linear
}
}
10 => {
InterpolationMethod::None
}
_ => InterpolationMethod::Linear }
}
}
pub struct MathematicalMunsellConverter {
renotation_data: &'static [((&'static str, f64, f64), (f64, f64, f64))],
source_illuminant: Illuminant,
target_illuminant: Illuminant,
adaptation_method: ChromaticAdaptation,
}
impl MathematicalMunsellConverter {
pub fn new() -> Result<Self> {
Ok(Self {
renotation_data: MUNSELL_RENOTATION_DATA,
source_illuminant: Illuminant::D65,
target_illuminant: Illuminant::D65,
adaptation_method: ChromaticAdaptation::Bradford,
})
}
pub fn with_illuminants(source: Illuminant, target: Illuminant, method: ChromaticAdaptation) -> Result<Self> {
Ok(Self {
renotation_data: MUNSELL_RENOTATION_DATA,
source_illuminant: source,
target_illuminant: target,
adaptation_method: method,
})
}
pub fn srgb_to_munsell(&self, rgb: [u8; 3]) -> Result<MunsellSpecification> {
let xyy = self.srgb_to_xyy(rgb)?;
self.xyy_to_munsell_specification(xyy)
}
pub fn srgb_to_xyy(&self, rgb: [u8; 3]) -> Result<CieXyY> {
let rgb_norm = [
rgb[0] as f64 / 255.0,
rgb[1] as f64 / 255.0,
rgb[2] as f64 / 255.0,
];
let srgb = Srgb::new(rgb_norm[0], rgb_norm[1], rgb_norm[2]);
let linear_rgb = srgb.into_linear();
let xyz_source: Xyz<D65, f64> = linear_rgb.into_color();
let (x_src, y_src, z_src) = xyz_source.into_components();
let xyz_src = [x_src, y_src, z_src];
let xyz_adapted = if self.source_illuminant == self.target_illuminant {
xyz_src
} else {
self.chromatic_adaptation(xyz_src, self.source_illuminant, self.target_illuminant)?
};
let xyy = self.xyz_to_xyy(xyz_adapted);
Ok(xyy)
}
fn chromatic_adaptation(&self, xyz: [f64; 3], source: Illuminant, target: Illuminant) -> Result<[f64; 3]> {
match self.adaptation_method {
ChromaticAdaptation::XYZScaling => {
let source_wp = source.white_point();
let target_wp = target.white_point();
if source_wp[0].abs() < 1e-15 || source_wp[1].abs() < 1e-15 || source_wp[2].abs() < 1e-15 {
return Err(MunsellError::ConvergenceFailed);
}
Ok([
xyz[0] * target_wp[0] / source_wp[0],
xyz[1] * target_wp[1] / source_wp[1],
xyz[2] * target_wp[2] / source_wp[2],
])
}
ChromaticAdaptation::Bradford => {
self.bradford_adaptation(xyz, source, target)
}
ChromaticAdaptation::CAT02 => {
self.cat02_adaptation(xyz, source, target)
}
}
}
fn bradford_adaptation(&self, xyz: [f64; 3], source: Illuminant, target: Illuminant) -> Result<[f64; 3]> {
let source_wp = source.white_point();
let target_wp = target.white_point();
let cone_src = self.matrix_multiply_3x3(&BRADFORD_MATRIX, &xyz);
let cone_src_wp = self.matrix_multiply_3x3(&BRADFORD_MATRIX, &source_wp);
let cone_tgt_wp = self.matrix_multiply_3x3(&BRADFORD_MATRIX, &target_wp);
if cone_src_wp[0].abs() < 1e-15 || cone_src_wp[1].abs() < 1e-15 || cone_src_wp[2].abs() < 1e-15 {
return Err(MunsellError::ConvergenceFailed);
}
let cone_adapted = [
cone_src[0] * cone_tgt_wp[0] / cone_src_wp[0],
cone_src[1] * cone_tgt_wp[1] / cone_src_wp[1],
cone_src[2] * cone_tgt_wp[2] / cone_src_wp[2],
];
Ok(self.matrix_multiply_3x3(&BRADFORD_MATRIX_INV, &cone_adapted))
}
fn cat02_adaptation(&self, xyz: [f64; 3], source: Illuminant, target: Illuminant) -> Result<[f64; 3]> {
let source_wp = source.white_point();
let target_wp = target.white_point();
let cat_src = self.matrix_multiply_3x3(&CAT02_MATRIX, &xyz);
let cat_src_wp = self.matrix_multiply_3x3(&CAT02_MATRIX, &source_wp);
let cat_tgt_wp = self.matrix_multiply_3x3(&CAT02_MATRIX, &target_wp);
if cat_src_wp[0].abs() < 1e-15 || cat_src_wp[1].abs() < 1e-15 || cat_src_wp[2].abs() < 1e-15 {
return Err(MunsellError::ConvergenceFailed);
}
let cat_adapted = [
cat_src[0] * cat_tgt_wp[0] / cat_src_wp[0],
cat_src[1] * cat_tgt_wp[1] / cat_src_wp[1],
cat_src[2] * cat_tgt_wp[2] / cat_src_wp[2],
];
Ok(self.matrix_multiply_3x3(&CAT02_MATRIX_INV, &cat_adapted))
}
fn xyz_to_xyy(&self, xyz: [f64; 3]) -> CieXyY {
let sum = xyz[0] + xyz[1] + xyz[2];
if sum < 1e-15 {
CieXyY { x: 0.0, y: 0.0, y_luminance: xyz[1] }
} else {
CieXyY {
x: xyz[0] / sum,
y: xyz[1] / sum,
y_luminance: xyz[1], }
}
}
fn matrix_multiply_3x3(&self, matrix: &[[f64; 3]; 3], vector: &[f64; 3]) -> [f64; 3] {
[
matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2],
]
}
pub fn xyy_to_munsell_specification(&self, xyy: CieXyY) -> Result<MunsellSpecification> {
use coordinate_transforms::*;
use hue_conversions::*;
const CONVERGENCE_THRESHOLD: f64 = THRESHOLD_INTEGER / 1e4;
let mut value = self.luminance_to_munsell_value(xyy.y_luminance)?;
if (value - value.round()).abs() < 1e-10 {
value = value.round();
}
let achromatic_spec = MunsellSpecification {
hue: 0.0,
family: "N".to_string(),
value,
chroma: 0.0,
};
let achromatic_xyy = self.munsell_specification_to_xyy(&achromatic_spec)?;
let x_center = achromatic_xyy.x;
let y_center = achromatic_xyy.y;
let (rho_input, phi_input_rad, _) = cartesian_to_cylindrical(
xyy.x - x_center,
xyy.y - y_center,
xyy.y_luminance
);
let phi_input = phi_input_rad.to_degrees();
let _is_debug_color = (xyy.x - 0.558939).abs() < 0.0001 && (xyy.y - 0.285274).abs() < 0.0001;
if rho_input < THRESHOLD_INTEGER || xyy.y_luminance < 1e-6 { return Ok(MunsellSpecification {
hue: 0.0,
family: "N".to_string(),
value,
chroma: 0.0,
});
}
let initial_spec = self.generate_initial_guess(xyy)?;
let mut hue_current = initial_spec.0;
let mut code_current = initial_spec.1;
let mut chroma_current = initial_spec.2;
for _outer_iteration in 0..MAX_OUTER_ITERATIONS {
let chroma_maximum = self.maximum_chroma_from_renotation(hue_current, value, code_current)?;
if chroma_current > chroma_maximum {
chroma_current = chroma_maximum;
}
let (x_current, y_current) = self.munsell_specification_to_xy(hue_current, value, chroma_current, code_current)?;
let (_rho_current, phi_current, _) = cartesian_to_cylindrical(
x_current - x_center,
y_current - y_center,
xyy.y_luminance
);
let mut phi_current_degrees = phi_current.to_degrees();
if phi_current_degrees < 0.0 {
phi_current_degrees += 360.0;
}
let mut phi_current_difference = (360.0 - phi_input + phi_current_degrees) % 360.0;
if phi_current_difference > 180.0 {
phi_current_difference -= 360.0;
}
let mut phi_differences_data = if phi_current_difference.abs() < 1e-6 {
vec![]
} else {
vec![phi_current_difference]
};
let mut hue_angles_differences_data = if phi_current_difference.abs() < 1e-6 {
vec![]
} else {
vec![0.0]
};
let hue_angle_current = hue_to_astm_hue(hue_current, code_current);
let mut extrapolate = false;
let mut iterations_inner = 0;
while (phi_differences_data.iter().all(|&d| d >= 0.0) ||
phi_differences_data.iter().all(|&d| d <= 0.0)) &&
!extrapolate {
iterations_inner += 1;
if iterations_inner > MAX_INNER_ITERATIONS {
break; }
let step = iterations_inner as f64 * (phi_input - phi_current_degrees);
let hue_angle_inner = if (hue_angle_current + step) < 0.0 {
((hue_angle_current + step) % 360.0 + 360.0) % 360.0
} else {
(hue_angle_current + step) % 360.0
};
let step_mod = if step < 0.0 {
((step % 360.0) + 360.0) % 360.0
} else {
step % 360.0
};
let hue_angle_difference_inner = if step_mod > 180.0 {
step_mod - 360.0
} else {
step_mod
};
let (hue_inner, code_inner) = hue_angle_to_hue(hue_angle_inner);
let (x_inner, y_inner) = self.munsell_specification_to_xy(hue_inner, value, chroma_current, code_inner)?;
if phi_differences_data.len() >= 2 {
extrapolate = true;
}
if !extrapolate {
let (_, phi_inner, _) = cartesian_to_cylindrical(
x_inner - x_center,
y_inner - y_center,
xyy.y_luminance
);
let mut phi_inner_degrees = phi_inner.to_degrees();
if phi_inner_degrees < 0.0 {
phi_inner_degrees += 360.0;
}
let mut phi_inner_difference = (360.0 - phi_input + phi_inner_degrees) % 360.0;
if phi_inner_difference > 180.0 {
phi_inner_difference -= 360.0;
}
phi_differences_data.push(phi_inner_difference);
hue_angles_differences_data.push(hue_angle_difference_inner);
}
}
let hue_angle_difference_new = if phi_differences_data.len() >= 2 {
let mut paired: Vec<_> = phi_differences_data.iter()
.zip(hue_angles_differences_data.iter())
.map(|(&p, &h)| (p, h))
.collect();
paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let sorted_phi: Vec<f64> = paired.iter().map(|&(p, _)| p).collect();
let sorted_hue: Vec<f64> = paired.iter().map(|&(_, h)| h).collect();
let result = self.linear_interpolate(&sorted_phi, &sorted_hue, 0.0)?;
if result < 0.0 {
result % 360.0 + 360.0
} else {
result % 360.0
}
} else {
0.0 };
let mut hue_angle_new = (hue_angle_current + hue_angle_difference_new) % 360.0;
if hue_angle_new < 0.0 {
hue_angle_new += 360.0;
}
let (hue_new, code_new) = hue_angle_to_hue(hue_angle_new);
hue_current = hue_new;
code_current = code_new;
let chroma_maximum = self.maximum_chroma_from_renotation(hue_current, value, code_current)?;
if chroma_current > chroma_maximum {
chroma_current = chroma_maximum;
}
let (x_current_new, y_current_new) = self.munsell_specification_to_xy(hue_current, value, chroma_current, code_current)?;
let (rho_current_new, _, _) = cartesian_to_cylindrical(
x_current_new - x_center,
y_current_new - y_center,
xyy.y_luminance
);
let mut rho_bounds_data = vec![rho_current_new];
let mut chroma_bounds_data = vec![chroma_current];
let mut iterations_inner = 0;
let mut loop_count = 0;
while loop_count < MAX_INNER_ITERATIONS {
let rho_min = *rho_bounds_data.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let rho_max = *rho_bounds_data.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
if rho_min <= rho_input && rho_input <= rho_max {
break;
}
loop_count += 1;
iterations_inner += 1;
if iterations_inner > MAX_INNER_ITERATIONS {
break;
}
let chroma_inner = if rho_current_new.abs() > 1e-10 {
let ratio = rho_input / rho_current_new;
let power = iterations_inner as f64;
let result = ratio.powf(power) * chroma_current;
result
} else {
chroma_current };
let chroma_max = self.maximum_chroma_from_renotation(hue_current, value, code_current)?;
let chroma_bounded = chroma_inner.min(chroma_max).max(0.0);
let (x_inner, y_inner) = self.munsell_specification_to_xy(hue_current, value, chroma_bounded, code_current)?;
let (rho_inner, _, _) = cartesian_to_cylindrical(
x_inner - x_center,
y_inner - y_center,
xyy.y_luminance
);
rho_bounds_data.push(rho_inner);
chroma_bounds_data.push(chroma_bounded);
}
let chroma_new = if rho_bounds_data.len() >= 2 {
let mut paired: Vec<_> = rho_bounds_data.iter()
.zip(chroma_bounds_data.iter())
.map(|(&r, &c)| (r, c))
.collect();
paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let sorted_rho: Vec<f64> = paired.iter().map(|&(r, _)| r).collect();
let sorted_chroma: Vec<f64> = paired.iter().map(|&(_, c)| c).collect();
let interpolated = self.linear_interpolate(&sorted_rho, &sorted_chroma, rho_input)?;
interpolated.max(0.0)
} else {
chroma_current };
chroma_current = chroma_new;
let (x_final, y_final) = self.munsell_specification_to_xy(hue_current, value, chroma_current, code_current)?;
let difference = ((xyy.x - x_final).powi(2) + (xyy.y - y_final).powi(2)).sqrt();
if difference < CONVERGENCE_THRESHOLD {
if chroma_current < 1e-10 {
return Ok(MunsellSpecification {
hue: 0.0,
family: "N".to_string(),
value,
chroma: 0.0,
});
}
let (normalized_hue, normalized_code) = Self::normalize_munsell_specification(hue_current, code_current);
let family = code_to_family(normalized_code);
return Ok(MunsellSpecification {
hue: normalized_hue,
family: family.to_string(),
value,
chroma: chroma_current,
});
}
}
if chroma_current < 1e-10 {
return Ok(MunsellSpecification {
hue: 0.0,
family: "N".to_string(),
value,
chroma: 0.0,
});
}
let (normalized_hue, normalized_code) = Self::normalize_munsell_specification(hue_current, code_current);
let family = code_to_family(normalized_code);
Ok(MunsellSpecification {
hue: normalized_hue,
family: family.to_string(),
value,
chroma: chroma_current,
})
}
fn normalize_munsell_specification(hue: f64, code: u8) -> (f64, u8) {
if hue.abs() < 0.01 && hue < 5.0 { let new_code = match code {
1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 8, 8 => 9, 9 => 10, 10 => 1, _ => code };
(10.0, new_code)
} else {
(hue, code)
}
}
fn generate_initial_guess(&self, xyy: CieXyY) -> Result<(f64, u8, f64)> {
use coordinate_transforms::cartesian_to_cylindrical;
use hue_conversions::hue_angle_to_hue;
let dx = xyy.x - ILLUMINANT_C[0];
let dy = xyy.y - ILLUMINANT_C[1];
let (rho, phi_rad, _) = cartesian_to_cylindrical(dx, dy, xyy.y_luminance);
let mut phi_deg = phi_rad.to_degrees();
if phi_deg < 0.0 {
phi_deg += 360.0;
}
let (hue_initial, code_initial) = hue_angle_to_hue(phi_deg);
let chroma_initial = rho * 50.0;
Ok((hue_initial, code_initial, chroma_initial))
}
#[allow(dead_code)]
fn lchab_to_munsell_specification(&self, _l: f64, _c: f64, hab: f64) -> (f64, u8) {
let code = if hab < 18.0 || hab >= 342.0 {
8 } else if hab < 54.0 {
7 } else if hab < 90.0 {
6 } else if hab < 126.0 {
5 } else if hab < 162.0 {
4 } else if hab < 198.0 {
3 } else if hab < 234.0 {
2 } else if hab < 270.0 {
1 } else if hab < 306.0 {
10 } else if hab < 342.0 {
9 } else {
8 };
let segment_start = ((hab / 36.0).floor() * 36.0) as f64;
let hab_in_segment = hab - segment_start;
let mut hue = (hab_in_segment / 36.0) * 10.0;
if hue == 0.0 && hab > 0.0 {
hue = 10.0;
}
(hue, code)
}
fn linear_interpolate(&self, x_values: &[f64], y_values: &[f64], x_target: f64) -> Result<f64> {
if x_values.len() != y_values.len() || x_values.len() < 2 {
return Err(MunsellError::InterpolationError {
message: "Invalid data for interpolation".to_string()
});
}
let mut best_result = y_values[0];
let mut best_distance = (x_values[0] - x_target).abs();
for i in 0..x_values.len()-1 {
let x1 = x_values[i];
let x2 = x_values[i+1];
let y1 = y_values[i];
let y2 = y_values[i+1];
if (x1 <= x_target && x_target <= x2) || (x2 <= x_target && x_target <= x1) {
if (x2 - x1).abs() < 1e-10 {
return Ok(y1);
}
let t = (x_target - x1) / (x2 - x1);
return Ok(y1 + t * (y2 - y1));
}
let distance = (x1 - x_target).abs();
if distance < best_distance {
best_distance = distance;
best_result = y1;
}
}
let last_idx = x_values.len() - 1;
let distance = (x_values[last_idx] - x_target).abs();
if distance < best_distance {
best_result = y_values[last_idx];
}
if x_values.len() >= 2 {
let mut indices: Vec<usize> = (0..x_values.len()).collect();
indices.sort_by(|&i, &j| x_values[i].partial_cmp(&x_values[j]).unwrap());
let sorted_x: Vec<f64> = indices.iter().map(|&i| x_values[i]).collect();
let sorted_y: Vec<f64> = indices.iter().map(|&i| y_values[i]).collect();
let first_x = sorted_x[0];
let last_x = sorted_x[sorted_x.len() - 1];
if x_target <= first_x {
return Ok(sorted_y[0]);
} else if x_target >= last_x {
return Ok(sorted_y[sorted_y.len() - 1]);
}
}
Ok(best_result)
}
fn maximum_chroma_from_renotation(&self, _hue: f64, value: f64, code: u8) -> Result<f64> {
use hue_conversions::code_to_family;
let family = code_to_family(code);
let value_rounded = (value * 2.0).round() / 2.0;
let mut max_chroma = 0.0;
for entry in self.renotation_data.iter() {
let ((hue_str, entry_value, entry_chroma), _) = entry;
let entry_family = hue_str.chars()
.skip_while(|c| c.is_numeric() || *c == '.')
.collect::<String>();
if entry_family != family {
continue;
}
if (entry_value - value_rounded).abs() > 0.25 {
continue;
}
if *entry_chroma > max_chroma {
max_chroma = *entry_chroma;
}
}
if max_chroma < 0.1 {
max_chroma = match value as i32 {
0..=2 => 10.0,
3..=5 => 15.0,
6..=8 => 20.0,
9..=10 => 10.0,
_ => 8.0,
};
}
Ok(max_chroma)
}
fn munsell_specification_to_xy(&self, hue: f64, value: f64, chroma: f64, code: u8) -> Result<(f64, f64)> {
let is_integer = (value - value.round()).abs() < 1e-10;
if is_integer {
self.xy_from_renotation_ovoid(hue, value.round(), chroma, code)
} else {
let value_minus = value.floor();
let value_plus = value_minus + 1.0;
let (x_minus, y_minus) = self.xy_from_renotation_ovoid(hue, value_minus, chroma, code)?;
let (x_plus, y_plus) = if value_plus >= 10.0 {
(x_minus, y_minus)
} else {
self.xy_from_renotation_ovoid(hue, value_plus, chroma, code)?
};
if value_minus == value_plus || (x_minus == x_plus && y_minus == y_plus) {
Ok((x_minus, y_minus))
} else {
let y_minus_lum = self.munsell_value_to_luminance(value_minus)?;
let y_plus_lum = self.munsell_value_to_luminance(value_plus)?;
let y_actual = self.munsell_value_to_luminance(value)?;
let t = (y_actual - y_minus_lum) / (y_plus_lum - y_minus_lum);
let x = x_minus + t * (x_plus - x_minus);
let y = y_minus + t * (y_plus - y_minus);
Ok((x, y))
}
}
}
fn lookup_xy_from_renotation(&self, hue: f64, value: f64, chroma: f64, code: u8) -> Result<(f64, f64)> {
if chroma.abs() < 1e-10 {
return Ok((ILLUMINANT_C[0], ILLUMINANT_C[1]));
}
let family = hue_conversions::code_to_family(code);
let hue_str = if hue == 10.0 || hue == 0.0 {
format!("10{}", family)
} else {
format!("{}{}", hue, family)
};
for &((ref entry_family, entry_value, entry_chroma), (x, y, _y)) in self.renotation_data {
if *entry_family == hue_str &&
(entry_value - value).abs() < 0.01 &&
(entry_chroma - chroma).abs() < 0.01 {
return Ok((x, y));
}
}
self.interpolate_from_renotation_data(&hue_str, value, chroma)
}
fn interpolate_from_renotation_data(&self, hue_str: &str, value: f64, chroma: f64) -> Result<(f64, f64)> {
let mut matching_entries = Vec::new();
let family = hue_str.chars().skip_while(|c| !c.is_alphabetic()).collect::<String>();
for &((ref entry_family, entry_value, entry_chroma), (x, y, _y)) in self.renotation_data {
if entry_family.ends_with(&family) {
matching_entries.push(((*entry_family).to_string(), entry_value, entry_chroma, x, y));
}
}
if matching_entries.is_empty() {
return Err(MunsellError::InterpolationError { message: format!("No entries found for hue {}", hue_str) });
}
matching_entries.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().then(a.2.partial_cmp(&b.2).unwrap()));
let mut value_lower = None;
let mut value_upper = None;
for &(_, entry_value, _, _, _) in &matching_entries {
if entry_value <= value {
value_lower = Some(entry_value);
}
if entry_value >= value && value_upper.is_none() {
value_upper = Some(entry_value);
break;
}
}
let target_value = if let (Some(v_low), Some(v_high)) = (value_lower, value_upper) {
if (value - v_low).abs() < (v_high - value).abs() { v_low } else { v_high }
} else if let Some(v) = value_lower.or(value_upper) {
v
} else {
matching_entries.iter().min_by(|a, b| {
(a.1 - value).abs().partial_cmp(&(b.1 - value).abs()).unwrap()
}).map(|&(_, v, _, _, _)| v).unwrap_or(5.0)
};
let value_entries: Vec<_> = matching_entries.iter()
.filter(|&&(_, entry_value, _, _, _)| (entry_value - target_value).abs() < 0.1)
.collect();
if value_entries.is_empty() {
return Err(MunsellError::InterpolationError { message: format!("No entries found for value {} in hue {}", target_value, hue_str) });
}
let mut chroma_lower = None;
let mut chroma_upper = None;
for &&(_, _, entry_chroma, _, _) in &value_entries {
if entry_chroma <= chroma {
chroma_lower = Some(entry_chroma);
}
if entry_chroma >= chroma && chroma_upper.is_none() {
chroma_upper = Some(entry_chroma);
break;
}
}
if let (Some(c_low), Some(c_high)) = (chroma_lower, chroma_upper) {
if (c_high - c_low).abs() < 1e-10 {
let entry = value_entries.iter()
.find(|&&(_, _, entry_chroma, _, _)| (entry_chroma - c_low).abs() < 1e-10)
.unwrap();
Ok((entry.3, entry.4))
} else {
let entry_low = value_entries.iter()
.find(|&&(_, _, entry_chroma, _, _)| (entry_chroma - c_low).abs() < 1e-10)
.unwrap();
let entry_high = value_entries.iter()
.find(|&&(_, _, entry_chroma, _, _)| (entry_chroma - c_high).abs() < 1e-10)
.unwrap();
let t = (chroma - c_low) / (c_high - c_low);
let x = entry_low.3 + t * (entry_high.3 - entry_low.3);
let y = entry_low.4 + t * (entry_high.4 - entry_low.4);
Ok((x, y))
}
} else {
let nearest = value_entries.iter()
.min_by(|a, b| (a.2 - chroma).abs().partial_cmp(&(b.2 - chroma).abs()).unwrap())
.unwrap();
Ok((nearest.3, nearest.4))
}
}
fn xy_from_renotation_ovoid(&self, hue: f64, value: f64, chroma: f64, code: u8) -> Result<(f64, f64)> {
let value_normalized = value.round();
const THRESHOLD_INTEGER: f64 = 1e-7;
let is_standard_hue = (hue.abs() < THRESHOLD_INTEGER) ||
((hue - 2.5).abs() < THRESHOLD_INTEGER) ||
((hue - 5.0).abs() < THRESHOLD_INTEGER) ||
((hue - 7.5).abs() < THRESHOLD_INTEGER) ||
((hue - 10.0).abs() < THRESHOLD_INTEGER);
let chroma_is_even = (chroma - 2.0 * (chroma / 2.0).round()).abs() < THRESHOLD_INTEGER;
if is_standard_hue && chroma_is_even {
let rounded_hue = 2.5 * (hue / 2.5).round();
let chroma_even = 2.0 * (chroma / 2.0).round();
return self.lookup_xy_from_renotation(rounded_hue, value_normalized, chroma_even, code);
}
if !chroma_is_even {
let chroma_lower = 2.0 * (chroma / 2.0).floor();
let chroma_upper = chroma_lower + 2.0;
let (x_lower, y_lower) = if is_standard_hue {
let rounded_hue = 2.5 * (hue / 2.5).round();
self.lookup_xy_from_renotation(rounded_hue, value_normalized, chroma_lower, code)?
} else {
self.xy_from_renotation_ovoid_for_even_chroma(hue, value_normalized, chroma_lower, code)?
};
let (x_upper, y_upper) = if is_standard_hue {
let rounded_hue = 2.5 * (hue / 2.5).round();
self.lookup_xy_from_renotation(rounded_hue, value_normalized, chroma_upper, code)?
} else {
self.xy_from_renotation_ovoid_for_even_chroma(hue, value_normalized, chroma_upper, code)?
};
let t = (chroma - chroma_lower) / (chroma_upper - chroma_lower);
let x = x_lower + t * (x_upper - x_lower);
let y = y_lower + t * (y_upper - y_lower);
return Ok((x, y));
}
let chroma_even = 2.0 * (chroma / 2.0).round();
self.xy_from_renotation_ovoid_for_even_chroma(hue, value_normalized, chroma_even, code)
}
fn xy_from_renotation_ovoid_for_even_chroma(&self, hue: f64, value: f64, chroma: f64, code: u8) -> Result<(f64, f64)> {
use crate::constants::ILLUMINANT_C;
if chroma.abs() < 1e-10 {
return Ok((ILLUMINANT_C[0], ILLUMINANT_C[1]));
}
let ((hue_cw, code_cw), (hue_ccw, code_ccw)) = hue_conversions::bounding_hues_from_renotation(hue, code);
let x_grey = ILLUMINANT_C[0];
let y_grey = ILLUMINANT_C[1];
let (x_minus, y_minus) = self.lookup_xy_from_renotation(hue_cw, value, chroma, code_cw)?;
let y_luminance_minus = self.get_y_luminance_from_renotation(hue_cw, value, chroma, code_cw)?;
let (x_plus, y_plus) = self.lookup_xy_from_renotation(hue_ccw, value, chroma, code_ccw)?;
let y_luminance_plus = self.get_y_luminance_from_renotation(hue_ccw, value, chroma, code_ccw)?;
let (rho_minus, phi_minus, _z_minus) = coordinate_transforms::cartesian_to_cylindrical(
x_minus - x_grey, y_minus - y_grey, y_luminance_minus
);
let phi_minus_deg = phi_minus.to_degrees();
let (rho_plus, phi_plus, _z_plus) = coordinate_transforms::cartesian_to_cylindrical(
x_plus - x_grey, y_plus - y_grey, y_luminance_plus
);
let mut phi_plus_deg = phi_plus.to_degrees();
let hue_angle_lower = hue_conversions::hue_to_hue_angle(hue_cw, code_cw);
let hue_angle = hue_conversions::hue_to_hue_angle(hue, code);
let hue_angle_upper = hue_conversions::hue_to_hue_angle(hue_ccw, code_ccw);
if phi_minus_deg - phi_plus_deg > 180.0 {
phi_plus_deg += 360.0;
}
let mut hue_angle_lower_corrected = hue_angle_lower;
let mut hue_angle_corrected = hue_angle;
if hue_angle_lower == 0.0 {
hue_angle_lower_corrected = 360.0;
}
if hue_angle_lower_corrected > hue_angle_upper {
if hue_angle_lower_corrected > hue_angle {
hue_angle_lower_corrected -= 360.0;
} else {
hue_angle_lower_corrected -= 360.0;
hue_angle_corrected -= 360.0;
}
}
let interpolation_method = self.get_interpolation_method(hue, value, chroma, code)?;
if interpolation_method == "Linear" {
let x = self.linear_interpolate_2d(
hue_angle_lower_corrected, hue_angle_upper, x_minus, x_plus, hue_angle_corrected
);
let y = self.linear_interpolate_2d(
hue_angle_lower_corrected, hue_angle_upper, y_minus, y_plus, hue_angle_corrected
);
Ok((x, y))
} else if interpolation_method == "Radial" {
let rho = self.linear_interpolate_2d(
hue_angle_lower_corrected, hue_angle_upper, rho_minus, rho_plus, hue_angle_corrected
);
let phi_deg = self.linear_interpolate_2d(
hue_angle_lower_corrected, hue_angle_upper, phi_minus_deg, phi_plus_deg, hue_angle_corrected
);
let phi_rad = phi_deg.to_radians();
let (x_offset, y_offset) = coordinate_transforms::polar_to_cartesian(rho, phi_rad);
let x = x_offset + x_grey;
let y = y_offset + y_grey;
Ok((x, y))
} else {
Err(MunsellError::InterpolationError {
message: format!("Invalid interpolation method: {}", interpolation_method)
})
}
}
fn get_y_luminance_from_renotation(&self, _hue: f64, value: f64, chroma: f64, code: u8) -> Result<f64> {
let family = hue_conversions::code_to_family(code);
for &((ref entry_family, entry_value, entry_chroma), (_, _, y_luminance)) in self.renotation_data {
if (*entry_family == family ||
(entry_family.len() > family.len() &&
entry_family.ends_with(family) &&
entry_family.chars().nth(entry_family.len() - family.len() - 1).unwrap_or(' ').is_numeric())) &&
(entry_value - value).abs() < 0.01 &&
(entry_chroma - chroma).abs() < 0.01 {
return Ok(y_luminance);
}
}
Ok(0.1 * value) }
fn get_interpolation_method(&self, _hue: f64, value: f64, chroma: f64, code: u8) -> Result<&'static str> {
let _family = hue_conversions::code_to_family(code);
if value <= 1.0 || chroma <= 2.0 {
return Ok("Linear");
}
if chroma >= 20.0 {
return Ok("Radial");
}
if chroma <= 10.0 {
return Ok("Linear");
}
Ok("Radial")
}
fn linear_interpolate_2d(&self, x1: f64, x2: f64, y1: f64, y2: f64, x: f64) -> f64 {
if (x2 - x1).abs() < 1e-10 {
return y1; }
y1 + (x - x1) * (y2 - y1) / (x2 - x1)
}
#[allow(dead_code)]
fn linear_interpolate_xy(&self, hue: f64, value: f64, chroma: f64, code: u8) -> Result<(f64, f64)> {
self.xy_from_renotation_ovoid(hue, value, chroma, code)
}
#[allow(dead_code)]
fn radial_interpolate_xy(&self, hue: f64, value: f64, chroma: f64, code: u8) -> Result<(f64, f64)> {
self.xy_from_renotation_ovoid(hue, value, chroma, code)
}
#[allow(dead_code)]
fn interpolate_hue_chroma_to_xy(&self, _hue: f64, value: f64, _chroma: f64, _code: u8) -> Result<(f64, f64)> {
let (_, _, _) = self.interpolate_hue_chroma(0.31006, 0.31616, value)?;
Ok((0.31006, 0.31616))
}
fn luminance_to_munsell_value(&self, y: f64) -> Result<f64> {
if y <= 0.0 {
return Ok(0.0);
}
let y_scaled = y * 100.0;
if y_scaled >= 100.0 {
return Ok(10.0);
}
let mut v = 10.0 * y.sqrt();
for _ in 0..NEWTON_RAPHSON_MAX_ITERATIONS {
let f = self.astm_polynomial(v) - y_scaled;
let df = self.astm_polynomial_derivative(v);
if df.abs() < 1e-15 {
return Err(MunsellError::ConvergenceFailed);
}
let delta = f / df;
v -= delta;
if delta.abs() < NEWTON_RAPHSON_TOLERANCE {
return Ok(v.max(0.0).min(10.0)); }
}
Err(MunsellError::ConvergenceFailed)
}
fn astm_polynomial(&self, v: f64) -> f64 {
let coeffs = &ASTM_D1535_COEFFICIENTS;
coeffs[0] * v +
coeffs[1] * v * v +
coeffs[2] * v * v * v +
coeffs[3] * v * v * v * v +
coeffs[4] * v * v * v * v * v
}
fn astm_polynomial_derivative(&self, v: f64) -> f64 {
let coeffs = &ASTM_D1535_COEFFICIENTS;
coeffs[0] +
2.0 * coeffs[1] * v +
3.0 * coeffs[2] * v * v +
4.0 * coeffs[3] * v * v * v +
5.0 * coeffs[4] * v * v * v * v
}
#[allow(dead_code)]
fn is_achromatic_d65(&self, x: f64, y: f64) -> bool {
if x == 0.0 && y == 0.0 {
return true;
}
const ILLUMINANT_D65: [f64; 2] = [0.31270, 0.32900];
let dx = x - ILLUMINANT_D65[0];
let dy = y - ILLUMINANT_D65[1];
let distance = (dx * dx + dy * dy).sqrt();
distance < ACHROMATIC_THRESHOLD
}
#[allow(dead_code)]
fn is_achromatic(&self, x: f64, y: f64) -> bool {
if x == 0.0 && y == 0.0 {
return true;
}
let dx = x - ILLUMINANT_C[0];
let dy = y - ILLUMINANT_C[1];
let distance = (dx * dx + dy * dy).sqrt();
distance < ACHROMATIC_THRESHOLD
}
#[allow(dead_code)]
fn interpolate_hue_chroma(&self, x: f64, y: f64, luma: f64) -> Result<(f64, String, f64)> {
let (initial_hue, initial_family, initial_chroma) = self.find_nearest_neighbor(x, y, luma)?;
let mut current_spec = MunsellSpecification {
hue: initial_hue,
family: initial_family.clone(),
value: self.luminance_to_munsell_value(luma)?,
chroma: initial_chroma,
};
for _outer_iteration in 0..MAX_OUTER_ITERATIONS {
let target_xyy = self.munsell_specification_to_xyy_interpolated(¤t_spec)?;
let error_x = x - target_xyy.x;
let error_y = y - target_xyy.y;
let error_magnitude = (error_x * error_x + error_y * error_y).sqrt();
if error_magnitude < TOLERANCE_ABSOLUTE_DEFAULT {
return Ok((current_spec.hue, current_spec.family, current_spec.chroma));
}
current_spec = self.refine_munsell_specification(¤t_spec, x, y, error_x, error_y)?;
}
Ok((initial_hue, initial_family, initial_chroma))
}
#[allow(dead_code)]
fn find_nearest_neighbor(&self, x: f64, y: f64, luma: f64) -> Result<(f64, String, f64)> {
let mut best_distance = f64::INFINITY;
let mut best_match: Option<&'static ((&'static str, f64, f64), (f64, f64, f64))> = None;
for entry in self.renotation_data {
let ((_, _, _), (entry_x, entry_y, entry_luma)) = entry;
let dx = x - entry_x;
let dy = y - entry_y;
let dluma = luma - entry_luma;
let distance = (dx * dx + dy * dy + dluma * dluma * 0.1).sqrt();
if distance < best_distance {
best_distance = distance;
best_match = Some(entry);
}
}
match best_match {
Some(((hue_str, _value, chroma), _)) => {
let (hue, family) = self.parse_hue_string(hue_str)?;
Ok((hue, family, *chroma))
}
None => Err(MunsellError::InterpolationError {
message: "No matching color found in renotation data".to_string(),
})
}
}
#[allow(dead_code)]
fn munsell_specification_to_xyy_interpolated(&self, spec: &MunsellSpecification) -> Result<CieXyY> {
if spec.family == "N" {
let y = self.munsell_value_to_luminance(spec.value)?;
return Ok(CieXyY {
x: ILLUMINANT_C[0],
y: ILLUMINANT_C[1],
y_luminance: y,
});
}
let hue_str = format!("{}{}", spec.hue, spec.family);
let neighbors = self.find_interpolation_neighbors(&hue_str, spec.value, spec.chroma);
if neighbors.is_empty() {
return Err(MunsellError::InterpolationError {
message: format!("No neighbors found for interpolation: {}", hue_str),
});
}
self.radial_basis_interpolation(&neighbors, spec.value, spec.chroma)
}
#[allow(dead_code)]
fn find_interpolation_neighbors(&self, target_hue: &str, target_value: f64, target_chroma: f64) -> Vec<&'static ((&'static str, f64, f64), (f64, f64, f64))> {
let mut neighbors = Vec::new();
for entry in self.renotation_data {
let ((entry_hue, entry_value, entry_chroma), _) = entry;
if self.hue_families_match(target_hue, entry_hue) {
let value_diff = (target_value - entry_value).abs();
let chroma_diff = (target_chroma - entry_chroma).abs();
if value_diff <= 2.0 && chroma_diff <= 4.0 {
neighbors.push(entry);
}
}
}
if neighbors.len() < 4 {
for entry in self.renotation_data {
let ((entry_hue, entry_value, entry_chroma), _) = entry;
let hue_distance = self.calculate_hue_distance(target_hue, entry_hue);
if hue_distance <= 2.5 { let value_diff = (target_value - entry_value).abs();
let chroma_diff = (target_chroma - entry_chroma).abs();
if value_diff <= 3.0 && chroma_diff <= 6.0 {
neighbors.push(entry);
}
}
}
}
neighbors
}
#[allow(dead_code)]
fn hue_families_match(&self, hue1: &str, hue2: &str) -> bool {
let family1 = hue1.chars().filter(|c| c.is_alphabetic()).collect::<String>();
let family2 = hue2.chars().filter(|c| c.is_alphabetic()).collect::<String>();
family1 == family2
}
#[allow(dead_code)]
fn calculate_hue_distance(&self, hue1: &str, hue2: &str) -> f64 {
let (num1, family1) = self.parse_hue_string(hue1).unwrap_or((0.0, "".to_string()));
let (num2, family2) = self.parse_hue_string(hue2).unwrap_or((0.0, "".to_string()));
if family1 == family2 {
(num1 - num2).abs()
} else {
let angle1 = self.hue_to_angle(num1, &family1);
let angle2 = self.hue_to_angle(num2, &family2);
let diff = (angle1 - angle2).abs();
diff.min(360.0 - diff) / 36.0 }
}
#[allow(dead_code)]
fn hue_to_angle(&self, hue: f64, family: &str) -> f64 {
let base_angle = match family {
"R" => 0.0, "YR" => 36.0, "Y" => 72.0, "GY" => 108.0, "G" => 144.0,
"BG" => 180.0, "B" => 216.0, "PB" => 252.0, "P" => 288.0, "RP" => 324.0,
_ => 0.0,
};
base_angle + (hue - 5.0) * 3.6 }
#[allow(dead_code)]
fn radial_basis_interpolation(&self, neighbors: &[&'static ((&'static str, f64, f64), (f64, f64, f64))], target_value: f64, target_chroma: f64) -> Result<CieXyY> {
if neighbors.is_empty() {
return Err(MunsellError::InterpolationError {
message: "No neighbors for radial basis interpolation".to_string(),
});
}
let mut weighted_x = 0.0;
let mut weighted_y = 0.0;
let mut total_weight = 0.0;
for neighbor in neighbors {
let ((_, neighbor_value, neighbor_chroma), (x, y, luma)) = neighbor;
let value_dist = target_value - neighbor_value;
let chroma_dist = target_chroma - neighbor_chroma;
let distance = (value_dist * value_dist + chroma_dist * chroma_dist).sqrt();
let weight = if distance < 0.001 {
1000.0 } else {
1.0 / (distance + 0.1) };
weighted_x += x * weight;
weighted_y += y * weight;
weighted_y += luma * weight;
total_weight += weight;
}
if total_weight < 1e-15 {
return Err(MunsellError::InterpolationError {
message: "Zero total weight in radial basis interpolation".to_string(),
});
}
Ok(CieXyY {
x: weighted_x / total_weight,
y: weighted_y / total_weight,
y_luminance: weighted_y / total_weight,
})
}
fn refine_munsell_specification(&self, spec: &MunsellSpecification, _target_x: f64, _target_y: f64, error_x: f64, error_y: f64) -> Result<MunsellSpecification> {
let step_size = 0.1;
let mut refined_spec = spec.clone();
let chroma_adjustment = (error_x * error_x + error_y * error_y).sqrt() * 2.0;
refined_spec.chroma = (spec.chroma + chroma_adjustment * step_size).max(0.0);
let hue_adjustment = error_x.atan2(error_y) * 180.0 / std::f64::consts::PI * 0.1;
refined_spec.hue = (spec.hue + hue_adjustment).rem_euclid(10.0);
Ok(refined_spec)
}
fn parse_hue_string(&self, hue_str: &str) -> Result<(f64, String)> {
let mut split_pos = 0;
for (i, c) in hue_str.char_indices() {
if c.is_alphabetic() {
split_pos = i;
break;
}
}
if split_pos == 0 {
return Err(MunsellError::InvalidNotation {
notation: hue_str.to_string(),
reason: "Hue string contains no alphabetic characters".to_string(),
});
}
let hue_str_num = &hue_str[..split_pos];
let family = hue_str[split_pos..].to_string();
let hue: f64 = hue_str_num.parse()
.map_err(|_| MunsellError::InvalidNotation {
notation: hue_str_num.to_string(),
reason: "Invalid numeric value in hue".to_string(),
})?;
Ok((hue, family))
}
pub fn munsell_specification_to_xyy(&self, spec: &MunsellSpecification) -> Result<CieXyY> {
if spec.family == "N" {
let y = self.munsell_value_to_luminance(spec.value)?;
return Ok(CieXyY {
x: ILLUMINANT_C[0],
y: ILLUMINANT_C[1],
y_luminance: y,
});
}
let hue_str = format!("{}{}", spec.hue, spec.family);
for entry in self.renotation_data {
let ((entry_hue, entry_value, entry_chroma), (x, y, luma)) = entry;
if entry_hue == &hue_str &&
(entry_value - spec.value).abs() < 0.1 &&
(entry_chroma - spec.chroma).abs() < 0.1 {
return Ok(CieXyY { x: *x, y: *y, y_luminance: *luma });
}
}
Err(MunsellError::InterpolationError {
message: format!("No matching renotation data for {}{} {:.1}/{:.1}",
spec.hue, spec.family, spec.value, spec.chroma),
})
}
fn munsell_value_to_luminance(&self, value: f64) -> Result<f64> {
if value < 0.0 || value > 10.0 {
return Err(MunsellError::InvalidNotation {
notation: value.to_string(),
reason: "Munsell Value must be between 0.0 and 10.0".to_string(),
});
}
Ok(self.astm_polynomial(value) / 100.0)
}
pub fn format_munsell_notation(&self, spec: &MunsellSpecification) -> String {
if spec.family == "N" {
format!("N {:.1}", spec.value)
} else {
format!("{:.1}{} {:.1}/{:.1}", spec.hue, spec.family, spec.value, spec.chroma)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mathematical_converter_creation() {
let converter = MathematicalMunsellConverter::new().unwrap();
assert_eq!(converter.renotation_data.len(), 4995);
}
#[test]
fn test_srgb_to_xyy_conversion() {
let converter = MathematicalMunsellConverter::new().unwrap();
let xyy = converter.srgb_to_xyy([255, 0, 0]).unwrap();
assert!(xyy.x > 0.6); assert!(xyy.y > 0.3 && xyy.y < 0.4); assert!(xyy.y_luminance > 0.2 && xyy.y_luminance < 0.3); }
#[test]
fn test_astm_polynomial() {
let converter = MathematicalMunsellConverter::new().unwrap();
assert!((converter.astm_polynomial(0.0) - 0.0).abs() < 1e-10);
assert!(converter.astm_polynomial(5.0) > 0.1);
assert!(converter.astm_polynomial(10.0) > 0.9); }
#[test]
fn test_luminance_to_munsell_value() {
let converter = MathematicalMunsellConverter::new().unwrap();
assert!((converter.luminance_to_munsell_value(0.0).unwrap() - 0.0).abs() < 1e-10);
assert!((converter.luminance_to_munsell_value(1.0).unwrap() - 10.0).abs() < 1e-10);
let test_value = 5.0;
let luminance = converter.munsell_value_to_luminance(test_value).unwrap(); let recovered_value = converter.luminance_to_munsell_value(luminance).unwrap();
assert!((recovered_value - test_value).abs() < 1e-6);
}
#[test]
fn test_achromatic_detection() {
let converter = MathematicalMunsellConverter::new().unwrap();
assert!(converter.is_achromatic(ILLUMINANT_C[0], ILLUMINANT_C[1]));
assert!(!converter.is_achromatic(0.7, 0.3)); assert!(!converter.is_achromatic(0.3, 0.6)); }
#[test]
fn test_hue_string_parsing() {
let converter = MathematicalMunsellConverter::new().unwrap();
let (hue, family) = converter.parse_hue_string("5R").unwrap();
assert_eq!(hue, 5.0);
assert_eq!(family, "R");
let (hue, family) = converter.parse_hue_string("2.5GY").unwrap();
assert_eq!(hue, 2.5);
assert_eq!(family, "GY");
}
#[test]
fn test_munsell_notation_formatting() {
let converter = MathematicalMunsellConverter::new().unwrap();
let neutral = MunsellSpecification {
hue: 0.0,
family: "N".to_string(),
value: 5.0,
chroma: 0.0,
};
assert_eq!(converter.format_munsell_notation(&neutral), "N 5.0");
let chromatic = MunsellSpecification {
hue: 5.0,
family: "R".to_string(),
value: 4.0,
chroma: 12.0,
};
assert_eq!(converter.format_munsell_notation(&chromatic), "5.0R 4.0/12.0");
}
#[test]
fn test_black_color_conversion() {
let converter = MathematicalMunsellConverter::new().unwrap();
let munsell = converter.srgb_to_munsell([0, 0, 0]).unwrap();
assert!(munsell.value < 1.0); assert!(munsell.chroma < 1.0); }
}