1#![deny(unsafe_code)]
2pub mod cifti;
33pub mod gifti;
34#[allow(unsafe_code)]
35pub mod mmap;
36pub mod qmri;
37
38pub use cifti::{BrainModel, CiftiHeader, read_cifti_header};
39pub use gifti::{GiftiImage, read_gifti_header};
40pub use qmri::QmriMetadata;
41
42use std::io::{BufReader, Read, Seek, SeekFrom};
43use std::path::Path;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[repr(i16)]
50pub enum DataType {
51 UInt8 = 2,
52 Int16 = 4,
53 Int32 = 8,
54 Float32 = 16,
55 Float64 = 64,
56 Int8 = 256,
57 UInt16 = 512,
58 UInt32 = 768,
59 Int64 = 1024,
60 UInt64 = 1280,
61 Unknown = -1,
62}
63
64impl DataType {
65 fn from_code(code: i16) -> Self {
66 match code {
67 2 => Self::UInt8,
68 4 => Self::Int16,
69 8 => Self::Int32,
70 16 => Self::Float32,
71 64 => Self::Float64,
72 256 => Self::Int8,
73 512 => Self::UInt16,
74 768 => Self::UInt32,
75 1024 => Self::Int64,
76 1280 => Self::UInt64,
77 _ => Self::Unknown,
78 }
79 }
80
81 fn bytes_per_voxel(self) -> usize {
82 match self {
83 Self::UInt8 | Self::Int8 => 1,
84 Self::Int16 | Self::UInt16 => 2,
85 Self::Int32 | Self::UInt32 | Self::Float32 => 4,
86 Self::Float64 | Self::Int64 | Self::UInt64 => 8,
87 Self::Unknown => 0,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
95pub struct NiftiHeader {
96 pub ndim: usize,
98 pub dim: [i64; 8],
100 pub pixdim: [f64; 8],
102 pub datatype: i16,
104 pub bitpix: i16,
106 pub version: u8,
108 pub vox_offset: u64,
110 pub scl_slope: f64,
113 pub scl_inter: f64,
115 pub affine: [[f64; 4]; 4],
118 pub sform_code: i16,
120 pub qform_code: i16,
122}
123
124impl NiftiHeader {
125 pub fn from_file(path: &Path) -> Result<Self, NiftiError> {
127 let name = path.to_string_lossy();
128 if name.ends_with(".gz") {
129 let file = std::fs::File::open(path)?;
130 let mut gz = flate2::read::GzDecoder::new(file);
131 Self::parse_reader(&mut gz)
132 } else {
133 let mut file = std::fs::File::open(path)?;
134 Self::parse_reader(&mut file)
135 }
136 }
137
138 fn parse_reader<R: Read>(reader: &mut R) -> Result<Self, NiftiError> {
139 let mut buf4 = [0u8; 4];
140 reader.read_exact(&mut buf4)?;
141 let sizeof_hdr = i32::from_le_bytes(buf4);
142
143 if sizeof_hdr == 348 {
144 Self::parse_nifti1(reader)
145 } else if sizeof_hdr == 540 {
146 Self::parse_nifti2(reader)
147 } else {
148 let sizeof_be = i32::from_be_bytes(buf4);
149 if sizeof_be == 348 || sizeof_be == 540 {
150 Err(NiftiError::BigEndian)
151 } else {
152 Err(NiftiError::InvalidHeader(sizeof_hdr))
153 }
154 }
155 }
156
157 fn parse_nifti1<R: Read>(reader: &mut R) -> Result<Self, NiftiError> {
158 let mut buf = [0u8; 344];
159 reader.read_exact(&mut buf)?;
160
161 let f32_at = |off: usize| -> f64 {
162 f32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]) as f64
163 };
164 let i16_at = |off: usize| -> i16 { i16::from_le_bytes([buf[off], buf[off + 1]]) };
165
166 let mut dim = [0i64; 8];
167 for (i, d) in dim.iter_mut().enumerate() {
168 *d = i16_at(36 + i * 2) as i64;
169 }
170
171 let datatype = i16_at(66);
172 let bitpix = i16_at(68);
173
174 let mut pixdim = [0.0f64; 8];
175 for (i, p) in pixdim.iter_mut().enumerate() {
176 *p = f32_at(72 + i * 4);
177 }
178
179 let vox_offset = f32_at(104) as u64;
180 let scl_slope = f32_at(108);
181 let scl_inter = f32_at(112);
182
183 let qform_code = i16_at(248);
185 let sform_code = i16_at(250);
186
187 let mut affine = [[0.0f64; 4]; 4];
189 if sform_code > 0 {
190 for (row, arow) in affine[..3].iter_mut().enumerate() {
191 for (col, acol) in arow.iter_mut().enumerate() {
192 *acol = f32_at(276 + (row * 4 + col) * 4);
193 }
194 }
195 affine[3][3] = 1.0;
196 } else if qform_code > 0 {
197 let qb = f32_at(256);
199 let qc = f32_at(260);
200 let qd = f32_at(264);
201 let qx = f32_at(268);
202 let qy = f32_at(272);
203 let qz = f32_at(276); let qa = (1.0 - qb * qb - qc * qc - qd * qd).max(0.0).sqrt();
207 let (i, j, k) = (pixdim[1], pixdim[2], pixdim[3]);
208 let qfac = if pixdim[0] < 0.0 { -1.0 } else { 1.0 };
209
210 affine[0][0] = (qa * qa + qb * qb - qc * qc - qd * qd) * i;
211 affine[0][1] = 2.0 * (qb * qc - qa * qd) * j;
212 affine[0][2] = 2.0 * (qb * qd + qa * qc) * k * qfac;
213 affine[0][3] = qx;
214 affine[1][0] = 2.0 * (qb * qc + qa * qd) * i;
215 affine[1][1] = (qa * qa + qc * qc - qb * qb - qd * qd) * j;
216 affine[1][2] = 2.0 * (qc * qd - qa * qb) * k * qfac;
217 affine[1][3] = qy;
218 affine[2][0] = 2.0 * (qb * qd - qa * qc) * i;
219 affine[2][1] = 2.0 * (qc * qd + qa * qb) * j;
220 affine[2][2] = (qa * qa + qd * qd - qb * qb - qc * qc) * k * qfac;
221 affine[2][3] = qz;
222 affine[3][3] = 1.0;
223 } else {
224 affine[0][0] = pixdim[1];
226 affine[1][1] = pixdim[2];
227 affine[2][2] = pixdim[3];
228 affine[3][3] = 1.0;
229 }
230
231 let ndim = dim[0].clamp(0, 7) as usize;
232
233 Ok(Self {
234 ndim,
235 dim,
236 pixdim,
237 datatype,
238 bitpix,
239 version: 1,
240 vox_offset,
241 scl_slope,
242 scl_inter,
243 affine,
244 sform_code,
245 qform_code,
246 })
247 }
248
249 fn parse_nifti2<R: Read>(reader: &mut R) -> Result<Self, NiftiError> {
250 let mut buf = [0u8; 536];
251 reader.read_exact(&mut buf)?;
252
253 let f64_at =
254 |off: usize| -> f64 { f64::from_le_bytes(buf[off..off + 8].try_into().unwrap()) };
255 let i16_at = |off: usize| -> i16 { i16::from_le_bytes([buf[off], buf[off + 1]]) };
256 let i64_at =
257 |off: usize| -> i64 { i64::from_le_bytes(buf[off..off + 8].try_into().unwrap()) };
258
259 let datatype = i16_at(8);
260 let bitpix = i16_at(10);
261
262 let mut dim = [0i64; 8];
263 for (i, d) in dim.iter_mut().enumerate() {
264 *d = i64_at(12 + i * 8);
265 }
266
267 let mut pixdim = [0.0f64; 8];
268 for (i, p) in pixdim.iter_mut().enumerate() {
269 *p = f64_at(100 + i * 8);
270 }
271
272 let vox_offset = i64_at(164) as u64;
273 let scl_slope = f64_at(172);
274 let scl_inter = f64_at(180);
275
276 let qform_code = i16_at(340);
278 let sform_code = i16_at(342);
279
280 let mut affine = [[0.0f64; 4]; 4];
282 if sform_code > 0 {
283 for (row, arow) in affine[..3].iter_mut().enumerate() {
284 for (col, acol) in arow.iter_mut().enumerate() {
285 *acol = f64_at(396 + (row * 4 + col) * 8);
286 }
287 }
288 affine[3][3] = 1.0;
289 } else {
290 affine[0][0] = pixdim[1];
291 affine[1][1] = pixdim[2];
292 affine[2][2] = pixdim[3];
293 affine[3][3] = 1.0;
294 }
295
296 let ndim = dim[0].clamp(0, 7) as usize;
297
298 Ok(Self {
299 ndim,
300 dim,
301 pixdim,
302 datatype,
303 bitpix,
304 version: 2,
305 vox_offset,
306 scl_slope,
307 scl_inter,
308 affine,
309 sform_code,
310 qform_code,
311 })
312 }
313
314 pub fn n_vols(&self) -> usize {
316 if self.ndim >= 4 {
317 self.dim[4].max(1) as usize
318 } else {
319 1
320 }
321 }
322
323 pub fn voxel_size(&self) -> (f64, f64, f64) {
325 (self.pixdim[1], self.pixdim[2], self.pixdim[3])
326 }
327
328 pub fn matrix_size(&self) -> (usize, usize, usize) {
330 (
331 self.dim[1].max(0) as usize,
332 self.dim[2].max(0) as usize,
333 self.dim[3].max(0) as usize,
334 )
335 }
336
337 pub fn tr(&self) -> Option<f64> {
339 if self.ndim >= 4 && self.pixdim[4] > 0.0 {
340 Some(self.pixdim[4])
341 } else {
342 None
343 }
344 }
345
346 pub fn n_voxels(&self) -> usize {
348 let mut n = 1usize;
349 for i in 1..=self.ndim {
350 n = n.saturating_mul(self.dim[i].max(0) as usize);
351 }
352 n
353 }
354
355 pub fn shape(&self) -> Vec<usize> {
357 (1..=self.ndim)
358 .map(|i| self.dim[i].max(0) as usize)
359 .collect()
360 }
361
362 pub fn has_scaling(&self) -> bool {
364 self.scl_slope != 0.0 && (self.scl_slope != 1.0 || self.scl_inter != 0.0)
365 }
366
367 pub fn data_type(&self) -> DataType {
369 DataType::from_code(self.datatype)
370 }
371
372 pub fn affine(&self) -> &[[f64; 4]; 4] {
375 &self.affine
376 }
377
378 pub fn voxel_to_world(&self, ijk: [f64; 3]) -> [f64; 3] {
380 let a = &self.affine;
381 [
382 a[0][0] * ijk[0] + a[0][1] * ijk[1] + a[0][2] * ijk[2] + a[0][3],
383 a[1][0] * ijk[0] + a[1][1] * ijk[1] + a[1][2] * ijk[2] + a[1][3],
384 a[2][0] * ijk[0] + a[2][1] * ijk[1] + a[2][2] * ijk[2] + a[2][3],
385 ]
386 }
387}
388
389#[derive(Clone)]
397pub struct NiftiImage {
398 pub header: NiftiHeader,
400 pub data: Vec<f64>,
403}
404
405impl std::fmt::Debug for NiftiImage {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 f.debug_struct("NiftiImage")
408 .field("shape", &self.header.shape())
409 .field("datatype", &self.header.data_type())
410 .field("n_voxels", &self.data.len())
411 .field("tr", &self.header.tr())
412 .finish()
413 }
414}
415
416impl std::fmt::Display for NiftiImage {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 let shape = self.header.shape();
419 let shape_str: Vec<String> = shape.iter().map(std::string::ToString::to_string).collect();
420 write!(f, "NiftiImage({})", shape_str.join("×"))
421 }
422}
423
424impl NiftiImage {
425 pub fn from_file(path: &Path) -> Result<Self, NiftiError> {
429 let name = path.to_string_lossy();
430 if name.ends_with(".gz") {
431 Self::load_gz(path)
432 } else if name.ends_with(".hdr") {
433 let img_path = path.with_extension("img");
435 Self::load_hdr_img(path, &img_path)
436 } else {
437 Self::load_nii(path)
438 }
439 }
440
441 pub fn from_file_volume(path: &Path, volume: usize) -> Result<Self, NiftiError> {
443 let name = path.to_string_lossy();
444 if name.ends_with(".gz") {
445 let mut img = Self::load_gz(path)?;
447 img.extract_volume_in_place(volume)?;
448 Ok(img)
449 } else {
450 Self::load_nii_volume(path, volume)
451 }
452 }
453
454 fn load_nii(path: &Path) -> Result<Self, NiftiError> {
455 let file = std::fs::File::open(path)?;
456 let mut reader = BufReader::with_capacity(256 * 1024, file);
457 let header = NiftiHeader::parse_reader(&mut reader)?;
458 let data = read_voxel_data_seekable(&mut reader, &header)?;
459 Ok(Self { header, data })
460 }
461
462 fn load_nii_volume(path: &Path, volume: usize) -> Result<Self, NiftiError> {
463 let file = std::fs::File::open(path)?;
464 let mut reader = BufReader::with_capacity(256 * 1024, file);
465 let header = NiftiHeader::parse_reader(&mut reader)?;
466 let data = read_single_volume_seekable(&mut reader, &header, volume)?;
467 let mut vol_header = header.clone();
468 if vol_header.ndim >= 4 {
470 vol_header.dim[4] = 1;
471 }
472 Ok(Self {
473 header: vol_header,
474 data,
475 })
476 }
477
478 fn load_gz(path: &Path) -> Result<Self, NiftiError> {
479 let file = std::fs::File::open(path)?;
480 let mut gz = flate2::read::GzDecoder::new(BufReader::with_capacity(256 * 1024, file));
481 let header = NiftiHeader::parse_reader(&mut gz)?;
482 let data = read_voxel_data_stream(&mut gz, &header)?;
483 Ok(Self { header, data })
484 }
485
486 fn load_hdr_img(hdr_path: &Path, img_path: &Path) -> Result<Self, NiftiError> {
487 let mut hdr_file = std::fs::File::open(hdr_path)?;
489 let header = NiftiHeader::parse_reader(&mut hdr_file)?;
490 let file = std::fs::File::open(img_path)?;
492 let mut reader = BufReader::with_capacity(256 * 1024, file);
493 let data = read_voxel_data_stream(&mut reader, &header)?;
494 Ok(Self { header, data })
495 }
496
497 fn extract_volume_in_place(&mut self, volume: usize) -> Result<(), NiftiError> {
498 if self.header.ndim < 4 {
499 return if volume == 0 {
500 Ok(())
501 } else {
502 Err(NiftiError::VolumeOutOfRange {
503 requested: volume,
504 available: 1,
505 })
506 };
507 }
508 let n_vols = self.header.n_vols();
509 if volume >= n_vols {
510 return Err(NiftiError::VolumeOutOfRange {
511 requested: volume,
512 available: n_vols,
513 });
514 }
515 let vol_size = self.header.n_voxels() / n_vols;
516 let start = volume * vol_size;
517 let end = start + vol_size;
518 self.data = self.data[start..end].to_vec();
519 self.header.dim[4] = 1;
520 Ok(())
521 }
522
523 #[cfg(feature = "safetensors")]
528 pub fn save_safetensors(&self, path: &std::path::Path) -> std::io::Result<()> {
529 use safetensors::tensor::{Dtype, TensorView};
530 use std::collections::HashMap;
531
532 let shape = self.header.shape();
533 let flat_bytes: &[u8] = bytemuck::cast_slice(&self.data);
534
535 let tensor = TensorView::new(Dtype::F64, shape.clone(), flat_bytes)
536 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
537
538 let mut tensors = HashMap::new();
539 tensors.insert("data".to_string(), tensor);
540
541 let mut metadata = HashMap::new();
542 metadata.insert("shape".to_string(), format!("{:?}", shape));
543 metadata.insert(
544 "voxel_size".to_string(),
545 format!("{:?}", self.header.voxel_size()),
546 );
547 if let Some(tr) = self.header.tr() {
548 metadata.insert("tr".to_string(), tr.to_string());
549 }
550 let aff: Vec<f64> = self
552 .header
553 .affine
554 .iter()
555 .flat_map(|r| r.iter().copied())
556 .collect();
557 metadata.insert("affine".to_string(), format!("{:?}", aff));
558
559 let bytes = safetensors::tensor::serialize(&tensors, &Some(metadata))
560 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
561 std::fs::write(path, bytes)
562 }
563
564 pub fn shape(&self) -> Vec<usize> {
566 self.header.shape()
567 }
568
569 #[cfg(feature = "ndarray")]
571 pub fn to_ndarray(&self) -> ndarray::ArrayD<f64> {
572 let shape: Vec<usize> = self.header.shape();
573 ndarray::ArrayD::from_shape_vec(ndarray::IxDyn(&shape), self.data.clone())
574 .unwrap_or_else(|_| ndarray::ArrayD::zeros(ndarray::IxDyn(&shape)))
575 }
576
577 pub fn get_voxel(&self, idx: &[usize]) -> Option<f64> {
579 let linear = self.linear_index(idx)?;
580 self.data.get(linear).copied()
581 }
582
583 pub fn get_volume(&self, t: usize) -> Option<Vec<f64>> {
586 let (nx, ny, nz) = self.header.matrix_size();
587 let vol_size = nx * ny * nz;
588 let n_vols = self.header.n_vols();
589 if t >= n_vols {
590 return None;
591 }
592 let start = t * vol_size;
593 let end = start + vol_size;
594 if end <= self.data.len() {
595 Some(self.data[start..end].to_vec())
596 } else {
597 None
598 }
599 }
600
601 pub fn get_timeseries(&self, x: usize, y: usize, z: usize) -> Option<Vec<f64>> {
603 let (nx, ny, nz) = self.header.matrix_size();
604 if x >= nx || y >= ny || z >= nz {
605 return None;
606 }
607 let vol_size = nx * ny * nz;
608 let n_vols = self.header.n_vols();
609 let voxel_offset = x + nx * (y + ny * z);
610 let mut ts = Vec::with_capacity(n_vols);
611 for t in 0..n_vols {
612 ts.push(self.data[t * vol_size + voxel_offset]);
613 }
614 Some(ts)
615 }
616
617 pub fn mean(&self) -> f64 {
619 if self.data.is_empty() {
620 return 0.0;
621 }
622 self.data.iter().sum::<f64>() / self.data.len() as f64
623 }
624
625 pub fn temporal_mean(&self) -> Vec<f64> {
627 let (nx, ny, nz) = self.header.matrix_size();
628 let vol_size = nx * ny * nz;
629 let n_vols = self.header.n_vols();
630 if n_vols <= 1 {
631 return self.data.clone();
632 }
633 let mut mean = vec![0.0f64; vol_size];
634 for t in 0..n_vols {
635 let start = t * vol_size;
636 for (m, &d) in mean
637 .iter_mut()
638 .zip(self.data[start..start + vol_size].iter())
639 {
640 *m += d;
641 }
642 }
643 let scale = 1.0 / n_vols as f64;
644 for v in &mut mean {
645 *v *= scale;
646 }
647 mean
648 }
649
650 fn linear_index(&self, idx: &[usize]) -> Option<usize> {
651 if idx.len() > self.header.ndim {
652 return None;
653 }
654 let mut linear = 0;
655 let mut stride = 1;
656 for (i, &ix) in idx.iter().enumerate() {
657 let dim_size = self.header.dim[i + 1] as usize;
658 if ix >= dim_size {
659 return None;
660 }
661 linear += ix * stride;
662 stride *= dim_size;
663 }
664 if linear < self.data.len() {
665 Some(linear)
666 } else {
667 None
668 }
669 }
670}
671
672fn read_voxel_data_seekable<R: Read + Seek>(
676 reader: &mut R,
677 header: &NiftiHeader,
678) -> Result<Vec<f64>, NiftiError> {
679 let hdr_size = if header.version == 1 { 348u64 } else { 540 };
680 let offset = if header.vox_offset > hdr_size {
681 header.vox_offset
682 } else {
683 hdr_size
684 };
685 reader.seek(SeekFrom::Start(offset))?;
686 decode_voxels(reader, header)
687}
688
689fn read_single_volume_seekable<R: Read + Seek>(
691 reader: &mut R,
692 header: &NiftiHeader,
693 volume: usize,
694) -> Result<Vec<f64>, NiftiError> {
695 let n_vols = header.n_vols();
696 if volume >= n_vols {
697 return Err(NiftiError::VolumeOutOfRange {
698 requested: volume,
699 available: n_vols,
700 });
701 }
702 let dt = DataType::from_code(header.datatype);
703 let bpv = dt.bytes_per_voxel();
704 if bpv == 0 {
705 return Err(NiftiError::UnsupportedDataType(header.datatype));
706 }
707
708 let vol_voxels = header.n_voxels() / n_vols;
709 let vol_bytes = vol_voxels * bpv;
710
711 let hdr_size = if header.version == 1 { 348u64 } else { 540 };
712 let offset = if header.vox_offset > hdr_size {
713 header.vox_offset
714 } else {
715 hdr_size
716 };
717 reader.seek(SeekFrom::Start(offset + volume as u64 * vol_bytes as u64))?;
718
719 let mut raw = vec![0u8; vol_bytes];
720 reader.read_exact(&mut raw)?;
721 decode_raw_to_f64(&raw, dt, header.scl_slope, header.scl_inter)
722}
723
724fn read_voxel_data_stream<R: Read>(
726 reader: &mut R,
727 header: &NiftiHeader,
728) -> Result<Vec<f64>, NiftiError> {
729 let hdr_size = if header.version == 1 { 348u64 } else { 540 };
732 let offset = if header.vox_offset > hdr_size {
733 header.vox_offset
734 } else {
735 hdr_size
736 };
737 let skip = offset - hdr_size;
738 if skip > 0 {
739 let mut discard = vec![0u8; skip as usize];
740 reader.read_exact(&mut discard)?;
741 }
742 decode_voxels(reader, header)
743}
744
745fn decode_voxels<R: Read>(reader: &mut R, header: &NiftiHeader) -> Result<Vec<f64>, NiftiError> {
747 let dt = DataType::from_code(header.datatype);
748 let bpv = dt.bytes_per_voxel();
749 if bpv == 0 {
750 return Err(NiftiError::UnsupportedDataType(header.datatype));
751 }
752
753 let n_voxels = header.n_voxels();
754 let total_bytes = n_voxels * bpv;
755 let mut raw = vec![0u8; total_bytes];
756 reader.read_exact(&mut raw)?;
757
758 decode_raw_to_f64(&raw, dt, header.scl_slope, header.scl_inter)
759}
760
761#[inline(never)]
764pub(crate) fn decode_raw_to_f64(
765 raw: &[u8],
766 dt: DataType,
767 scl_slope: f64,
768 scl_inter: f64,
769) -> Result<Vec<f64>, NiftiError> {
770 let bpv = dt.bytes_per_voxel();
771 let n = raw.len() / bpv;
772
773 let mut data = vec![0.0f64; n];
774
775 let apply_scaling = scl_slope != 0.0 && (scl_slope != 1.0 || scl_inter != 0.0);
776 let slope = if scl_slope == 0.0 { 1.0 } else { scl_slope };
777 let inter = scl_inter;
778
779 macro_rules! le_read {
782 (i16, $src:expr, $off:expr) => {
783 i16::from_le_bytes([$src[$off], $src[$off + 1]])
784 };
785 (u16, $src:expr, $off:expr) => {
786 u16::from_le_bytes([$src[$off], $src[$off + 1]])
787 };
788 (i32, $src:expr, $off:expr) => {
789 i32::from_le_bytes([$src[$off], $src[$off + 1], $src[$off + 2], $src[$off + 3]])
790 };
791 (u32, $src:expr, $off:expr) => {
792 u32::from_le_bytes([$src[$off], $src[$off + 1], $src[$off + 2], $src[$off + 3]])
793 };
794 (f32, $src:expr, $off:expr) => {
795 f32::from_le_bytes([$src[$off], $src[$off + 1], $src[$off + 2], $src[$off + 3]])
796 };
797 (f64, $src:expr, $off:expr) => {
798 f64::from_le_bytes([
799 $src[$off],
800 $src[$off + 1],
801 $src[$off + 2],
802 $src[$off + 3],
803 $src[$off + 4],
804 $src[$off + 5],
805 $src[$off + 6],
806 $src[$off + 7],
807 ])
808 };
809 (i64, $src:expr, $off:expr) => {
810 i64::from_le_bytes([
811 $src[$off],
812 $src[$off + 1],
813 $src[$off + 2],
814 $src[$off + 3],
815 $src[$off + 4],
816 $src[$off + 5],
817 $src[$off + 6],
818 $src[$off + 7],
819 ])
820 };
821 (u64, $src:expr, $off:expr) => {
822 u64::from_le_bytes([
823 $src[$off],
824 $src[$off + 1],
825 $src[$off + 2],
826 $src[$off + 3],
827 $src[$off + 4],
828 $src[$off + 5],
829 $src[$off + 6],
830 $src[$off + 7],
831 ])
832 };
833 }
834
835 match dt {
836 DataType::UInt8 => {
837 for (dst, &src) in data.iter_mut().zip(raw.iter()) {
838 *dst = src as f64;
839 }
840 }
841 DataType::Int8 => {
842 for (dst, &src) in data.iter_mut().zip(raw.iter()) {
843 *dst = (src as i8) as f64;
844 }
845 }
846 DataType::Int16 => {
847 for (i, dst) in data.iter_mut().enumerate() {
848 *dst = le_read!(i16, raw, i * 2) as f64;
849 }
850 }
851 DataType::UInt16 => {
852 for (i, dst) in data.iter_mut().enumerate() {
853 *dst = le_read!(u16, raw, i * 2) as f64;
854 }
855 }
856 DataType::Int32 => {
857 for (i, dst) in data.iter_mut().enumerate() {
858 *dst = le_read!(i32, raw, i * 4) as f64;
859 }
860 }
861 DataType::UInt32 => {
862 for (i, dst) in data.iter_mut().enumerate() {
863 *dst = le_read!(u32, raw, i * 4) as f64;
864 }
865 }
866 DataType::Float32 => {
867 for (i, dst) in data.iter_mut().enumerate() {
868 *dst = le_read!(f32, raw, i * 4) as f64;
869 }
870 }
871 DataType::Float64 => {
872 for (i, dst) in data.iter_mut().enumerate() {
873 *dst = le_read!(f64, raw, i * 8);
874 }
875 }
876 DataType::Int64 => {
877 for (i, dst) in data.iter_mut().enumerate() {
878 *dst = le_read!(i64, raw, i * 8) as f64;
879 }
880 }
881 DataType::UInt64 => {
882 for (i, dst) in data.iter_mut().enumerate() {
883 *dst = le_read!(u64, raw, i * 8) as f64;
884 }
885 }
886 DataType::Unknown => {
887 return Err(NiftiError::UnsupportedDataType(dt as i16));
888 }
889 }
890
891 if apply_scaling {
892 for v in &mut data {
893 *v = *v * slope + inter;
894 }
895 }
896
897 Ok(data)
898}
899
900#[derive(Debug, thiserror::Error)]
903pub enum NiftiError {
904 #[error("IO error: {0}")]
905 Io(#[from] std::io::Error),
906 #[error("Invalid NIfTI header size: {0}")]
907 InvalidHeader(i32),
908 #[error("Big-endian NIfTI not supported (swap bytes externally)")]
909 BigEndian,
910 #[error("Unsupported NIfTI datatype code: {0}")]
911 UnsupportedDataType(i16),
912 #[error("Volume {requested} out of range (available: {available})")]
913 VolumeOutOfRange { requested: usize, available: usize },
914}
915
916#[cfg(test)]
919mod tests {
920 use super::*;
921 use std::io::Write;
922
923 fn build_nifti1(dims: &[i16; 8], pixdims: &[f32; 8], data: &[f32]) -> Vec<u8> {
925 let mut header = vec![0u8; 348];
926 header[0..4].copy_from_slice(&348i32.to_le_bytes());
927 for (i, &d) in dims.iter().enumerate() {
928 let off = 40 + i * 2;
929 header[off..off + 2].copy_from_slice(&d.to_le_bytes());
930 }
931 header[70..72].copy_from_slice(&16i16.to_le_bytes());
933 header[72..74].copy_from_slice(&32i16.to_le_bytes());
935 for (i, &p) in pixdims.iter().enumerate() {
936 let off = 76 + i * 4;
937 header[off..off + 4].copy_from_slice(&p.to_le_bytes());
938 }
939 header[108..112].copy_from_slice(&352.0f32.to_le_bytes());
941 header[112..116].copy_from_slice(&1.0f32.to_le_bytes());
943 header[116..120].copy_from_slice(&0.0f32.to_le_bytes());
945 header[344..348].copy_from_slice(b"n+1\0");
947 header.extend_from_slice(&[0u8; 4]);
949 for &v in data {
951 header.extend_from_slice(&v.to_le_bytes());
952 }
953 header
954 }
955
956 fn write_nifti1_file(path: &Path, dims: &[i16; 8], pixdims: &[f32; 8], data: &[f32]) {
957 let bytes = build_nifti1(dims, pixdims, data);
958 std::fs::write(path, &bytes).unwrap();
959 }
960
961 fn write_nifti1_gz(path: &Path, dims: &[i16; 8], pixdims: &[f32; 8], data: &[f32]) {
962 let bytes = build_nifti1(dims, pixdims, data);
963 let file = std::fs::File::create(path).unwrap();
964 let mut gz = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
965 gz.write_all(&bytes).unwrap();
966 gz.finish().unwrap();
967 }
968
969 #[test]
970 fn test_header_nifti1() {
971 let dir = std::env::temp_dir().join("bids_nifti_hdr1");
972 std::fs::create_dir_all(&dir).unwrap();
973 let path = dir.join("test.nii");
974 let dims: [i16; 8] = [4, 64, 64, 32, 100, 1, 1, 1];
975 let pixdims: [f32; 8] = [1.0, 3.0, 3.0, 3.5, 2.0, 0.0, 0.0, 0.0];
976 let data: Vec<f32> = vec![0.0; 64 * 64 * 32 * 100];
977 write_nifti1_file(&path, &dims, &pixdims, &data);
978
979 let hdr = NiftiHeader::from_file(&path).unwrap();
980 assert_eq!(hdr.version, 1);
981 assert_eq!(hdr.ndim, 4);
982 assert_eq!(hdr.n_vols(), 100);
983 assert_eq!(hdr.matrix_size(), (64, 64, 32));
984 assert!((hdr.tr().unwrap() - 2.0).abs() < 0.01);
985 assert_eq!(hdr.n_voxels(), 64 * 64 * 32 * 100);
986 assert_eq!(hdr.shape(), vec![64, 64, 32, 100]);
987
988 std::fs::remove_dir_all(&dir).unwrap();
989 }
990
991 #[test]
992 fn test_load_3d_image() {
993 let dir = std::env::temp_dir().join("bids_nifti_3d");
994 std::fs::create_dir_all(&dir).unwrap();
995 let path = dir.join("anat.nii");
996 let dims: [i16; 8] = [3, 4, 4, 3, 1, 1, 1, 1];
997 let pixdims: [f32; 8] = [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0];
998 let data: Vec<f32> = (0..48).map(|i| i as f32).collect();
1000 write_nifti1_file(&path, &dims, &pixdims, &data);
1001
1002 let img = NiftiImage::from_file(&path).unwrap();
1003 assert_eq!(img.shape(), vec![4, 4, 3]);
1004 assert_eq!(img.data.len(), 48);
1005 assert_eq!(img.get_voxel(&[0, 0, 0]), Some(0.0));
1007 assert_eq!(img.get_voxel(&[1, 0, 0]), Some(1.0));
1009 assert_eq!(img.get_voxel(&[0, 1, 0]), Some(4.0));
1011 assert_eq!(img.get_voxel(&[0, 0, 1]), Some(16.0));
1013 assert_eq!(img.get_voxel(&[4, 0, 0]), None);
1015
1016 assert!((img.mean() - 23.5).abs() < 0.01);
1017
1018 std::fs::remove_dir_all(&dir).unwrap();
1019 }
1020
1021 #[test]
1022 fn test_load_4d_image() {
1023 let dir = std::env::temp_dir().join("bids_nifti_4d");
1024 std::fs::create_dir_all(&dir).unwrap();
1025 let path = dir.join("bold.nii");
1026 let nx = 4;
1027 let ny = 4;
1028 let nz = 3;
1029 let nt = 10;
1030 let dims: [i16; 8] = [4, nx as i16, ny as i16, nz as i16, nt as i16, 1, 1, 1];
1031 let pixdims: [f32; 8] = [1.0, 2.0, 2.0, 2.0, 1.5, 0.0, 0.0, 0.0];
1032 let vol_size = nx * ny * nz;
1033 let data: Vec<f32> = (0..(vol_size * nt)).map(|i| i as f32).collect();
1034 write_nifti1_file(&path, &dims, &pixdims, &data);
1035
1036 let img = NiftiImage::from_file(&path).unwrap();
1037 assert_eq!(img.shape(), vec![4, 4, 3, 10]);
1038 assert_eq!(img.data.len(), vol_size * nt);
1039 assert_eq!(img.header.n_vols(), 10);
1040
1041 let vol0 = img.get_volume(0).unwrap();
1043 assert_eq!(vol0.len(), vol_size);
1044 assert_eq!(vol0[0], 0.0);
1045 assert_eq!(vol0[vol_size - 1], (vol_size - 1) as f64);
1046
1047 let vol1 = img.get_volume(1).unwrap();
1049 assert_eq!(vol1[0], vol_size as f64);
1050
1051 let ts = img.get_timeseries(0, 0, 0).unwrap();
1053 assert_eq!(ts.len(), nt);
1054 for t in 0..nt {
1055 assert_eq!(ts[t], (t * vol_size) as f64);
1056 }
1057
1058 let tmean = img.temporal_mean();
1060 assert_eq!(tmean.len(), vol_size);
1061 let expected_mean = (0..nt).map(|t| (t * vol_size) as f64).sum::<f64>() / nt as f64;
1063 assert!((tmean[0] - expected_mean).abs() < 0.01);
1064
1065 std::fs::remove_dir_all(&dir).unwrap();
1066 }
1067
1068 #[test]
1069 fn test_load_single_volume() {
1070 let dir = std::env::temp_dir().join("bids_nifti_vol");
1071 std::fs::create_dir_all(&dir).unwrap();
1072 let path = dir.join("bold.nii");
1073 let nx = 4;
1074 let ny = 4;
1075 let nz = 3;
1076 let nt = 5;
1077 let dims: [i16; 8] = [4, nx as i16, ny as i16, nz as i16, nt as i16, 1, 1, 1];
1078 let pixdims: [f32; 8] = [1.0, 2.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0];
1079 let vol_size = nx * ny * nz;
1080 let data: Vec<f32> = (0..(vol_size * nt)).map(|i| i as f32).collect();
1081 write_nifti1_file(&path, &dims, &pixdims, &data);
1082
1083 let img = NiftiImage::from_file_volume(&path, 2).unwrap();
1084 assert_eq!(img.data.len(), vol_size);
1085 assert_eq!(img.data[0], (2 * vol_size) as f64);
1086
1087 assert!(NiftiImage::from_file_volume(&path, 5).is_err());
1089
1090 std::fs::remove_dir_all(&dir).unwrap();
1091 }
1092
1093 #[test]
1094 fn test_load_gzip() {
1095 let dir = std::env::temp_dir().join("bids_nifti_gz");
1096 std::fs::create_dir_all(&dir).unwrap();
1097 let path = dir.join("test.nii.gz");
1098 let dims: [i16; 8] = [3, 8, 8, 4, 1, 1, 1, 1];
1099 let pixdims: [f32; 8] = [1.0, 1.5, 1.5, 1.5, 0.0, 0.0, 0.0, 0.0];
1100 let data: Vec<f32> = (0..256).map(|i| i as f32 * 0.5).collect();
1101 write_nifti1_gz(&path, &dims, &pixdims, &data);
1102
1103 let img = NiftiImage::from_file(&path).unwrap();
1104 assert_eq!(img.shape(), vec![8, 8, 4]);
1105 assert_eq!(img.data.len(), 256);
1106 assert!((img.data[0] - 0.0).abs() < 0.01);
1107 assert!((img.data[1] - 0.5).abs() < 0.01);
1108 assert!((img.data[255] - 127.5).abs() < 0.01);
1109
1110 std::fs::remove_dir_all(&dir).unwrap();
1111 }
1112
1113 #[test]
1114 fn test_scaling() {
1115 let dir = std::env::temp_dir().join("bids_nifti_scl");
1116 std::fs::create_dir_all(&dir).unwrap();
1117 let path = dir.join("scaled.nii");
1118 let dims: [i16; 8] = [3, 2, 2, 2, 1, 1, 1, 1];
1119 let pixdims: [f32; 8] = [1.0; 8];
1120 let raw_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1121
1122 let mut bytes = build_nifti1(&dims, &pixdims, &raw_data);
1124 bytes[112..116].copy_from_slice(&2.0f32.to_le_bytes()); bytes[116..120].copy_from_slice(&10.0f32.to_le_bytes()); std::fs::write(&path, &bytes).unwrap();
1128
1129 let img = NiftiImage::from_file(&path).unwrap();
1130 assert!(img.header.has_scaling());
1131 assert!((img.data[0] - 12.0).abs() < 0.01); assert!((img.data[7] - 26.0).abs() < 0.01); std::fs::remove_dir_all(&dir).unwrap();
1136 }
1137
1138 #[test]
1139 fn test_int16_datatype() {
1140 let dir = std::env::temp_dir().join("bids_nifti_i16");
1141 std::fs::create_dir_all(&dir).unwrap();
1142 let path = dir.join("int16.nii");
1143
1144 let dims: [i16; 8] = [3, 4, 4, 2, 1, 1, 1, 1];
1145 let n_voxels = 32usize;
1146
1147 let mut header = vec![0u8; 348];
1148 header[0..4].copy_from_slice(&348i32.to_le_bytes());
1149 for (i, &d) in dims.iter().enumerate() {
1150 let off = 40 + i * 2;
1151 header[off..off + 2].copy_from_slice(&d.to_le_bytes());
1152 }
1153 header[70..72].copy_from_slice(&4i16.to_le_bytes());
1155 header[72..74].copy_from_slice(&16i16.to_le_bytes());
1156 let pixdims = [1.0f32; 8];
1157 for (i, &p) in pixdims.iter().enumerate() {
1158 let off = 76 + i * 4;
1159 header[off..off + 4].copy_from_slice(&p.to_le_bytes());
1160 }
1161 header[108..112].copy_from_slice(&352.0f32.to_le_bytes());
1162 header[112..116].copy_from_slice(&0.0f32.to_le_bytes()); header[344..348].copy_from_slice(b"n+1\0");
1164 header.extend_from_slice(&[0u8; 4]);
1165
1166 for i in 0..n_voxels as i16 {
1168 header.extend_from_slice(&(i * 100).to_le_bytes());
1169 }
1170 std::fs::write(&path, &header).unwrap();
1171
1172 let img = NiftiImage::from_file(&path).unwrap();
1173 assert_eq!(img.data.len(), n_voxels);
1174 assert_eq!(img.data[0], 0.0);
1175 assert_eq!(img.data[1], 100.0);
1176 assert_eq!(img.data[31], 3100.0);
1177
1178 std::fs::remove_dir_all(&dir).unwrap();
1179 }
1180
1181 #[test]
1182 fn test_large_4d_performance() {
1183 let dir = std::env::temp_dir().join("bids_nifti_perf");
1184 std::fs::create_dir_all(&dir).unwrap();
1185 let path = dir.join("bold.nii");
1186
1187 let nx = 64;
1189 let ny = 64;
1190 let nz = 32;
1191 let nt = 100;
1192 let dims: [i16; 8] = [4, nx, ny, nz, nt, 1, 1, 1];
1193 let pixdims: [f32; 8] = [1.0, 3.0, 3.0, 3.5, 2.0, 0.0, 0.0, 0.0];
1194 let n = (nx as usize) * (ny as usize) * (nz as usize) * (nt as usize);
1195 let data: Vec<f32> = (0..n).map(|i| (i % 1000) as f32 * 0.1).collect();
1197 write_nifti1_file(&path, &dims, &pixdims, &data);
1198
1199 let file_mb = std::fs::metadata(&path).unwrap().len() as f64 / 1e6;
1200
1201 let _ = NiftiImage::from_file(&path).unwrap();
1203
1204 let t = std::time::Instant::now();
1205 let img = NiftiImage::from_file(&path).unwrap();
1206 let ms = t.elapsed().as_secs_f64() * 1000.0;
1207
1208 assert_eq!(img.data.len(), n);
1209 assert_eq!(img.header.n_vols(), nt as usize);
1210
1211 eprintln!(
1212 " NIfTI 64×64×32×100 float32 ({:.0}MB): {:.1}ms ({:.0} MB/s)",
1213 file_mb,
1214 ms,
1215 file_mb / (ms / 1000.0)
1216 );
1217
1218 assert!(ms < 500.0, "NIfTI read took {:.0}ms, expected <500ms", ms);
1220
1221 std::fs::remove_dir_all(&dir).unwrap();
1222 }
1223
1224 #[test]
1225 fn test_header_nifti2_synthetic() {
1226 let mut header = vec![0u8; 540];
1227 header[0..4].copy_from_slice(&540i32.to_le_bytes());
1228 let dims: [i64; 8] = [4, 96, 96, 48, 200, 1, 1, 1];
1229 for (i, &d) in dims.iter().enumerate() {
1230 let off = 16 + i * 8;
1231 header[off..off + 8].copy_from_slice(&d.to_le_bytes());
1232 }
1233 let pixdims: [f64; 8] = [1.0, 2.0, 2.0, 2.0, 1.5, 0.0, 0.0, 0.0];
1234 for (i, &p) in pixdims.iter().enumerate() {
1235 let off = 104 + i * 8;
1236 header[off..off + 8].copy_from_slice(&p.to_le_bytes());
1237 }
1238
1239 let mut cursor = std::io::Cursor::new(header);
1240 let hdr = NiftiHeader::parse_reader(&mut cursor).unwrap();
1241
1242 assert_eq!(hdr.version, 2);
1243 assert_eq!(hdr.n_vols(), 200);
1244 assert_eq!(hdr.matrix_size(), (96, 96, 48));
1245 assert!((hdr.tr().unwrap() - 1.5).abs() < 0.01);
1246 }
1247
1248 #[test]
1249 fn test_sform_affine() {
1250 let dir = std::env::temp_dir().join("bids_nifti_affine");
1251 std::fs::create_dir_all(&dir).unwrap();
1252 let path = dir.join("affine.nii");
1253
1254 let dims: [i16; 8] = [3, 4, 4, 3, 1, 1, 1, 1];
1255 let pixdims: [f32; 8] = [1.0, 2.0, 2.0, 3.0, 0.0, 0.0, 0.0, 0.0];
1256 let data: Vec<f32> = vec![0.0; 48];
1257 let mut bytes = build_nifti1(&dims, &pixdims, &data);
1258
1259 bytes[254..256].copy_from_slice(&1i16.to_le_bytes());
1261 let sform_vals: [f32; 12] = [
1264 2.0, 0.0, 0.0, 10.0, 0.0, 2.0, 0.0, 20.0, 0.0, 0.0, 3.0, 30.0,
1265 ];
1266 for (i, &v) in sform_vals.iter().enumerate() {
1267 let off = 280 + i * 4;
1268 bytes[off..off + 4].copy_from_slice(&v.to_le_bytes());
1269 }
1270 std::fs::write(&path, &bytes).unwrap();
1271
1272 let hdr = NiftiHeader::from_file(&path).unwrap();
1273 assert_eq!(hdr.sform_code, 1);
1274 assert!((hdr.affine[0][0] - 2.0).abs() < 1e-6);
1275 assert!((hdr.affine[1][1] - 2.0).abs() < 1e-6);
1276 assert!((hdr.affine[2][2] - 3.0).abs() < 1e-6);
1277 assert!((hdr.affine[0][3] - 10.0).abs() < 1e-6);
1278 assert!((hdr.affine[1][3] - 20.0).abs() < 1e-6);
1279 assert!((hdr.affine[2][3] - 30.0).abs() < 1e-6);
1280 assert!((hdr.affine[3][3] - 1.0).abs() < 1e-6);
1281
1282 let world = hdr.voxel_to_world([1.0, 2.0, 3.0]);
1284 assert!((world[0] - 12.0).abs() < 1e-6);
1285 assert!((world[1] - 24.0).abs() < 1e-6);
1286 assert!((world[2] - 39.0).abs() < 1e-6);
1287
1288 std::fs::remove_dir_all(&dir).unwrap();
1289 }
1290}