#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color(pub u8, pub u8, pub u8);
impl Color {
pub const RED: Self = Self(255, 0, 0);
pub const GREEN: Self = Self(0, 255, 0);
pub const BLUE: Self = Self(0, 0, 255);
pub const WHITE: Self = Self(255, 255, 255);
pub const BLACK: Self = Self(0, 0, 0);
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self(r, g, b)
}
#[must_use]
pub const fn from_index(index: usize) -> Self {
let color = COLORS[index % COLORS.len()];
Self(color[0], color[1], color[2])
}
#[must_use]
pub const fn from_pose_index(index: usize) -> Self {
let color = POSE_COLORS[index % POSE_COLORS.len()];
Self(color[0], color[1], color[2])
}
#[must_use]
pub fn to_hex(self) -> String {
format!("#{:02X}{:02X}{:02X}", self.0, self.1, self.2)
}
}
pub const COLORS: [[u8; 3]; 20] = [
[4, 42, 255], [11, 219, 235], [243, 243, 243], [0, 223, 183], [17, 31, 104], [255, 111, 221], [255, 68, 79], [204, 237, 0], [0, 243, 68], [189, 0, 255], [0, 180, 255], [221, 0, 186], [0, 255, 255], [38, 192, 0], [1, 255, 179], [125, 36, 255], [123, 0, 104], [255, 27, 108], [252, 109, 47], [162, 255, 11], ];
pub const POSE_COLORS: [[u8; 3]; 20] = [
[255, 128, 0], [255, 153, 51], [255, 178, 102], [230, 230, 0], [255, 153, 255], [153, 204, 255], [255, 102, 255], [255, 51, 255], [102, 178, 255], [51, 153, 255], [255, 153, 153], [255, 102, 102], [255, 51, 51], [153, 255, 153], [102, 255, 102], [51, 255, 51], [0, 255, 0], [0, 0, 255], [255, 0, 0], [255, 255, 255], ];
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn inferno(t: f32) -> [u8; 3] {
const C: [[f32; 3]; 7] = [
[0.000_218_940_4, 0.001_651_005, -0.019_480_9],
[0.106_513_4, 0.563_956_4, 3.932_712],
[11.602_49, -3.972_854, -15.942_39],
[-41.704, 17.436_4, 44.354_15],
[77.162_94, -33.402_36, -81.807_31],
[-71.319_43, 32.626_06, 73.209_52],
[25.131_13, -12.242_67, -23.070_32],
];
let t = t.clamp(0.0, 1.0);
let mut out = [0u8; 3];
for (ch, o) in out.iter_mut().enumerate() {
let mut v = C[6][ch];
for row in C.iter().take(6).rev() {
v = v.mul_add(t, row[ch]);
}
*o = (v.clamp(0.0, 1.0) * 255.0).round() as u8;
}
out
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn jet(t: f32) -> [u8; 3] {
let t = t.clamp(0.0, 1.0);
let ch = |center: f32| (1.5 - t.mul_add(4.0, -center).abs()).clamp(0.0, 1.0);
[
(ch(3.0) * 255.0).round() as u8,
(ch(2.0) * 255.0).round() as u8,
(ch(1.0) * 255.0).round() as u8,
]
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn spectral(t: f32) -> [u8; 3] {
const A: [[u8; 3]; 11] = [
[94, 79, 162],
[51, 135, 188],
[102, 194, 165],
[170, 220, 164],
[230, 245, 152],
[255, 254, 190],
[254, 224, 139],
[253, 173, 96],
[244, 109, 67],
[212, 61, 79],
[158, 1, 66],
];
let x = t.clamp(0.0, 1.0) * 10.0;
let i = (x as usize).min(9);
let f = x - i as f32;
let (lo, hi) = (A[i], A[i + 1]);
let lerp = |a: u8, b: u8| f.mul_add(f32::from(b) - f32::from(a), f32::from(a)).round() as u8;
[lerp(lo[0], hi[0]), lerp(lo[1], hi[1]), lerp(lo[2], hi[2])]
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn gray(t: f32) -> [u8; 3] {
let g = (t.clamp(0.0, 1.0) * 255.0).round() as u8;
[g, g, g]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Colormap {
#[default]
Inferno,
Jet,
Spectral,
Gray,
}
impl Colormap {
#[must_use]
pub fn sample(self, t: f32) -> [u8; 3] {
match self {
Self::Inferno => inferno(t),
Self::Jet => jet(t),
Self::Spectral => spectral(t),
Self::Gray => gray(t),
}
}
}
impl std::str::FromStr for Colormap {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"inferno" => Ok(Self::Inferno),
"jet" => Ok(Self::Jet),
"spectral" | "spectral_r" => Ok(Self::Spectral),
"gray" | "grey" | "grayscale" => Ok(Self::Gray),
_ => Err(format!(
"invalid colormap '{s}', expected one of: inferno, jet, spectral, gray"
)),
}
}
}
impl std::fmt::Display for Colormap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Inferno => "inferno",
Self::Jet => "jet",
Self::Spectral => "spectral",
Self::Gray => "gray",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DepthViz {
#[default]
Metric,
Disparity,
}
impl std::str::FromStr for DepthViz {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"metric" => Ok(Self::Metric),
"disparity" | "depthanything" => Ok(Self::Disparity),
_ => Err(format!(
"invalid depth-viz '{s}', expected one of: metric, disparity"
)),
}
}
}
impl std::fmt::Display for DepthViz {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Metric => "metric",
Self::Disparity => "disparity",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jet_and_colormap() {
assert_eq!(jet(0.0), [0, 0, 128]);
assert_eq!(jet(1.0), [128, 0, 0]);
assert_eq!(Colormap::Inferno.sample(0.5), inferno(0.5));
assert_eq!(Colormap::Jet.sample(0.5), jet(0.5));
assert_eq!("jet".parse::<Colormap>().unwrap(), Colormap::Jet);
assert_eq!("INFERNO".parse::<Colormap>().unwrap(), Colormap::Inferno);
assert!("magma".parse::<Colormap>().is_err());
assert_eq!(gray(0.0), [0, 0, 0]);
assert_eq!(gray(1.0), [255, 255, 255]);
assert_eq!(Colormap::Gray.sample(0.5), gray(0.5));
assert_eq!("gray".parse::<Colormap>().unwrap(), Colormap::Gray);
}
#[test]
fn test_spectral_and_depth_viz() {
assert_eq!(spectral(0.0), [94, 79, 162]);
assert_eq!(spectral(1.0), [158, 1, 66]);
assert_eq!(spectral(0.5), [255, 254, 190]);
assert_eq!(spectral(-1.0), [94, 79, 162]); assert_eq!(Colormap::Spectral.sample(1.0), [158, 1, 66]);
assert_eq!(
"spectral_r".parse::<Colormap>().unwrap(),
Colormap::Spectral
);
assert_eq!("metric".parse::<DepthViz>().unwrap(), DepthViz::Metric);
assert_eq!(
"disparity".parse::<DepthViz>().unwrap(),
DepthViz::Disparity
);
assert_eq!(DepthViz::default(), DepthViz::Metric);
assert!("log".parse::<DepthViz>().is_err());
}
#[test]
fn test_inferno_range_and_clamp() {
let lo = inferno(0.0);
let hi = inferno(1.0);
assert!(lo.iter().all(|&c| c < 20), "low end should be dark: {lo:?}");
assert!(
hi[0] > 200 && hi[1] > 200,
"high end should be bright: {hi:?}"
);
assert_eq!(inferno(-1.0), lo);
assert_eq!(inferno(2.0), hi);
}
#[test]
fn test_color_constants() {
assert_eq!(Color::RED, Color(255, 0, 0));
assert_eq!(Color::BLUE, Color(0, 0, 255));
}
#[test]
fn test_from_index() {
assert_eq!(Color::from_index(0), Color(4, 42, 255));
assert_eq!(Color::from_index(COLORS.len()), Color(4, 42, 255));
}
#[test]
fn test_from_pose_index() {
assert_eq!(Color::from_pose_index(0), Color(255, 128, 0));
}
#[test]
fn test_to_hex() {
assert_eq!(Color::from_index(0).to_hex(), "#042AFF");
assert_eq!(Color::BLACK.to_hex(), "#000000");
}
}