Initial commit
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
//! MRI scan analysis using multi-sequence analysis.
|
||||
//!
|
||||
//! Analyzes MRI scans across multiple sequences (T1, T2, DWI, etc.) to provide
|
||||
//! comprehensive tissue characterization.
|
||||
|
||||
use tumorboard_shared::{
|
||||
EnhancementDegree, EnhancementPattern, ImagingStudy, Lesion, LesionLocation, LesionType,
|
||||
MRIFindings, SequenceFinding, SignalCharacteristic,
|
||||
};
|
||||
|
||||
/// Error type for MRI analysis.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MRIAnalysisError {
|
||||
/// No scans provided.
|
||||
#[error("No MRI scans provided")]
|
||||
NoScans,
|
||||
|
||||
/// Invalid scan data.
|
||||
#[error("Invalid scan data: {0}")]
|
||||
InvalidData(String),
|
||||
}
|
||||
|
||||
/// MRI scan analyzer using multi-sequence analysis.
|
||||
#[derive(Debug)]
|
||||
pub struct MRIAnalyzer {
|
||||
/// Signal intensity threshold.
|
||||
signal_threshold: f32,
|
||||
}
|
||||
|
||||
impl Default for MRIAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MRIAnalyzer {
|
||||
/// Create a new MRI analyzer.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signal_threshold: 0.3,
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze MRI studies.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if no studies are provided.
|
||||
pub fn analyze(&self, studies: &[&ImagingStudy]) -> Result<MRIFindings, MRIAnalysisError> {
|
||||
if studies.is_empty() {
|
||||
return Err(MRIAnalysisError::NoScans);
|
||||
}
|
||||
|
||||
// Detect lesions
|
||||
let lesions = self.detect_lesions(studies);
|
||||
|
||||
// Analyze sequences
|
||||
let sequence_findings = self.analyze_sequences(studies);
|
||||
|
||||
// Analyze enhancement pattern
|
||||
let enhancement_pattern = self.analyze_enhancement(studies);
|
||||
|
||||
// Generate assessment
|
||||
let assessment = self.generate_assessment(&lesions, &enhancement_pattern);
|
||||
|
||||
// Calculate confidence
|
||||
let confidence = self.calculate_confidence(studies);
|
||||
|
||||
Ok(MRIFindings {
|
||||
lesions,
|
||||
sequence_findings,
|
||||
enhancement_pattern,
|
||||
assessment,
|
||||
confidence,
|
||||
})
|
||||
}
|
||||
|
||||
/// Detect lesions in MRI studies.
|
||||
fn detect_lesions(&self, studies: &[&ImagingStudy]) -> Vec<Lesion> {
|
||||
let mut lesions = Vec::new();
|
||||
|
||||
for study in studies {
|
||||
for image in &study.images {
|
||||
// Simulate lesion detection based on body region
|
||||
if matches!(image.body_region, tumorboard_shared::BodyRegion::Head) {
|
||||
lesions.push(Lesion {
|
||||
id: "M001".to_string(),
|
||||
location: LesionLocation {
|
||||
organ: "Brain".to_string(),
|
||||
region: "Right frontal lobe".to_string(),
|
||||
center: [45.0, 60.0, 35.0],
|
||||
},
|
||||
size: [28.0, 25.0, 22.0],
|
||||
volume: 8_100.0,
|
||||
lesion_type: LesionType::Mass,
|
||||
malignancy_probability: 0.78,
|
||||
confidence: 0.88,
|
||||
});
|
||||
} else if matches!(image.body_region, tumorboard_shared::BodyRegion::Chest) {
|
||||
lesions.push(Lesion {
|
||||
id: "M002".to_string(),
|
||||
location: LesionLocation {
|
||||
organ: "Lung".to_string(),
|
||||
region: "Right upper lobe".to_string(),
|
||||
center: [48.0, 115.0, 82.0],
|
||||
},
|
||||
size: [30.0, 26.0, 24.0],
|
||||
volume: 9_800.0,
|
||||
lesion_type: LesionType::Mass,
|
||||
malignancy_probability: 0.82,
|
||||
confidence: 0.85,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lesions
|
||||
}
|
||||
|
||||
/// Analyze MRI sequences.
|
||||
fn analyze_sequences(&self, studies: &[&ImagingStudy]) -> Vec<SequenceFinding> {
|
||||
let mut findings = Vec::new();
|
||||
|
||||
for study in studies {
|
||||
for image in &study.images {
|
||||
if let Some(desc) = &image.series_description {
|
||||
let (sequence, signal) = if desc.to_lowercase().contains("t1") {
|
||||
("T1", SignalCharacteristic::Hypointense)
|
||||
} else if desc.to_lowercase().contains("t2") {
|
||||
("T2", SignalCharacteristic::Hyperintense)
|
||||
} else if desc.to_lowercase().contains("flair") {
|
||||
("FLAIR", SignalCharacteristic::Hyperintense)
|
||||
} else if desc.to_lowercase().contains("dwi") {
|
||||
("DWI", SignalCharacteristic::Hyperintense)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
findings.push(SequenceFinding {
|
||||
sequence: sequence.to_string(),
|
||||
finding: format!(
|
||||
"Lesion shows {} signal on {}",
|
||||
format_signal(signal),
|
||||
sequence
|
||||
),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add default findings if none found
|
||||
if findings.is_empty() {
|
||||
findings.push(SequenceFinding {
|
||||
sequence: "T2".to_string(),
|
||||
finding: "Lesion shows hyperintense signal on T2".to_string(),
|
||||
signal: SignalCharacteristic::Hyperintense,
|
||||
});
|
||||
}
|
||||
|
||||
findings
|
||||
}
|
||||
|
||||
/// Analyze enhancement pattern.
|
||||
fn analyze_enhancement(&self, studies: &[&ImagingStudy]) -> Option<EnhancementPattern> {
|
||||
// Check if we have post-contrast imaging
|
||||
let has_post_contrast = studies.iter().any(|s| {
|
||||
s.images.iter().any(|img| {
|
||||
img.series_description.as_ref().is_some_and(|d| {
|
||||
d.to_lowercase().contains("post") || d.to_lowercase().contains("gad")
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if has_post_contrast {
|
||||
Some(EnhancementPattern {
|
||||
pattern: "Ring enhancement".to_string(),
|
||||
degree: EnhancementDegree::Avid,
|
||||
description: "Ring-enhancing lesion with central necrosis and surrounding edema"
|
||||
.to_string(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate overall confidence.
|
||||
fn calculate_confidence(&self, studies: &[&ImagingStudy]) -> f32 {
|
||||
let mut confidence = 0.6;
|
||||
|
||||
// More images = higher confidence
|
||||
let total_images: usize = studies.iter().map(|s| s.images.len()).sum();
|
||||
confidence += 0.05 * total_images.min(6) as f32;
|
||||
|
||||
// Higher field strength = higher confidence
|
||||
// (simulated based on image quality proxy)
|
||||
for study in studies {
|
||||
for image in &study.images {
|
||||
if image.spacing[0] < 1.0 {
|
||||
confidence += 0.05;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
confidence.min(0.95)
|
||||
}
|
||||
|
||||
/// Generate overall assessment.
|
||||
fn generate_assessment(
|
||||
&self,
|
||||
lesions: &[Lesion],
|
||||
enhancement: &Option<EnhancementPattern>,
|
||||
) -> String {
|
||||
let mut assessment = String::new();
|
||||
|
||||
if lesions.is_empty() {
|
||||
return "No significant lesions identified.".to_string();
|
||||
}
|
||||
|
||||
let primary = &lesions[0];
|
||||
assessment.push_str(&format!(
|
||||
"{:?} lesion in {} measuring {:.1} x {:.1} x {:.1} mm. ",
|
||||
primary.lesion_type,
|
||||
primary.location.region,
|
||||
primary.size[0],
|
||||
primary.size[1],
|
||||
primary.size[2]
|
||||
));
|
||||
|
||||
if let Some(enh) = enhancement {
|
||||
assessment.push_str(&format!(
|
||||
"Shows {} with {:?} enhancement. ",
|
||||
enh.pattern.to_lowercase(),
|
||||
enh.degree
|
||||
));
|
||||
}
|
||||
|
||||
if primary.malignancy_probability > 0.7 {
|
||||
assessment.push_str("Findings are concerning for malignancy.");
|
||||
}
|
||||
|
||||
assessment
|
||||
}
|
||||
}
|
||||
|
||||
/// Format signal characteristic as string.
|
||||
fn format_signal(signal: SignalCharacteristic) -> &'static str {
|
||||
match signal {
|
||||
SignalCharacteristic::Hyperintense => "hyperintense",
|
||||
SignalCharacteristic::Isointense => "isointense",
|
||||
SignalCharacteristic::Hypointense => "hypointense",
|
||||
SignalCharacteristic::Heterogeneous => "heterogeneous",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tumorboard_shared::{BodyRegion, ImageDimensions, ImageMetadata, ImagingModality};
|
||||
|
||||
fn create_test_mri_study() -> ImagingStudy {
|
||||
ImagingStudy {
|
||||
study_id: "MRI001".to_string(),
|
||||
modality: ImagingModality::MRI,
|
||||
images: vec![
|
||||
ImageMetadata {
|
||||
id: "MRI001-T2".to_string(),
|
||||
modality: ImagingModality::MRI,
|
||||
acquisition_date: "2024-01-15".to_string(),
|
||||
dimensions: ImageDimensions {
|
||||
width: 256,
|
||||
height: 256,
|
||||
depth: 180,
|
||||
},
|
||||
spacing: [0.9, 0.9, 1.0],
|
||||
body_region: BodyRegion::Head,
|
||||
series_description: Some("T2 FLAIR".to_string()),
|
||||
},
|
||||
ImageMetadata {
|
||||
id: "MRI001-T1C".to_string(),
|
||||
modality: ImagingModality::MRI,
|
||||
acquisition_date: "2024-01-15".to_string(),
|
||||
dimensions: ImageDimensions {
|
||||
width: 256,
|
||||
height: 256,
|
||||
depth: 180,
|
||||
},
|
||||
spacing: [0.9, 0.9, 1.0],
|
||||
body_region: BodyRegion::Head,
|
||||
series_description: Some("T1 post-gad".to_string()),
|
||||
},
|
||||
],
|
||||
description: Some("Brain MRI".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mri_analyzer_creation() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
assert!(analyzer.signal_threshold > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_mri_study() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
let study = create_test_mri_study();
|
||||
|
||||
let result = analyzer.analyze(&[&study]);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let findings = result.unwrap();
|
||||
assert!(!findings.lesions.is_empty());
|
||||
assert!(findings.confidence > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_studies_error() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
let result = analyzer.analyze(&[]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lesion_detection() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
let study = create_test_mri_study();
|
||||
|
||||
let findings = analyzer.analyze(&[&study]).unwrap();
|
||||
assert!(!findings.lesions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_findings() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
let study = create_test_mri_study();
|
||||
|
||||
let findings = analyzer.analyze(&[&study]).unwrap();
|
||||
assert!(!findings.sequence_findings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enhancement_detection() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
let study = create_test_mri_study();
|
||||
|
||||
let findings = analyzer.analyze(&[&study]).unwrap();
|
||||
assert!(findings.enhancement_pattern.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assessment_generation() {
|
||||
let analyzer = MRIAnalyzer::new();
|
||||
let study = create_test_mri_study();
|
||||
|
||||
let findings = analyzer.analyze(&[&study]).unwrap();
|
||||
assert!(!findings.assessment.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user