1use crate::dcm::pixel::RawImage;
4use crate::dcm::study::Instance;
5
6#[derive(Debug, Clone, Copy)]
13pub struct RoiStats {
14 pub mean: f64,
16 pub std: f64,
19 pub min: f64,
21 pub max: f64,
23 pub count: usize,
25}
26
27impl RoiStats {
28 pub fn label(&self, modality: &str) -> String {
30 let unit = if modality.eq_ignore_ascii_case("CT") {
31 " HU"
32 } else {
33 ""
34 };
35 format!(
36 "n={} μ={:.1}{u} σ={:.1} min={:.0} max={:.0}",
37 self.count,
38 self.mean,
39 self.std,
40 self.min,
41 self.max,
42 u = unit
43 )
44 }
45}
46
47fn bbox(p1: [f32; 2], p2: [f32; 2], w: u32, h: u32) -> (u32, u32, u32, u32) {
48 let x0 = p1[0].min(p2[0]).max(0.0).round() as i32;
49 let y0 = p1[1].min(p2[1]).max(0.0).round() as i32;
50 let x1 = p1[0].max(p2[0]).round() as i32;
51 let y1 = p1[1].max(p2[1]).round() as i32;
52 let x0 = x0.clamp(0, w as i32 - 1) as u32;
53 let y0 = y0.clamp(0, h as i32 - 1) as u32;
54 let x1 = x1.clamp(0, w as i32 - 1) as u32;
55 let y1 = y1.clamp(0, h as i32 - 1) as u32;
56 (x0, y0, x1.max(x0), y1.max(y0))
57}
58
59pub fn rect_stats(raw: &RawImage, p1: [f32; 2], p2: [f32; 2]) -> Option<RoiStats> {
65 let (x0, y0, x1, y1) = bbox(p1, p2, raw.width, raw.height);
66 if x1 <= x0 || y1 <= y0 {
67 return None;
68 }
69 let mut sum = 0.0f64;
70 let mut sumsq = 0.0f64;
71 let mut mn = f64::INFINITY;
72 let mut mx = f64::NEG_INFINITY;
73 let mut count = 0usize;
74 let stride = raw.width as usize;
75 for y in y0..=y1 {
76 for x in x0..=x1 {
77 let v = raw.values[y as usize * stride + x as usize] as f64;
78 sum += v;
79 sumsq += v * v;
80 if v < mn {
81 mn = v;
82 }
83 if v > mx {
84 mx = v;
85 }
86 count += 1;
87 }
88 }
89 if count == 0 {
90 return None;
91 }
92 let mean = sum / count as f64;
93 let var = (sumsq / count as f64 - mean * mean).max(0.0);
94 Some(RoiStats {
95 mean,
96 std: var.sqrt(),
97 min: mn,
98 max: mx,
99 count,
100 })
101}
102
103pub fn ellipse_stats(raw: &RawImage, p1: [f32; 2], p2: [f32; 2]) -> Option<RoiStats> {
109 let (x0, y0, x1, y1) = bbox(p1, p2, raw.width, raw.height);
110 if x1 <= x0 || y1 <= y0 {
111 return None;
112 }
113 let cx = (x0 as f64 + x1 as f64) / 2.0;
114 let cy = (y0 as f64 + y1 as f64) / 2.0;
115 let rx = ((x1 - x0) as f64 / 2.0).max(0.5);
116 let ry = ((y1 - y0) as f64 / 2.0).max(0.5);
117 let mut sum = 0.0f64;
118 let mut sumsq = 0.0f64;
119 let mut mn = f64::INFINITY;
120 let mut mx = f64::NEG_INFINITY;
121 let mut count = 0usize;
122 let stride = raw.width as usize;
123 for y in y0..=y1 {
124 for x in x0..=x1 {
125 let dx = (x as f64 - cx) / rx;
126 let dy = (y as f64 - cy) / ry;
127 if dx * dx + dy * dy > 1.0 {
128 continue;
129 }
130 let v = raw.values[y as usize * stride + x as usize] as f64;
131 sum += v;
132 sumsq += v * v;
133 if v < mn {
134 mn = v;
135 }
136 if v > mx {
137 mx = v;
138 }
139 count += 1;
140 }
141 }
142 if count == 0 {
143 return None;
144 }
145 let mean = sum / count as f64;
146 let var = (sumsq / count as f64 - mean * mean).max(0.0);
147 Some(RoiStats {
148 mean,
149 std: var.sqrt(),
150 min: mn,
151 max: mx,
152 count,
153 })
154}
155
156pub fn length_label(p1: [f32; 2], p2: [f32; 2], inst: &Instance, raw: &RawImage) -> String {
159 let dx_px = (p2[0] - p1[0]) as f64;
160 let dy_px = (p2[1] - p1[1]) as f64;
161 let scale = raw.display_scale.max(1) as f64;
162 if let Some((row_sp, col_sp)) = inst.pixel_spacing {
163 let dx = dx_px * col_sp * scale;
164 let dy = dy_px * row_sp * scale;
165 let mm = (dx * dx + dy * dy).sqrt();
166 format!("{:.1} mm", mm)
167 } else {
168 let px = (dx_px * dx_px + dy_px * dy_px).sqrt();
169 format!("{:.0} px", px)
170 }
171}
172
173pub fn angle_deg(p1: [f32; 2], v: [f32; 2], p2: [f32; 2]) -> f64 {
177 let a = (p1[0] - v[0], p1[1] - v[1]);
178 let b = (p2[0] - v[0], p2[1] - v[1]);
179 let dot = (a.0 * b.0 + a.1 * b.1) as f64;
180 let la = ((a.0 * a.0 + a.1 * a.1) as f64).sqrt();
181 let lb = ((b.0 * b.0 + b.1 * b.1) as f64).sqrt();
182 if la < 1e-6 || lb < 1e-6 {
183 return 0.0;
184 }
185 (dot / (la * lb)).clamp(-1.0, 1.0).acos().to_degrees()
186}