{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Multi-Task Pneumonia Detection, Region Localization, Severity Assessment, and Explainable Analysis\n\nThis notebook implements an end-to-end, state-of-the-art deep learning system for multi-task chest X-ray analysis. It is designed to run locally or directly on Kaggle with GPU acceleration.\n\n## Project Goal\nGiven a chest X-ray image, the model outputs:\n1. **Pneumonia Detection**: Binary classification (Normal vs. Pneumonia)\n2. **Pneumonia Region Localization**: Semantic segmentation mask (256x256)\n3. **Severity Classification**: Multi-class classification (Normal, Mild, Moderate, Severe)\n4. **Explainable Infection Percentage**: Affected Lung Area % computed directly from the predicted mask.\n\n## Model Architecture\n- **Shared Encoder**: Pretrained DenseNet121\n- **Segmentation Decoder**: Attention U-Net Decoder (restoring resolution to 256x256 via gated skip connections)\n- **Classification Heads**: Global Average Pooling followed by task-specific dense/dropout layers.","metadata":{}},{"cell_type":"code","source":"import os\nimport pydicom\nimport cv2\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\n\n# Set random seed for reproducibility\nnp.random.seed(42)\ntf.random.set_seed(42)\n\n# Detect environment\nKAGGLE_MODE = os.path.exists(\"/kaggle/input\")\nif KAGGLE_MODE:\n    print(\"Running on Kaggle.\")\n    DATA_DIR = \"/kaggle/input/competitions/rsna-pneumonia-detection-challenge\"\n    OUTPUT_DIR = \"/kaggle/working/processed_dataset\"\n    SAMPLE_MODE = False  # Run on FULL dataset on Kaggle\nelse:\n    print(\"Running locally.\")\n    DATA_DIR = \".\"\n    OUTPUT_DIR = os.path.join(DATA_DIR, \"processed_dataset\")\n    SAMPLE_MODE = True  # Run on a sample locally to save time/space\n\nTRAIN_IMAGES_DIR = os.path.join(DATA_DIR, \"stage_2_train_images\")\nTRAIN_LABELS_CSV = os.path.join(DATA_DIR, \"stage_2_train_labels.csv\")\nCLASS_INFO_CSV = os.path.join(DATA_DIR, \"stage_2_detailed_class_info.csv\")\n\nOUTPUT_IMAGES_DIR = os.path.join(OUTPUT_DIR, \"images\")\nOUTPUT_MASKS_DIR = os.path.join(OUTPUT_DIR, \"masks\")\nMETADATA_CSV = os.path.join(OUTPUT_DIR, \"preprocessed_metadata.csv\")\n\nprint(f\"Data Directory: {DATA_DIR}\")\nprint(f\"Output Directory: {OUTPUT_DIR}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-18T11:48:47.560035Z","iopub.execute_input":"2026-06-18T11:48:47.560306Z","iopub.status.idle":"2026-06-18T11:49:04.656225Z","shell.execute_reply.started":"2026-06-18T11:48:47.560285Z","shell.execute_reply":"2026-06-18T11:49:04.655245Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Part 1: Dataset Preprocessing\n\nIn this section we:\n1. Merge the annotations and subclass class info.\n2. Generate binary classification targets (Normal = 0, Pneumonia = 1).\n3. Calculate the Composite Severity Score combining Infection Area %, bilateral lung involvement, and opacity region counts.\n4. Categorize Severity labels: Normal (Target == 0), Mild (0 - 33rd percentile), Moderate (33rd - 66th percentile), Severe (66th - 100th percentile) based on the Composite Severity Score.\n5. Rescale raw chest X-rays (from DICOM files) and binary masks to 256x256, saving them as PNGs.\n6. Perform a stratified Train/Val/Test split (70% / 15% / 15%) based on the severity label.","metadata":{}},{"cell_type":"code","source":"def read_dicom(path):\n    try:\n        ds = pydicom.dcmread(path)\n        img = ds.pixel_array\n        if getattr(ds, \"PhotometricInterpretation\", \"\") == \"MONOCHROME1\":\n            img = np.max(img) - img\n        img_min, img_max = img.min(), img.max()\n        if img_max - img_min > 0:\n            img = ((img - img_min) / (img_max - img_min) * 255.0).astype(np.uint8)\n        else:\n            img = np.zeros_like(img, dtype=np.uint8)\n        return img\n    except Exception as e:\n        print(f\"Error reading DICOM {path}: {e}\")\n        return None\n\ndef stratified_split(df, stratify_col, test_size=0.15, random_state=42):\n    groups = df.groupby(stratify_col)\n    train_list = []\n    test_list = []\n    for name, group in groups:\n        shuffled = group.sample(frac=1, random_state=random_state).reset_index(drop=True)\n        split_idx = int(len(shuffled) * (1 - test_size))\n        train_list.append(shuffled.iloc[:split_idx])\n        test_list.append(shuffled.iloc[split_idx:])\n    df_train = pd.concat(train_list).sample(frac=1, random_state=random_state).reset_index(drop=True)\n    df_test = pd.concat(test_list).sample(frac=1, random_state=random_state).reset_index(drop=True)\n    return df_train, df_test\n\nprint(\"Generating dataset metadata...\")\ndf_labels = pd.read_csv(TRAIN_LABELS_CSV)\ndf_class = pd.read_csv(CLASS_INFO_CSV)\ndf_class_clean = df_class.drop_duplicates(subset=[\"patientId\"])\ndf_merged = pd.merge(df_labels, df_class_clean, on=\"patientId\", how=\"left\")\n\ndf_unique_patients = df_merged.drop_duplicates(subset=[\"patientId\"])[[\"patientId\", \"Target\", \"class\"]].copy()\nboxes_dict = {}\ndf_pneumonia = df_merged[df_merged[\"Target\"] == 1]\nfor pid, group in df_pneumonia.groupby(\"patientId\"):\n    boxes_dict[pid] = list(zip(group[\"x\"], group[\"y\"], group[\"width\"], group[\"height\"]))\n\npatient_records = []\nfor idx, row in df_unique_patients.iterrows():\n    pid = row[\"patientId\"]\n    target = row[\"Target\"]\n    cls = row[\"class\"]\n    if target == 0:\n        inf_pct = 0.0\n        bilateral = 0.0\n        region_count = 0\n        region_score = 0.0\n        sev_score = 0.0\n    else:\n        mask = np.zeros((1024, 1024), dtype=np.uint8)\n        boxes = boxes_dict.get(pid, [])\n        for x, y, w, h in boxes:\n            mask[int(y):int(y+h), int(x):int(x+w)] = 1\n        infected_pixels = np.sum(mask)\n        inf_pct = (infected_pixels / (1024.0 * 1024.0)) * 100.0\n        \n        # Bilateral involvement (midline at X = 512)\n        has_left = False\n        has_right = False\n        for x, y, w, h in boxes:\n            center_x = x + w / 2.0\n            if center_x < 512:\n                has_left = True\n            else:\n                has_right = True\n        bilateral = 1.0 if (has_left and has_right) else 0.0\n        \n        # Region count\n        region_count = len(boxes)\n        region_score = min(region_count, 3) / 3.0\n        \n        # Composite Severity Score\n        sev_score = 0.6 * inf_pct + 0.25 * bilateral + 0.15 * region_score\n            \n    patient_records.append({\n        \"patientId\": pid, \"Target\": target, \"class\": cls, \n        \"infection_percentage\": inf_pct,\n        \"bilateral\": bilateral,\n        \"region_count\": region_count,\n        \"severity_score\": sev_score\n    })\n\ndf_patients = pd.DataFrame(patient_records)\n\n# Calculate percentile thresholds from positive cases\npositive_scores = df_patients.loc[df_patients[\"Target\"] == 1, \"severity_score\"]\nif len(positive_scores) > 0:\n    p33 = np.percentile(positive_scores, 33.0)\n    p66 = np.percentile(positive_scores, 66.0)\nelse:\n    p33, p66 = 5.0, 15.0\n\nprint(f\"Calculated severity thresholds (33rd percentile: {p33:.4f}, 66th percentile: {p66:.4f})\")\n\ndef assign_severity(row):\n    if row[\"Target\"] == 0:\n        return \"Normal\"\n    elif row[\"severity_score\"] <= p33:\n        return \"Mild\"\n    elif row[\"severity_score\"] <= p66:\n        return \"Moderate\"\n    else:\n        return \"Severe\"\n\ndf_patients[\"severity\"] = df_patients.apply(assign_severity, axis=1)\n\nif SAMPLE_MODE:\n    print(\"Sample Mode is active locally: selecting 1000 patients...\")\n    samples = []\n    for sev, group in df_patients.groupby(\"severity\"):\n        sz = min(len(group), int(1000 * (len(group) / len(df_patients))))\n        if sev in [\"Moderate\", \"Severe\"] and len(group) > 50:\n            sz = max(sz, 50)\n        samples.append(group.sample(n=min(len(group), sz), random_state=42))\n    df_processed = pd.concat(samples).sample(frac=1, random_state=42).reset_index(drop=True)\nelse:\n    df_processed = df_patients.copy()\n\n# Perform 70/15/15 stratified split\ndf_train, df_temp = stratified_split(df_processed, \"severity\", test_size=0.30, random_state=42)\ndf_val, df_test = stratified_split(df_temp, \"severity\", test_size=0.50, random_state=42)\n\nsplit_map = {}\nfor pid in df_train[\"patientId\"]: split_map[pid] = \"train\"\nfor pid in df_val[\"patientId\"]: split_map[pid] = \"val\"\nfor pid in df_test[\"patientId\"]: split_map[pid] = \"test\"\ndf_processed[\"split\"] = df_processed[\"patientId\"].map(split_map)\n\nos.makedirs(OUTPUT_IMAGES_DIR, exist_ok=True)\nos.makedirs(OUTPUT_MASKS_DIR, exist_ok=True)\n\nprint(\"Processing DICOMs to 256x256 PNG files...\")\nfinal_rows = []\ntotal = len(df_processed)\nfor idx, row in df_processed.iterrows():\n    pid = row[\"patientId\"]\n    target = row[\"Target\"]\n    severity = row[\"severity\"]\n    split = row[\"split\"]\n    \n    dicom_path = os.path.join(TRAIN_IMAGES_DIR, f\"{pid}.dcm\")\n    img = read_dicom(dicom_path)\n    if img is None: continue\n    \n    mask = np.zeros((1024, 1024), dtype=np.uint8)\n    if target == 1:\n        boxes = boxes_dict.get(pid, [])\n        for x, y, w, h in boxes:\n            mask[int(y):int(y+h), int(x):int(x+w)] = 1\n            \n    img_resized = cv2.resize(img, (256, 256), interpolation=cv2.INTER_LINEAR)\n    mask_resized = cv2.resize(mask, (256, 256), interpolation=cv2.INTER_NEAREST)\n    \n    img_save_path = os.path.join(OUTPUT_IMAGES_DIR, f\"{pid}.png\")\n    mask_save_path = os.path.join(OUTPUT_MASKS_DIR, f\"{pid}_mask.png\")\n    cv2.imwrite(img_save_path, img_resized)\n    cv2.imwrite(mask_save_path, mask_resized * 255)\n    \n    final_rows.append({\n        \"patientId\": pid,\n        \"image_path\": os.path.join(\"processed_dataset\", \"images\", f\"{pid}.png\"),\n        \"mask_path\": os.path.join(\"processed_dataset\", \"masks\", f\"{pid}_mask.png\"),\n        \"detection_label\": target,\n        \"severity_label\": severity,\n        \"split\": split\n    })\n    \ndf_final = pd.DataFrame(final_rows)\ndf_final.to_csv(METADATA_CSV, index=False)\nprint(f\"Preprocessing complete. Metadata CSV saved to {METADATA_CSV}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-18T11:49:04.657821Z","iopub.execute_input":"2026-06-18T11:49:04.658265Z","iopub.status.idle":"2026-06-18T11:57:57.329389Z","shell.execute_reply.started":"2026-06-18T11:49:04.658240Z","shell.execute_reply":"2026-06-18T11:57:57.328696Z"}},"outputs":[],"execution_count":null}]}