{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":52254,"databundleVersionId":9674523},{"sourceType":"datasetVersion","sourceId":15021397,"datasetId":9615554,"databundleVersionId":15898882},{"sourceType":"datasetVersion","sourceId":14785973,"datasetId":9452485,"databundleVersionId":15639569},{"sourceType":"datasetVersion","sourceId":15029974,"datasetId":9621214,"databundleVersionId":15908191},{"sourceType":"datasetVersion","sourceId":15021548,"datasetId":9615651,"databundleVersionId":15899045}],"dockerImageVersionId":31286,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# RSNA Spleen Viewer - CT + Mask + GradCAM + Metadata\nGradCAM is embedded into full DICOM volume at correct crop bbox position.","metadata":{"_uuid":"d419ac9f-2072-44b6-9707-fe97a16a5175","_cell_guid":"9729dc64-c276-41e9-98dc-ec6a75deff68","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"!pip install dicomsdl","metadata":{"_uuid":"3bb949cc-5336-4532-bf7f-2ca8f4add4ef","_cell_guid":"ad7ad610-6b8f-4d8b-becb-6542e1f51b46","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, json\nfrom glob import glob\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport pydicom\nimport nibabel as nib\nimport dicomsdl\nimport pandas as pd\nimport uuid\nimport cv2\nfrom scipy.ndimage import zoom\nfrom ipywidgets import IntSlider, HBox, Button, Output, Dropdown, VBox\nfrom IPython.display import FileLink, display","metadata":{"_uuid":"79c08138-39d7-42d0-90c7-66ce83584e3c","_cell_guid":"56e72591-b35c-4921-960f-051e0393e145","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ROOT_PATH    = \"/kaggle/input/competitions/rsna-2023-abdominal-trauma-detection\"\nMASKS_PATH   = \"/kaggle/input/datasets/bmohamedelamine/rsna-2023-spleen-masks\"\nGRADCAM_DIR  = \"/kaggle/input/datasets/bmohamedelamine/rsna-spleen-cls-gcam-masks/gradcam_volumes\"\nSERIES_CSV   = \"/kaggle/input/datasets/bmohamedelamine/rsna-spleen-cls-labels/spleen_cls_train_series.csv\"\nCROP_META    = \"/kaggle/input/datasets/bmohamedelamine/rsna-2023-cropped-spleen/cropped_spleen/crop_metadata.json\"","metadata":{"_uuid":"b0aaac53-9df5-4efc-bf20-71f0d4039e70","_cell_guid":"0b1b8c90-773b-466c-8b45-7533ef711bfb","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n#  DICOM helpers\n# ================================================================\n\ndef __dataset__to_numpy_image(self, index=0):\n    info = self.getPixelDataInfo()\n    dtype = info[\"dtype\"]\n    if info[\"SamplesPerPixel\"] != 1:\n        raise RuntimeError(\"SamplesPerPixel != 1\")\n    shape = [info[\"Rows\"], info[\"Cols\"]]\n    arr = np.empty(shape, dtype=dtype)\n    self.copyFrameData(index, arr)\n    return arr\n\nif not hasattr(dicomsdl._dicomsdl.DataSet, \"to_numpy_image\"):\n    dicomsdl._dicomsdl.DataSet.to_numpy_image = __dataset__to_numpy_image\n\n\ndef fast_glob_sorted(path):\n    return sorted(glob(path), key=lambda x: int(os.path.basename(x).split(\".\")[0]))\n\n\ndef fast_window(img, center, width):\n    low = center - width / 2\n    high = center + width / 2\n    img = np.clip(img, low, high)\n    return (img - low) / (high - low + 1e-6)\n\n\ndef load_dicom_series_fast(series_dir, center, width):\n    paths = fast_glob_sorted(os.path.join(series_dir, \"*.dcm\"))\n    if not paths:\n        raise FileNotFoundError(\"No DICOM files found\")\n    volume = []\n    for p in paths:\n        dcm = dicomsdl.open(p)\n        img = dcm.to_numpy_image().astype(np.float32)\n        slope = getattr(dcm, \"RescaleSlope\", 1.0)\n        intercept = getattr(dcm, \"RescaleIntercept\", 0.0)\n        img = img * slope + intercept\n        img = fast_window(img, center, width)\n        volume.append(img)\n    return np.stack(volume).astype(np.float32)","metadata":{"_uuid":"b1e99747-12ad-4e88-8959-7b460aa343fa","_cell_guid":"2e121dca-9b80-4edc-91e7-bae41c52b066","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n#  Mask loader\n# ================================================================\n\ndef load_mask(mask_path, volume_shape):\n    import shutil\n    tmp = os.path.join(\"/kaggle/working\", \"tmp_mask.nii.gz\")\n    shutil.copy(mask_path, tmp)\n    mask_nii = nib.load(tmp)\n    mask_data = mask_nii.get_fdata()\n    mask_data = np.transpose(mask_data, (2, 1, 0))\n    mask_data = mask_data[::-1, :, :]\n    factors = np.array(volume_shape) / np.array(mask_data.shape)\n    resized = zoom(mask_data, factors, order=0)\n    return (resized > 0.5).astype(np.uint8)","metadata":{"_uuid":"9f5ea01a-0251-4e5a-8091-2e3587c5ee38","_cell_guid":"aa6f9697-9662-4e66-9ae2-83f96d0ca5a3","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n#  GradCAM helpers  (Option A: embed into full volume via crop bbox)\n# ================================================================\n\nCLASS_NAMES = [\"Healthy\", \"Low Damage\", \"High Damage\"]\n\n\ndef _load_crop_metadata(crop_meta_path):\n    \"\"\"Load crop_metadata.json and index by (pid, sid).\"\"\"\n    if not os.path.isfile(crop_meta_path):\n        print(f\"  crop_metadata.json not found at {crop_meta_path}\")\n        return {}\n    with open(crop_meta_path) as f:\n        entries = json.load(f)\n    lookup = {}\n    for entry in entries:\n        key = (str(entry[\"patient_id\"]), str(entry[\"series_id\"]))\n        lookup[key] = entry\n    return lookup\n\n\ndef load_gradcam(gradcam_dir, crop_lookup, pid, sid, full_volume_shape):\n    \"\"\"\n    Load GradCAM .npz (whose cam matches the CROPPED spleen region)\n    and embed it into a zero-filled array of full_volume_shape using\n    the crop_bbox from crop_metadata.json.\n    \"\"\"\n    npz_path = os.path.join(gradcam_dir, f\"{pid}_{sid}.npz\")\n    if not os.path.isfile(npz_path):\n        return None, None, None, None\n\n    # Look up crop bbox\n    meta_entry = crop_lookup.get((str(pid), str(sid)))\n    if meta_entry is None:\n        print(f\"  No crop metadata for {pid}_{sid}, skipping GradCAM\")\n        return None, None, None, None\n\n    try:\n        g = np.load(npz_path)\n        cam_crop = g[\"cam\"].astype(np.float32)\n        probs = g[\"probs\"]\n        pred = int(g[\"pred\"])\n        true_label = int(g[\"true_label\"])\n\n        # Normalize the cropped cam to [0, 1]\n        lo, hi = cam_crop.min(), cam_crop.max()\n        if hi > lo:\n            cam_crop = (cam_crop - lo) / (hi - lo)\n        else:\n            cam_crop = np.zeros_like(cam_crop)\n\n        # Get crop bbox coordinates\n        bbox = meta_entry[\"crop_bbox\"]\n        z0, z1 = bbox[\"z0\"], bbox[\"z1\"]\n        y0, y1 = bbox[\"y0\"], bbox[\"y1\"]\n        x0, x1 = bbox[\"x0\"], bbox[\"x1\"]\n        bbox_shape = (z1 - z0, y1 - y0, x1 - x0)\n\n        # Resize cam_crop to match exact bbox size if minor mismatch\n        if cam_crop.shape != bbox_shape:\n            factors = np.array(bbox_shape) / np.array(cam_crop.shape)\n            cam_crop = zoom(cam_crop, factors, order=1).astype(np.float32)\n            lo2, hi2 = cam_crop.min(), cam_crop.max()\n            if hi2 > lo2:\n                cam_crop = (cam_crop - lo2) / (hi2 - lo2)\n\n        # Embed into full-volume-sized array\n        cam_full = np.zeros(full_volume_shape, dtype=np.float32)\n        # Clamp bbox to actual volume dims (safety)\n        vz, vy, vx = full_volume_shape\n        ez0, ez1 = min(z0, vz), min(z1, vz)\n        ey0, ey1 = min(y0, vy), min(y1, vy)\n        ex0, ex1 = min(x0, vx), min(x1, vx)\n        cz = ez1 - ez0\n        cy = ey1 - ey0\n        cx = ex1 - ex0\n        cam_full[ez0:ez1, ey0:ey1, ex0:ex1] = cam_crop[:cz, :cy, :cx]\n\n        return cam_full, probs, pred, true_label\n\n    except Exception as e:\n        print(f\"  GradCAM load failed for {pid}_{sid}: {e}\")\n        return None, None, None, None\n\n\ndef make_gradcam_overlay(ct_slice, cam_slice):\n    \"\"\"Blend CT slice with GradCAM heatmap (JET colourmap).\"\"\"\n    u8 = (ct_slice * 255).astype(np.uint8)\n    rgb = np.stack([u8] * 3, axis=-1)\n    jet = cv2.applyColorMap((cam_slice * 255).astype(np.uint8), cv2.COLORMAP_JET)\n    jet = cv2.cvtColor(jet, cv2.COLOR_BGR2RGB)\n    return cv2.addWeighted(rgb, 0.55, jet, 0.45, 0)","metadata":{"_uuid":"110bd3d3-4cc9-4432-b2b7-a257b3da0890","_cell_guid":"041b42ae-3526-49d4-a7dc-76280d94bbe3","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n#  Metadata formatting\n# ================================================================\n\ndef _fmt_dicom_time(raw):\n    s = str(raw).strip()\n    if len(s) >= 6:\n        return f\"{s[0:2]}:{s[2:4]}:{s[4:6]}\"\n    return s\n\n\ndef _fmt_dicom_date(raw):\n    s = str(raw).strip()\n    if len(s) >= 8:\n        return f\"{s[0:4]}-{s[4:6]}-{s[6:8]}\"\n    return s","metadata":{"_uuid":"b07c0cd5-73f8-40cb-a8cd-8c334dd3cb39","_cell_guid":"67757ebc-0e6a-4f2c-92a6-68393d298acc","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n#  MAIN VIEWER\n# ================================================================\n\ndef rsna_full_viewer(root_path, masks_path, gradcam_dir, series_csv,\n                     crop_meta_path, center=50, width=400):\n\n    meta_df = pd.read_csv(series_csv)\n    meta_df[\"patient_id\"] = meta_df[\"patient_id\"].astype(str)\n    meta_df[\"series_id\"] = meta_df[\"series_id\"].astype(str)\n\n    # Load crop metadata once for GradCAM bbox embedding\n    crop_lookup = _load_crop_metadata(crop_meta_path)\n    print(f\"Crop metadata loaded: {len(crop_lookup)} entries\")\n\n    # -- Filter dropdowns\n    spleen_dd = Dropdown(description=\"Spleen\",\n                         options=[\"Any\", \"Healthy\", \"Low\", \"High\"],\n                         value=\"Any\", layout={\"width\": \"180px\"})\n    liver_dd = Dropdown(description=\"Liver\",\n                        options=[\"Any\", \"Healthy\", \"Low\", \"High\"],\n                        value=\"Any\", layout={\"width\": \"180px\"})\n    kidney_dd = Dropdown(description=\"Kidney\",\n                         options=[\"Any\", \"Healthy\", \"Low\", \"High\"],\n                         value=\"Any\", layout={\"width\": \"190px\"})\n    bowel_dd = Dropdown(description=\"Bowel\",\n                        options=[\"Any\", \"Healthy\", \"Injury\"],\n                        value=\"Any\", layout={\"width\": \"180px\"})\n    extra_dd = Dropdown(description=\"Extravasation\",\n                        options=[\"Any\", \"Healthy\", \"Injury\"],\n                        value=\"Any\", layout={\"width\": \"220px\"})\n\n    patient_dd = Dropdown(description=\"Patient:\")\n    series_dd = Dropdown(description=\"Series:\")\n    slider = IntSlider(description=\"Slice\")\n    save_btn = Button(description=\"Save Slice\", button_style=\"success\")\n\n    labels_out = Output()\n    img_out = Output()\n    save_out = Output()\n\n    # -- Filtering logic\n    def apply_filter():\n        t = meta_df.copy()\n        if spleen_dd.value == \"Low\":\n            t = t[t.spleen_low == 1]\n        elif spleen_dd.value == \"High\":\n            t = t[t.spleen_high == 1]\n        elif spleen_dd.value == \"Healthy\":\n            t = t[(t.spleen_low == 0) & (t.spleen_high == 0)]\n\n        for organ, dd in [(\"liver\", liver_dd), (\"kidney\", kidney_dd)]:\n            lo_col, hi_col = f\"{organ}_low\", f\"{organ}_high\"\n            if lo_col in t.columns and hi_col in t.columns:\n                if dd.value == \"Low\":\n                    t = t[t[lo_col] == 1]\n                elif dd.value == \"High\":\n                    t = t[t[hi_col] == 1]\n                elif dd.value == \"Healthy\":\n                    t = t[(t[lo_col] == 0) & (t[hi_col] == 0)]\n\n        if \"extravasation_injury\" in t.columns:\n            if extra_dd.value == \"Injury\":\n                t = t[t.extravasation_injury == 1]\n            elif extra_dd.value == \"Healthy\":\n                t = t[t.extravasation_injury == 0]\n\n        if \"bowel_injury\" in t.columns:\n            if bowel_dd.value == \"Injury\":\n                t = t[t.bowel_injury == 1]\n            elif bowel_dd.value == \"Healthy\":\n                t = t[t.bowel_injury == 0]\n\n        return t\n\n    def update_patients(change):\n        filtered = apply_filter()\n        if filtered.empty:\n            patient_dd.options = []\n            with labels_out:\n                labels_out.clear_output()\n                print(\"No matching patients\")\n            return\n        pts = sorted(filtered.patient_id.unique().tolist())\n        patient_dd.options = pts\n        patient_dd.value = pts[0]\n\n    # -- Show metadata\n    def show_metadata(pid, sid):\n        with labels_out:\n            labels_out.clear_output()\n            row = meta_df[(meta_df.patient_id == str(pid)) &\n                          (meta_df.series_id == str(sid))]\n            if row.empty:\n                row = meta_df[meta_df.patient_id == str(pid)]\n            if row.empty:\n                print(f\"Patient {pid} -- no metadata found\")\n                return\n            r = row.iloc[0]\n\n            age = r.get(\"Age\", \"?\")\n            sex = r.get(\"Sex\", \"?\")\n            date = _fmt_dicom_date(r.get(\"Date\", \"\"))\n            t0 = _fmt_dicom_time(r.get(\"Start time\", \"\"))\n            t1 = _fmt_dicom_time(r.get(\"End time\", \"\"))\n            injury = r.get(\"Injury name\", \"\")\n            injury_str = injury if pd.notna(injury) and injury != \"\" else \"None\"\n            aortic = r.get(\"aortic_hu\", \"?\")\n\n            print(f\"===  Patient {pid}  |  Series {sid}  ===\")\n            print(f\"  Age : {age}     Sex : {sex}\")\n            print(f\"  Date: {date}    Time: {t0} -> {t1}\")\n            print(f\"  Aortic HU: {aortic}\")\n            print(f\"  Injury   : {injury_str}\")\n\n            sp_parts = []\n            if r.get(\"spleen_healthy\", 0) == 1:\n                sp_parts.append(\"Healthy\")\n            if r.get(\"spleen_low\", 0) == 1:\n                sp_parts.append(\"Low\")\n            if r.get(\"spleen_high\", 0) == 1:\n                sp_parts.append(\"High\")\n            print(f\"  Spleen   : {', '.join(sp_parts) if sp_parts else '--'}\")\n\n            if r.get(\"extravasation_injury\", 0) == 1:\n                print(\"  Extravasation: Injury\")\n            elif r.get(\"extravasation_healthy\", 0) == 1:\n                print(\"  Extravasation: Healthy\")\n\n    def update_series(change):\n        pid = patient_dd.value\n        if pid is None:\n            return\n        patient_dir = os.path.join(root_path, \"train_images\", pid)\n        if not os.path.isdir(patient_dir):\n            return\n        series = sorted(os.listdir(patient_dir))\n        series_dd.options = series\n        if series:\n            series_dd.value = series[0]\n        load_series(None)\n\n    # -- Load volume + mask + GradCAM\n    def load_series(change):\n        pid = patient_dd.value\n        sid = series_dd.value\n        if pid is None or sid is None:\n            return\n\n        show_metadata(pid, sid)\n\n        series_dir = os.path.join(root_path, \"train_images\", pid, sid)\n        volume = load_dicom_series_fast(series_dir, center, width)\n\n        # Spleen mask\n        mask = None\n        mask_dir = os.path.join(masks_path, str(pid), str(sid))\n        if os.path.isdir(mask_dir):\n            mask_files = [f for f in os.listdir(mask_dir)\n                          if f.endswith(\".nii.gz\") or f.endswith(\"nii_gz\")]\n            if mask_files:\n                try:\n                    mask = load_mask(os.path.join(mask_dir, mask_files[0]),\n                                     volume.shape)\n                except Exception as e:\n                    print(f\"  Mask load failed: {e}\")\n\n        # GradCAM -- embed into full volume via crop bbox\n        cam, probs, pred, true_lbl = load_gradcam(\n            gradcam_dir, crop_lookup, pid, sid, volume.shape)\n\n        has_mask = mask is not None\n        has_cam = cam is not None\n        n_cols = 1 + int(has_mask) + int(has_cam) * 2\n\n        slider.min = 0\n        slider.max = volume.shape[0] - 1\n        slider.value = volume.shape[0] // 2\n\n        def _draw_mask_overlay(ax, ct_sl, mask_sl):\n            ax.imshow(ct_sl, cmap=\"gray\")\n            if mask_sl is not None and mask_sl.any():\n                rgba = np.zeros((*mask_sl.shape, 4), dtype=np.float32)\n                rgba[mask_sl == 1] = [0.0, 1.0, 0.2, 0.35]\n                ax.imshow(rgba)\n                ax.contour(mask_sl, levels=[0.5], colors=\"lime\", linewidths=1.5)\n\n        def update_slice(change):\n            z = slider.value\n            with img_out:\n                img_out.clear_output(wait=True)\n                fig_w = 5 * n_cols\n                fig, axes = plt.subplots(\n                    1, n_cols, figsize=(fig_w, 5), facecolor=\"#111111\")\n                if n_cols == 1:\n                    axes = [axes]\n\n                col = 0\n                axes[col].imshow(volume[z], cmap=\"gray\")\n                axes[col].set_title(\n                    f\"CT  (slice {z+1}/{volume.shape[0]})\",\n                    color=\"white\", fontsize=9)\n                axes[col].axis(\"off\")\n                col += 1\n\n                if has_mask:\n                    _draw_mask_overlay(axes[col], volume[z], mask[z])\n                    axes[col].set_title(\"Spleen Mask\",\n                                        color=\"white\", fontsize=9)\n                    axes[col].axis(\"off\")\n                    col += 1\n\n                if has_cam:\n                    overlay = make_gradcam_overlay(volume[z], cam[z])\n                    axes[col].imshow(overlay)\n                    axes[col].set_title(\"GradCAM Overlay\",\n                                        color=\"white\", fontsize=9)\n                    axes[col].axis(\"off\")\n                    col += 1\n\n                    axes[col].imshow(cam[z], cmap=\"jet\", vmin=0, vmax=1)\n                    axes[col].set_title(\"GradCAM\",\n                                        color=\"white\", fontsize=9)\n                    axes[col].axis(\"off\")\n                    col += 1\n\n                title_parts = [f\"Patient {pid}  |  Series {sid}\"]\n                if has_cam:\n                    correct = pred == true_lbl\n                    title_parts.append(\n                        f\"True: {CLASS_NAMES[true_lbl]}   \"\n                        f\"Pred: {CLASS_NAMES[pred]}  \"\n                        f\"{'CORRECT' if correct else 'WRONG'}   \"\n                        f\"P(H)={probs[0]:.3f}  \"\n                        f\"P(L)={probs[1]:.3f}  \"\n                        f\"P(Hi)={probs[2]:.3f}\")\n                    title_color = \"#2ecc71\" if correct else \"#e74c3c\"\n                else:\n                    title_color = \"white\"\n\n                fig.suptitle(\"\\n\".join(title_parts),\n                             fontsize=10, fontweight=\"bold\",\n                             color=title_color)\n                plt.tight_layout()\n                plt.show()\n\n        slider.observe(update_slice, names=\"value\")\n        update_slice(None)\n\n        def save_slice(b):\n            z = slider.value\n            fname = f\"{pid}_{sid}_slice{z+1}_{uuid.uuid4().hex[:6]}.png\"\n            n = n_cols\n            fig, axes = plt.subplots(1, n, figsize=(5 * n, 5),\n                                     facecolor=\"#111111\")\n            if n == 1:\n                axes = [axes]\n            c = 0\n            axes[c].imshow(volume[z], cmap=\"gray\")\n            axes[c].axis(\"off\")\n            c += 1\n            if has_mask:\n                _draw_mask_overlay(axes[c], volume[z], mask[z])\n                axes[c].axis(\"off\")\n                c += 1\n            if has_cam:\n                axes[c].imshow(make_gradcam_overlay(volume[z], cam[z]))\n                axes[c].axis(\"off\")\n                c += 1\n                axes[c].imshow(cam[z], cmap=\"jet\", vmin=0, vmax=1)\n                axes[c].axis(\"off\")\n                c += 1\n            fig.savefig(fname, bbox_inches=\"tight\",\n                        pad_inches=0, facecolor=\"#111111\")\n            plt.close(fig)\n            with save_out:\n                save_out.clear_output()\n                print(\"Saved:\")\n                display(FileLink(fname))\n\n        save_btn._click_handlers.callbacks = []\n        save_btn.on_click(save_slice)\n\n    for w in [spleen_dd, liver_dd, kidney_dd, bowel_dd, extra_dd]:\n        w.observe(update_patients, names=\"value\")\n    patient_dd.observe(update_series, names=\"value\")\n    series_dd.observe(load_series, names=\"value\")\n    update_patients(None)\n\n    display(VBox([\n        HBox([spleen_dd, liver_dd, kidney_dd, bowel_dd, extra_dd]),\n        patient_dd, series_dd, labels_out,\n        HBox([slider, save_btn]),\n        img_out, save_out,\n    ]))","metadata":{"_uuid":"6ce61744-04b5-4334-b3aa-5dc8e1f72244","_cell_guid":"c0ed36dd-bfb1-4a06-bba1-279c86b1237b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Soft-tissue window (WC=50, WW=400)\nrsna_full_viewer(ROOT_PATH, MASKS_PATH, GRADCAM_DIR, SERIES_CSV, CROP_META,\n                 center=50, width=400)","metadata":{"_uuid":"cae10888-ebc4-43bd-84d4-cb60fbec8859","_cell_guid":"5b5862dc-f7b2-4f67-9b86-d17dad0bdfdf","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Wider window (WC=50, WW=800)\nrsna_full_viewer(ROOT_PATH, MASKS_PATH, GRADCAM_DIR, SERIES_CSV, CROP_META,\n                 center=50, width=800)","metadata":{"_uuid":"585760a3-f032-45f3-9bd4-2ed1714476fb","_cell_guid":"b2e8ac6c-6b3a-484e-bc30-4756da3014ee","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null}]}