Marwan Alwali ab2c4a36c5 update
2025-10-02 10:13:03 +03:00

353 lines
14 KiB
Python

"""
Django management command for DICOM utilities and operations.
"""
import os
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from pydicom import dcmread
from pydicom.uid import generate_uid
from radiology.models import DICOMImage, ImagingSeries, ImagingStudy
from radiology.services import DICOMGenerator, DICOMValidator, DICOMUtilities
class Command(BaseCommand):
help = 'DICOM utilities and operations'
def add_arguments(self, parser):
parser.add_argument(
'--inspect-file',
type=str,
help='Inspect a DICOM file and show metadata'
)
parser.add_argument(
'--import-dicom',
type=str,
help='Import existing DICOM file into database'
)
parser.add_argument(
'--export-metadata',
type=str,
help='Export DICOM metadata to JSON file'
)
parser.add_argument(
'--generate-uids',
type=int,
help='Generate specified number of DICOM UIDs'
)
parser.add_argument(
'--cleanup-orphaned',
action='store_true',
help='Clean up orphaned DICOM files'
)
parser.add_argument(
'--verify-integrity',
action='store_true',
help='Verify integrity of all DICOM files'
)
def handle(self, *args, **options):
try:
if options['inspect_file']:
self._inspect_dicom_file(options['inspect_file'])
elif options['import_dicom']:
self._import_dicom_file(options['import_dicom'])
elif options['export_metadata']:
self._export_metadata(options['export_metadata'])
elif options['generate_uids']:
self._generate_uids(options['generate_uids'])
elif options['cleanup_orphaned']:
self._cleanup_orphaned_files()
elif options['verify_integrity']:
self._verify_integrity()
else:
self.stdout.write("Please specify an operation. Use --help for options.")
except Exception as e:
raise CommandError(f'Error in DICOM utilities: {str(e)}')
def _inspect_dicom_file(self, file_path):
"""Inspect a DICOM file and display metadata."""
if not os.path.exists(file_path):
raise CommandError(f'File not found: {file_path}')
self.stdout.write(f"Inspecting DICOM file: {file_path}")
self.stdout.write("-" * 60)
try:
ds = dcmread(file_path)
# Basic information
self.stdout.write(f"Patient Name: {getattr(ds, 'PatientName', 'N/A')}")
self.stdout.write(f"Patient ID: {getattr(ds, 'PatientID', 'N/A')}")
self.stdout.write(f"Study Date: {getattr(ds, 'StudyDate', 'N/A')}")
self.stdout.write(f"Study Description: {getattr(ds, 'StudyDescription', 'N/A')}")
self.stdout.write(f"Modality: {getattr(ds, 'Modality', 'N/A')}")
self.stdout.write(f"Series Number: {getattr(ds, 'SeriesNumber', 'N/A')}")
self.stdout.write(f"Instance Number: {getattr(ds, 'InstanceNumber', 'N/A')}")
# Image dimensions
self.stdout.write(f"Rows: {getattr(ds, 'Rows', 'N/A')}")
self.stdout.write(f"Columns: {getattr(ds, 'Columns', 'N/A')}")
self.stdout.write(f"Bits Allocated: {getattr(ds, 'BitsAllocated', 'N/A')}")
self.stdout.write(f"Bits Stored: {getattr(ds, 'BitsStored', 'N/A')}")
# UIDs
self.stdout.write(f"Study Instance UID: {getattr(ds, 'StudyInstanceUID', 'N/A')}")
self.stdout.write(f"Series Instance UID: {getattr(ds, 'SeriesInstanceUID', 'N/A')}")
self.stdout.write(f"SOP Instance UID: {getattr(ds, 'SOPInstanceUID', 'N/A')}")
self.stdout.write(f"SOP Class UID: {getattr(ds, 'SOPClassUID', 'N/A')}")
# File information
file_size = os.path.getsize(file_path)
self.stdout.write(f"File Size: {file_size} bytes ({file_size / (1024*1024):.2f} MB)")
# Validation
validation = DICOMValidator.validate_dicom_file(file_path)
if validation['valid']:
self.stdout.write(self.style.SUCCESS("✓ File is valid DICOM"))
else:
self.stdout.write(self.style.ERROR(f"✗ File validation failed: {validation['errors']}"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error reading DICOM file: {str(e)}"))
def _import_dicom_file(self, file_path):
"""Import existing DICOM file into database."""
if not os.path.exists(file_path):
raise CommandError(f'File not found: {file_path}')
self.stdout.write(f"Importing DICOM file: {file_path}")
try:
ds = dcmread(file_path)
# Extract metadata
study_uid = str(ds.StudyInstanceUID)
series_uid = str(ds.SeriesInstanceUID)
sop_uid = str(ds.SOPInstanceUID)
# Check if image already exists
if DICOMImage.objects.filter(sop_instance_uid=sop_uid).exists():
self.stdout.write(f"DICOM image already exists: {sop_uid}")
return
# Find or create study
try:
study = ImagingStudy.objects.get(study_instance_uid=study_uid)
except ImagingStudy.DoesNotExist:
self.stdout.write(f"Study not found in database: {study_uid}")
return
# Find or create series
series, created = ImagingSeries.objects.get_or_create(
series_instance_uid=series_uid,
defaults={
'study': study,
'series_number': int(getattr(ds, 'SeriesNumber', 1)),
'modality': str(getattr(ds, 'Modality', 'CT')),
'series_description': str(getattr(ds, 'SeriesDescription', '')),
'series_datetime': timezone.now(),
}
)
if created:
self.stdout.write(f"Created new series: {series_uid}")
# Create DICOM image record
dicom_image = DICOMImage.objects.create(
series=series,
sop_instance_uid=sop_uid,
instance_number=int(getattr(ds, 'InstanceNumber', 1)),
sop_class_uid=str(getattr(ds, 'SOPClassUID', '')),
rows=int(getattr(ds, 'Rows', 512)),
columns=int(getattr(ds, 'Columns', 512)),
bits_allocated=int(getattr(ds, 'BitsAllocated', 16)),
bits_stored=int(getattr(ds, 'BitsStored', 16)),
file_path=file_path,
file_size=os.path.getsize(file_path),
window_center=float(getattr(ds, 'WindowCenter', 0)) if hasattr(ds, 'WindowCenter') else None,
window_width=float(getattr(ds, 'WindowWidth', 0)) if hasattr(ds, 'WindowWidth') else None,
)
self.stdout.write(
self.style.SUCCESS(f"Successfully imported DICOM image: {sop_uid}")
)
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error importing DICOM file: {str(e)}"))
def _export_metadata(self, output_file):
"""Export DICOM metadata to JSON file."""
import json
self.stdout.write("Exporting DICOM metadata...")
metadata = []
for image in DICOMImage.objects.all():
image_data = {
'image_id': str(image.image_id),
'sop_instance_uid': image.sop_instance_uid,
'instance_number': image.instance_number,
'rows': image.rows,
'columns': image.columns,
'bits_allocated': image.bits_allocated,
'file_path': image.file_path,
'file_size': image.file_size,
'series': {
'series_id': str(image.series.series_id),
'series_instance_uid': image.series.series_instance_uid,
'series_number': image.series.series_number,
'modality': image.series.modality,
'series_description': image.series.series_description,
},
'study': {
'study_id': str(image.study.study_id),
'study_instance_uid': image.study.study_instance_uid,
'accession_number': image.study.accession_number,
'study_description': image.study.study_description,
},
'patient': {
'patient_id': image.patient.patient_id,
'first_name': image.patient.first_name,
'last_name': image.patient.last_name,
}
}
metadata.append(image_data)
with open(output_file, 'w') as f:
json.dump(metadata, f, indent=2, default=str)
self.stdout.write(
self.style.SUCCESS(f"Exported metadata for {len(metadata)} images to {output_file}")
)
def _generate_uids(self, count):
"""Generate DICOM UIDs."""
self.stdout.write(f"Generating {count} DICOM UIDs:")
self.stdout.write("-" * 60)
for i in range(count):
uid = generate_uid()
self.stdout.write(f"{i+1:3d}: {uid}")
def _cleanup_orphaned_files(self):
"""Clean up orphaned DICOM files."""
self.stdout.write("Cleaning up orphaned DICOM files...")
generator = DICOMGenerator()
dicom_dir = generator.dicom_storage_path
if not os.path.exists(dicom_dir):
self.stdout.write("DICOM storage directory does not exist.")
return
# Get all file paths from database
db_file_paths = set(
DICOMImage.objects.filter(file_path__isnull=False)
.exclude(file_path='')
.values_list('file_path', flat=True)
)
# Find all DICOM files on disk
disk_files = []
for root, dirs, files in os.walk(dicom_dir):
for file in files:
if file.endswith('.dcm'):
disk_files.append(os.path.join(root, file))
# Find orphaned files
orphaned_files = [f for f in disk_files if f not in db_file_paths]
if not orphaned_files:
self.stdout.write("No orphaned DICOM files found.")
return
self.stdout.write(f"Found {len(orphaned_files)} orphaned files:")
for file_path in orphaned_files:
self.stdout.write(f" {file_path}")
# Ask for confirmation
confirm = input("Delete these files? (y/N): ")
if confirm.lower() == 'y':
deleted_count = 0
for file_path in orphaned_files:
try:
os.remove(file_path)
deleted_count += 1
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error deleting {file_path}: {str(e)}"))
self.stdout.write(
self.style.SUCCESS(f"Deleted {deleted_count} orphaned files")
)
else:
self.stdout.write("Cleanup cancelled.")
def _verify_integrity(self):
"""Verify integrity of all DICOM files."""
self.stdout.write("Verifying integrity of all DICOM files...")
images = DICOMImage.objects.all()
total_images = images.count()
if total_images == 0:
self.stdout.write("No DICOM images found.")
return
valid_count = 0
invalid_count = 0
missing_count = 0
for image in images:
if not image.file_path:
missing_count += 1
self.stdout.write(f"⚠ No file path: {image.sop_instance_uid}")
continue
if not image.has_dicom_file():
missing_count += 1
self.stdout.write(f"⚠ Missing file: {image.file_path}")
continue
# Validate file
validation = DICOMValidator.validate_dicom_file(image.file_path)
if validation['valid']:
valid_count += 1
# Check if metadata matches
if validation['sop_instance_uid'] != image.sop_instance_uid:
self.stdout.write(
self.style.WARNING(
f"⚠ UID mismatch: {image.sop_instance_uid} vs {validation['sop_instance_uid']}"
)
)
if validation['file_size'] != image.file_size:
self.stdout.write(
self.style.WARNING(
f"⚠ Size mismatch: {image.file_size} vs {validation['file_size']}"
)
)
else:
invalid_count += 1
self.stdout.write(
self.style.ERROR(
f"✗ Invalid: {image.sop_instance_uid} - {validation['errors']}"
)
)
self.stdout.write("-" * 60)
self.stdout.write(f"Total images: {total_images}")
self.stdout.write(f"Valid files: {valid_count}")
self.stdout.write(f"Invalid files: {invalid_count}")
self.stdout.write(f"Missing files: {missing_count}")
if invalid_count == 0 and missing_count == 0:
self.stdout.write(self.style.SUCCESS("✓ All DICOM files are valid and present"))
else:
self.stdout.write(
self.style.WARNING(f"⚠ Found {invalid_count + missing_count} issues")
)