import pydicom import numpy as np from PIL import Image import time import uuid from datetime import datetime from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.lib.units import inch import os import zipfile import subprocess import nibabel as nib def convert_dicom_zip_to_nifti(zip_file): """ Takes a uploaded ZIP file containing DICOM files. Runs dcm2niix to convert to NIfTI. Returns list of (sequence_name, PIL Image) tuples. """ SKIP_KEYWORDS = ["localizer", "scout", "loc"] # Step 1: Setup temp directories unique_id = uuid.uuid4().hex tmp_dir = f"/tmp/dicom_{unique_id}" nifti_dir = f"/tmp/nifti_{unique_id}" os.makedirs(tmp_dir, exist_ok=True) os.makedirs(nifti_dir, exist_ok=True) # Step 2: Unzip the uploaded file with zipfile.ZipFile(zip_file.name, 'r') as z: z.extractall(tmp_dir) # Step 3: Run dcm2niix on the extracted folder result = subprocess.run([ "dcm2niix", "-o", nifti_dir, # output directory "-z", "y", # compress output (.nii.gz) "-f", "%p_%s", # filename = protocol name + series number tmp_dir # input directory ], capture_output=True, text=True) print("dcm2niix output:", result.stdout) print("dcm2niix errors:", result.stderr) # Step 4: Read each NIfTI and extract middle slice sequence_images = [] nifti_files = [f for f in os.listdir(nifti_dir) if f.endswith(".nii.gz")] if not nifti_files: print("No NIfTI files generated — check DICOM folder structure") return [] for nifti_file in sorted(nifti_files): sequence_name = nifti_file.replace(".nii.gz", "") # Skip localizer images if any(skip in sequence_name.lower() for skip in SKIP_KEYWORDS): print(f"Skipping localizer: {sequence_name}") continue nifti_path = os.path.join(nifti_dir, nifti_file) try: # Load the 3D volume img = nib.as_closest_canonical(nifti_path) volume = img.get_fdata() # Extract middle axial slice mid = volume.shape[2] // 2 slice_2d = volume[:, :, mid] # Rotate to correct display orientation slice_2d = np.rot90(slice_2d) # Normalize to 0-255 s_min, s_max = slice_2d.min(), slice_2d.max() if s_max - s_min == 0: continue normalized = (slice_2d - s_min) / (s_max - s_min) * 255 image = Image.fromarray( normalized.astype(np.uint8) ).convert("RGB") sequence_images.append((sequence_name, image)) print(f"Loaded sequence: {sequence_name}") except Exception as e: print(f"Could not load {nifti_file}: {e}") continue return sequence_images # def load_dicoms(filepaths): # """ # Accepts a list of DICOM file objects. # Returns a list of PIL Images, one per sequence. # """ # images=[] # for file in filepaths: # dicom = pydicom.dcmread(file.name) # # Extract the pixel array # pixel_array = dicom.pixel_array.astype(float) # # Normalize to 0-255 range # pixel_min = pixel_array.min() # pixel_max = pixel_array.max() # if pixel_max - pixel_min == 0: # continue #to handle sequences not added # normalized = (pixel_array - pixel_min) / (pixel_max - pixel_min) * 255 # # Convert to uint8 RGB image # image = Image.fromarray(normalized.astype(np.uint8)).convert("RGB") # images.append(image) # return images def generate_pdf(report_text): """ Takes report text, returns path to a saved PDF file. """ if not report_text or not report_text.strip(): return None filename = f"brain_mri_report_{int(time.time())}.pdf" path = f"/tmp/{filename}" c = canvas.Canvas(path, pagesize=letter) width, height = letter left_margin = 0.75 * inch top_margin = height - 0.75 * inch line_height = 16 max_width = width - 2 * left_margin # Title c.setFont("Helvetica-Bold", 14) c.drawString(left_margin, top_margin, "Brain MRI Radiology Report") # Date c.setFont("Helvetica", 9) c.drawString(left_margin, top_margin - 16, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}") # Divider line c.line(left_margin, top_margin - 24, width - left_margin, top_margin - 24) y = top_margin - 0.55 * inch # Write report body line by line c.setFont("Helvetica", 11) for paragraph in report_text.split("\n"): words = paragraph.split(" ") line = "" for word in words: test = (line + " " + word).strip() if c.stringWidth(test, "Helvetica", 11) <= max_width: line = test else: if y < 0.75 * inch: # new page if near bottom c.showPage() c.setFont("Helvetica", 11) y = height - 0.75 * inch c.drawString(left_margin, y, line) y -= line_height line = word # Draw remaining line if y < 0.75 * inch: c.showPage() c.setFont("Helvetica", 11) y = height - 0.75 * inch c.drawString(left_margin, y, line) y -= line_height c.save() return path