#![allow(clippy::needless_range_loop)]
#[allow(unused_imports)]
use super::functions::*;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct LandmarkSet {
pub landmarks: Vec<Landmark>,
pub space: String,
}
impl LandmarkSet {
pub fn new(space: &str) -> Self {
Self {
landmarks: Vec::new(),
space: space.to_string(),
}
}
pub fn add(&mut self, lm: Landmark) {
self.landmarks.push(lm);
}
pub fn find(&self, name: &str) -> Option<&Landmark> {
self.landmarks.iter().find(|l| l.name == name)
}
pub fn centroid(&self) -> Option<[f64; 3]> {
if self.landmarks.is_empty() {
return None;
}
let n = self.landmarks.len() as f64;
let mut c = [0.0f64; 3];
for lm in &self.landmarks {
for d in 0..3 {
c[d] += lm.position[d];
}
}
for d in 0..3 {
c[d] /= n;
}
Some(c)
}
}
#[derive(Clone, Debug)]
pub struct DicomElement {
pub tag: DicomTag,
pub vr: DicomVr,
}
impl DicomElement {
pub fn new(tag: DicomTag, vr: DicomVr) -> Self {
Self { tag, vr }
}
}
#[derive(Clone, Debug)]
pub struct VtkImageData {
pub dimensions: [usize; 3],
pub origin: [f64; 3],
pub spacing: [f64; 3],
pub scalar_arrays: HashMap<String, Vec<f64>>,
pub vector_arrays: HashMap<String, Vec<[f64; 3]>>,
}
impl VtkImageData {
pub fn new(dimensions: [usize; 3], origin: [f64; 3], spacing: [f64; 3]) -> Self {
Self {
dimensions,
origin,
spacing,
scalar_arrays: HashMap::new(),
vector_arrays: HashMap::new(),
}
}
pub fn n_voxels(&self) -> usize {
self.dimensions[0] * self.dimensions[1] * self.dimensions[2]
}
pub fn add_scalar(&mut self, name: &str, data: Vec<f64>) {
self.scalar_arrays.insert(name.to_string(), data);
}
pub fn add_vector(&mut self, name: &str, data: Vec<[f64; 3]>) {
self.vector_arrays.insert(name.to_string(), data);
}
pub fn index_to_world(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
[
self.origin[0] + i as f64 * self.spacing[0],
self.origin[1] + j as f64 * self.spacing[1],
self.origin[2] + k as f64 * self.spacing[2],
]
}
pub fn to_vtk_string(&self, scalar_name: &str) -> String {
let [nx, ny, nz] = self.dimensions;
let [ox, oy, oz] = self.origin;
let [dx, dy, dz] = self.spacing;
let n = self.n_voxels();
let mut s = format!(
"# vtk DataFile Version 3.0\nVTK ImageData\nASCII\nDATASET STRUCTURED_POINTS\n\
DIMENSIONS {} {} {}\nORIGIN {} {} {}\nSPACING {} {} {}\n\
POINT_DATA {}\n",
nx, ny, nz, ox, oy, oz, dx, dy, dz, n
);
if let Some(arr) = self.scalar_arrays.get(scalar_name) {
s.push_str(&format!(
"SCALARS {} float 1\nLOOKUP_TABLE default\n",
scalar_name
));
for &v in arr.iter().take(10) {
s.push_str(&format!("{:.6} ", v));
}
}
s
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum NiftiQformCode {
Unknown,
ScannerAnat,
AlignedAnat,
Talairach,
Mni152,
}
impl NiftiQformCode {
pub fn as_code(&self) -> u16 {
match self {
NiftiQformCode::Unknown => 0,
NiftiQformCode::ScannerAnat => 1,
NiftiQformCode::AlignedAnat => 2,
NiftiQformCode::Talairach => 3,
NiftiQformCode::Mni152 => 4,
}
}
pub fn from_code(code: u16) -> Self {
match code {
1 => NiftiQformCode::ScannerAnat,
2 => NiftiQformCode::AlignedAnat,
3 => NiftiQformCode::Talairach,
4 => NiftiQformCode::Mni152,
_ => NiftiQformCode::Unknown,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum NrrdEncoding {
Raw,
Text,
Gzip,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DicomTag {
pub group: u16,
pub element: u16,
}
impl DicomTag {
pub fn new(group: u16, element: u16) -> Self {
Self { group, element }
}
pub fn patient_name() -> Self {
Self::new(0x0010, 0x0010)
}
pub fn patient_id() -> Self {
Self::new(0x0010, 0x0020)
}
pub fn modality() -> Self {
Self::new(0x0008, 0x0060)
}
pub fn rows() -> Self {
Self::new(0x0028, 0x0010)
}
pub fn columns() -> Self {
Self::new(0x0028, 0x0011)
}
pub fn pixel_spacing() -> Self {
Self::new(0x0028, 0x0030)
}
pub fn slice_thickness() -> Self {
Self::new(0x0050, 0x0018)
}
pub fn window_center() -> Self {
Self::new(0x0028, 0x1050)
}
pub fn window_width() -> Self {
Self::new(0x0028, 0x1051)
}
}
#[derive(Clone, Debug)]
pub struct NiftiReader {
pub file_path: String,
pub dim: [usize; 8],
pub pixdim: [f64; 8],
pub datatype: NiftiDtype,
pub affine: [f64; 16],
pub descrip: String,
pub data: Vec<f64>,
}
impl NiftiReader {
pub fn new(file_path: &str) -> Self {
let mut affine = [0.0f64; 16];
affine[0] = 1.0;
affine[5] = 1.0;
affine[10] = 1.0;
affine[15] = 1.0;
Self {
file_path: file_path.to_string(),
dim: [3, 64, 64, 32, 1, 1, 1, 1],
pixdim: [1.0; 8],
datatype: NiftiDtype::Float32,
affine,
descrip: String::new(),
data: Vec::new(),
}
}
pub fn n_voxels(&self) -> usize {
self.dim[1] * self.dim[2] * self.dim[3]
}
pub fn init_data(&mut self) {
self.data = vec![0.0; self.n_voxels()];
}
pub fn voxel_to_world(&self, i: f64, j: f64, k: f64) -> [f64; 3] {
let a = &self.affine;
[
a[0] * i + a[1] * j + a[2] * k + a[3],
a[4] * i + a[5] * j + a[6] * k + a[7],
a[8] * i + a[9] * j + a[10] * k + a[11],
]
}
pub fn get_voxel(&self, i: usize, j: usize, k: usize) -> Option<f64> {
let nx = self.dim[1];
let ny = self.dim[2];
let nz = self.dim[3];
if i < nx && j < ny && k < nz {
let idx = k * nx * ny + j * nx + i;
self.data.get(idx).copied()
} else {
None
}
}
}
#[derive(Clone, Debug)]
pub struct Landmark {
pub name: String,
pub position: [f64; 3],
pub uncertainty: f64,
pub manual: bool,
}
impl Landmark {
pub fn new(name: &str, position: [f64; 3], uncertainty: f64) -> Self {
Self {
name: name.to_string(),
position,
uncertainty,
manual: true,
}
}
}
#[derive(Clone, Debug)]
pub struct NiftiTransform {
pub qform_code: NiftiQformCode,
pub sform_code: u16,
pub quatern: [f64; 3],
pub qoffset: [f64; 3],
pub qfac: f64,
pub sform_matrix: [[f64; 4]; 3],
}
impl NiftiTransform {
pub fn identity() -> Self {
Self {
qform_code: NiftiQformCode::Unknown,
sform_code: 0,
quatern: [0.0, 0.0, 0.0],
qoffset: [0.0, 0.0, 0.0],
qfac: 1.0,
sform_matrix: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
}
}
pub fn sform_to_world(&self, i: f64, j: f64, k: f64) -> [f64; 3] {
let m = &self.sform_matrix;
[
m[0][0] * i + m[0][1] * j + m[0][2] * k + m[0][3],
m[1][0] * i + m[1][1] * j + m[1][2] * k + m[1][3],
m[2][0] * i + m[2][1] * j + m[2][2] * k + m[2][3],
]
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum NiftiDtype {
Uint8,
Int16,
Int32,
Float32,
Float64,
}
#[derive(Clone, Debug)]
pub struct StlTriangle {
pub normal: [f32; 3],
pub vertices: [[f32; 3]; 3],
}
impl StlTriangle {
pub fn compute_normal(v0: [f32; 3], v1: [f32; 3], v2: [f32; 3]) -> [f32; 3] {
let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
let n = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-30);
[n[0] / len, n[1] / len, n[2] / len]
}
}
#[derive(Clone, Debug)]
pub struct MedicalMesh {
pub vertices: Vec<[f64; 3]>,
pub triangles: Vec<[usize; 3]>,
pub normals: Vec<[f64; 3]>,
pub name: String,
}
impl MedicalMesh {
pub fn new(name: &str) -> Self {
Self {
vertices: Vec::new(),
triangles: Vec::new(),
normals: Vec::new(),
name: name.to_string(),
}
}
pub fn add_triangle(&mut self, v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) {
let i0 = self.vertices.len();
self.vertices.push(v0);
self.vertices.push(v1);
self.vertices.push(v2);
self.triangles.push([i0, i0 + 1, i0 + 2]);
let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
let n = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-30);
let nn = [n[0] / len, n[1] / len, n[2] / len];
self.normals.push(nn);
self.normals.push(nn);
self.normals.push(nn);
}
pub fn surface_area(&self) -> f64 {
let mut area = 0.0;
for &[i0, i1, i2] in &self.triangles {
let v0 = self.vertices[i0];
let v1 = self.vertices[i1];
let v2 = self.vertices[i2];
let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
let cx = e1[1] * e2[2] - e1[2] * e2[1];
let cy = e1[2] * e2[0] - e1[0] * e2[2];
let cz = e1[0] * e2[1] - e1[1] * e2[0];
area += 0.5 * (cx * cx + cy * cy + cz * cz).sqrt();
}
area
}
pub fn n_triangles(&self) -> usize {
self.triangles.len()
}
}
#[derive(Clone, Debug)]
pub struct VolumeReconstructor {
pub rows: usize,
pub cols: usize,
pub pixel_spacing: [f64; 2],
pub slice_thickness: f64,
pub data: Vec<u16>,
pub n_slices: usize,
}
impl VolumeReconstructor {
pub fn new(rows: usize, cols: usize, pixel_spacing: [f64; 2], slice_thickness: f64) -> Self {
Self {
rows,
cols,
pixel_spacing,
slice_thickness,
data: Vec::new(),
n_slices: 0,
}
}
pub fn add_slice(&mut self, slice: &[u16]) {
assert_eq!(slice.len(), self.rows * self.cols, "slice size mismatch");
self.data.extend_from_slice(slice);
self.n_slices += 1;
}
pub fn voxel(&self, row: usize, col: usize, slice: usize) -> Option<u16> {
if row < self.rows && col < self.cols && slice < self.n_slices {
Some(self.data[slice * self.rows * self.cols + row * self.cols + col])
} else {
None
}
}
pub fn physical_size(&self) -> [f64; 3] {
[
self.rows as f64 * self.pixel_spacing[0],
self.cols as f64 * self.pixel_spacing[1],
self.n_slices as f64 * self.slice_thickness,
]
}
}
#[derive(Clone, Debug)]
pub struct NrrdReader {
pub fields: HashMap<String, String>,
pub encoding: NrrdEncoding,
pub data: Vec<f64>,
pub sizes: Vec<usize>,
}
impl NrrdReader {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
encoding: NrrdEncoding::Raw,
data: Vec::new(),
sizes: Vec::new(),
}
}
pub fn parse_header(&mut self, header: &str) {
for line in header.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once(':') {
let key = k.trim().to_lowercase();
let val = v.trim().to_string();
if key == "encoding" {
self.encoding = match val.as_str() {
"raw" => NrrdEncoding::Raw,
"text" | "ascii" => NrrdEncoding::Text,
"gzip" | "gz" => NrrdEncoding::Gzip,
_ => NrrdEncoding::Raw,
};
}
if key == "sizes" {
self.sizes = val
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
}
self.fields.insert(key, val);
}
}
}
pub fn load_raw_f32(&mut self, bytes: &[u8]) {
self.data = bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
.collect();
}
pub fn load_text(&mut self, text: &str) {
self.data = text
.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect();
}
pub fn n_voxels(&self) -> usize {
self.sizes.iter().product::<usize>()
}
}
#[derive(Clone, Debug)]
pub struct MincVolume {
pub dimensions: Vec<MincDimension>,
pub data: Vec<f64>,
pub valid_range: [f64; 2],
pub attributes: HashMap<String, String>,
}
impl MincVolume {
pub fn new(dimensions: Vec<MincDimension>) -> Self {
let n: usize = dimensions.iter().map(|d| d.length).product();
Self {
dimensions,
data: vec![0.0; n],
valid_range: [0.0, 1.0],
attributes: HashMap::new(),
}
}
pub fn n_voxels(&self) -> usize {
self.dimensions.iter().map(|d| d.length).product()
}
pub fn set_attr(&mut self, key: &str, value: &str) {
self.attributes.insert(key.to_string(), value.to_string());
}
pub fn index3(&self, i: usize, j: usize, k: usize) -> Option<usize> {
if self.dimensions.len() < 3 {
return None;
}
let nx = self.dimensions[0].length;
let ny = self.dimensions[1].length;
let nz = self.dimensions[2].length;
if i < nx && j < ny && k < nz {
Some(i * ny * nz + j * nz + k)
} else {
None
}
}
}
#[derive(Clone, Debug)]
pub struct MhaMhdFormat {
pub header: HashMap<String, String>,
pub data: Vec<f64>,
pub is_mha: bool,
}
impl MhaMhdFormat {
pub fn new(is_mha: bool) -> Self {
let mut header = HashMap::new();
header.insert("ObjectType".to_string(), "Image".to_string());
header.insert("NDims".to_string(), "3".to_string());
header.insert("ElementType".to_string(), "MET_FLOAT".to_string());
Self {
header,
data: Vec::new(),
is_mha,
}
}
pub fn set(&mut self, key: &str, value: &str) {
self.header.insert(key.to_string(), value.to_string());
}
pub fn get(&self, key: &str) -> Option<&str> {
self.header.get(key).map(|s| s.as_str())
}
pub fn dimensions(&self) -> Option<Vec<usize>> {
self.get("DimSize").map(|s| {
s.split_whitespace()
.filter_map(|t| t.parse().ok())
.collect()
})
}
pub fn serialize_header(&self) -> String {
let mut lines: Vec<String> = self
.header
.iter()
.map(|(k, v)| format!("{} = {}", k, v))
.collect();
lines.sort();
if self.is_mha {
lines.push("ElementDataFile = LOCAL".to_string());
}
lines.join("\n")
}
}
#[derive(Clone, Debug)]
pub struct DicomReader {
pub file_path: String,
pub header: HashMap<DicomTag, String>,
pub pixel_data: Vec<u16>,
pub rows: usize,
pub columns: usize,
}
impl DicomReader {
pub fn new(file_path: &str) -> Self {
Self {
file_path: file_path.to_string(),
header: HashMap::new(),
pixel_data: Vec::new(),
rows: 0,
columns: 0,
}
}
pub fn set_tag(&mut self, tag: DicomTag, value: &str) {
self.header.insert(tag, value.to_string());
}
pub fn get_tag(&self, tag: &DicomTag) -> Option<&str> {
self.header.get(tag).map(|s| s.as_str())
}
pub fn load_synthetic(&mut self, rows: usize, cols: usize) {
self.rows = rows;
self.columns = cols;
self.pixel_data = (0..(rows * cols))
.map(|i| {
let r = i / cols;
let c = i % cols;
if (r + c).is_multiple_of(2) {
2048u16
} else {
512u16
}
})
.collect();
self.set_tag(DicomTag::rows(), &rows.to_string());
self.set_tag(DicomTag::columns(), &cols.to_string());
}
pub fn pixel(&self, row: usize, col: usize) -> Option<u16> {
if row < self.rows && col < self.columns {
Some(self.pixel_data[row * self.columns + col])
} else {
None
}
}
pub fn patient_name(&self) -> Option<&str> {
self.get_tag(&DicomTag::patient_name())
}
pub fn modality(&self) -> Option<&str> {
self.get_tag(&DicomTag::modality())
}
}
#[derive(Clone, Debug)]
pub struct NiftiWriter {
pub output_path: String,
pub scl_slope: f64,
pub scl_inter: f64,
pub gzip: bool,
}
impl NiftiWriter {
pub fn new(output_path: &str) -> Self {
Self {
output_path: output_path.to_string(),
scl_slope: 1.0,
scl_inter: 0.0,
gzip: false,
}
}
pub fn build_header(&self, reader: &NiftiReader) -> Vec<u8> {
let mut header = vec![0u8; 348];
let sizeof_hdr = 348u32.to_le_bytes();
header[0] = sizeof_hdr[0];
header[1] = sizeof_hdr[1];
let ndim = reader.dim[0] as u16;
let bytes = ndim.to_le_bytes();
header[40] = bytes[0];
header[41] = bytes[1];
header[344] = b'n';
header[345] = b'+';
header[346] = b'1';
header[347] = 0;
header
}
pub fn write_to_buffer(&self, reader: &NiftiReader) -> Vec<u8> {
let header = self.build_header(reader);
let mut buf = header;
for &v in &reader.data {
let scaled = (v * self.scl_slope + self.scl_inter) as f32;
buf.extend_from_slice(&scaled.to_le_bytes());
}
buf
}
}
#[derive(Clone, Debug)]
pub struct SegmentationMask {
pub labels: Vec<u8>,
pub dimensions: [usize; 3],
pub spacing: [f64; 3],
pub label_names: HashMap<u8, String>,
}
impl SegmentationMask {
pub fn new(dimensions: [usize; 3], spacing: [f64; 3]) -> Self {
let n = dimensions[0] * dimensions[1] * dimensions[2];
Self {
labels: vec![0; n],
dimensions,
spacing,
label_names: HashMap::new(),
}
}
pub fn set_label(&mut self, i: usize, j: usize, k: usize, label: u8) {
let [nx, ny, _nz] = self.dimensions;
let idx = k * nx * ny + j * nx + i;
if idx < self.labels.len() {
self.labels[idx] = label;
}
}
pub fn get_label(&self, i: usize, j: usize, k: usize) -> Option<u8> {
let [nx, ny, _nz] = self.dimensions;
let idx = k * nx * ny + j * nx + i;
self.labels.get(idx).copied()
}
pub fn count_label(&self, label: u8) -> usize {
self.labels.iter().filter(|&&l| l == label).count()
}
pub fn volume_mm3(&self, label: u8) -> f64 {
let n = self.count_label(label) as f64;
let voxel_vol = self.spacing[0] * self.spacing[1] * self.spacing[2];
n * voxel_vol
}
pub fn bounding_box(&self, label: u8) -> Option<[usize; 6]> {
let [nx, ny, nz] = self.dimensions;
let mut min_i = nx;
let mut max_i = 0;
let mut min_j = ny;
let mut max_j = 0;
let mut min_k = nz;
let mut max_k = 0;
let mut found = false;
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
if self.get_label(i, j, k) == Some(label) {
min_i = min_i.min(i);
max_i = max_i.max(i);
min_j = min_j.min(j);
max_j = max_j.max(j);
min_k = min_k.min(k);
max_k = max_k.max(k);
found = true;
}
}
}
}
if found {
Some([min_i, max_i, min_j, max_j, min_k, max_k])
} else {
None
}
}
pub fn name_label(&mut self, label: u8, name: &str) {
self.label_names.insert(label, name.to_string());
}
}
#[derive(Clone, Debug, Default)]
pub struct DicomDataSet {
pub elements: Vec<DicomElement>,
}
impl DicomDataSet {
pub fn new() -> Self {
Self {
elements: Vec::new(),
}
}
pub fn set(&mut self, element: DicomElement) {
if let Some(pos) = self.elements.iter().position(|e| e.tag == element.tag) {
self.elements[pos] = element;
} else {
self.elements.push(element);
self.elements.sort_by(|a, b| {
a.tag
.group
.cmp(&b.tag.group)
.then(a.tag.element.cmp(&b.tag.element))
});
}
}
pub fn get(&self, tag: &DicomTag) -> Option<&DicomElement> {
self.elements.iter().find(|e| &e.tag == tag)
}
pub fn get_us(&self, tag: &DicomTag) -> Option<u16> {
self.get(tag)?.vr.as_us()
}
pub fn get_ds(&self, tag: &DicomTag) -> Option<f64> {
self.get(tag)?.vr.as_ds()
}
pub fn get_str(&self, tag: &DicomTag) -> Option<&str> {
self.get(tag)?.vr.as_str()
}
pub fn parse_bytes(bytes: &[u8]) -> Self {
let mut ds = DicomDataSet::new();
let mut pos = 0usize;
while pos + 8 <= bytes.len() {
let group = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
let element = u16::from_le_bytes([bytes[pos + 2], bytes[pos + 3]]);
let vr_code = [bytes[pos + 4], bytes[pos + 5]];
let length = u16::from_le_bytes([bytes[pos + 6], bytes[pos + 7]]) as usize;
pos += 8;
if pos + length > bytes.len() {
break;
}
let data = &bytes[pos..pos + length];
pos += length;
let tag = DicomTag::new(group, element);
let vr = match &vr_code {
b"US" if length == 2 => DicomVr::Us(u16::from_le_bytes([data[0], data[1]])),
b"DS" => {
let s = std::str::from_utf8(data).unwrap_or("0").trim();
DicomVr::Ds(s.parse::<f64>().unwrap_or(0.0))
}
b"LO" => DicomVr::Lo(String::from_utf8_lossy(data).trim().to_string()),
b"UI" => DicomVr::Ui(String::from_utf8_lossy(data).trim().to_string()),
b"OW" => DicomVr::OW(data.to_vec()),
_ => continue,
};
ds.set(DicomElement::new(tag, vr));
}
ds
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
for elem in &self.elements {
let vr_code: &[u8; 2];
let data: Vec<u8>;
match &elem.vr {
DicomVr::Us(v) => {
vr_code = b"US";
data = v.to_le_bytes().to_vec();
}
DicomVr::Ds(v) => {
vr_code = b"DS";
data = format!("{:.6}", v).into_bytes();
}
DicomVr::Lo(s) => {
vr_code = b"LO";
data = s.as_bytes().to_vec();
}
DicomVr::Ui(s) => {
vr_code = b"UI";
data = s.as_bytes().to_vec();
}
DicomVr::OW(bytes) => {
vr_code = b"OW";
data = bytes.clone();
}
DicomVr::Sq(_) => continue,
}
let length = data.len() as u16;
out.extend_from_slice(&elem.tag.group.to_le_bytes());
out.extend_from_slice(&elem.tag.element.to_le_bytes());
out.extend_from_slice(vr_code);
out.extend_from_slice(&length.to_le_bytes());
out.extend_from_slice(&data);
}
out
}
}
#[derive(Clone, Debug)]
pub struct FiberTract {
pub points: Vec<[f64; 3]>,
pub scalars: Vec<f64>,
}
impl FiberTract {
pub fn new(points: Vec<[f64; 3]>) -> Self {
let n = points.len();
Self {
points,
scalars: vec![0.0; n],
}
}
pub fn arc_length(&self) -> f64 {
if self.points.len() < 2 {
return 0.0;
}
self.points
.windows(2)
.map(|w| {
let d = [w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
})
.sum()
}
pub fn n_points(&self) -> usize {
self.points.len()
}
}
#[derive(Clone, Debug)]
pub struct DoseVolume {
pub dose: Vec<f64>,
pub dimensions: [usize; 3],
pub spacing: [f64; 3],
pub prescription_dose: f64,
}
impl DoseVolume {
pub fn new(dimensions: [usize; 3], spacing: [f64; 3], prescription_dose: f64) -> Self {
let n = dimensions[0] * dimensions[1] * dimensions[2];
Self {
dose: vec![0.0; n],
dimensions,
spacing,
prescription_dose,
}
}
pub fn get_dose(&self, i: usize, j: usize, k: usize) -> Option<f64> {
let [nx, ny, _nz] = self.dimensions;
let idx = k * nx * ny + j * nx + i;
self.dose.get(idx).copied()
}
pub fn set_dose(&mut self, i: usize, j: usize, k: usize, d: f64) {
let [nx, ny, _nz] = self.dimensions;
let idx = k * nx * ny + j * nx + i;
if idx < self.dose.len() {
self.dose[idx] = d;
}
}
pub fn max_dose(&self) -> f64 {
self.dose.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
}
pub fn mean_dose(&self) -> f64 {
let nonzero: Vec<f64> = self.dose.iter().cloned().filter(|&d| d > 0.0).collect();
if nonzero.is_empty() {
return 0.0;
}
nonzero.iter().sum::<f64>() / nonzero.len() as f64
}
pub fn dvh(&self, n_bins: usize) -> (Vec<f64>, Vec<f64>) {
let d_max = self.max_dose();
if d_max <= 0.0 {
return (vec![0.0; n_bins], vec![0.0; n_bins]);
}
let bin_width = d_max / n_bins as f64;
let mut counts = vec![0usize; n_bins];
let n_total = self.dose.len();
for &d in &self.dose {
let bin = ((d / d_max) * (n_bins - 1) as f64) as usize;
counts[bin.min(n_bins - 1)] += 1;
}
let mut cumulative = vec![0usize; n_bins];
let mut running = 0usize;
for i in (0..n_bins).rev() {
running += counts[i];
cumulative[i] = running;
}
let dose_bins: Vec<f64> = (0..n_bins).map(|i| i as f64 * bin_width).collect();
let vol_frac: Vec<f64> = cumulative
.iter()
.map(|&c| c as f64 / n_total as f64)
.collect();
(dose_bins, vol_frac)
}
pub fn v_dose(&self, dose_threshold: f64) -> f64 {
let above = self.dose.iter().filter(|&&d| d >= dose_threshold).count();
above as f64 / self.dose.len() as f64
}
pub fn d_volume(&self, volume_fraction: f64) -> f64 {
let mut sorted = self.dose.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((1.0 - volume_fraction) * sorted.len() as f64) as usize;
sorted[idx.min(sorted.len() - 1)]
}
}
pub struct TractVtkWriter {
pub label: String,
}
impl TractVtkWriter {
pub fn new(label: &str) -> Self {
Self {
label: label.to_string(),
}
}
pub fn to_vtk_string(&self, fibers: &[FiberTract]) -> String {
let total_pts: usize = fibers.iter().map(|f| f.n_points()).sum();
let total_lines: usize = fibers.len();
let cell_size: usize = fibers.iter().map(|f| f.n_points() + 1).sum();
let mut s = format!(
"# vtk DataFile Version 3.0\n{}\nASCII\nDATASET POLYDATA\nPOINTS {} float\n",
self.label, total_pts
);
for fiber in fibers {
for p in &fiber.points {
s.push_str(&format!("{:.4} {:.4} {:.4}\n", p[0], p[1], p[2]));
}
}
s.push_str(&format!(
"LINES {} {}\n",
total_lines,
total_lines + cell_size
));
let mut offset = 0usize;
for fiber in fibers {
s.push_str(&format!("{}", fiber.n_points()));
for k in 0..fiber.n_points() {
s.push_str(&format!(" {}", offset + k));
}
s.push('\n');
offset += fiber.n_points();
}
if total_pts > 0 {
s.push_str(&format!(
"POINT_DATA {}\nSCALARS scalars float 1\nLOOKUP_TABLE default\n",
total_pts
));
for fiber in fibers {
for &sc in &fiber.scalars {
s.push_str(&format!("{:.6} ", sc));
}
}
}
s
}
}
#[derive(Clone, Debug)]
pub struct DicomImageData {
pub pixels: Vec<u16>,
pub rows: usize,
pub cols: usize,
pub n_slices: usize,
pub pixel_spacing: [f64; 2],
pub slice_thickness: f64,
pub orientation: [f64; 6],
pub window_center: f64,
pub window_width: f64,
pub rescale_slope: f64,
pub rescale_intercept: f64,
}
impl DicomImageData {
pub fn new(rows: usize, cols: usize, n_slices: usize) -> Self {
Self {
pixels: vec![0; rows * cols * n_slices],
rows,
cols,
n_slices,
pixel_spacing: [1.0, 1.0],
slice_thickness: 1.0,
orientation: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
window_center: 40.0,
window_width: 400.0,
rescale_slope: 1.0,
rescale_intercept: -1024.0,
}
}
pub fn to_hu(&self, pixel: u16) -> f64 {
pixel as f64 * self.rescale_slope + self.rescale_intercept
}
pub fn window_level(&self, hu: f64) -> u8 {
let low = self.window_center - self.window_width / 2.0;
let high = self.window_center + self.window_width / 2.0;
let norm = (hu - low) / (high - low);
(norm.clamp(0.0, 1.0) * 255.0) as u8
}
pub fn physical_size(&self) -> [f64; 3] {
[
self.rows as f64 * self.pixel_spacing[0],
self.cols as f64 * self.pixel_spacing[1],
self.n_slices as f64 * self.slice_thickness,
]
}
pub fn voxel(&self, row: usize, col: usize, slice: usize) -> Option<u16> {
if row < self.rows && col < self.cols && slice < self.n_slices {
Some(self.pixels[slice * self.rows * self.cols + row * self.cols + col])
} else {
None
}
}
}
#[allow(dead_code)]
pub struct StlExporter;
impl StlExporter {
pub fn to_binary(triangles: &[StlTriangle]) -> Vec<u8> {
let mut buf = vec![0u8; 80];
let count = triangles.len() as u32;
buf.extend_from_slice(&count.to_le_bytes());
for tri in triangles {
for &v in &tri.normal {
buf.extend_from_slice(&v.to_le_bytes());
}
for vtx in &tri.vertices {
for &c in vtx {
buf.extend_from_slice(&c.to_le_bytes());
}
}
buf.extend_from_slice(&0u16.to_le_bytes());
}
buf
}
pub fn from_binary(data: &[u8]) -> Vec<StlTriangle> {
if data.len() < 84 {
return Vec::new();
}
let count = u32::from_le_bytes([data[80], data[81], data[82], data[83]]) as usize;
let mut triangles = Vec::with_capacity(count);
let mut pos = 84usize;
for _ in 0..count {
if pos + 50 > data.len() {
break;
}
let read_f32 = |p: &mut usize| -> f32 {
let v = f32::from_le_bytes([data[*p], data[*p + 1], data[*p + 2], data[*p + 3]]);
*p += 4;
v
};
let nx = read_f32(&mut pos);
let ny = read_f32(&mut pos);
let nz = read_f32(&mut pos);
let mut verts = [[0f32; 3]; 3];
for vtx in &mut verts {
vtx[0] = read_f32(&mut pos);
vtx[1] = read_f32(&mut pos);
vtx[2] = read_f32(&mut pos);
}
pos += 2;
triangles.push(StlTriangle {
normal: [nx, ny, nz],
vertices: verts,
});
}
triangles
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum DicomVr {
Us(u16),
Ds(f64),
Lo(String),
Ui(String),
Sq(Vec<HashMap<DicomTag, DicomVr>>),
OW(Vec<u8>),
}
impl DicomVr {
pub fn vr_code(&self) -> &'static str {
match self {
DicomVr::Us(_) => "US",
DicomVr::Ds(_) => "DS",
DicomVr::Lo(_) => "LO",
DicomVr::Ui(_) => "UI",
DicomVr::Sq(_) => "SQ",
DicomVr::OW(_) => "OW",
}
}
pub fn as_us(&self) -> Option<u16> {
if let DicomVr::Us(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_ds(&self) -> Option<f64> {
if let DicomVr::Ds(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
DicomVr::Lo(s) | DicomVr::Ui(s) => Some(s.as_str()),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct MincDimension {
pub name: String,
pub length: usize,
pub step: f64,
pub start: f64,
}
impl MincDimension {
pub fn new(name: &str, length: usize, step: f64, start: f64) -> Self {
Self {
name: name.to_string(),
length,
step,
start,
}
}
pub fn world_coord(&self, i: usize) -> f64 {
self.start + i as f64 * self.step
}
}