Initial commit
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
//! 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<CTFindings, CTAnalysisError> {
|
||||
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<Lesion> {
|
||||
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<OrganMeasurement> {
|
||||
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<RegionHU> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user