volren_core/
window_level.rs1#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct WindowLevel {
19 pub center: f64,
21 pub width: f64,
23}
24
25impl WindowLevel {
26 #[must_use]
31 pub fn new(center: f64, width: f64) -> Self {
32 debug_assert!(width > 0.0, "window width must be positive");
33 Self { center, width }
34 }
35
36 #[must_use]
41 pub fn apply(&self, value: f64, out_min: f64, out_max: f64) -> f64 {
42 let t = (value - self.center + 0.5) / self.width + 0.5;
44 let t = t.clamp(0.0, 1.0);
45 out_min + t * (out_max - out_min)
46 }
47
48 #[inline]
50 #[must_use]
51 pub fn normalise(&self, value: f64) -> f64 {
52 self.apply(value, 0.0, 1.0)
53 }
54
55 #[must_use]
59 pub fn denormalise(&self, t: f64) -> f64 {
60 (t - 0.5) * self.width + self.center - 0.5
61 }
62
63 pub fn adjust_center(&mut self, delta: f64) {
65 self.center += delta;
66 }
67
68 pub fn adjust_width(&mut self, factor: f64) {
72 self.width = (self.width * factor).max(1.0);
73 }
74
75 #[must_use]
79 pub fn from_scalar_range(min: f64, max: f64) -> Self {
80 Self::new((min + max) * 0.5, (max - min).max(1.0))
81 }
82}
83
84pub mod presets {
86 use super::WindowLevel;
87
88 pub const SOFT_TISSUE: WindowLevel = WindowLevel {
90 center: 40.0,
91 width: 400.0,
92 };
93
94 pub const LUNG: WindowLevel = WindowLevel {
96 center: -600.0,
97 width: 1500.0,
98 };
99
100 pub const BONE: WindowLevel = WindowLevel {
102 center: 400.0,
103 width: 1500.0,
104 };
105
106 pub const BRAIN: WindowLevel = WindowLevel {
108 center: 40.0,
109 width: 80.0,
110 };
111
112 pub const LIVER: WindowLevel = WindowLevel {
114 center: 60.0,
115 width: 160.0,
116 };
117
118 pub const ABDOMEN: WindowLevel = WindowLevel {
120 center: 60.0,
121 width: 400.0,
122 };
123}
124
125#[cfg(test)]
128mod tests {
129 use super::*;
130 use approx::assert_abs_diff_eq;
131
132 #[test]
133 fn center_maps_to_midpoint() {
134 let wl = WindowLevel::new(40.0, 400.0);
135 assert_abs_diff_eq!(wl.normalise(39.5), 0.5, epsilon = 1e-10);
137 }
138
139 #[test]
140 fn below_window_clamps_to_zero() {
141 let wl = WindowLevel::new(40.0, 400.0);
142 assert_abs_diff_eq!(wl.normalise(-160.0 - 1.0), 0.0, epsilon = 1e-10);
143 }
144
145 #[test]
146 fn above_window_clamps_to_one() {
147 let wl = WindowLevel::new(40.0, 400.0);
148 assert_abs_diff_eq!(wl.normalise(240.0 + 1.0), 1.0, epsilon = 1e-10);
149 }
150
151 #[test]
152 fn normalise_denormalise_round_trip() {
153 let wl = WindowLevel::new(100.0, 200.0);
154 for v in [0.0, 50.0, 100.0, 150.0, 199.0] {
157 let t = wl.normalise(v);
158 let back = wl.denormalise(t);
159 assert_abs_diff_eq!(back, v, epsilon = 1e-8);
160 }
161 }
162
163 #[test]
164 fn apply_custom_range() {
165 let wl = WindowLevel::new(0.0, 2.0);
166 let out = wl.apply(-0.5, 0.0, 255.0);
168 assert_abs_diff_eq!(out, 127.5, epsilon = 0.5);
169 }
170
171 #[test]
172 fn presets_are_valid() {
173 let st = presets::SOFT_TISSUE.width;
175 let lu = presets::LUNG.width;
176 let bo = presets::BONE.width;
177 assert!(st > 0.0);
178 assert!(lu > 0.0);
179 assert!(bo > 0.0);
180 }
181
182 #[test]
183 fn from_scalar_range() {
184 let wl = WindowLevel::from_scalar_range(100.0, 300.0);
185 assert_abs_diff_eq!(wl.center, 200.0, epsilon = 1e-10);
186 assert_abs_diff_eq!(wl.width, 200.0, epsilon = 1e-10);
187 }
188
189 #[test]
190 fn from_scalar_range_degenerate() {
191 let wl = WindowLevel::from_scalar_range(50.0, 50.0);
192 assert!(wl.width >= 1.0, "width should be at least 1.0");
193 }
194}