1use crate::dcm::annotation::{Annotation, AnnotationStore};
5use crate::dcm::pixel::{auto_window, load_raw, render_rgba, RawImage};
6use crate::dcm::roi::{angle_deg, ellipse_stats, length_label, rect_stats};
7use crate::dcm::study::{Instance, Series};
8use anyhow::{Context, Result};
9use dicom::core::value::{DataSetSequence, PrimitiveValue, Value};
10use dicom::core::{DataElement, VR};
11use dicom::object::open_file;
12use dicom_dictionary_std::tags;
13use std::fs;
14use std::path::Path;
15
16struct Canvas {
18 w: u32,
19 h: u32,
20 data: Vec<u8>,
21}
22
23impl Canvas {
24 fn from_raw(raw: &RawImage, window: (f64, f64), invert: bool) -> Self {
25 let data = render_rgba(raw, window.0, window.1, invert);
26 Self {
27 w: raw.width,
28 h: raw.height,
29 data,
30 }
31 }
32
33 fn put(&mut self, x: i32, y: i32, color: [u8; 4]) {
34 if x < 0 || y < 0 || x >= self.w as i32 || y >= self.h as i32 {
35 return;
36 }
37 let i = (y as usize * self.w as usize + x as usize) * 4;
38 self.data[i] = color[0];
39 self.data[i + 1] = color[1];
40 self.data[i + 2] = color[2];
41 self.data[i + 3] = color[3];
42 }
43
44 fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: [u8; 4]) {
45 let dx = (x1 - x0).abs();
47 let sx = if x0 < x1 { 1 } else { -1 };
48 let dy = -(y1 - y0).abs();
49 let sy = if y0 < y1 { 1 } else { -1 };
50 let mut err = dx + dy;
51 let mut x = x0;
52 let mut y = y0;
53 loop {
54 self.put(x, y, color);
55 if x == x1 && y == y1 {
56 break;
57 }
58 let e2 = 2 * err;
59 if e2 >= dy {
60 err += dy;
61 x += sx;
62 }
63 if e2 <= dx {
64 err += dx;
65 y += sy;
66 }
67 }
68 }
69
70 fn rect_outline(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: [u8; 4]) {
71 self.line(x0, y0, x1, y0, color);
72 self.line(x1, y0, x1, y1, color);
73 self.line(x1, y1, x0, y1, color);
74 self.line(x0, y1, x0, y0, color);
75 }
76
77 fn ellipse_outline(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, color: [u8; 4]) {
78 let steps = 96;
80 let mut prev: Option<(i32, i32)> = None;
81 for i in 0..=steps {
82 let t = (i as f32) * std::f32::consts::TAU / steps as f32;
83 let x = (cx + rx * t.cos()).round() as i32;
84 let y = (cy + ry * t.sin()).round() as i32;
85 if let Some((px, py)) = prev {
86 self.line(px, py, x, y, color);
87 }
88 prev = Some((x, y));
89 }
90 }
91
92 fn write_png(&self, path: &Path) -> Result<()> {
93 image::save_buffer(path, &self.data, self.w, self.h, image::ColorType::Rgba8)
94 .with_context(|| format!("write png {}", path.display()))?;
95 Ok(())
96 }
97}
98
99#[allow(clippy::too_many_arguments)]
116pub fn export_current_view(
117 inst: &Instance,
118 raw: &RawImage,
119 window: (f64, f64),
120 invert: bool,
121 annotations: &[Annotation],
122 burn_annotations: bool,
123 out_path: &Path,
124 modality: &str,
125) -> Result<()> {
126 let mut canvas = Canvas::from_raw(raw, window, invert);
127 if burn_annotations {
128 burn_into_canvas(&mut canvas, annotations, raw, inst, modality);
129 }
130 canvas.write_png(out_path)
131}
132
133pub fn export_series_pngs(
140 series: &Series,
141 out_dir: &Path,
142 store: &AnnotationStore,
143 burn_annotations: bool,
144) -> Result<usize> {
145 fs::create_dir_all(out_dir).with_context(|| format!("create {}", out_dir.display()))?;
146 let mut written = 0usize;
147 for (i, inst) in series.instances.iter().enumerate() {
148 let raw = match load_raw(&inst.path) {
149 Ok(r) => r,
150 Err(e) => {
151 tracing::warn!(path = %inst.path.display(), error = %e, "skip slice");
152 continue;
153 }
154 };
155 let window = auto_window(&raw);
156 let anns = store.for_instance(&inst.sop_instance_uid);
157 let mut canvas = Canvas::from_raw(&raw, window, false);
158 if burn_annotations && !anns.is_empty() {
159 burn_into_canvas(&mut canvas, anns, &raw, inst, &series.modality);
160 }
161 let name = format!("slice_{:04}.png", i + 1);
162 canvas.write_png(&out_dir.join(&name))?;
163 written += 1;
164 }
165 Ok(written)
166}
167
168fn burn_into_canvas(
169 canvas: &mut Canvas,
170 anns: &[Annotation],
171 raw: &RawImage,
172 inst: &Instance,
173 modality: &str,
174) {
175 let yellow = [240, 220, 80, 255];
176 let cyan = [120, 220, 255, 255];
177 let green = [120, 240, 160, 255];
178 for ann in anns {
179 match ann {
180 Annotation::Length { p1, p2 } => {
181 canvas.line(
182 p1[0] as i32,
183 p1[1] as i32,
184 p2[0] as i32,
185 p2[1] as i32,
186 yellow,
187 );
188 let _ = length_label(*p1, *p2, inst, raw);
189 }
190 Annotation::Angle { p1, v, p2 } => {
191 canvas.line(v[0] as i32, v[1] as i32, p1[0] as i32, p1[1] as i32, yellow);
192 canvas.line(v[0] as i32, v[1] as i32, p2[0] as i32, p2[1] as i32, yellow);
193 let _ = angle_deg(*p1, *v, *p2);
194 }
195 Annotation::Rect { p1, p2 } => {
196 canvas.rect_outline(p1[0] as i32, p1[1] as i32, p2[0] as i32, p2[1] as i32, cyan);
197 let _ = rect_stats(raw, *p1, *p2);
198 let _ = modality;
199 }
200 Annotation::Ellipse { p1, p2 } => {
201 let cx = (p1[0] + p2[0]) / 2.0;
202 let cy = (p1[1] + p2[1]) / 2.0;
203 let rx = (p2[0] - p1[0]).abs() / 2.0;
204 let ry = (p2[1] - p1[1]).abs() / 2.0;
205 canvas.ellipse_outline(cx, cy, rx, ry, green);
206 let _ = ellipse_stats(raw, *p1, *p2);
207 }
208 }
209 }
210}
211
212pub fn anonymize_file(src: &Path, dst: &Path) -> Result<()> {
226 let mut obj = open_file(src).with_context(|| format!("open {}", src.display()))?;
227
228 let blank_pn = DataElement::new(
229 tags::PATIENT_NAME,
230 VR::PN,
231 Value::Primitive(PrimitiveValue::from("Anonymized")),
232 );
233 let blank_id = DataElement::new(
234 tags::PATIENT_ID,
235 VR::LO,
236 Value::Primitive(PrimitiveValue::from("ANON")),
237 );
238 let blank_date = DataElement::new(
239 tags::PATIENT_BIRTH_DATE,
240 VR::DA,
241 Value::Primitive(PrimitiveValue::from("")),
242 );
243 let blank_sex = DataElement::new(
244 tags::PATIENT_SEX,
245 VR::CS,
246 Value::Primitive(PrimitiveValue::from("")),
247 );
248 let blank_acc = DataElement::new(
249 tags::ACCESSION_NUMBER,
250 VR::SH,
251 Value::Primitive(PrimitiveValue::from("")),
252 );
253 let blank_ref = DataElement::new(
254 tags::REFERRING_PHYSICIAN_NAME,
255 VR::PN,
256 Value::Primitive(PrimitiveValue::from("")),
257 );
258 let blank_op = DataElement::new(
259 tags::OPERATORS_NAME,
260 VR::PN,
261 Value::Primitive(PrimitiveValue::from("")),
262 );
263
264 for e in [
265 blank_pn, blank_id, blank_date, blank_sex, blank_acc, blank_ref, blank_op,
266 ] {
267 obj.put_element(e);
268 }
269
270 for tag in [
272 tags::INSTITUTION_NAME,
273 tags::INSTITUTION_ADDRESS,
274 tags::STATION_NAME,
275 tags::PATIENT_AGE,
276 tags::PATIENT_ADDRESS,
277 tags::PATIENT_TELEPHONE_NUMBERS,
278 ] {
279 let _ = obj.remove_element(tag);
280 }
281
282 if let Some(parent) = dst.parent() {
283 fs::create_dir_all(parent).ok();
284 }
285 obj.write_to_file(dst)
286 .with_context(|| format!("write {}", dst.display()))?;
287 Ok(())
288}
289
290pub fn anonymize_study(study: &crate::dcm::Study, out_dir: &Path) -> Result<(usize, usize)> {
296 let mut ok = 0usize;
297 let mut fail = 0usize;
298 fs::create_dir_all(out_dir).ok();
299 for series in &study.series {
300 let series_dir = out_dir.join(safe_name(&series.series_instance_uid));
301 for (i, inst) in series.instances.iter().enumerate() {
302 let out = series_dir.join(format!("{:04}.dcm", i + 1));
303 match anonymize_file(&inst.path, &out) {
304 Ok(()) => ok += 1,
305 Err(e) => {
306 tracing::warn!(error = %e, path = %inst.path.display(), "anonymize failed");
307 fail += 1;
308 }
309 }
310 }
311 }
312 Ok((ok, fail))
313}
314
315fn safe_name(s: &str) -> String {
317 s.chars()
318 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
319 .collect::<String>()
320 .chars()
321 .take(40)
322 .collect()
323}
324
325#[allow(dead_code)]
326fn _force_use(
327 d: DataSetSequence<dicom::object::InMemDicomObject>,
328) -> DataSetSequence<dicom::object::InMemDicomObject> {
329 d
330}