Initial commit
This commit is contained in:
@@ -0,0 +1,541 @@
|
||||
//! DICOM file loading and data extraction
|
||||
//!
|
||||
//! This module provides functionality for loading DICOM files and extracting their data.
|
||||
//! It includes support for single files, series, and directories.
|
||||
|
||||
use super::parser::{DicomElement, DicomParser, DicomTag};
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// DICOM data structure containing parsed elements
|
||||
#[derive(Debug)]
|
||||
pub struct DicomData {
|
||||
/// File path of the DICOM file
|
||||
pub file_path: String,
|
||||
/// Parsed DICOM elements indexed by tag
|
||||
pub elements: HashMap<DicomTag, DicomElement>,
|
||||
}
|
||||
|
||||
impl DicomData {
|
||||
/// Get string value by tag
|
||||
pub fn get_string(&self, group: u16, element: u16) -> Option<String> {
|
||||
let tag = DicomTag { group, element };
|
||||
match self.elements.get(&tag) {
|
||||
Some(DicomElement::String(s)) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get uint16 value by tag
|
||||
pub fn get_uint16(&self, group: u16, element: u16) -> Option<u16> {
|
||||
let tag = DicomTag { group, element };
|
||||
match self.elements.get(&tag) {
|
||||
Some(DicomElement::UInt16(v)) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get uint32 value by tag
|
||||
pub fn get_uint32(&self, group: u16, element: u16) -> Option<u32> {
|
||||
let tag = DicomTag { group, element };
|
||||
match self.elements.get(&tag) {
|
||||
Some(DicomElement::UInt32(v)) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get float64 value by tag
|
||||
pub fn get_float64(&self, group: u16, element: u16) -> Option<f64> {
|
||||
let tag = DicomTag { group, element };
|
||||
match self.elements.get(&tag) {
|
||||
Some(DicomElement::Float64(v)) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get bytes by tag
|
||||
pub fn get_bytes(&self, group: u16, element: u16) -> Option<&[u8]> {
|
||||
let tag = DicomTag { group, element };
|
||||
match self.elements.get(&tag) {
|
||||
Some(DicomElement::Bytes(bytes)) => Some(bytes),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DICOM loader for medical images
|
||||
pub struct DicomLoader;
|
||||
|
||||
impl DicomLoader {
|
||||
/// Load a single DICOM file
|
||||
pub fn load_single(dicom_path: &str) -> MedicalResult<DicomData> {
|
||||
if !Path::new(dicom_path).exists() {
|
||||
return Err(MedicalError::Dicom(format!(
|
||||
"DICOM file not found: {}",
|
||||
dicom_path
|
||||
)));
|
||||
}
|
||||
|
||||
// Parse DICOM file
|
||||
let dicom_data = Self::parse_dicom_file(dicom_path)?;
|
||||
|
||||
Ok(dicom_data)
|
||||
}
|
||||
|
||||
/// Load a DICOM series (multiple related slices)
|
||||
pub fn load_series(dicom_paths: &[String]) -> MedicalResult<Vec<DicomData>> {
|
||||
if dicom_paths.is_empty() {
|
||||
return Err(MedicalError::Dicom("No DICOM files provided".to_string()));
|
||||
}
|
||||
|
||||
// Load and parse all DICOM files
|
||||
let mut dicom_files = Vec::new();
|
||||
for path in dicom_paths {
|
||||
let dicom_data = Self::parse_dicom_file(path)?;
|
||||
dicom_files.push(dicom_data);
|
||||
}
|
||||
|
||||
// Sort by slice location or instance number
|
||||
Self::sort_dicom_series(&mut dicom_files)?;
|
||||
|
||||
Ok(dicom_files)
|
||||
}
|
||||
|
||||
/// Load DICOM directory (automatically find and group series)
|
||||
pub fn load_directory(dicom_dir: &str) -> MedicalResult<HashMap<String, Vec<DicomData>>> {
|
||||
let dicom_files = Self::find_dicom_files(dicom_dir)?;
|
||||
let series_groups = Self::group_by_series(&dicom_files)?;
|
||||
|
||||
let mut series_map = HashMap::new();
|
||||
for (series_uid, file_paths) in series_groups {
|
||||
match Self::load_series(&file_paths) {
|
||||
Ok(series_data) => {
|
||||
series_map.insert(series_uid, series_data);
|
||||
}
|
||||
Err(_) => { /* Skip failed series */ }
|
||||
}
|
||||
}
|
||||
|
||||
Ok(series_map)
|
||||
}
|
||||
|
||||
/// Find all DICOM files in a directory
|
||||
fn find_dicom_files(dir_path: &str) -> MedicalResult<Vec<String>> {
|
||||
let mut dicom_files = Vec::new();
|
||||
|
||||
let entries = std::fs::read_dir(dir_path).map_err(|e| MedicalError::Io(e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| MedicalError::Io(e))?;
|
||||
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Some(path_str) = path.to_str() {
|
||||
// Check if file is DICOM (basic heuristic)
|
||||
if Self::is_dicom_file(path_str)? {
|
||||
dicom_files.push(path_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dicom_files)
|
||||
}
|
||||
|
||||
/// Check if file is a DICOM file
|
||||
fn is_dicom_file(file_path: &str) -> MedicalResult<bool> {
|
||||
let file = std::fs::File::open(file_path).map_err(|e| MedicalError::Io(e))?;
|
||||
|
||||
let mut buffer = [0u8; 132];
|
||||
use std::io::Read;
|
||||
let mut file = file;
|
||||
|
||||
// Read first 132 bytes
|
||||
match file.read(&mut buffer) {
|
||||
Ok(bytes_read) if bytes_read >= 132 => {
|
||||
// Check for DICOM prefix at offset 128
|
||||
let dicm = &buffer[128..132];
|
||||
Ok(dicm == b"DICM")
|
||||
}
|
||||
_ => {
|
||||
// If can't read header, try alternative methods
|
||||
// Check file extension or other heuristics
|
||||
let path = Path::new(file_path);
|
||||
if let Some(ext) = path.extension() {
|
||||
let ext_str = ext.to_string_lossy().to_lowercase();
|
||||
Ok(matches!(ext_str.as_str(), "dcm" | "dicom" | "ima" | ""))
|
||||
} else {
|
||||
// No extension, assume it might be DICOM
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse DICOM file and extract data elements
|
||||
fn parse_dicom_file(file_path: &str) -> MedicalResult<DicomData> {
|
||||
// This is a simplified DICOM parser
|
||||
// In production, you'd use a proper DICOM library like dicom-rs
|
||||
|
||||
let file_data = std::fs::read(file_path).map_err(|e| MedicalError::Io(e))?;
|
||||
|
||||
// Skip DICOM preamble (128 bytes) and DICM prefix (4 bytes)
|
||||
if file_data.len() < 132 || &file_data[128..132] != b"DICM" {
|
||||
return Err(MedicalError::Dicom("Invalid DICOM file format".to_string()));
|
||||
}
|
||||
|
||||
let data_start = 132;
|
||||
let mut parser = DicomParser::new(&file_data[data_start..]);
|
||||
let elements = parser.parse_elements()?;
|
||||
|
||||
Ok(DicomData {
|
||||
file_path: file_path.to_string(),
|
||||
elements,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract pixel data dimensions from DICOM
|
||||
pub fn get_pixel_dimensions(dicom_data: &DicomData) -> MedicalResult<(usize, usize)> {
|
||||
let rows = dicom_data
|
||||
.get_uint16(0x0028, 0x0010)
|
||||
.ok_or_else(|| MedicalError::Dicom("Missing Rows (0028,0010)".to_string()))?
|
||||
as usize;
|
||||
|
||||
let columns = dicom_data
|
||||
.get_uint16(0x0028, 0x0011)
|
||||
.ok_or_else(|| MedicalError::Dicom("Missing Columns (0028,0011)".to_string()))?
|
||||
as usize;
|
||||
|
||||
Ok((rows, columns))
|
||||
}
|
||||
|
||||
/// Extract raw pixel data bytes from DICOM
|
||||
pub fn get_pixel_data_bytes(dicom_data: &DicomData) -> MedicalResult<&[u8]> {
|
||||
dicom_data
|
||||
.get_bytes(0x7FE0, 0x0010)
|
||||
.ok_or_else(|| MedicalError::Dicom("Missing Pixel Data (7FE0,0010)".to_string()))
|
||||
}
|
||||
|
||||
/// Get bits allocated per pixel
|
||||
pub fn get_bits_allocated(dicom_data: &DicomData) -> u16 {
|
||||
dicom_data.get_uint16(0x0028, 0x0100).unwrap_or(16)
|
||||
}
|
||||
|
||||
/// Get rescale slope and intercept
|
||||
pub fn get_rescale_params(dicom_data: &DicomData) -> (f32, f32) {
|
||||
let slope = dicom_data.get_float64(0x0028, 0x1053).unwrap_or(1.0) as f32;
|
||||
let intercept = dicom_data.get_float64(0x0028, 0x1052).unwrap_or(0.0) as f32;
|
||||
(slope, intercept)
|
||||
}
|
||||
|
||||
/// Parse pixel spacing from DICOM string
|
||||
pub fn parse_pixel_spacing(spacing_str: &str) -> Option<(f32, f32)> {
|
||||
let parts: Vec<&str> = spacing_str.split('\\').collect();
|
||||
if parts.len() >= 2 {
|
||||
let row_spacing = parts[0].parse::<f32>().ok()?;
|
||||
let col_spacing = parts[1].parse::<f32>().ok()?;
|
||||
Some((col_spacing, row_spacing)) // Note: DICOM uses row\column order
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort DICOM files in a series by slice location or instance number
|
||||
fn sort_dicom_series(dicom_files: &mut [DicomData]) -> MedicalResult<()> {
|
||||
// Try to sort by slice location first
|
||||
let has_slice_location = dicom_files
|
||||
.iter()
|
||||
.all(|d| d.get_float64(0x0020, 0x1041).is_some());
|
||||
|
||||
if has_slice_location {
|
||||
dicom_files.sort_by(|a, b| {
|
||||
let loc_a = a.get_float64(0x0020, 0x1041).unwrap_or(0.0);
|
||||
let loc_b = b.get_float64(0x0020, 0x1041).unwrap_or(0.0);
|
||||
loc_a.total_cmp(&loc_b)
|
||||
});
|
||||
} else {
|
||||
// Fall back to instance number
|
||||
dicom_files.sort_by(|a, b| {
|
||||
let inst_a = a.get_uint32(0x0020, 0x0013).unwrap_or(0);
|
||||
let inst_b = b.get_uint32(0x0020, 0x0013).unwrap_or(0);
|
||||
inst_a.cmp(&inst_b)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Group DICOM files by series UID
|
||||
fn group_by_series(dicom_files: &[String]) -> MedicalResult<HashMap<String, Vec<String>>> {
|
||||
let mut series_groups: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for file_path in dicom_files {
|
||||
let dicom_data = Self::parse_dicom_file(file_path)?;
|
||||
let series_uid = dicom_data
|
||||
.get_string(0x0020, 0x000E)
|
||||
.unwrap_or_else(|| "UNKNOWN".to_string());
|
||||
|
||||
series_groups
|
||||
.entry(series_uid)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(file_path.clone());
|
||||
}
|
||||
|
||||
Ok(series_groups)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dicom_data_get_string() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0010,
|
||||
element: 0x0020,
|
||||
},
|
||||
DicomElement::String("PT12345".to_string()),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
assert_eq!(data.get_string(0x0010, 0x0020), Some("PT12345".to_string()));
|
||||
assert_eq!(data.get_string(0x0010, 0x0021), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_data_get_uint16() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x0010,
|
||||
},
|
||||
DicomElement::UInt16(512),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
assert_eq!(data.get_uint16(0x0028, 0x0010), Some(512));
|
||||
assert_eq!(data.get_uint16(0x0028, 0x0011), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_data_get_uint32() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0020,
|
||||
element: 0x0013,
|
||||
},
|
||||
DicomElement::UInt32(123456),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
assert_eq!(data.get_uint32(0x0020, 0x0013), Some(123456));
|
||||
assert_eq!(data.get_uint32(0x0020, 0x0014), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_data_get_float64() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x1053,
|
||||
},
|
||||
DicomElement::Float64(1.5),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
assert_eq!(data.get_float64(0x0028, 0x1053), Some(1.5));
|
||||
assert_eq!(data.get_float64(0x0028, 0x1054), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_data_get_bytes() {
|
||||
let mut elements = HashMap::new();
|
||||
let pixel_data = vec![1u8, 2, 3, 4, 5];
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x7FE0,
|
||||
element: 0x0010,
|
||||
},
|
||||
DicomElement::Bytes(pixel_data.clone()),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
assert_eq!(data.get_bytes(0x7FE0, 0x0010), Some(pixel_data.as_slice()));
|
||||
assert_eq!(data.get_bytes(0x7FE0, 0x0011), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pixel_spacing_valid() {
|
||||
let spacing = DicomLoader::parse_pixel_spacing("0.5\\0.5");
|
||||
assert_eq!(spacing, Some((0.5, 0.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pixel_spacing_different_values() {
|
||||
let spacing = DicomLoader::parse_pixel_spacing("0.5\\1.0");
|
||||
assert_eq!(spacing, Some((1.0, 0.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pixel_spacing_invalid() {
|
||||
let spacing = DicomLoader::parse_pixel_spacing("invalid");
|
||||
assert_eq!(spacing, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pixel_spacing_single_value() {
|
||||
let spacing = DicomLoader::parse_pixel_spacing("0.5");
|
||||
assert_eq!(spacing, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pixel_dimensions() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x0010,
|
||||
},
|
||||
DicomElement::UInt16(512),
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x0011,
|
||||
},
|
||||
DicomElement::UInt16(512),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
let dims = DicomLoader::get_pixel_dimensions(&data).unwrap();
|
||||
assert_eq!(dims, (512, 512));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pixel_dimensions_missing_rows() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x0011,
|
||||
},
|
||||
DicomElement::UInt16(512),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
let result = DicomLoader::get_pixel_dimensions(&data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_bits_allocated_default() {
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements: HashMap::new(),
|
||||
};
|
||||
|
||||
let bits = DicomLoader::get_bits_allocated(&data);
|
||||
assert_eq!(bits, 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_bits_allocated_custom() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x0100,
|
||||
},
|
||||
DicomElement::UInt16(8),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
let bits = DicomLoader::get_bits_allocated(&data);
|
||||
assert_eq!(bits, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_rescale_params_default() {
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements: HashMap::new(),
|
||||
};
|
||||
|
||||
let (slope, intercept) = DicomLoader::get_rescale_params(&data);
|
||||
assert_eq!(slope, 1.0);
|
||||
assert_eq!(intercept, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_rescale_params_custom() {
|
||||
let mut elements = HashMap::new();
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x1053,
|
||||
},
|
||||
DicomElement::Float64(1.5),
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag {
|
||||
group: 0x0028,
|
||||
element: 0x1052,
|
||||
},
|
||||
DicomElement::Float64(-1024.0),
|
||||
);
|
||||
|
||||
let data = DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
};
|
||||
|
||||
let (slope, intercept) = DicomLoader::get_rescale_params(&data);
|
||||
assert_eq!(slope, 1.5);
|
||||
assert_eq!(intercept, -1024.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_series_empty_returns_error() {
|
||||
let result = DicomLoader::load_series(&[]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
//! DICOM file format support
|
||||
//!
|
||||
//! This module provides DICOM file loading, parsing, and validation functionality.
|
||||
//! It includes support for single files, series, and directories.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - Load single DICOM files or entire series
|
||||
//! - Parse DICOM data elements and extract metadata
|
||||
//! - Validate DICOM files and series for consistency
|
||||
//! - Extract pixel data and associated parameters
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use rtx_medical_core::dicom::{DicomLoader, DicomValidator};
|
||||
//!
|
||||
//! // Load a single DICOM file
|
||||
//! let dicom_data = DicomLoader::load_single("path/to/file.dcm")?;
|
||||
//!
|
||||
//! // Validate the file
|
||||
//! let report = DicomValidator::validate_file(&dicom_data)?;
|
||||
//! if !report.is_valid {
|
||||
//! eprintln!("Errors: {:?}", report.errors);
|
||||
//! }
|
||||
//!
|
||||
//! // Load a DICOM series
|
||||
//! let series = DicomLoader::load_series(&vec![
|
||||
//! "slice1.dcm".to_string(),
|
||||
//! "slice2.dcm".to_string(),
|
||||
//! "slice3.dcm".to_string(),
|
||||
//! ])?;
|
||||
//! ```
|
||||
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
|
||||
// Submodules
|
||||
mod loader;
|
||||
mod parser;
|
||||
mod validator;
|
||||
|
||||
// Re-export public types from loader
|
||||
pub use loader::{DicomData, DicomLoader};
|
||||
|
||||
// Re-export public types from parser
|
||||
pub use parser::{DicomElement, DicomTag};
|
||||
|
||||
// Re-export public types from validator
|
||||
pub use validator::{DicomValidator, FileValidationReport, SeriesValidationReport};
|
||||
|
||||
// Keep the basic types from the original scaffold for compatibility
|
||||
|
||||
/// DICOM Transfer Syntax UIDs (subset)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TransferSyntax {
|
||||
/// Implicit VR Little Endian
|
||||
ImplicitVRLittleEndian,
|
||||
/// Explicit VR Little Endian
|
||||
ExplicitVRLittleEndian,
|
||||
/// Explicit VR Big Endian
|
||||
ExplicitVRBigEndian,
|
||||
}
|
||||
|
||||
impl TransferSyntax {
|
||||
/// Get UID string for transfer syntax
|
||||
pub fn uid(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ImplicitVRLittleEndian => "1.2.840.10008.1.2",
|
||||
Self::ExplicitVRLittleEndian => "1.2.840.10008.1.2.1",
|
||||
Self::ExplicitVRBigEndian => "1.2.840.10008.1.2.2",
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from UID string
|
||||
pub fn from_uid(uid: &str) -> MedicalResult<Self> {
|
||||
match uid {
|
||||
"1.2.840.10008.1.2" => Ok(Self::ImplicitVRLittleEndian),
|
||||
"1.2.840.10008.1.2.1" => Ok(Self::ExplicitVRLittleEndian),
|
||||
"1.2.840.10008.1.2.2" => Ok(Self::ExplicitVRBigEndian),
|
||||
_ => Err(MedicalError::Dicom(format!(
|
||||
"Unknown transfer syntax UID: {}",
|
||||
uid
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if little endian
|
||||
pub fn is_little_endian(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ImplicitVRLittleEndian | Self::ExplicitVRLittleEndian
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if explicit VR
|
||||
pub fn is_explicit_vr(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ExplicitVRLittleEndian | Self::ExplicitVRBigEndian
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// DICOM modality codes
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Modality {
|
||||
/// Computed Tomography
|
||||
CT,
|
||||
/// Magnetic Resonance
|
||||
MR,
|
||||
/// Positron Emission Tomography
|
||||
PT,
|
||||
/// Single Photon Emission Computed Tomography
|
||||
SPECT,
|
||||
/// X-Ray Angiography
|
||||
XA,
|
||||
/// X-Ray (general)
|
||||
CR,
|
||||
/// Digital Radiography
|
||||
DX,
|
||||
/// Ultrasound
|
||||
US,
|
||||
/// Mammography
|
||||
MG,
|
||||
/// Slide Microscopy
|
||||
SM,
|
||||
/// Ophthalmic Photography
|
||||
OP,
|
||||
/// Other/Unknown
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Modality {
|
||||
/// Get standard DICOM code string
|
||||
pub fn code(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CT => "CT",
|
||||
Self::MR => "MR",
|
||||
Self::PT => "PT",
|
||||
Self::SPECT => "ST",
|
||||
Self::XA => "XA",
|
||||
Self::CR => "CR",
|
||||
Self::DX => "DX",
|
||||
Self::US => "US",
|
||||
Self::MG => "MG",
|
||||
Self::SM => "SM",
|
||||
Self::OP => "OP",
|
||||
Self::Other => "OT",
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from code string
|
||||
pub fn from_code(code: &str) -> Self {
|
||||
match code {
|
||||
"CT" => Self::CT,
|
||||
"MR" => Self::MR,
|
||||
"PT" => Self::PT,
|
||||
"ST" => Self::SPECT,
|
||||
"XA" => Self::XA,
|
||||
"CR" => Self::CR,
|
||||
"DX" => Self::DX,
|
||||
"US" => Self::US,
|
||||
"MG" => Self::MG,
|
||||
"SM" => Self::SM,
|
||||
"OP" => Self::OP,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic DICOM metadata
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DicomMetadata {
|
||||
/// Patient ID
|
||||
pub patient_id: Option<String>,
|
||||
/// Study Instance UID
|
||||
pub study_uid: Option<String>,
|
||||
/// Series Instance UID
|
||||
pub series_uid: Option<String>,
|
||||
/// Modality
|
||||
pub modality: Modality,
|
||||
/// Transfer syntax
|
||||
pub transfer_syntax: TransferSyntax,
|
||||
}
|
||||
|
||||
impl DicomMetadata {
|
||||
/// Create new DICOM metadata with required fields
|
||||
pub fn new(modality: Modality, transfer_syntax: TransferSyntax) -> Self {
|
||||
Self {
|
||||
patient_id: None,
|
||||
study_uid: None,
|
||||
series_uid: None,
|
||||
modality,
|
||||
transfer_syntax,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set patient ID
|
||||
pub fn with_patient_id(mut self, patient_id: String) -> Self {
|
||||
self.patient_id = Some(patient_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set study UID
|
||||
pub fn with_study_uid(mut self, study_uid: String) -> Self {
|
||||
self.study_uid = Some(study_uid);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set series UID
|
||||
pub fn with_series_uid(mut self, series_uid: String) -> Self {
|
||||
self.series_uid = Some(series_uid);
|
||||
self
|
||||
}
|
||||
|
||||
/// Extract metadata from DicomData
|
||||
pub fn from_dicom_data(dicom_data: &DicomData) -> Self {
|
||||
let modality_str = dicom_data
|
||||
.get_string(0x0008, 0x0060)
|
||||
.unwrap_or_else(|| "OT".to_string());
|
||||
let modality = Modality::from_code(&modality_str);
|
||||
|
||||
Self {
|
||||
patient_id: dicom_data.get_string(0x0010, 0x0020),
|
||||
study_uid: dicom_data.get_string(0x0020, 0x000D),
|
||||
series_uid: dicom_data.get_string(0x0020, 0x000E),
|
||||
modality,
|
||||
transfer_syntax: TransferSyntax::ExplicitVRLittleEndian, // Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_uid_implicit_vr() {
|
||||
assert_eq!(
|
||||
TransferSyntax::ImplicitVRLittleEndian.uid(),
|
||||
"1.2.840.10008.1.2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_uid_explicit_vr_little() {
|
||||
assert_eq!(
|
||||
TransferSyntax::ExplicitVRLittleEndian.uid(),
|
||||
"1.2.840.10008.1.2.1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_uid_explicit_vr_big() {
|
||||
assert_eq!(
|
||||
TransferSyntax::ExplicitVRBigEndian.uid(),
|
||||
"1.2.840.10008.1.2.2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_from_uid_implicit() {
|
||||
let ts = TransferSyntax::from_uid("1.2.840.10008.1.2").unwrap();
|
||||
assert_eq!(ts, TransferSyntax::ImplicitVRLittleEndian);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_from_uid_explicit_little() {
|
||||
let ts = TransferSyntax::from_uid("1.2.840.10008.1.2.1").unwrap();
|
||||
assert_eq!(ts, TransferSyntax::ExplicitVRLittleEndian);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_from_uid_explicit_big() {
|
||||
let ts = TransferSyntax::from_uid("1.2.840.10008.1.2.2").unwrap();
|
||||
assert_eq!(ts, TransferSyntax::ExplicitVRBigEndian);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_from_uid_unknown() {
|
||||
let result = TransferSyntax::from_uid("1.2.840.10008.1.2.99");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(MedicalError::Dicom(msg)) => {
|
||||
assert!(msg.contains("Unknown transfer syntax UID"));
|
||||
}
|
||||
_ => panic!("Expected Dicom error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_round_trip() {
|
||||
let original = TransferSyntax::ExplicitVRLittleEndian;
|
||||
let uid = original.uid();
|
||||
let restored = TransferSyntax::from_uid(uid).unwrap();
|
||||
assert_eq!(original, restored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_is_little_endian_true() {
|
||||
assert!(TransferSyntax::ImplicitVRLittleEndian.is_little_endian());
|
||||
assert!(TransferSyntax::ExplicitVRLittleEndian.is_little_endian());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_is_little_endian_false() {
|
||||
assert!(!TransferSyntax::ExplicitVRBigEndian.is_little_endian());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_is_explicit_vr_true() {
|
||||
assert!(TransferSyntax::ExplicitVRLittleEndian.is_explicit_vr());
|
||||
assert!(TransferSyntax::ExplicitVRBigEndian.is_explicit_vr());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_is_explicit_vr_false() {
|
||||
assert!(!TransferSyntax::ImplicitVRLittleEndian.is_explicit_vr());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_ct() {
|
||||
assert_eq!(Modality::CT.code(), "CT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_mr() {
|
||||
assert_eq!(Modality::MR.code(), "MR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_pt() {
|
||||
assert_eq!(Modality::PT.code(), "PT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_spect() {
|
||||
assert_eq!(Modality::SPECT.code(), "ST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_xa() {
|
||||
assert_eq!(Modality::XA.code(), "XA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_cr() {
|
||||
assert_eq!(Modality::CR.code(), "CR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_dx() {
|
||||
assert_eq!(Modality::DX.code(), "DX");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_us() {
|
||||
assert_eq!(Modality::US.code(), "US");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_mg() {
|
||||
assert_eq!(Modality::MG.code(), "MG");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_code_other() {
|
||||
assert_eq!(Modality::Other.code(), "OT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_from_code_ct() {
|
||||
assert_eq!(Modality::from_code("CT"), Modality::CT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_from_code_mr() {
|
||||
assert_eq!(Modality::from_code("MR"), Modality::MR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_from_code_pt() {
|
||||
assert_eq!(Modality::from_code("PT"), Modality::PT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_from_code_spect() {
|
||||
assert_eq!(Modality::from_code("ST"), Modality::SPECT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_from_code_unknown() {
|
||||
assert_eq!(Modality::from_code("UNKNOWN"), Modality::Other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_round_trip() {
|
||||
let original = Modality::MR;
|
||||
let code = original.code();
|
||||
let restored = Modality::from_code(code);
|
||||
assert_eq!(original, restored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_metadata_new() {
|
||||
let metadata = DicomMetadata::new(Modality::CT, TransferSyntax::ExplicitVRLittleEndian);
|
||||
assert_eq!(metadata.modality, Modality::CT);
|
||||
assert_eq!(
|
||||
metadata.transfer_syntax,
|
||||
TransferSyntax::ExplicitVRLittleEndian
|
||||
);
|
||||
assert!(metadata.patient_id.is_none());
|
||||
assert!(metadata.study_uid.is_none());
|
||||
assert!(metadata.series_uid.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_metadata_with_patient_id() {
|
||||
let metadata = DicomMetadata::new(Modality::MR, TransferSyntax::ImplicitVRLittleEndian)
|
||||
.with_patient_id("PT12345".to_string());
|
||||
assert_eq!(metadata.patient_id, Some("PT12345".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_metadata_with_study_uid() {
|
||||
let metadata = DicomMetadata::new(Modality::MR, TransferSyntax::ImplicitVRLittleEndian)
|
||||
.with_study_uid("1.2.3.4.5".to_string());
|
||||
assert_eq!(metadata.study_uid, Some("1.2.3.4.5".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_metadata_with_series_uid() {
|
||||
let metadata = DicomMetadata::new(Modality::MR, TransferSyntax::ImplicitVRLittleEndian)
|
||||
.with_series_uid("1.2.3.4.5.6".to_string());
|
||||
assert_eq!(metadata.series_uid, Some("1.2.3.4.5.6".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_metadata_builder_chain() {
|
||||
let metadata = DicomMetadata::new(Modality::CT, TransferSyntax::ExplicitVRLittleEndian)
|
||||
.with_patient_id("PT001".to_string())
|
||||
.with_study_uid("1.2.3".to_string())
|
||||
.with_series_uid("1.2.3.4".to_string());
|
||||
|
||||
assert_eq!(metadata.patient_id, Some("PT001".to_string()));
|
||||
assert_eq!(metadata.study_uid, Some("1.2.3".to_string()));
|
||||
assert_eq!(metadata.series_uid, Some("1.2.3.4".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_metadata_clone() {
|
||||
let metadata1 = DicomMetadata::new(Modality::MR, TransferSyntax::ExplicitVRLittleEndian)
|
||||
.with_patient_id("PT001".to_string());
|
||||
let metadata2 = metadata1.clone();
|
||||
assert_eq!(metadata1.patient_id, metadata2.patient_id);
|
||||
assert_eq!(metadata1.modality, metadata2.modality);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_syntax_equality() {
|
||||
assert_eq!(
|
||||
TransferSyntax::ImplicitVRLittleEndian,
|
||||
TransferSyntax::ImplicitVRLittleEndian
|
||||
);
|
||||
assert_ne!(
|
||||
TransferSyntax::ImplicitVRLittleEndian,
|
||||
TransferSyntax::ExplicitVRLittleEndian
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modality_equality() {
|
||||
assert_eq!(Modality::CT, Modality::CT);
|
||||
assert_ne!(Modality::CT, Modality::MR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
//! DICOM parser for reading and parsing DICOM data elements
|
||||
//!
|
||||
//! This module provides simplified DICOM parsing functionality.
|
||||
//! In production, consider using a full-featured DICOM library like dicom-rs.
|
||||
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// DICOM tag (group, element)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DicomTag {
|
||||
/// Group identifier (16-bit)
|
||||
pub group: u16,
|
||||
/// Element identifier (16-bit)
|
||||
pub element: u16,
|
||||
}
|
||||
|
||||
impl DicomTag {
|
||||
/// Create a new DICOM tag
|
||||
pub fn new(group: u16, element: u16) -> Self {
|
||||
Self { group, element }
|
||||
}
|
||||
}
|
||||
|
||||
/// DICOM element value
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DicomElement {
|
||||
/// String value
|
||||
String(String),
|
||||
/// Unsigned 16-bit integer
|
||||
UInt16(u16),
|
||||
/// Unsigned 32-bit integer
|
||||
UInt32(u32),
|
||||
/// 64-bit floating point
|
||||
Float64(f64),
|
||||
/// Raw bytes
|
||||
Bytes(Vec<u8>),
|
||||
}
|
||||
|
||||
/// Simplified DICOM parser
|
||||
pub(crate) struct DicomParser<'a> {
|
||||
data: &'a [u8],
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl<'a> DicomParser<'a> {
|
||||
/// Create a new DICOM parser
|
||||
pub(crate) fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, position: 0 }
|
||||
}
|
||||
|
||||
/// Parse DICOM data elements
|
||||
pub(crate) fn parse_elements(&mut self) -> MedicalResult<HashMap<DicomTag, DicomElement>> {
|
||||
let mut elements = HashMap::new();
|
||||
|
||||
// This is a simplified parser - in production use dicom-rs or similar
|
||||
while self.position + 8 < self.data.len() {
|
||||
match self.parse_element() {
|
||||
Ok((tag, element)) => {
|
||||
elements.insert(tag, element);
|
||||
}
|
||||
Err(_) => {
|
||||
// Skip parsing errors and continue
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(elements)
|
||||
}
|
||||
|
||||
/// Parse a single DICOM element
|
||||
fn parse_element(&mut self) -> MedicalResult<(DicomTag, DicomElement)> {
|
||||
if self.position + 8 > self.data.len() {
|
||||
return Err(MedicalError::Dicom(
|
||||
"Unexpected end of DICOM data".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Read tag (4 bytes)
|
||||
let group = u16::from_le_bytes([self.data[self.position], self.data[self.position + 1]]);
|
||||
let element =
|
||||
u16::from_le_bytes([self.data[self.position + 2], self.data[self.position + 3]]);
|
||||
let tag = DicomTag { group, element };
|
||||
|
||||
self.position += 4;
|
||||
|
||||
// Read VR (2 bytes) - Value Representation
|
||||
let vr = [self.data[self.position], self.data[self.position + 1]];
|
||||
self.position += 2;
|
||||
|
||||
// Read length (2 or 4 bytes depending on VR)
|
||||
let length = if Self::is_explicit_vr(&vr) {
|
||||
// Reserved 2 bytes + 4-byte length
|
||||
self.position += 2; // Skip reserved bytes
|
||||
u32::from_le_bytes([
|
||||
self.data[self.position],
|
||||
self.data[self.position + 1],
|
||||
self.data[self.position + 2],
|
||||
self.data[self.position + 3],
|
||||
]) as usize
|
||||
} else {
|
||||
// 2-byte length
|
||||
u16::from_le_bytes([self.data[self.position], self.data[self.position + 1]]) as usize
|
||||
};
|
||||
self.position += if Self::is_explicit_vr(&vr) { 4 } else { 2 };
|
||||
|
||||
// Read value
|
||||
if self.position + length > self.data.len() {
|
||||
return Err(MedicalError::Dicom("Invalid element length".to_string()));
|
||||
}
|
||||
|
||||
let value_data = &self.data[self.position..self.position + length];
|
||||
let element = self.parse_value(&vr, value_data)?;
|
||||
|
||||
self.position += length;
|
||||
|
||||
Ok((tag, element))
|
||||
}
|
||||
|
||||
/// Parse element value based on VR
|
||||
fn parse_value(&self, vr: &[u8; 2], data: &[u8]) -> MedicalResult<DicomElement> {
|
||||
match vr {
|
||||
b"CS" | b"SH" | b"LO" | b"ST" | b"LT" | b"PN" | b"UI" | b"DA" | b"TM" | b"DT" => {
|
||||
let string = String::from_utf8_lossy(data)
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
Ok(DicomElement::String(string))
|
||||
}
|
||||
b"US" => {
|
||||
if data.len() >= 2 {
|
||||
let value = u16::from_le_bytes([data[0], data[1]]);
|
||||
Ok(DicomElement::UInt16(value))
|
||||
} else {
|
||||
Err(MedicalError::Dicom("Invalid US value length".to_string()))
|
||||
}
|
||||
}
|
||||
b"UL" => {
|
||||
if data.len() >= 4 {
|
||||
let value = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
Ok(DicomElement::UInt32(value))
|
||||
} else {
|
||||
Err(MedicalError::Dicom("Invalid UL value length".to_string()))
|
||||
}
|
||||
}
|
||||
b"DS" | b"IS" => {
|
||||
let string = String::from_utf8_lossy(data)
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
if let Ok(value) = string.parse::<f64>() {
|
||||
Ok(DicomElement::Float64(value))
|
||||
} else {
|
||||
Ok(DicomElement::String(string))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Default to bytes for unknown or binary VRs
|
||||
Ok(DicomElement::Bytes(data.to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if VR uses explicit length encoding
|
||||
fn is_explicit_vr(vr: &[u8; 2]) -> bool {
|
||||
matches!(vr, b"OB" | b"OW" | b"OF" | b"SQ" | b"UT" | b"UN")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dicom_tag_creation() {
|
||||
let tag = DicomTag::new(0x0008, 0x0060);
|
||||
assert_eq!(tag.group, 0x0008);
|
||||
assert_eq!(tag.element, 0x0060);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_tag_equality() {
|
||||
let tag1 = DicomTag::new(0x0008, 0x0060);
|
||||
let tag2 = DicomTag::new(0x0008, 0x0060);
|
||||
let tag3 = DicomTag::new(0x0008, 0x0061);
|
||||
|
||||
assert_eq!(tag1, tag2);
|
||||
assert_ne!(tag1, tag3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_tag_hash() {
|
||||
let tag = DicomTag::new(0x0008, 0x0060);
|
||||
let mut map = HashMap::new();
|
||||
map.insert(tag, DicomElement::String("CT".to_string()));
|
||||
|
||||
assert!(map.contains_key(&tag));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_element_string() {
|
||||
let elem = DicomElement::String("CT".to_string());
|
||||
match elem {
|
||||
DicomElement::String(s) => assert_eq!(s, "CT"),
|
||||
_ => panic!("Expected String variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_element_uint16() {
|
||||
let elem = DicomElement::UInt16(512);
|
||||
match elem {
|
||||
DicomElement::UInt16(v) => assert_eq!(v, 512),
|
||||
_ => panic!("Expected UInt16 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_element_uint32() {
|
||||
let elem = DicomElement::UInt32(123456);
|
||||
match elem {
|
||||
DicomElement::UInt32(v) => assert_eq!(v, 123456),
|
||||
_ => panic!("Expected UInt32 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_element_float64() {
|
||||
let elem = DicomElement::Float64(3.14159);
|
||||
match elem {
|
||||
DicomElement::Float64(v) => assert!((v - 3.14159).abs() < f64::EPSILON),
|
||||
_ => panic!("Expected Float64 variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dicom_element_bytes() {
|
||||
let data = vec![1, 2, 3, 4, 5];
|
||||
let elem = DicomElement::Bytes(data.clone());
|
||||
match elem {
|
||||
DicomElement::Bytes(b) => assert_eq!(b, data),
|
||||
_ => panic!("Expected Bytes variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_is_explicit_vr_true() {
|
||||
assert!(DicomParser::is_explicit_vr(b"OB"));
|
||||
assert!(DicomParser::is_explicit_vr(b"OW"));
|
||||
assert!(DicomParser::is_explicit_vr(b"OF"));
|
||||
assert!(DicomParser::is_explicit_vr(b"SQ"));
|
||||
assert!(DicomParser::is_explicit_vr(b"UT"));
|
||||
assert!(DicomParser::is_explicit_vr(b"UN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_is_explicit_vr_false() {
|
||||
assert!(!DicomParser::is_explicit_vr(b"CS"));
|
||||
assert!(!DicomParser::is_explicit_vr(b"US"));
|
||||
assert!(!DicomParser::is_explicit_vr(b"UL"));
|
||||
assert!(!DicomParser::is_explicit_vr(b"DS"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
//! DICOM validation utilities for ensuring data integrity and consistency
|
||||
|
||||
use super::loader::DicomData;
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
|
||||
/// Series validation report
|
||||
#[derive(Debug)]
|
||||
pub struct SeriesValidationReport {
|
||||
/// Whether the series is valid
|
||||
pub is_valid: bool,
|
||||
/// Warning messages (non-critical issues)
|
||||
pub warnings: Vec<String>,
|
||||
/// Error messages (critical issues)
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// File validation report
|
||||
#[derive(Debug)]
|
||||
pub struct FileValidationReport {
|
||||
/// Whether the file is valid
|
||||
pub is_valid: bool,
|
||||
/// Warning messages (non-critical issues)
|
||||
pub warnings: Vec<String>,
|
||||
/// Error messages (critical issues)
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// DICOM validation utilities
|
||||
pub struct DicomValidator;
|
||||
|
||||
impl DicomValidator {
|
||||
/// Validate DICOM series consistency
|
||||
pub fn validate_series(dicom_files: &[DicomData]) -> MedicalResult<SeriesValidationReport> {
|
||||
if dicom_files.is_empty() {
|
||||
return Err(MedicalError::Dicom(
|
||||
"No DICOM files to validate".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut report = SeriesValidationReport {
|
||||
is_valid: true,
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
};
|
||||
|
||||
// Check series UID consistency
|
||||
let first_series_uid = dicom_files[0].get_string(0x0020, 0x000E);
|
||||
for (i, file) in dicom_files.iter().enumerate().skip(1) {
|
||||
if file.get_string(0x0020, 0x000E) != first_series_uid {
|
||||
report
|
||||
.errors
|
||||
.push(format!("File {} has different Series UID", i));
|
||||
report.is_valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check image dimensions consistency
|
||||
let first_rows = dicom_files[0].get_uint16(0x0028, 0x0010);
|
||||
let first_cols = dicom_files[0].get_uint16(0x0028, 0x0011);
|
||||
|
||||
for (i, file) in dicom_files.iter().enumerate().skip(1) {
|
||||
if file.get_uint16(0x0028, 0x0010) != first_rows
|
||||
|| file.get_uint16(0x0028, 0x0011) != first_cols
|
||||
{
|
||||
report
|
||||
.errors
|
||||
.push(format!("File {} has different image dimensions", i));
|
||||
report.is_valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing slice locations
|
||||
let slice_locations: Vec<Option<f64>> = dicom_files
|
||||
.iter()
|
||||
.map(|f| f.get_float64(0x0020, 0x1041))
|
||||
.collect();
|
||||
|
||||
if slice_locations.iter().any(|loc| loc.is_none()) {
|
||||
report
|
||||
.warnings
|
||||
.push("Some slices missing slice location".to_string());
|
||||
}
|
||||
|
||||
// Check for gaps in slice locations
|
||||
let mut actual_locations: Vec<f64> =
|
||||
slice_locations.iter().filter_map(|&loc| loc).collect();
|
||||
actual_locations.sort_by(|a, b| a.total_cmp(b));
|
||||
|
||||
if actual_locations.len() > 1 {
|
||||
let spacing_variations: Vec<f64> = actual_locations
|
||||
.windows(2)
|
||||
.map(|pair| pair[1] - pair[0])
|
||||
.collect();
|
||||
|
||||
let mean_spacing =
|
||||
spacing_variations.iter().sum::<f64>() / spacing_variations.len() as f64;
|
||||
let max_variation = spacing_variations
|
||||
.iter()
|
||||
.map(|&s| (s - mean_spacing).abs())
|
||||
.fold(0.0, f64::max);
|
||||
|
||||
if max_variation > 0.1 {
|
||||
// 0.1mm tolerance
|
||||
report.warnings.push(format!(
|
||||
"Irregular slice spacing (max variation: {:.2}mm)",
|
||||
max_variation
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Validate individual DICOM file
|
||||
pub fn validate_file(dicom_data: &DicomData) -> MedicalResult<FileValidationReport> {
|
||||
let mut report = FileValidationReport {
|
||||
is_valid: true,
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
};
|
||||
|
||||
// Check required string tags
|
||||
let required_string_tags = [
|
||||
(0x0008, 0x0016, "SOP Class UID"),
|
||||
(0x0008, 0x0018, "SOP Instance UID"),
|
||||
(0x0020, 0x000D, "Study Instance UID"),
|
||||
(0x0020, 0x000E, "Series Instance UID"),
|
||||
];
|
||||
|
||||
for &(group, element, name) in &required_string_tags {
|
||||
if dicom_data.get_string(group, element).is_none() {
|
||||
report
|
||||
.errors
|
||||
.push(format!("Missing required tag: {}", name));
|
||||
report.is_valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check required numeric tags (Rows and Columns are UInt16)
|
||||
if dicom_data.get_uint16(0x0028, 0x0010).is_none() {
|
||||
report.errors.push("Missing required tag: Rows".to_string());
|
||||
report.is_valid = false;
|
||||
}
|
||||
if dicom_data.get_uint16(0x0028, 0x0011).is_none() {
|
||||
report
|
||||
.errors
|
||||
.push("Missing required tag: Columns".to_string());
|
||||
report.is_valid = false;
|
||||
}
|
||||
|
||||
// Check pixel data
|
||||
if dicom_data.get_bytes(0x7FE0, 0x0010).is_none() {
|
||||
report.errors.push("Missing pixel data".to_string());
|
||||
report.is_valid = false;
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dicom::parser::{DicomElement, DicomTag};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn create_test_dicom_data(
|
||||
series_uid: &str,
|
||||
rows: u16,
|
||||
cols: u16,
|
||||
slice_location: Option<f64>,
|
||||
has_pixel_data: bool,
|
||||
) -> DicomData {
|
||||
let mut elements = HashMap::new();
|
||||
|
||||
// Required tags
|
||||
elements.insert(
|
||||
DicomTag::new(0x0008, 0x0016),
|
||||
DicomElement::String("1.2.840.10008.5.1.4.1.1.2".to_string()), // SOP Class UID
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag::new(0x0008, 0x0018),
|
||||
DicomElement::String("1.2.3.4.5.6.7".to_string()), // SOP Instance UID
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag::new(0x0020, 0x000D),
|
||||
DicomElement::String("1.2.3.4.5".to_string()), // Study UID
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag::new(0x0020, 0x000E),
|
||||
DicomElement::String(series_uid.to_string()), // Series UID
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag::new(0x0028, 0x0010),
|
||||
DicomElement::UInt16(rows), // Rows
|
||||
);
|
||||
elements.insert(
|
||||
DicomTag::new(0x0028, 0x0011),
|
||||
DicomElement::UInt16(cols), // Columns
|
||||
);
|
||||
|
||||
// Optional slice location
|
||||
if let Some(loc) = slice_location {
|
||||
elements.insert(DicomTag::new(0x0020, 0x1041), DicomElement::Float64(loc));
|
||||
}
|
||||
|
||||
// Pixel data
|
||||
if has_pixel_data {
|
||||
elements.insert(
|
||||
DicomTag::new(0x7FE0, 0x0010),
|
||||
DicomElement::Bytes(vec![0u8; 100]),
|
||||
);
|
||||
}
|
||||
|
||||
DicomData {
|
||||
file_path: "test.dcm".to_string(),
|
||||
elements,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_series_empty_returns_error() {
|
||||
let result = DicomValidator::validate_series(&[]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_series_consistent() {
|
||||
let files = vec![
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(11.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(12.0), true),
|
||||
];
|
||||
|
||||
let report = DicomValidator::validate_series(&files).unwrap();
|
||||
assert!(report.is_valid);
|
||||
assert!(report.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_series_different_series_uid() {
|
||||
let files = vec![
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.7", 512, 512, Some(11.0), true),
|
||||
];
|
||||
|
||||
let report = DicomValidator::validate_series(&files).unwrap();
|
||||
assert!(!report.is_valid);
|
||||
assert!(!report.errors.is_empty());
|
||||
assert!(report.errors[0].contains("different Series UID"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_series_different_dimensions() {
|
||||
let files = vec![
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.6", 256, 256, Some(11.0), true),
|
||||
];
|
||||
|
||||
let report = DicomValidator::validate_series(&files).unwrap();
|
||||
assert!(!report.is_valid);
|
||||
assert!(!report.errors.is_empty());
|
||||
assert!(report.errors[0].contains("different image dimensions"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_series_missing_slice_location_warns() {
|
||||
let files = vec![
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, None, true),
|
||||
];
|
||||
|
||||
let report = DicomValidator::validate_series(&files).unwrap();
|
||||
assert!(report.is_valid); // Still valid, but with warnings
|
||||
assert!(!report.warnings.is_empty());
|
||||
assert!(report.warnings[0].contains("missing slice location"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_series_irregular_spacing() {
|
||||
let files = vec![
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(11.0), true),
|
||||
create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(12.5), true), // Irregular spacing
|
||||
];
|
||||
|
||||
let report = DicomValidator::validate_series(&files).unwrap();
|
||||
assert!(report.is_valid);
|
||||
assert!(!report.warnings.is_empty());
|
||||
assert!(
|
||||
report
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("Irregular slice spacing"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_file_valid() {
|
||||
let data = create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), true);
|
||||
let report = DicomValidator::validate_file(&data).unwrap();
|
||||
assert!(report.is_valid);
|
||||
assert!(report.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_file_missing_pixel_data() {
|
||||
let data = create_test_dicom_data("1.2.3.4.5.6", 512, 512, Some(10.0), false);
|
||||
let report = DicomValidator::validate_file(&data).unwrap();
|
||||
assert!(!report.is_valid);
|
||||
assert!(
|
||||
report
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| e.contains("Missing pixel data"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_validation_report_structure() {
|
||||
let report = SeriesValidationReport {
|
||||
is_valid: true,
|
||||
warnings: vec!["test warning".to_string()],
|
||||
errors: vec![],
|
||||
};
|
||||
assert!(report.is_valid);
|
||||
assert_eq!(report.warnings.len(), 1);
|
||||
assert_eq!(report.errors.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_validation_report_structure() {
|
||||
let report = FileValidationReport {
|
||||
is_valid: false,
|
||||
warnings: vec![],
|
||||
errors: vec!["test error".to_string()],
|
||||
};
|
||||
assert!(!report.is_valid);
|
||||
assert_eq!(report.warnings.len(), 0);
|
||||
assert_eq!(report.errors.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user