#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WindowLevel {
pub center: f64,
pub width: f64,
}
impl WindowLevel {
#[must_use]
pub fn new(center: f64, width: f64) -> Self {
debug_assert!(width > 0.0, "window width must be positive");
Self { center, width }
}
#[must_use]
pub fn apply(&self, value: f64, out_min: f64, out_max: f64) -> f64 {
let t = (value - self.center + 0.5) / self.width + 0.5;
let t = t.clamp(0.0, 1.0);
out_min + t * (out_max - out_min)
}
#[inline]
#[must_use]
pub fn normalise(&self, value: f64) -> f64 {
self.apply(value, 0.0, 1.0)
}
#[must_use]
pub fn denormalise(&self, t: f64) -> f64 {
(t - 0.5) * self.width + self.center - 0.5
}
pub fn adjust_center(&mut self, delta: f64) {
self.center += delta;
}
pub fn adjust_width(&mut self, factor: f64) {
self.width = (self.width * factor).max(1.0);
}
#[must_use]
pub fn from_scalar_range(min: f64, max: f64) -> Self {
Self::new((min + max) * 0.5, (max - min).max(1.0))
}
}
pub mod presets {
use super::WindowLevel;
pub const SOFT_TISSUE: WindowLevel = WindowLevel {
center: 40.0,
width: 400.0,
};
pub const LUNG: WindowLevel = WindowLevel {
center: -600.0,
width: 1500.0,
};
pub const BONE: WindowLevel = WindowLevel {
center: 400.0,
width: 1500.0,
};
pub const BRAIN: WindowLevel = WindowLevel {
center: 40.0,
width: 80.0,
};
pub const LIVER: WindowLevel = WindowLevel {
center: 60.0,
width: 160.0,
};
pub const ABDOMEN: WindowLevel = WindowLevel {
center: 60.0,
width: 400.0,
};
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn center_maps_to_midpoint() {
let wl = WindowLevel::new(40.0, 400.0);
assert_abs_diff_eq!(wl.normalise(39.5), 0.5, epsilon = 1e-10);
}
#[test]
fn below_window_clamps_to_zero() {
let wl = WindowLevel::new(40.0, 400.0);
assert_abs_diff_eq!(wl.normalise(-160.0 - 1.0), 0.0, epsilon = 1e-10);
}
#[test]
fn above_window_clamps_to_one() {
let wl = WindowLevel::new(40.0, 400.0);
assert_abs_diff_eq!(wl.normalise(240.0 + 1.0), 1.0, epsilon = 1e-10);
}
#[test]
fn normalise_denormalise_round_trip() {
let wl = WindowLevel::new(100.0, 200.0);
for v in [0.0, 50.0, 100.0, 150.0, 199.0] {
let t = wl.normalise(v);
let back = wl.denormalise(t);
assert_abs_diff_eq!(back, v, epsilon = 1e-8);
}
}
#[test]
fn apply_custom_range() {
let wl = WindowLevel::new(0.0, 2.0);
let out = wl.apply(-0.5, 0.0, 255.0);
assert_abs_diff_eq!(out, 127.5, epsilon = 0.5);
}
#[test]
fn presets_are_valid() {
let st = presets::SOFT_TISSUE.width;
let lu = presets::LUNG.width;
let bo = presets::BONE.width;
assert!(st > 0.0);
assert!(lu > 0.0);
assert!(bo > 0.0);
}
#[test]
fn from_scalar_range() {
let wl = WindowLevel::from_scalar_range(100.0, 300.0);
assert_abs_diff_eq!(wl.center, 200.0, epsilon = 1e-10);
assert_abs_diff_eq!(wl.width, 200.0, epsilon = 1e-10);
}
#[test]
fn from_scalar_range_degenerate() {
let wl = WindowLevel::from_scalar_range(50.0, 50.0);
assert!(wl.width >= 1.0, "width should be at least 1.0");
}
}