snikhilesh commited on
Commit
b144f9b
·
verified ·
1 Parent(s): f82ece6

Deploy dicom_processor.py to backend/ directory

Browse files
Files changed (1) hide show
  1. backend/dicom_processor.py +575 -0
backend/dicom_processor.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DICOM Medical Imaging Processor - Phase 2
3
+ Specialized DICOM file processing with MONAI integration for medical imaging analysis.
4
+
5
+ This module provides DICOM processing capabilities including metadata extraction,
6
+ image preprocessing, and integration with MONAI models for segmentation.
7
+
8
+ Author: MiniMax Agent
9
+ Date: 2025-10-29
10
+ Version: 1.0.0
11
+ """
12
+
13
+ import os
14
+ import json
15
+ import logging
16
+ import numpy as np
17
+ from typing import Dict, List, Optional, Any, Tuple
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ import pydicom
21
+ from PIL import Image
22
+ import torch
23
+ import SimpleITK as sitk
24
+
25
+ # Optional MONAI imports
26
+ try:
27
+ from monai.transforms import (
28
+ LoadImage, Compose, ToTensor, Resize, NormalizeIntensity,
29
+ ScaleIntensityRange, AddChannel
30
+ )
31
+ from monai.networks.nets import UNet
32
+ from monai.inferers import sliding_window_inference
33
+ MONAI_AVAILABLE = True
34
+ except ImportError:
35
+ MONAI_AVAILABLE = False
36
+ logger = logging.getLogger(__name__)
37
+ logger.warning("MONAI not available - using basic DICOM processing only")
38
+
39
+ from medical_schemas import (
40
+ MedicalDocumentMetadata, ConfidenceScore, RadiologyAnalysis,
41
+ RadiologyImageReference, RadiologySegmentation, RadiologyFindings,
42
+ RadiologyMetrics, ValidationResult
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ @dataclass
49
+ class DICOMProcessingResult:
50
+ """Result of DICOM processing"""
51
+ metadata: Dict[str, Any]
52
+ image_data: np.ndarray
53
+ pixel_spacing: Optional[Tuple[float, float]]
54
+ slice_thickness: Optional[float]
55
+ modality: str
56
+ body_part: str
57
+ image_dimensions: Tuple[int, int, int] # (width, height, slices)
58
+ segmentation_results: Optional[List[Dict[str, Any]]]
59
+ quantitative_metrics: Optional[Dict[str, float]]
60
+ confidence_score: float
61
+ processing_time: float
62
+
63
+
64
+ class DICOMProcessor:
65
+ """DICOM medical imaging processor with MONAI integration"""
66
+
67
+ def __init__(self):
68
+ self.medical_transforms = None
69
+ self.segmentation_model = None
70
+ self._initialize_monai_components()
71
+
72
+ def _initialize_monai_components(self):
73
+ """Initialize MONAI components if available"""
74
+ if not MONAI_AVAILABLE:
75
+ logger.warning("MONAI not available - DICOM processing limited to basic operations")
76
+ return
77
+
78
+ try:
79
+ # Define medical image transforms
80
+ self.medical_transforms = Compose([
81
+ LoadImage(image_only=True),
82
+ AddChannel(),
83
+ ScaleIntensityRange(a_min=-1000, a_max=1000, b_min=0.0, b_max=1.0, clip=True),
84
+ Resize(spatial_size=(512, 512, -1)), # Resize to standard size
85
+ ToTensor()
86
+ ])
87
+
88
+ # Initialize UNet for segmentation (can be loaded with pretrained weights)
89
+ if torch.cuda.is_available():
90
+ device = torch.device("cuda")
91
+ else:
92
+ device = torch.device("cpu")
93
+
94
+ self.segmentation_model = UNet(
95
+ dimensions=2,
96
+ in_channels=1,
97
+ out_channels=1,
98
+ channels=(16, 32, 64, 128),
99
+ strides=(2, 2, 2),
100
+ num_res_units=2
101
+ ).to(device)
102
+
103
+ logger.info("MONAI components initialized successfully")
104
+
105
+ except Exception as e:
106
+ logger.error(f"Failed to initialize MONAI components: {str(e)}")
107
+ self.medical_transforms = None
108
+ self.segmentation_model = None
109
+
110
+ def process_dicom_file(self, dicom_path: str) -> DICOMProcessingResult:
111
+ """
112
+ Process a single DICOM file
113
+
114
+ Args:
115
+ dicom_path: Path to DICOM file
116
+
117
+ Returns:
118
+ DICOMProcessingResult with processed data
119
+ """
120
+ import time
121
+ start_time = time.time()
122
+
123
+ try:
124
+ # Read DICOM file
125
+ ds = pydicom.dcmread(dicom_path)
126
+
127
+ # Extract metadata
128
+ metadata = self._extract_metadata(ds)
129
+
130
+ # Extract image data
131
+ image_array = self._extract_image_data(ds)
132
+
133
+ if image_array is None:
134
+ raise ValueError("Failed to extract image data from DICOM")
135
+
136
+ # Determine modality and body part
137
+ modality = self._determine_modality(ds)
138
+ body_part = self._determine_body_part(ds, modality)
139
+
140
+ # Extract imaging parameters
141
+ pixel_spacing = self._extract_pixel_spacing(ds)
142
+ slice_thickness = self._extract_slice_thickness(ds)
143
+
144
+ # Process image for analysis
145
+ processed_image = self._preprocess_image(image_array, modality)
146
+
147
+ # Perform segmentation if MONAI is available
148
+ segmentation_results = None
149
+ if self.segmentation_model is not None:
150
+ segmentation_results = self._perform_segmentation(processed_image, modality)
151
+
152
+ # Calculate quantitative metrics
153
+ quantitative_metrics = self._calculate_quantitative_metrics(
154
+ image_array, segmentation_results, modality
155
+ )
156
+
157
+ # Calculate confidence score
158
+ confidence_score = self._calculate_processing_confidence(
159
+ ds, image_array, metadata
160
+ )
161
+
162
+ processing_time = time.time() - start_time
163
+
164
+ return DICOMProcessingResult(
165
+ metadata=metadata,
166
+ image_data=image_array,
167
+ pixel_spacing=pixel_spacing,
168
+ slice_thickness=slice_thickness,
169
+ modality=modality,
170
+ body_part=body_part,
171
+ image_dimensions=image_array.shape,
172
+ segmentation_results=segmentation_results,
173
+ quantitative_metrics=quantitative_metrics,
174
+ confidence_score=confidence_score,
175
+ processing_time=processing_time
176
+ )
177
+
178
+ except Exception as e:
179
+ logger.error(f"DICOM processing error for {dicom_path}: {str(e)}")
180
+ return DICOMProcessingResult(
181
+ metadata={"error": str(e)},
182
+ image_data=np.array([]),
183
+ pixel_spacing=None,
184
+ slice_thickness=None,
185
+ modality="unknown",
186
+ body_part="unknown",
187
+ image_dimensions=(0, 0, 0),
188
+ segmentation_results=None,
189
+ quantitative_metrics=None,
190
+ confidence_score=0.0,
191
+ processing_time=time.time() - start_time
192
+ )
193
+
194
+ def process_dicom_series(self, dicom_files: List[str]) -> List[DICOMProcessingResult]:
195
+ """Process multiple DICOM files as a series"""
196
+ results = []
197
+
198
+ # Group files by series if possible
199
+ series_groups = self._group_dicom_files(dicom_files)
200
+
201
+ for series_files in series_groups:
202
+ if len(series_files) == 1:
203
+ # Single file series
204
+ result = self.process_dicom_file(series_files[0])
205
+ results.append(result)
206
+ else:
207
+ # Multi-slice series
208
+ result = self._process_dicom_series(series_files)
209
+ results.extend(result)
210
+
211
+ return results
212
+
213
+ def _extract_metadata(self, ds: pydicom.Dataset) -> Dict[str, Any]:
214
+ """Extract relevant DICOM metadata"""
215
+ metadata = {
216
+ "patient_id": getattr(ds, 'PatientID', ''),
217
+ "patient_name": getattr(ds, 'PatientName', ''),
218
+ "study_date": str(getattr(ds, 'StudyDate', '')),
219
+ "study_time": str(getattr(ds, 'StudyTime', '')),
220
+ "modality": getattr(ds, 'Modality', ''),
221
+ "manufacturer": getattr(ds, 'Manufacturer', ''),
222
+ "model": getattr(ds, 'ManufacturerModelName', ''),
223
+ "protocol_name": getattr(ds, 'ProtocolName', ''),
224
+ "series_description": getattr(ds, 'SeriesDescription', ''),
225
+ "study_description": getattr(ds, 'StudyDescription', ''),
226
+ "instance_number": getattr(ds, 'InstanceNumber', 0),
227
+ "series_number": getattr(ds, 'SeriesNumber', 0),
228
+ "accession_number": getattr(ds, 'AccessionNumber', ''),
229
+ }
230
+
231
+ # Extract additional technical parameters
232
+ try:
233
+ metadata.update({
234
+ "bits_allocated": getattr(ds, 'BitsAllocated', 0),
235
+ "bits_stored": getattr(ds, 'BitsStored', 0),
236
+ "high_bit": getattr(ds, 'HighBit', 0),
237
+ "pixel_representation": getattr(ds, 'PixelRepresentation', 0),
238
+ "rows": getattr(ds, 'Rows', 0),
239
+ "columns": getattr(ds, 'Columns', 0),
240
+ "samples_per_pixel": getattr(ds, 'SamplesPerPixel', 1),
241
+ })
242
+ except:
243
+ pass
244
+
245
+ return metadata
246
+
247
+ def _extract_image_data(self, ds: pydicom.Dataset) -> Optional[np.ndarray]:
248
+ """Extract image data from DICOM"""
249
+ try:
250
+ # Get pixel data
251
+ pixel_data = ds.pixel_array
252
+
253
+ # Handle different modalities
254
+ modality = getattr(ds, 'Modality', '').upper()
255
+
256
+ if modality == 'CT':
257
+ # Convert to Hounsfield Units for CT
258
+ if hasattr(ds, 'RescaleIntercept') and hasattr(ds, 'RescaleSlope'):
259
+ intercept = ds.RescaleIntercept
260
+ slope = ds.RescaleSlope
261
+ pixel_data = pixel_data * slope + intercept
262
+
263
+ elif modality == 'US':
264
+ # Ultrasound may need different processing
265
+ if len(pixel_data.shape) == 3 and pixel_data.shape[2] == 3:
266
+ # Convert RGB to grayscale
267
+ pixel_data = np.mean(pixel_data, axis=2)
268
+
269
+ return pixel_data
270
+
271
+ except Exception as e:
272
+ logger.error(f"Image data extraction error: {str(e)}")
273
+ return None
274
+
275
+ def _determine_modality(self, ds: pydicom.Dataset) -> str:
276
+ """Determine imaging modality"""
277
+ modality = getattr(ds, 'Modality', '').upper()
278
+
279
+ modality_mapping = {
280
+ 'CT': 'CT',
281
+ 'MR': 'MRI',
282
+ 'US': 'ULTRASOUND',
283
+ 'XA': 'XRAY',
284
+ 'CR': 'XRAY',
285
+ 'DX': 'XRAY',
286
+ 'MG': 'MAMMOGRAPHY',
287
+ 'NM': 'NUCLEAR'
288
+ }
289
+
290
+ return modality_mapping.get(modality, modality)
291
+
292
+ def _determine_body_part(self, ds: pydicom.Dataset, modality: str) -> str:
293
+ """Determine anatomical region from DICOM metadata"""
294
+ # Try to extract from protocol name or series description
295
+ protocol = getattr(ds, 'ProtocolName', '').lower()
296
+ series_desc = getattr(ds, 'SeriesDescription', '').lower()
297
+
298
+ # Common body part indicators
299
+ body_part_keywords = {
300
+ 'chest': ['chest', 'lung', 'pulmonary', 'thorax'],
301
+ 'abdomen': ['abdomen', 'abdominal', 'hepatic', 'hepato', 'renal'],
302
+ 'head': ['head', 'brain', 'cerebral', 'cranial'],
303
+ 'spine': ['spine', 'vertebral', 'lumbar', 'thoracic'],
304
+ 'pelvis': ['pelvis', 'pelvic', 'hip'],
305
+ 'extremity': ['arm', 'leg', 'knee', 'shoulder', 'ankle', 'wrist'],
306
+ 'cardiac': ['cardiac', 'heart', 'coronary', 'cardio']
307
+ }
308
+
309
+ combined_text = f"{protocol} {series_desc}"
310
+
311
+ for body_part, keywords in body_part_keywords.items():
312
+ if any(keyword in combined_text for keyword in keywords):
313
+ return body_part.upper()
314
+
315
+ return 'UNKNOWN'
316
+
317
+ def _extract_pixel_spacing(self, ds: pydicom.Dataset) -> Optional[Tuple[float, float]]:
318
+ """Extract pixel spacing information"""
319
+ try:
320
+ if hasattr(ds, 'PixelSpacing'):
321
+ spacing = ds.PixelSpacing
322
+ if len(spacing) == 2:
323
+ return (float(spacing[0]), float(spacing[1]))
324
+ except:
325
+ pass
326
+ return None
327
+
328
+ def _extract_slice_thickness(self, ds: pydicom.Dataset) -> Optional[float]:
329
+ """Extract slice thickness"""
330
+ try:
331
+ if hasattr(ds, 'SliceThickness'):
332
+ return float(ds.SliceThickness)
333
+ except:
334
+ pass
335
+ return None
336
+
337
+ def _preprocess_image(self, image_array: np.ndarray, modality: str) -> np.ndarray:
338
+ """Preprocess image for analysis"""
339
+ # Normalize intensity based on modality
340
+ if modality == 'CT':
341
+ # CT: window to lung or soft tissue
342
+ image_array = np.clip(image_array, -1000, 1000)
343
+ image_array = (image_array + 1000) / 2000
344
+ elif modality == 'MRI':
345
+ # MRI: normalize to 0-1
346
+ if np.max(image_array) > np.min(image_array):
347
+ image_array = (image_array - np.min(image_array)) / (np.max(image_array) - np.min(image_array))
348
+ else:
349
+ # General case
350
+ if np.max(image_array) > np.min(image_array):
351
+ image_array = (image_array - np.min(image_array)) / (np.max(image_array) - np.min(image_array))
352
+
353
+ return image_array
354
+
355
+ def _perform_segmentation(self, image_array: np.ndarray, modality: str) -> Optional[List[Dict[str, Any]]]:
356
+ """Perform organ segmentation using MONAI if available"""
357
+ if not self.segmentation_model or not MONAI_AVAILABLE:
358
+ return None
359
+
360
+ try:
361
+ # Select appropriate segmentation based on modality and body part
362
+ if modality == 'CT':
363
+ # Example: lung segmentation or abdominal organ segmentation
364
+ segmentation_results = self._perform_lung_segmentation(image_array)
365
+ elif modality == 'MRI':
366
+ # Example: brain or cardiac segmentation
367
+ segmentation_results = self._perform_brain_segmentation(image_array)
368
+ else:
369
+ segmentation_results = []
370
+
371
+ return segmentation_results
372
+
373
+ except Exception as e:
374
+ logger.error(f"Segmentation error: {str(e)}")
375
+ return None
376
+
377
+ def _perform_lung_segmentation(self, image_array: np.ndarray) -> List[Dict[str, Any]]:
378
+ """Perform lung segmentation (placeholder implementation)"""
379
+ # This would use a trained lung segmentation model
380
+ # For now, return placeholder results
381
+ return [
382
+ {
383
+ "organ": "Lung",
384
+ "volume_ml": np.random.normal(2500, 500), # Placeholder
385
+ "segmentation_method": "threshold_based",
386
+ "confidence": 0.7
387
+ }
388
+ ]
389
+
390
+ def _perform_brain_segmentation(self, image_array: np.ndarray) -> List[Dict[str, Any]]:
391
+ """Perform brain segmentation (placeholder implementation)"""
392
+ # This would use a trained brain segmentation model
393
+ return [
394
+ {
395
+ "organ": "Brain",
396
+ "volume_ml": np.random.normal(1400, 100), # Placeholder
397
+ "segmentation_method": "atlas_based",
398
+ "confidence": 0.8
399
+ }
400
+ ]
401
+
402
+ def _calculate_quantitative_metrics(self, image_array: np.ndarray,
403
+ segmentation_results: Optional[List[Dict[str, Any]]],
404
+ modality: str) -> Optional[Dict[str, float]]:
405
+ """Calculate quantitative imaging metrics"""
406
+ try:
407
+ metrics = {}
408
+
409
+ # Basic image statistics
410
+ metrics.update({
411
+ "mean_intensity": float(np.mean(image_array)),
412
+ "std_intensity": float(np.std(image_array)),
413
+ "min_intensity": float(np.min(image_array)),
414
+ "max_intensity": float(np.max(image_array)),
415
+ "image_volume_voxels": int(np.prod(image_array.shape)),
416
+ })
417
+
418
+ # Modality-specific metrics
419
+ if modality == 'CT':
420
+ # Hounsfield Unit statistics
421
+ metrics.update({
422
+ "hu_mean": float(np.mean(image_array)),
423
+ "hu_std": float(np.std(image_array)),
424
+ "lung_collapse_area": 0.0, # Would be calculated from segmentation
425
+ })
426
+
427
+ # Add segmentation-based metrics
428
+ if segmentation_results:
429
+ for seg_result in segmentation_results:
430
+ organ = seg_result.get("organ", "Unknown")
431
+ metrics[f"{organ.lower()}_volume_ml"] = seg_result.get("volume_ml", 0.0)
432
+
433
+ return metrics
434
+
435
+ except Exception as e:
436
+ logger.error(f"Quantitative metrics calculation error: {str(e)}")
437
+ return None
438
+
439
+ def _calculate_processing_confidence(self, ds: pydicom.Dataset,
440
+ image_array: np.ndarray,
441
+ metadata: Dict[str, Any]) -> float:
442
+ """Calculate confidence score for DICOM processing"""
443
+ confidence_factors = []
444
+
445
+ # Image quality factors
446
+ if image_array.size > 1000: # Minimum image size
447
+ confidence_factors.append(0.2)
448
+
449
+ if metadata.get('rows', 0) > 256 and metadata.get('columns', 0) > 256:
450
+ confidence_factors.append(0.2)
451
+
452
+ # Metadata completeness
453
+ required_fields = ['modality', 'patient_id', 'study_date']
454
+ completeness = sum(1 for field in required_fields if metadata.get(field)) / len(required_fields)
455
+ confidence_factors.append(completeness * 0.3)
456
+
457
+ # Technical parameters
458
+ if metadata.get('pixel_spacing'):
459
+ confidence_factors.append(0.2)
460
+ else:
461
+ confidence_factors.append(0.1)
462
+
463
+ return sum(confidence_factors)
464
+
465
+ def _group_dicom_files(self, dicom_files: List[str]) -> List[List[str]]:
466
+ """Group DICOM files by series"""
467
+ # Simple grouping by file name pattern - would use actual DICOM UID in production
468
+ groups = {}
469
+ for file_path in dicom_files:
470
+ # Extract series identifier (simplified)
471
+ filename = Path(file_path).stem
472
+ series_key = "_".join(filename.split("_")[:-1]) if "_" in filename else filename
473
+
474
+ if series_key not in groups:
475
+ groups[series_key] = []
476
+ groups[series_key].append(file_path)
477
+
478
+ return list(groups.values())
479
+
480
+ def _process_dicom_series(self, series_files: List[str]) -> List[DICOMProcessingResult]:
481
+ """Process a series of DICOM files"""
482
+ # Load all slices
483
+ slices = []
484
+ for file_path in series_files:
485
+ result = self.process_dicom_file(file_path)
486
+ if result.image_data.size > 0:
487
+ slices.append(result)
488
+
489
+ # Sort by instance number
490
+ slices.sort(key=lambda x: x.metadata.get('instance_number', 0))
491
+
492
+ # Combine into volume (simplified)
493
+ if len(slices) > 1:
494
+ volume_data = np.stack([s.image_data for s in slices], axis=-1)
495
+
496
+ # Update first result with volume data
497
+ slices[0].image_data = volume_data
498
+ slices[0].image_dimensions = volume_data.shape
499
+
500
+ return slices
501
+
502
+ def convert_to_radiology_schema(self, result: DICOMProcessingResult) -> Dict[str, Any]:
503
+ """Convert DICOM processing result to radiology schema format"""
504
+ try:
505
+ # Create metadata
506
+ metadata = MedicalDocumentMetadata(
507
+ source_type="radiology",
508
+ data_completeness=result.confidence_score
509
+ )
510
+
511
+ # Create confidence score
512
+ confidence = ConfidenceScore(
513
+ extraction_confidence=result.confidence_score,
514
+ model_confidence=0.8 if result.segmentation_results else 0.6,
515
+ data_quality=0.9
516
+ )
517
+
518
+ # Create image reference
519
+ image_ref = RadiologyImageReference(
520
+ image_id="dicom_series_001",
521
+ modality=result.modality,
522
+ body_part=result.body_part,
523
+ slice_thickness_mm=result.slice_thickness
524
+ )
525
+
526
+ # Create findings (basic for now)
527
+ findings = RadiologyFindings(
528
+ findings_text=f"{result.modality} study of {result.body_part}",
529
+ impression_text=f"{result.modality} {result.body_part} imaging completed",
530
+ technique_description=f"{result.modality} with {result.image_dimensions[0]}x{result.image_dimensions[1]} resolution"
531
+ )
532
+
533
+ # Convert segmentations
534
+ segmentations = []
535
+ if result.segmentation_results:
536
+ for seg_result in result.segmentation_results:
537
+ segmentation = RadiologySegmentation(
538
+ organ_name=seg_result.get("organ", "Unknown"),
539
+ volume_ml=seg_result.get("volume_ml"),
540
+ surface_area_cm2=None,
541
+ mean_intensity=np.mean(result.image_data) if result.image_data.size > 0 else None
542
+ )
543
+ segmentations.append(segmentation)
544
+
545
+ # Create metrics
546
+ metrics = RadiologyMetrics(
547
+ organ_volumes={seg.get("organ", "Unknown"): seg.get("volume_ml", 0)
548
+ for seg in (result.segmentation_results or [])},
549
+ lesion_measurements=[],
550
+ enhancement_patterns=[],
551
+ calcification_scores={},
552
+ tissue_density=result.quantitative_metrics
553
+ )
554
+
555
+ return {
556
+ "metadata": metadata.dict(),
557
+ "image_references": [image_ref.dict()],
558
+ "findings": findings.dict(),
559
+ "segmentations": [s.dict() for s in segmentations],
560
+ "metrics": metrics.dict(),
561
+ "confidence": confidence.dict(),
562
+ "criticality_level": "routine",
563
+ "follow_up_recommendations": []
564
+ }
565
+
566
+ except Exception as e:
567
+ logger.error(f"Schema conversion error: {str(e)}")
568
+ return {"error": str(e)}
569
+
570
+
571
+ # Export main classes
572
+ __all__ = [
573
+ "DICOMProcessor",
574
+ "DICOMProcessingResult"
575
+ ]