use crate::color::{Color, NormalizedColor};
use nalgebra::{DMatrix, DVector, Vector3};
const EPSILON: f64 = 1e-10;
pub const DEFAULT_COLOR_CLOSENESS_THRESHOLD: f64 = 0.05;
#[derive(Debug, Clone)]
pub struct UnmixResult {
pub weights: Vec<f64>,
pub alpha: f64,
}
pub fn unmix_colors(
observed: Color,
foreground_colors: &[NormalizedColor],
background: NormalizedColor,
) -> UnmixResult {
unmix_colors_internal(observed, foreground_colors, background, true)
}
pub(crate) fn unmix_colors_internal(
observed: Color,
foreground_colors: &[NormalizedColor],
background: NormalizedColor,
optimize_opacity: bool,
) -> UnmixResult {
let observed = Vector3::new(
observed[0] as f64 / 255.0,
observed[1] as f64 / 255.0,
observed[2] as f64 / 255.0,
);
match foreground_colors.len() {
0 => UnmixResult {
weights: vec![],
alpha: 0.0,
},
1 => unmix_single_color(observed, foreground_colors[0], background),
_ => {
if optimize_opacity {
unmix_multiple_colors_optimized(observed, foreground_colors, background)
} else {
unmix_multiple_colors_simple(observed, foreground_colors, background)
}
}
}
}
fn unmix_single_color(
observed: Vector3<f64>,
foreground: NormalizedColor,
background: NormalizedColor,
) -> UnmixResult {
let fg = Vector3::from_row_slice(&foreground);
let bg = Vector3::from_row_slice(&background);
let obs_minus_bg = observed - bg;
let fg_minus_bg = fg - bg;
let weight = if fg_minus_bg.norm() > EPSILON {
let dot = obs_minus_bg.dot(&fg_minus_bg);
let norm_sq = fg_minus_bg.dot(&fg_minus_bg);
(dot / norm_sq).clamp(0.0, 1.0)
} else {
0.0
};
UnmixResult {
weights: vec![weight],
alpha: weight,
}
}
fn unmix_multiple_colors_simple(
observed: Vector3<f64>,
foreground_colors: &[NormalizedColor],
background: NormalizedColor,
) -> UnmixResult {
let n = foreground_colors.len();
let mut matrix_data = Vec::with_capacity(3 * n);
for fg in foreground_colors {
matrix_data.push(fg[0] - background[0]);
matrix_data.push(fg[1] - background[1]);
matrix_data.push(fg[2] - background[2]);
}
let a = DMatrix::from_column_slice(3, n, &matrix_data);
let b = observed - Vector3::from_row_slice(&background);
let b_vec = DVector::from_column_slice(&[b[0], b[1], b[2]]);
let weights = match a.pseudo_inverse(EPSILON) {
Ok(a_inv) => {
let solution = a_inv * b_vec;
solution.iter().map(|&w| w.max(0.0)).collect()
}
Err(_) => {
let mut weights = vec![0.0; n];
weights[0] = 1.0;
weights
}
};
let sum: f64 = weights.iter().sum();
let (final_weights, alpha) = if sum > 1.0 {
let normalized: Vec<f64> = weights.iter().map(|w| w / sum).collect();
(normalized, 1.0)
} else {
(weights, sum)
};
UnmixResult {
weights: final_weights,
alpha,
}
}
fn unmix_multiple_colors_optimized(
observed: Vector3<f64>,
foreground_colors: &[NormalizedColor],
background: NormalizedColor,
) -> UnmixResult {
let n = foreground_colors.len();
let bg = Vector3::from_row_slice(&background);
let target = observed - bg;
let mut best_weights = vec![0.0; n];
let mut best_alpha = 0.0;
let mut matrix_data = Vec::with_capacity(3 * n);
for fg in foreground_colors {
matrix_data.push(fg[0] - background[0]);
matrix_data.push(fg[1] - background[1]);
matrix_data.push(fg[2] - background[2]);
}
let a = DMatrix::from_column_slice(3, n, &matrix_data);
let b_vec = DVector::from_column_slice(&[target[0], target[1], target[2]]);
if let Ok(a_inv) = a.pseudo_inverse(EPSILON) {
let solution = a_inv * b_vec.clone();
let weights: Vec<f64> = solution.iter().map(|&w| w.max(0.0)).collect();
let sum: f64 = weights.iter().sum();
if sum > 0.0 {
let alpha = sum.min(1.0);
if alpha > best_alpha {
best_weights = if sum > 1.0 {
weights.iter().map(|w| w / sum).collect()
} else {
weights
};
best_alpha = alpha;
}
}
}
for (i, fg) in foreground_colors.iter().enumerate() {
let fg_vec = Vector3::from_row_slice(fg);
let fg_minus_bg = fg_vec - bg;
if fg_minus_bg.norm() > EPSILON {
let dot = target.dot(&fg_minus_bg);
let norm_sq = fg_minus_bg.dot(&fg_minus_bg);
let weight = (dot / norm_sq).clamp(0.0, 1.0);
let reconstructed = weight * fg_vec + (1.0 - weight) * bg;
let error = (reconstructed - observed).norm();
if weight > best_alpha && error < 0.01 {
best_weights = vec![0.0; n];
best_weights[i] = weight;
best_alpha = weight;
}
}
}
if n >= 2 && best_alpha < 0.99 {
for i in 0..n {
for j in (i + 1)..n {
let fg_i = foreground_colors[i];
let fg_j = foreground_colors[j];
let pair_matrix = DMatrix::from_column_slice(
3,
2,
&[
fg_i[0] - background[0],
fg_j[0] - background[0],
fg_i[1] - background[1],
fg_j[1] - background[1],
fg_i[2] - background[2],
fg_j[2] - background[2],
],
);
if let Ok(pair_inv) = pair_matrix.pseudo_inverse(EPSILON) {
let pair_solution = pair_inv * b_vec.clone();
let w_i = pair_solution[0].max(0.0);
let w_j = pair_solution[1].max(0.0);
let sum = w_i + w_j;
if sum > 0.0 {
let alpha = sum.min(1.0);
let normalized_wi = if sum > 1.0 { w_i / sum } else { w_i };
let normalized_wj = if sum > 1.0 { w_j / sum } else { w_j };
let reconstructed = normalized_wi * Vector3::from_row_slice(&fg_i)
+ normalized_wj * Vector3::from_row_slice(&fg_j)
+ (1.0 - normalized_wi - normalized_wj) * bg;
let error = (reconstructed - observed).norm();
if alpha > best_alpha && error < 0.01 {
best_weights = vec![0.0; n];
if sum > 1.0 {
best_weights[i] = w_i / sum;
best_weights[j] = w_j / sum;
best_alpha = 1.0;
} else {
best_weights[i] = w_i;
best_weights[j] = w_j;
best_alpha = alpha;
}
}
}
}
}
}
}
UnmixResult {
weights: best_weights,
alpha: best_alpha,
}
}
fn color_distance(color1: Vector3<f64>, color2: Vector3<f64>) -> f64 {
(color1 - color2).norm()
}
pub fn is_color_close_to_foreground(
observed: Vector3<f64>,
foreground_colors: &[NormalizedColor],
background: NormalizedColor,
threshold: f64,
) -> bool {
for fg in foreground_colors {
let fg_vec = Vector3::from_row_slice(fg);
let bg_vec = Vector3::from_row_slice(&background);
let fg_minus_bg = fg_vec - bg_vec;
if fg_minus_bg.norm() > EPSILON {
let obs_minus_bg = observed - bg_vec;
let dot = obs_minus_bg.dot(&fg_minus_bg);
let norm_sq = fg_minus_bg.dot(&fg_minus_bg);
let weight = (dot / norm_sq).clamp(0.0, 1.0);
let reconstructed = weight * fg_vec + (1.0 - weight) * bg_vec;
if color_distance(reconstructed, observed) < threshold {
return true;
}
}
}
false
}
pub fn compute_result_color(
unmix_result: &UnmixResult,
foreground_colors: &[NormalizedColor],
) -> (NormalizedColor, f64) {
if unmix_result.alpha == 0.0 {
return ([0.0, 0.0, 0.0], 0.0);
}
let mut result = [0.0, 0.0, 0.0];
let sum_weights: f64 = unmix_result.weights.iter().sum();
if sum_weights > 0.0 {
for (i, &weight) in unmix_result.weights.iter().enumerate() {
if let Some(fg) = foreground_colors.get(i) {
result[0] += weight * fg[0];
result[1] += weight * fg[1];
result[2] += weight * fg[2];
}
}
result[0] /= sum_weights;
result[1] /= sum_weights;
result[2] /= sum_weights;
}
(result, unmix_result.alpha)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unmix_no_foreground_colors() {
let result = unmix_colors([128, 128, 128], &[], [0.0, 0.0, 0.0]);
assert_eq!(result.weights.len(), 0);
assert_eq!(result.alpha, 0.0);
}
#[test]
fn test_unmix_single_color_pure() {
let result = unmix_colors([255, 0, 0], &[[1.0, 0.0, 0.0]], [0.0, 0.0, 0.0]);
assert_eq!(result.weights.len(), 1);
assert!((result.weights[0] - 1.0).abs() < EPSILON);
assert!((result.alpha - 1.0).abs() < EPSILON);
}
#[test]
fn test_unmix_single_color_blend() {
let result = unmix_colors([127, 0, 0], &[[1.0, 0.0, 0.0]], [0.0, 0.0, 0.0]);
assert!((result.weights[0] - 0.498).abs() < 0.01); assert!((result.alpha - 0.498).abs() < 0.01);
}
#[test]
fn test_unmix_single_color_identical_fg_bg() {
let result = unmix_colors([255, 0, 0], &[[1.0, 0.0, 0.0]], [1.0, 0.0, 0.0]);
assert_eq!(result.weights[0], 0.0);
assert_eq!(result.alpha, 0.0);
}
#[test]
fn test_unmix_multiple_colors() {
let result = unmix_colors(
[255, 255, 0],
&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[0.0, 0.0, 0.0],
);
assert_eq!(result.weights.len(), 2);
assert!((result.weights[0] - 0.5).abs() < 0.1);
assert!((result.weights[1] - 0.5).abs() < 0.1);
assert!((result.alpha - 1.0).abs() < 0.1);
}
#[test]
fn test_compute_result_color_zero_alpha() {
let unmix = UnmixResult {
weights: vec![1.0],
alpha: 0.0,
};
let (color, alpha) = compute_result_color(&unmix, &[[1.0, 0.0, 0.0]]);
assert_eq!(color, [0.0, 0.0, 0.0]);
assert_eq!(alpha, 0.0);
}
#[test]
fn test_compute_result_color_weighted() {
let unmix = UnmixResult {
weights: vec![0.3, 0.7],
alpha: 1.0,
};
let (color, alpha) = compute_result_color(&unmix, &[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
assert!((color[0] - 0.3).abs() < EPSILON);
assert!((color[1] - 0.7).abs() < EPSILON);
assert_eq!(color[2], 0.0);
assert_eq!(alpha, 1.0);
}
#[test]
fn test_unmix_edge_cases() {
let result1 = unmix_colors(
[128, 0, 0],
&[[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], [0.0, 0.0, 0.0],
);
assert_eq!(result1.weights.len(), 2);
let total_weight: f64 = result1.weights.iter().sum();
assert!((total_weight - 0.502).abs() < 0.01);
let result2 = unmix_colors(
[128, 128, 128],
&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[0.502, 0.502, 0.502], );
assert!(result2.alpha < 0.01); }
#[test]
fn test_color_distance() {
let color1 = Vector3::new(0.0, 0.0, 0.0);
let color2 = Vector3::new(1.0, 0.0, 0.0);
assert!((color_distance(color1, color2) - 1.0).abs() < EPSILON);
let color3 = Vector3::new(0.0, 0.0, 0.0);
let color4 = Vector3::new(1.0, 1.0, 1.0);
assert!((color_distance(color3, color4) - 1.732).abs() < 0.01); }
#[test]
fn test_is_color_close_to_foreground() {
let red = [1.0, 0.0, 0.0];
let black = [0.0, 0.0, 0.0];
let observed = Vector3::new(0.5, 0.0, 0.0);
assert!(is_color_close_to_foreground(observed, &[red], black, 0.1));
let purple = Vector3::new(0.5, 0.0, 0.5);
assert!(!is_color_close_to_foreground(purple, &[red], black, 0.1));
}
}