1#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions::*;
8use std::collections::HashMap;
9
10#[derive(Clone, Debug)]
12pub struct LandmarkSet {
13 pub landmarks: Vec<Landmark>,
15 pub space: String,
17}
18impl LandmarkSet {
19 pub fn new(space: &str) -> Self {
21 Self {
22 landmarks: Vec::new(),
23 space: space.to_string(),
24 }
25 }
26 pub fn add(&mut self, lm: Landmark) {
28 self.landmarks.push(lm);
29 }
30 pub fn find(&self, name: &str) -> Option<&Landmark> {
32 self.landmarks.iter().find(|l| l.name == name)
33 }
34 pub fn centroid(&self) -> Option<[f64; 3]> {
36 if self.landmarks.is_empty() {
37 return None;
38 }
39 let n = self.landmarks.len() as f64;
40 let mut c = [0.0f64; 3];
41 for lm in &self.landmarks {
42 for d in 0..3 {
43 c[d] += lm.position[d];
44 }
45 }
46 for d in 0..3 {
47 c[d] /= n;
48 }
49 Some(c)
50 }
51}
52#[derive(Clone, Debug)]
54pub struct DicomElement {
55 pub tag: DicomTag,
57 pub vr: DicomVr,
59}
60impl DicomElement {
61 pub fn new(tag: DicomTag, vr: DicomVr) -> Self {
63 Self { tag, vr }
64 }
65}
66#[derive(Clone, Debug)]
68pub struct VtkImageData {
69 pub dimensions: [usize; 3],
71 pub origin: [f64; 3],
73 pub spacing: [f64; 3],
75 pub scalar_arrays: HashMap<String, Vec<f64>>,
77 pub vector_arrays: HashMap<String, Vec<[f64; 3]>>,
79}
80impl VtkImageData {
81 pub fn new(dimensions: [usize; 3], origin: [f64; 3], spacing: [f64; 3]) -> Self {
83 Self {
84 dimensions,
85 origin,
86 spacing,
87 scalar_arrays: HashMap::new(),
88 vector_arrays: HashMap::new(),
89 }
90 }
91 pub fn n_voxels(&self) -> usize {
93 self.dimensions[0] * self.dimensions[1] * self.dimensions[2]
94 }
95 pub fn add_scalar(&mut self, name: &str, data: Vec<f64>) {
97 self.scalar_arrays.insert(name.to_string(), data);
98 }
99 pub fn add_vector(&mut self, name: &str, data: Vec<[f64; 3]>) {
101 self.vector_arrays.insert(name.to_string(), data);
102 }
103 pub fn index_to_world(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
105 [
106 self.origin[0] + i as f64 * self.spacing[0],
107 self.origin[1] + j as f64 * self.spacing[1],
108 self.origin[2] + k as f64 * self.spacing[2],
109 ]
110 }
111 pub fn to_vtk_string(&self, scalar_name: &str) -> String {
113 let [nx, ny, nz] = self.dimensions;
114 let [ox, oy, oz] = self.origin;
115 let [dx, dy, dz] = self.spacing;
116 let n = self.n_voxels();
117 let mut s = format!(
118 "# vtk DataFile Version 3.0\nVTK ImageData\nASCII\nDATASET STRUCTURED_POINTS\n\
119 DIMENSIONS {} {} {}\nORIGIN {} {} {}\nSPACING {} {} {}\n\
120 POINT_DATA {}\n",
121 nx, ny, nz, ox, oy, oz, dx, dy, dz, n
122 );
123 if let Some(arr) = self.scalar_arrays.get(scalar_name) {
124 s.push_str(&format!(
125 "SCALARS {} float 1\nLOOKUP_TABLE default\n",
126 scalar_name
127 ));
128 for &v in arr.iter().take(10) {
129 s.push_str(&format!("{:.6} ", v));
130 }
131 }
132 s
133 }
134}
135#[derive(Clone, Debug, PartialEq)]
137pub enum NiftiQformCode {
138 Unknown,
140 ScannerAnat,
142 AlignedAnat,
144 Talairach,
146 Mni152,
148}
149impl NiftiQformCode {
150 pub fn as_code(&self) -> u16 {
152 match self {
153 NiftiQformCode::Unknown => 0,
154 NiftiQformCode::ScannerAnat => 1,
155 NiftiQformCode::AlignedAnat => 2,
156 NiftiQformCode::Talairach => 3,
157 NiftiQformCode::Mni152 => 4,
158 }
159 }
160 pub fn from_code(code: u16) -> Self {
162 match code {
163 1 => NiftiQformCode::ScannerAnat,
164 2 => NiftiQformCode::AlignedAnat,
165 3 => NiftiQformCode::Talairach,
166 4 => NiftiQformCode::Mni152,
167 _ => NiftiQformCode::Unknown,
168 }
169 }
170}
171#[derive(Clone, Debug, PartialEq)]
173pub enum NrrdEncoding {
174 Raw,
176 Text,
178 Gzip,
180}
181#[derive(Clone, Debug, PartialEq, Eq, Hash)]
183pub struct DicomTag {
184 pub group: u16,
186 pub element: u16,
188}
189impl DicomTag {
190 pub fn new(group: u16, element: u16) -> Self {
192 Self { group, element }
193 }
194 pub fn patient_name() -> Self {
196 Self::new(0x0010, 0x0010)
197 }
198 pub fn patient_id() -> Self {
200 Self::new(0x0010, 0x0020)
201 }
202 pub fn modality() -> Self {
204 Self::new(0x0008, 0x0060)
205 }
206 pub fn rows() -> Self {
208 Self::new(0x0028, 0x0010)
209 }
210 pub fn columns() -> Self {
212 Self::new(0x0028, 0x0011)
213 }
214 pub fn pixel_spacing() -> Self {
216 Self::new(0x0028, 0x0030)
217 }
218 pub fn slice_thickness() -> Self {
220 Self::new(0x0050, 0x0018)
221 }
222 pub fn window_center() -> Self {
224 Self::new(0x0028, 0x1050)
225 }
226 pub fn window_width() -> Self {
228 Self::new(0x0028, 0x1051)
229 }
230}
231#[derive(Clone, Debug)]
233pub struct NiftiReader {
234 pub file_path: String,
236 pub dim: [usize; 8],
238 pub pixdim: [f64; 8],
240 pub datatype: NiftiDtype,
242 pub affine: [f64; 16],
244 pub descrip: String,
246 pub data: Vec<f64>,
248}
249impl NiftiReader {
250 pub fn new(file_path: &str) -> Self {
252 let mut affine = [0.0f64; 16];
253 affine[0] = 1.0;
254 affine[5] = 1.0;
255 affine[10] = 1.0;
256 affine[15] = 1.0;
257 Self {
258 file_path: file_path.to_string(),
259 dim: [3, 64, 64, 32, 1, 1, 1, 1],
260 pixdim: [1.0; 8],
261 datatype: NiftiDtype::Float32,
262 affine,
263 descrip: String::new(),
264 data: Vec::new(),
265 }
266 }
267 pub fn n_voxels(&self) -> usize {
269 self.dim[1] * self.dim[2] * self.dim[3]
270 }
271 pub fn init_data(&mut self) {
273 self.data = vec![0.0; self.n_voxels()];
274 }
275 pub fn voxel_to_world(&self, i: f64, j: f64, k: f64) -> [f64; 3] {
277 let a = &self.affine;
278 [
279 a[0] * i + a[1] * j + a[2] * k + a[3],
280 a[4] * i + a[5] * j + a[6] * k + a[7],
281 a[8] * i + a[9] * j + a[10] * k + a[11],
282 ]
283 }
284 pub fn get_voxel(&self, i: usize, j: usize, k: usize) -> Option<f64> {
286 let nx = self.dim[1];
287 let ny = self.dim[2];
288 let nz = self.dim[3];
289 if i < nx && j < ny && k < nz {
290 let idx = k * nx * ny + j * nx + i;
291 self.data.get(idx).copied()
292 } else {
293 None
294 }
295 }
296}
297#[derive(Clone, Debug)]
299pub struct Landmark {
300 pub name: String,
302 pub position: [f64; 3],
304 pub uncertainty: f64,
306 pub manual: bool,
308}
309impl Landmark {
310 pub fn new(name: &str, position: [f64; 3], uncertainty: f64) -> Self {
312 Self {
313 name: name.to_string(),
314 position,
315 uncertainty,
316 manual: true,
317 }
318 }
319}
320#[derive(Clone, Debug)]
322pub struct NiftiTransform {
323 pub qform_code: NiftiQformCode,
325 pub sform_code: u16,
327 pub quatern: [f64; 3],
329 pub qoffset: [f64; 3],
331 pub qfac: f64,
333 pub sform_matrix: [[f64; 4]; 3],
335}
336impl NiftiTransform {
337 pub fn identity() -> Self {
339 Self {
340 qform_code: NiftiQformCode::Unknown,
341 sform_code: 0,
342 quatern: [0.0, 0.0, 0.0],
343 qoffset: [0.0, 0.0, 0.0],
344 qfac: 1.0,
345 sform_matrix: [
346 [1.0, 0.0, 0.0, 0.0],
347 [0.0, 1.0, 0.0, 0.0],
348 [0.0, 0.0, 1.0, 0.0],
349 ],
350 }
351 }
352 pub fn sform_to_world(&self, i: f64, j: f64, k: f64) -> [f64; 3] {
354 let m = &self.sform_matrix;
355 [
356 m[0][0] * i + m[0][1] * j + m[0][2] * k + m[0][3],
357 m[1][0] * i + m[1][1] * j + m[1][2] * k + m[1][3],
358 m[2][0] * i + m[2][1] * j + m[2][2] * k + m[2][3],
359 ]
360 }
361}
362#[derive(Clone, Debug, PartialEq)]
364pub enum NiftiDtype {
365 Uint8,
367 Int16,
369 Int32,
371 Float32,
373 Float64,
375}
376#[derive(Clone, Debug)]
378pub struct StlTriangle {
379 pub normal: [f32; 3],
381 pub vertices: [[f32; 3]; 3],
383}
384impl StlTriangle {
385 pub fn compute_normal(v0: [f32; 3], v1: [f32; 3], v2: [f32; 3]) -> [f32; 3] {
387 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
388 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
389 let n = [
390 e1[1] * e2[2] - e1[2] * e2[1],
391 e1[2] * e2[0] - e1[0] * e2[2],
392 e1[0] * e2[1] - e1[1] * e2[0],
393 ];
394 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-30);
395 [n[0] / len, n[1] / len, n[2] / len]
396 }
397}
398#[derive(Clone, Debug)]
400pub struct MedicalMesh {
401 pub vertices: Vec<[f64; 3]>,
403 pub triangles: Vec<[usize; 3]>,
405 pub normals: Vec<[f64; 3]>,
407 pub name: String,
409}
410impl MedicalMesh {
411 pub fn new(name: &str) -> Self {
413 Self {
414 vertices: Vec::new(),
415 triangles: Vec::new(),
416 normals: Vec::new(),
417 name: name.to_string(),
418 }
419 }
420 pub fn add_triangle(&mut self, v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) {
422 let i0 = self.vertices.len();
423 self.vertices.push(v0);
424 self.vertices.push(v1);
425 self.vertices.push(v2);
426 self.triangles.push([i0, i0 + 1, i0 + 2]);
427 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
428 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
429 let n = [
430 e1[1] * e2[2] - e1[2] * e2[1],
431 e1[2] * e2[0] - e1[0] * e2[2],
432 e1[0] * e2[1] - e1[1] * e2[0],
433 ];
434 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-30);
435 let nn = [n[0] / len, n[1] / len, n[2] / len];
436 self.normals.push(nn);
437 self.normals.push(nn);
438 self.normals.push(nn);
439 }
440 pub fn surface_area(&self) -> f64 {
442 let mut area = 0.0;
443 for &[i0, i1, i2] in &self.triangles {
444 let v0 = self.vertices[i0];
445 let v1 = self.vertices[i1];
446 let v2 = self.vertices[i2];
447 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
448 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
449 let cx = e1[1] * e2[2] - e1[2] * e2[1];
450 let cy = e1[2] * e2[0] - e1[0] * e2[2];
451 let cz = e1[0] * e2[1] - e1[1] * e2[0];
452 area += 0.5 * (cx * cx + cy * cy + cz * cz).sqrt();
453 }
454 area
455 }
456 pub fn n_triangles(&self) -> usize {
458 self.triangles.len()
459 }
460}
461#[derive(Clone, Debug)]
466pub struct VolumeReconstructor {
467 pub rows: usize,
469 pub cols: usize,
471 pub pixel_spacing: [f64; 2],
473 pub slice_thickness: f64,
475 pub data: Vec<u16>,
477 pub n_slices: usize,
479}
480impl VolumeReconstructor {
481 pub fn new(rows: usize, cols: usize, pixel_spacing: [f64; 2], slice_thickness: f64) -> Self {
483 Self {
484 rows,
485 cols,
486 pixel_spacing,
487 slice_thickness,
488 data: Vec::new(),
489 n_slices: 0,
490 }
491 }
492 pub fn add_slice(&mut self, slice: &[u16]) {
494 assert_eq!(slice.len(), self.rows * self.cols, "slice size mismatch");
495 self.data.extend_from_slice(slice);
496 self.n_slices += 1;
497 }
498 pub fn voxel(&self, row: usize, col: usize, slice: usize) -> Option<u16> {
500 if row < self.rows && col < self.cols && slice < self.n_slices {
501 Some(self.data[slice * self.rows * self.cols + row * self.cols + col])
502 } else {
503 None
504 }
505 }
506 pub fn physical_size(&self) -> [f64; 3] {
508 [
509 self.rows as f64 * self.pixel_spacing[0],
510 self.cols as f64 * self.pixel_spacing[1],
511 self.n_slices as f64 * self.slice_thickness,
512 ]
513 }
514}
515#[derive(Clone, Debug)]
517pub struct NrrdReader {
518 pub fields: HashMap<String, String>,
520 pub encoding: NrrdEncoding,
522 pub data: Vec<f64>,
524 pub sizes: Vec<usize>,
526}
527impl NrrdReader {
528 pub fn new() -> Self {
530 Self {
531 fields: HashMap::new(),
532 encoding: NrrdEncoding::Raw,
533 data: Vec::new(),
534 sizes: Vec::new(),
535 }
536 }
537 pub fn parse_header(&mut self, header: &str) {
541 for line in header.lines() {
542 let line = line.trim();
543 if line.is_empty() || line.starts_with('#') {
544 continue;
545 }
546 if let Some((k, v)) = line.split_once(':') {
547 let key = k.trim().to_lowercase();
548 let val = v.trim().to_string();
549 if key == "encoding" {
550 self.encoding = match val.as_str() {
551 "raw" => NrrdEncoding::Raw,
552 "text" | "ascii" => NrrdEncoding::Text,
553 "gzip" | "gz" => NrrdEncoding::Gzip,
554 _ => NrrdEncoding::Raw,
555 };
556 }
557 if key == "sizes" {
558 self.sizes = val
559 .split_whitespace()
560 .filter_map(|s| s.parse().ok())
561 .collect();
562 }
563 self.fields.insert(key, val);
564 }
565 }
566 }
567 pub fn load_raw_f32(&mut self, bytes: &[u8]) {
569 self.data = bytes
570 .chunks_exact(4)
571 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
572 .collect();
573 }
574 pub fn load_text(&mut self, text: &str) {
576 self.data = text
577 .split_whitespace()
578 .filter_map(|s| s.parse::<f64>().ok())
579 .collect();
580 }
581 pub fn n_voxels(&self) -> usize {
583 self.sizes.iter().product::<usize>()
584 }
585}
586#[derive(Clone, Debug)]
588pub struct MincVolume {
589 pub dimensions: Vec<MincDimension>,
591 pub data: Vec<f64>,
593 pub valid_range: [f64; 2],
595 pub attributes: HashMap<String, String>,
597}
598impl MincVolume {
599 pub fn new(dimensions: Vec<MincDimension>) -> Self {
601 let n: usize = dimensions.iter().map(|d| d.length).product();
602 Self {
603 dimensions,
604 data: vec![0.0; n],
605 valid_range: [0.0, 1.0],
606 attributes: HashMap::new(),
607 }
608 }
609 pub fn n_voxels(&self) -> usize {
611 self.dimensions.iter().map(|d| d.length).product()
612 }
613 pub fn set_attr(&mut self, key: &str, value: &str) {
615 self.attributes.insert(key.to_string(), value.to_string());
616 }
617 pub fn index3(&self, i: usize, j: usize, k: usize) -> Option<usize> {
619 if self.dimensions.len() < 3 {
620 return None;
621 }
622 let nx = self.dimensions[0].length;
623 let ny = self.dimensions[1].length;
624 let nz = self.dimensions[2].length;
625 if i < nx && j < ny && k < nz {
626 Some(i * ny * nz + j * nz + k)
627 } else {
628 None
629 }
630 }
631}
632#[derive(Clone, Debug)]
634pub struct MhaMhdFormat {
635 pub header: HashMap<String, String>,
637 pub data: Vec<f64>,
639 pub is_mha: bool,
641}
642impl MhaMhdFormat {
643 pub fn new(is_mha: bool) -> Self {
645 let mut header = HashMap::new();
646 header.insert("ObjectType".to_string(), "Image".to_string());
647 header.insert("NDims".to_string(), "3".to_string());
648 header.insert("ElementType".to_string(), "MET_FLOAT".to_string());
649 Self {
650 header,
651 data: Vec::new(),
652 is_mha,
653 }
654 }
655 pub fn set(&mut self, key: &str, value: &str) {
657 self.header.insert(key.to_string(), value.to_string());
658 }
659 pub fn get(&self, key: &str) -> Option<&str> {
661 self.header.get(key).map(|s| s.as_str())
662 }
663 pub fn dimensions(&self) -> Option<Vec<usize>> {
665 self.get("DimSize").map(|s| {
666 s.split_whitespace()
667 .filter_map(|t| t.parse().ok())
668 .collect()
669 })
670 }
671 pub fn serialize_header(&self) -> String {
673 let mut lines: Vec<String> = self
674 .header
675 .iter()
676 .map(|(k, v)| format!("{} = {}", k, v))
677 .collect();
678 lines.sort();
679 if self.is_mha {
680 lines.push("ElementDataFile = LOCAL".to_string());
681 }
682 lines.join("\n")
683 }
684}
685#[derive(Clone, Debug)]
687pub struct DicomReader {
688 pub file_path: String,
690 pub header: HashMap<DicomTag, String>,
692 pub pixel_data: Vec<u16>,
694 pub rows: usize,
696 pub columns: usize,
698}
699impl DicomReader {
700 pub fn new(file_path: &str) -> Self {
702 Self {
703 file_path: file_path.to_string(),
704 header: HashMap::new(),
705 pixel_data: Vec::new(),
706 rows: 0,
707 columns: 0,
708 }
709 }
710 pub fn set_tag(&mut self, tag: DicomTag, value: &str) {
712 self.header.insert(tag, value.to_string());
713 }
714 pub fn get_tag(&self, tag: &DicomTag) -> Option<&str> {
716 self.header.get(tag).map(|s| s.as_str())
717 }
718 pub fn load_synthetic(&mut self, rows: usize, cols: usize) {
720 self.rows = rows;
721 self.columns = cols;
722 self.pixel_data = (0..(rows * cols))
723 .map(|i| {
724 let r = i / cols;
725 let c = i % cols;
726 if (r + c).is_multiple_of(2) {
727 2048u16
728 } else {
729 512u16
730 }
731 })
732 .collect();
733 self.set_tag(DicomTag::rows(), &rows.to_string());
734 self.set_tag(DicomTag::columns(), &cols.to_string());
735 }
736 pub fn pixel(&self, row: usize, col: usize) -> Option<u16> {
738 if row < self.rows && col < self.columns {
739 Some(self.pixel_data[row * self.columns + col])
740 } else {
741 None
742 }
743 }
744 pub fn patient_name(&self) -> Option<&str> {
746 self.get_tag(&DicomTag::patient_name())
747 }
748 pub fn modality(&self) -> Option<&str> {
750 self.get_tag(&DicomTag::modality())
751 }
752}
753#[derive(Clone, Debug)]
755pub struct NiftiWriter {
756 pub output_path: String,
758 pub scl_slope: f64,
760 pub scl_inter: f64,
762 pub gzip: bool,
764}
765impl NiftiWriter {
766 pub fn new(output_path: &str) -> Self {
768 Self {
769 output_path: output_path.to_string(),
770 scl_slope: 1.0,
771 scl_inter: 0.0,
772 gzip: false,
773 }
774 }
775 pub fn build_header(&self, reader: &NiftiReader) -> Vec<u8> {
777 let mut header = vec![0u8; 348];
778 let sizeof_hdr = 348u32.to_le_bytes();
779 header[0] = sizeof_hdr[0];
780 header[1] = sizeof_hdr[1];
781 let ndim = reader.dim[0] as u16;
782 let bytes = ndim.to_le_bytes();
783 header[40] = bytes[0];
784 header[41] = bytes[1];
785 header[344] = b'n';
786 header[345] = b'+';
787 header[346] = b'1';
788 header[347] = 0;
789 header
790 }
791 pub fn write_to_buffer(&self, reader: &NiftiReader) -> Vec<u8> {
793 let header = self.build_header(reader);
794 let mut buf = header;
795 for &v in &reader.data {
796 let scaled = (v * self.scl_slope + self.scl_inter) as f32;
797 buf.extend_from_slice(&scaled.to_le_bytes());
798 }
799 buf
800 }
801}
802#[derive(Clone, Debug)]
804pub struct SegmentationMask {
805 pub labels: Vec<u8>,
807 pub dimensions: [usize; 3],
809 pub spacing: [f64; 3],
811 pub label_names: HashMap<u8, String>,
813}
814impl SegmentationMask {
815 pub fn new(dimensions: [usize; 3], spacing: [f64; 3]) -> Self {
817 let n = dimensions[0] * dimensions[1] * dimensions[2];
818 Self {
819 labels: vec![0; n],
820 dimensions,
821 spacing,
822 label_names: HashMap::new(),
823 }
824 }
825 pub fn set_label(&mut self, i: usize, j: usize, k: usize, label: u8) {
827 let [nx, ny, _nz] = self.dimensions;
828 let idx = k * nx * ny + j * nx + i;
829 if idx < self.labels.len() {
830 self.labels[idx] = label;
831 }
832 }
833 pub fn get_label(&self, i: usize, j: usize, k: usize) -> Option<u8> {
835 let [nx, ny, _nz] = self.dimensions;
836 let idx = k * nx * ny + j * nx + i;
837 self.labels.get(idx).copied()
838 }
839 pub fn count_label(&self, label: u8) -> usize {
841 self.labels.iter().filter(|&&l| l == label).count()
842 }
843 pub fn volume_mm3(&self, label: u8) -> f64 {
845 let n = self.count_label(label) as f64;
846 let voxel_vol = self.spacing[0] * self.spacing[1] * self.spacing[2];
847 n * voxel_vol
848 }
849 pub fn bounding_box(&self, label: u8) -> Option<[usize; 6]> {
851 let [nx, ny, nz] = self.dimensions;
852 let mut min_i = nx;
853 let mut max_i = 0;
854 let mut min_j = ny;
855 let mut max_j = 0;
856 let mut min_k = nz;
857 let mut max_k = 0;
858 let mut found = false;
859 for k in 0..nz {
860 for j in 0..ny {
861 for i in 0..nx {
862 if self.get_label(i, j, k) == Some(label) {
863 min_i = min_i.min(i);
864 max_i = max_i.max(i);
865 min_j = min_j.min(j);
866 max_j = max_j.max(j);
867 min_k = min_k.min(k);
868 max_k = max_k.max(k);
869 found = true;
870 }
871 }
872 }
873 }
874 if found {
875 Some([min_i, max_i, min_j, max_j, min_k, max_k])
876 } else {
877 None
878 }
879 }
880 pub fn name_label(&mut self, label: u8, name: &str) {
882 self.label_names.insert(label, name.to_string());
883 }
884}
885#[derive(Clone, Debug, Default)]
887pub struct DicomDataSet {
888 pub elements: Vec<DicomElement>,
890}
891impl DicomDataSet {
892 pub fn new() -> Self {
894 Self {
895 elements: Vec::new(),
896 }
897 }
898 pub fn set(&mut self, element: DicomElement) {
900 if let Some(pos) = self.elements.iter().position(|e| e.tag == element.tag) {
901 self.elements[pos] = element;
902 } else {
903 self.elements.push(element);
904 self.elements.sort_by(|a, b| {
905 a.tag
906 .group
907 .cmp(&b.tag.group)
908 .then(a.tag.element.cmp(&b.tag.element))
909 });
910 }
911 }
912 pub fn get(&self, tag: &DicomTag) -> Option<&DicomElement> {
914 self.elements.iter().find(|e| &e.tag == tag)
915 }
916 pub fn get_us(&self, tag: &DicomTag) -> Option<u16> {
918 self.get(tag)?.vr.as_us()
919 }
920 pub fn get_ds(&self, tag: &DicomTag) -> Option<f64> {
922 self.get(tag)?.vr.as_ds()
923 }
924 pub fn get_str(&self, tag: &DicomTag) -> Option<&str> {
926 self.get(tag)?.vr.as_str()
927 }
928 pub fn parse_bytes(bytes: &[u8]) -> Self {
933 let mut ds = DicomDataSet::new();
934 let mut pos = 0usize;
935 while pos + 8 <= bytes.len() {
936 let group = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
937 let element = u16::from_le_bytes([bytes[pos + 2], bytes[pos + 3]]);
938 let vr_code = [bytes[pos + 4], bytes[pos + 5]];
939 let length = u16::from_le_bytes([bytes[pos + 6], bytes[pos + 7]]) as usize;
940 pos += 8;
941 if pos + length > bytes.len() {
942 break;
943 }
944 let data = &bytes[pos..pos + length];
945 pos += length;
946 let tag = DicomTag::new(group, element);
947 let vr = match &vr_code {
948 b"US" if length == 2 => DicomVr::Us(u16::from_le_bytes([data[0], data[1]])),
949 b"DS" => {
950 let s = std::str::from_utf8(data).unwrap_or("0").trim();
951 DicomVr::Ds(s.parse::<f64>().unwrap_or(0.0))
952 }
953 b"LO" => DicomVr::Lo(String::from_utf8_lossy(data).trim().to_string()),
954 b"UI" => DicomVr::Ui(String::from_utf8_lossy(data).trim().to_string()),
955 b"OW" => DicomVr::OW(data.to_vec()),
956 _ => continue,
957 };
958 ds.set(DicomElement::new(tag, vr));
959 }
960 ds
961 }
962 pub fn to_bytes(&self) -> Vec<u8> {
964 let mut out = Vec::new();
965 for elem in &self.elements {
966 let vr_code: &[u8; 2];
967 let data: Vec<u8>;
968 match &elem.vr {
969 DicomVr::Us(v) => {
970 vr_code = b"US";
971 data = v.to_le_bytes().to_vec();
972 }
973 DicomVr::Ds(v) => {
974 vr_code = b"DS";
975 data = format!("{:.6}", v).into_bytes();
976 }
977 DicomVr::Lo(s) => {
978 vr_code = b"LO";
979 data = s.as_bytes().to_vec();
980 }
981 DicomVr::Ui(s) => {
982 vr_code = b"UI";
983 data = s.as_bytes().to_vec();
984 }
985 DicomVr::OW(bytes) => {
986 vr_code = b"OW";
987 data = bytes.clone();
988 }
989 DicomVr::Sq(_) => continue,
990 }
991 let length = data.len() as u16;
992 out.extend_from_slice(&elem.tag.group.to_le_bytes());
993 out.extend_from_slice(&elem.tag.element.to_le_bytes());
994 out.extend_from_slice(vr_code);
995 out.extend_from_slice(&length.to_le_bytes());
996 out.extend_from_slice(&data);
997 }
998 out
999 }
1000}
1001#[derive(Clone, Debug)]
1003pub struct FiberTract {
1004 pub points: Vec<[f64; 3]>,
1006 pub scalars: Vec<f64>,
1008}
1009impl FiberTract {
1010 pub fn new(points: Vec<[f64; 3]>) -> Self {
1012 let n = points.len();
1013 Self {
1014 points,
1015 scalars: vec![0.0; n],
1016 }
1017 }
1018 pub fn arc_length(&self) -> f64 {
1020 if self.points.len() < 2 {
1021 return 0.0;
1022 }
1023 self.points
1024 .windows(2)
1025 .map(|w| {
1026 let d = [w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]];
1027 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
1028 })
1029 .sum()
1030 }
1031 pub fn n_points(&self) -> usize {
1033 self.points.len()
1034 }
1035}
1036#[derive(Clone, Debug)]
1038pub struct DoseVolume {
1039 pub dose: Vec<f64>,
1041 pub dimensions: [usize; 3],
1043 pub spacing: [f64; 3],
1045 pub prescription_dose: f64,
1047}
1048impl DoseVolume {
1049 pub fn new(dimensions: [usize; 3], spacing: [f64; 3], prescription_dose: f64) -> Self {
1051 let n = dimensions[0] * dimensions[1] * dimensions[2];
1052 Self {
1053 dose: vec![0.0; n],
1054 dimensions,
1055 spacing,
1056 prescription_dose,
1057 }
1058 }
1059 pub fn get_dose(&self, i: usize, j: usize, k: usize) -> Option<f64> {
1061 let [nx, ny, _nz] = self.dimensions;
1062 let idx = k * nx * ny + j * nx + i;
1063 self.dose.get(idx).copied()
1064 }
1065 pub fn set_dose(&mut self, i: usize, j: usize, k: usize, d: f64) {
1067 let [nx, ny, _nz] = self.dimensions;
1068 let idx = k * nx * ny + j * nx + i;
1069 if idx < self.dose.len() {
1070 self.dose[idx] = d;
1071 }
1072 }
1073 pub fn max_dose(&self) -> f64 {
1075 self.dose.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
1076 }
1077 pub fn mean_dose(&self) -> f64 {
1079 let nonzero: Vec<f64> = self.dose.iter().cloned().filter(|&d| d > 0.0).collect();
1080 if nonzero.is_empty() {
1081 return 0.0;
1082 }
1083 nonzero.iter().sum::<f64>() / nonzero.len() as f64
1084 }
1085 pub fn dvh(&self, n_bins: usize) -> (Vec<f64>, Vec<f64>) {
1089 let d_max = self.max_dose();
1090 if d_max <= 0.0 {
1091 return (vec![0.0; n_bins], vec![0.0; n_bins]);
1092 }
1093 let bin_width = d_max / n_bins as f64;
1094 let mut counts = vec![0usize; n_bins];
1095 let n_total = self.dose.len();
1096 for &d in &self.dose {
1097 let bin = ((d / d_max) * (n_bins - 1) as f64) as usize;
1098 counts[bin.min(n_bins - 1)] += 1;
1099 }
1100 let mut cumulative = vec![0usize; n_bins];
1101 let mut running = 0usize;
1102 for i in (0..n_bins).rev() {
1103 running += counts[i];
1104 cumulative[i] = running;
1105 }
1106 let dose_bins: Vec<f64> = (0..n_bins).map(|i| i as f64 * bin_width).collect();
1107 let vol_frac: Vec<f64> = cumulative
1108 .iter()
1109 .map(|&c| c as f64 / n_total as f64)
1110 .collect();
1111 (dose_bins, vol_frac)
1112 }
1113 pub fn v_dose(&self, dose_threshold: f64) -> f64 {
1115 let above = self.dose.iter().filter(|&&d| d >= dose_threshold).count();
1116 above as f64 / self.dose.len() as f64
1117 }
1118 pub fn d_volume(&self, volume_fraction: f64) -> f64 {
1120 let mut sorted = self.dose.clone();
1121 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1122 let idx = ((1.0 - volume_fraction) * sorted.len() as f64) as usize;
1123 sorted[idx.min(sorted.len() - 1)]
1124 }
1125}
1126pub struct TractVtkWriter {
1128 pub label: String,
1130}
1131impl TractVtkWriter {
1132 pub fn new(label: &str) -> Self {
1134 Self {
1135 label: label.to_string(),
1136 }
1137 }
1138 pub fn to_vtk_string(&self, fibers: &[FiberTract]) -> String {
1140 let total_pts: usize = fibers.iter().map(|f| f.n_points()).sum();
1141 let total_lines: usize = fibers.len();
1142 let cell_size: usize = fibers.iter().map(|f| f.n_points() + 1).sum();
1143 let mut s = format!(
1144 "# vtk DataFile Version 3.0\n{}\nASCII\nDATASET POLYDATA\nPOINTS {} float\n",
1145 self.label, total_pts
1146 );
1147 for fiber in fibers {
1148 for p in &fiber.points {
1149 s.push_str(&format!("{:.4} {:.4} {:.4}\n", p[0], p[1], p[2]));
1150 }
1151 }
1152 s.push_str(&format!(
1153 "LINES {} {}\n",
1154 total_lines,
1155 total_lines + cell_size
1156 ));
1157 let mut offset = 0usize;
1158 for fiber in fibers {
1159 s.push_str(&format!("{}", fiber.n_points()));
1160 for k in 0..fiber.n_points() {
1161 s.push_str(&format!(" {}", offset + k));
1162 }
1163 s.push('\n');
1164 offset += fiber.n_points();
1165 }
1166 if total_pts > 0 {
1167 s.push_str(&format!(
1168 "POINT_DATA {}\nSCALARS scalars float 1\nLOOKUP_TABLE default\n",
1169 total_pts
1170 ));
1171 for fiber in fibers {
1172 for &sc in &fiber.scalars {
1173 s.push_str(&format!("{:.6} ", sc));
1174 }
1175 }
1176 }
1177 s
1178 }
1179}
1180#[derive(Clone, Debug)]
1182pub struct DicomImageData {
1183 pub pixels: Vec<u16>,
1185 pub rows: usize,
1187 pub cols: usize,
1189 pub n_slices: usize,
1191 pub pixel_spacing: [f64; 2],
1193 pub slice_thickness: f64,
1195 pub orientation: [f64; 6],
1197 pub window_center: f64,
1199 pub window_width: f64,
1201 pub rescale_slope: f64,
1203 pub rescale_intercept: f64,
1205}
1206impl DicomImageData {
1207 pub fn new(rows: usize, cols: usize, n_slices: usize) -> Self {
1209 Self {
1210 pixels: vec![0; rows * cols * n_slices],
1211 rows,
1212 cols,
1213 n_slices,
1214 pixel_spacing: [1.0, 1.0],
1215 slice_thickness: 1.0,
1216 orientation: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
1217 window_center: 40.0,
1218 window_width: 400.0,
1219 rescale_slope: 1.0,
1220 rescale_intercept: -1024.0,
1221 }
1222 }
1223 pub fn to_hu(&self, pixel: u16) -> f64 {
1225 pixel as f64 * self.rescale_slope + self.rescale_intercept
1226 }
1227 pub fn window_level(&self, hu: f64) -> u8 {
1229 let low = self.window_center - self.window_width / 2.0;
1230 let high = self.window_center + self.window_width / 2.0;
1231 let norm = (hu - low) / (high - low);
1232 (norm.clamp(0.0, 1.0) * 255.0) as u8
1233 }
1234 pub fn physical_size(&self) -> [f64; 3] {
1236 [
1237 self.rows as f64 * self.pixel_spacing[0],
1238 self.cols as f64 * self.pixel_spacing[1],
1239 self.n_slices as f64 * self.slice_thickness,
1240 ]
1241 }
1242 pub fn voxel(&self, row: usize, col: usize, slice: usize) -> Option<u16> {
1244 if row < self.rows && col < self.cols && slice < self.n_slices {
1245 Some(self.pixels[slice * self.rows * self.cols + row * self.cols + col])
1246 } else {
1247 None
1248 }
1249 }
1250}
1251#[allow(dead_code)]
1253pub struct StlExporter;
1254impl StlExporter {
1255 pub fn to_binary(triangles: &[StlTriangle]) -> Vec<u8> {
1257 let mut buf = vec![0u8; 80];
1258 let count = triangles.len() as u32;
1259 buf.extend_from_slice(&count.to_le_bytes());
1260 for tri in triangles {
1261 for &v in &tri.normal {
1262 buf.extend_from_slice(&v.to_le_bytes());
1263 }
1264 for vtx in &tri.vertices {
1265 for &c in vtx {
1266 buf.extend_from_slice(&c.to_le_bytes());
1267 }
1268 }
1269 buf.extend_from_slice(&0u16.to_le_bytes());
1270 }
1271 buf
1272 }
1273 pub fn from_binary(data: &[u8]) -> Vec<StlTriangle> {
1275 if data.len() < 84 {
1276 return Vec::new();
1277 }
1278 let count = u32::from_le_bytes([data[80], data[81], data[82], data[83]]) as usize;
1279 let mut triangles = Vec::with_capacity(count);
1280 let mut pos = 84usize;
1281 for _ in 0..count {
1282 if pos + 50 > data.len() {
1283 break;
1284 }
1285 let read_f32 = |p: &mut usize| -> f32 {
1286 let v = f32::from_le_bytes([data[*p], data[*p + 1], data[*p + 2], data[*p + 3]]);
1287 *p += 4;
1288 v
1289 };
1290 let nx = read_f32(&mut pos);
1291 let ny = read_f32(&mut pos);
1292 let nz = read_f32(&mut pos);
1293 let mut verts = [[0f32; 3]; 3];
1294 for vtx in &mut verts {
1295 vtx[0] = read_f32(&mut pos);
1296 vtx[1] = read_f32(&mut pos);
1297 vtx[2] = read_f32(&mut pos);
1298 }
1299 pos += 2;
1300 triangles.push(StlTriangle {
1301 normal: [nx, ny, nz],
1302 vertices: verts,
1303 });
1304 }
1305 triangles
1306 }
1307}
1308#[derive(Clone, Debug, PartialEq)]
1310pub enum DicomVr {
1311 Us(u16),
1313 Ds(f64),
1315 Lo(String),
1317 Ui(String),
1319 Sq(Vec<HashMap<DicomTag, DicomVr>>),
1321 OW(Vec<u8>),
1323}
1324impl DicomVr {
1325 pub fn vr_code(&self) -> &'static str {
1327 match self {
1328 DicomVr::Us(_) => "US",
1329 DicomVr::Ds(_) => "DS",
1330 DicomVr::Lo(_) => "LO",
1331 DicomVr::Ui(_) => "UI",
1332 DicomVr::Sq(_) => "SQ",
1333 DicomVr::OW(_) => "OW",
1334 }
1335 }
1336 pub fn as_us(&self) -> Option<u16> {
1338 if let DicomVr::Us(v) = self {
1339 Some(*v)
1340 } else {
1341 None
1342 }
1343 }
1344 pub fn as_ds(&self) -> Option<f64> {
1346 if let DicomVr::Ds(v) = self {
1347 Some(*v)
1348 } else {
1349 None
1350 }
1351 }
1352 pub fn as_str(&self) -> Option<&str> {
1354 match self {
1355 DicomVr::Lo(s) | DicomVr::Ui(s) => Some(s.as_str()),
1356 _ => None,
1357 }
1358 }
1359}
1360#[derive(Clone, Debug)]
1362pub struct MincDimension {
1363 pub name: String,
1365 pub length: usize,
1367 pub step: f64,
1369 pub start: f64,
1371}
1372impl MincDimension {
1373 pub fn new(name: &str, length: usize, step: f64, start: f64) -> Self {
1375 Self {
1376 name: name.to_string(),
1377 length,
1378 step,
1379 start,
1380 }
1381 }
1382 pub fn world_coord(&self, i: usize) -> f64 {
1384 self.start + i as f64 * self.step
1385 }
1386}