| 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"] |
|
|
| |
| 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) |
|
|
| |
| with zipfile.ZipFile(zip_file.name, 'r') as z: |
| z.extractall(tmp_dir) |
|
|
| |
| result = subprocess.run([ |
| "dcm2niix", |
| "-o", nifti_dir, |
| "-z", "y", |
| "-f", "%p_%s", |
| tmp_dir |
| ], capture_output=True, text=True) |
|
|
| print("dcm2niix output:", result.stdout) |
| print("dcm2niix errors:", result.stderr) |
|
|
| |
| 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", "") |
| |
| |
| 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: |
| |
| img = nib.as_closest_canonical(nifti_path) |
| volume = img.get_fdata() |
|
|
| |
| mid = volume.shape[2] // 2 |
| slice_2d = volume[:, :, mid] |
|
|
| |
| slice_2d = np.rot90(slice_2d) |
|
|
| |
| 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 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 |
|
|
| |
| c.setFont("Helvetica-Bold", 14) |
| c.drawString(left_margin, top_margin, "Brain MRI Radiology Report") |
|
|
| |
| c.setFont("Helvetica", 9) |
| c.drawString(left_margin, top_margin - 16, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}") |
|
|
| |
| c.line(left_margin, top_margin - 24, width - left_margin, top_margin - 24) |
|
|
| y = top_margin - 0.55 * inch |
|
|
| |
| 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: |
| c.showPage() |
| c.setFont("Helvetica", 11) |
| y = height - 0.75 * inch |
| c.drawString(left_margin, y, line) |
| y -= line_height |
| line = word |
|
|
| |
| 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 |