//! CT scan analysis using 3D CNN. //! //! Analyzes CT scans to detect lesions, measure dimensions, and characterize findings. use tumorboard_shared::{ CTFindings, ImagingStudy, Lesion, LesionLocation, LesionType, OrganMeasurement, RegionHU, }; /// Error type for CT analysis. #[derive(Debug, thiserror::Error)] pub enum CTAnalysisError { /// No scans provided. #[error("No CT scans provided")] NoScans, /// Invalid scan data. #[error("Invalid scan data: {0}")] InvalidData(String), } /// CT scan analyzer using simulated 3D CNN. #[derive(Debug)] pub struct CTAnalyzer { /// Detection threshold. detection_threshold: f32, /// Minimum lesion size (mm). min_lesion_size: f32, } impl Default for CTAnalyzer { fn default() -> Self { Self::new() } } impl CTAnalyzer { /// Create a new CT analyzer. #[must_use] pub fn new() -> Self { Self { detection_threshold: 0.5, min_lesion_size: 3.0, } } /// Create with custom thresholds. #[must_use] pub fn with_thresholds(detection_threshold: f32, min_lesion_size: f32) -> Self { Self { detection_threshold, min_lesion_size, } } /// Analyze CT studies. /// /// # Errors /// /// Returns error if no studies are provided or data is invalid. pub fn analyze(&self, studies: &[&ImagingStudy]) -> Result { if studies.is_empty() { return Err(CTAnalysisError::NoScans); } // Detect lesions let lesions = self.detect_lesions(studies); // Get organ measurements let organ_measurements = self.measure_organs(studies); // Analyze Hounsfield units let hu_analysis = self.analyze_hu(studies); // Generate assessment let assessment = self.generate_assessment(&lesions); // Calculate confidence let confidence = self.calculate_confidence(studies, &lesions); Ok(CTFindings { lesions, organ_measurements, hu_analysis, assessment, confidence, }) } /// Detect lesions in the CT studies. fn detect_lesions(&self, studies: &[&ImagingStudy]) -> Vec { let mut lesions = Vec::new(); // Simulate lesion detection based on study properties // In real implementation, this would use a 3D CNN for study in studies { for image in &study.images { // Check body region for lung lesion simulation if matches!(image.body_region, tumorboard_shared::BodyRegion::Chest) { // Primary lung lesion let primary_lesion = Lesion { id: "L001".to_string(), location: LesionLocation { organ: "Lung".to_string(), region: "Right upper lobe".to_string(), center: [45.0, 120.0, 85.0], }, size: [32.0, 28.0, 25.0], volume: 11_700.0, lesion_type: LesionType::Nodule, malignancy_probability: 0.85, confidence: 0.92, }; lesions.push(primary_lesion); // Possible satellite nodule (higher res imaging) if image.spacing[2] <= 2.0 { let satellite = Lesion { id: "L002".to_string(), location: LesionLocation { organ: "Lung".to_string(), region: "Right upper lobe, satellite".to_string(), center: [52.0, 115.0, 82.0], }, size: [8.0, 7.0, 6.0], volume: 175.0, lesion_type: LesionType::Nodule, malignancy_probability: 0.65, confidence: 0.78, }; lesions.push(satellite); } } } } // Filter by detection threshold lesions .into_iter() .filter(|l| l.confidence >= self.detection_threshold) .filter(|l| l.size[0] >= self.min_lesion_size) .collect() } /// Measure organs. fn measure_organs(&self, studies: &[&ImagingStudy]) -> Vec { let mut measurements = Vec::new(); for study in studies { for image in &study.images { if matches!(image.body_region, tumorboard_shared::BodyRegion::Chest) { // Heart measurements measurements.push(OrganMeasurement { organ: "Heart".to_string(), measurement_type: "Cardiothoracic ratio".to_string(), value: 0.48, unit: "ratio".to_string(), normal_range: Some((0.40, 0.50)), is_abnormal: false, }); // Aorta measurements.push(OrganMeasurement { organ: "Aorta".to_string(), measurement_type: "Ascending diameter".to_string(), value: 35.0, unit: "mm".to_string(), normal_range: Some((20.0, 37.0)), is_abnormal: false, }); } } } measurements } /// Analyze Hounsfield units. fn analyze_hu(&self, _studies: &[&ImagingStudy]) -> Vec { vec![ RegionHU { region: "Lesion L001".to_string(), mean: 35.0, std_dev: 12.0, interpretation: "Soft tissue density, consistent with solid mass".to_string(), }, RegionHU { region: "Normal lung".to_string(), mean: -850.0, std_dev: 50.0, interpretation: "Normal aerated lung parenchyma".to_string(), }, ] } /// Calculate overall confidence. fn calculate_confidence(&self, studies: &[&ImagingStudy], lesions: &[Lesion]) -> f32 { let mut confidence: f32 = 0.7; // Better resolution = higher confidence for study in studies { for image in &study.images { if image.spacing[2] <= 1.0 { confidence += 0.15; } else if image.spacing[2] <= 2.0 { confidence += 0.1; } } } // More lesions detected = more confident in analysis if !lesions.is_empty() { confidence += 0.05; } confidence.min(0.98) } /// Generate overall assessment. fn generate_assessment(&self, lesions: &[Lesion]) -> String { if lesions.is_empty() { return "No significant lesions identified.".to_string(); } let primary = &lesions[0]; let mut assessment = format!( "Suspicious {:?} in {} measuring {:.1} x {:.1} x {:.1} mm.", primary.lesion_type, primary.location.region, primary.size[0], primary.size[1], primary.size[2] ); if primary.malignancy_probability > 0.7 { assessment.push_str(" Highly suspicious for malignancy."); } else if primary.malignancy_probability > 0.4 { assessment.push_str(" Intermediate suspicion for malignancy."); } if lesions.len() > 1 { assessment.push_str(&format!( " {} additional nodule(s) identified.", lesions.len() - 1 )); } assessment } } #[cfg(test)] mod tests { use super::*; use tumorboard_shared::{BodyRegion, ImageDimensions, ImageMetadata, ImagingModality}; fn create_test_study() -> ImagingStudy { ImagingStudy { study_id: "CT001".to_string(), modality: ImagingModality::CT, images: vec![ImageMetadata { id: "CT001-001".to_string(), modality: ImagingModality::CT, acquisition_date: "2024-01-15".to_string(), dimensions: ImageDimensions { width: 512, height: 512, depth: 300, }, spacing: [0.7, 0.7, 1.25], body_region: BodyRegion::Chest, series_description: Some("Chest CT".to_string()), }], description: Some("Chest CT with contrast".to_string()), } } #[test] fn test_ct_analyzer_creation() { let analyzer = CTAnalyzer::new(); assert!(analyzer.detection_threshold > 0.0); } #[test] fn test_analyze_single_study() { let analyzer = CTAnalyzer::new(); let study = create_test_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 = CTAnalyzer::new(); let result = analyzer.analyze(&[]); assert!(result.is_err()); } #[test] fn test_lesion_detection() { let analyzer = CTAnalyzer::new(); let study = create_test_study(); let findings = analyzer.analyze(&[&study]).unwrap(); // Should detect the primary lung lesion assert!(!findings.lesions.is_empty()); let primary = &findings.lesions[0]; assert!(primary.location.region.contains("upper lobe")); assert!(primary.malignancy_probability > 0.5); } #[test] fn test_organ_measurements() { let analyzer = CTAnalyzer::new(); let study = create_test_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(!findings.organ_measurements.is_empty()); } #[test] fn test_hu_analysis() { let analyzer = CTAnalyzer::new(); let study = create_test_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(!findings.hu_analysis.is_empty()); } #[test] fn test_custom_thresholds() { let analyzer = CTAnalyzer::with_thresholds(0.9, 10.0); let study = create_test_study(); let findings = analyzer.analyze(&[&study]).unwrap(); // Higher threshold may filter out some lesions // The primary lesion has confidence 0.92, so it should still be detected assert!(!findings.lesions.is_empty()); } #[test] fn test_assessment_generation() { let analyzer = CTAnalyzer::new(); let study = create_test_study(); let findings = analyzer.analyze(&[&study]).unwrap(); assert!(!findings.assessment.is_empty()); assert!(findings.assessment.contains("Suspicious")); } }