{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":24800,"databundleVersionId":1831594,"sourceType":"competition"},{"sourceId":954111,"sourceType":"datasetVersion","datasetId":518432},{"sourceId":1799615,"sourceType":"datasetVersion","datasetId":1069544},{"sourceId":1799839,"sourceType":"datasetVersion","datasetId":1069682},{"sourceId":1800066,"sourceType":"datasetVersion","datasetId":1069809},{"sourceId":1800067,"sourceType":"datasetVersion","datasetId":1069810},{"sourceId":1800825,"sourceType":"datasetVersion","datasetId":1070245},{"sourceId":367961,"sourceType":"modelInstanceVersion","modelInstanceId":304875,"modelId":325323}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport torch\nfrom PIL import Image\nimport pandas as pd\nimport SimpleITK as sitk\nfrom sklearn.model_selection import StratifiedKFold\nfrom torchmetrics.detection import MeanAveragePrecision\nimport torchvision\nfrom torchvision.models.detection.faster_rcnn import FastRCNNPredictor\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport random\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint('Using device:', device)\n\ndef collate_fn(batch):\n    return tuple(zip(*batch))\n\nclass JSRTDataset:\n    PIXEL_SPACING = 0.175\n    \n    def __init__(self, root, csv_file, debug=False):\n        self.root = root\n        self.debug = debug\n        \n        self.transform = torchvision.transforms.Compose([\n            torchvision.transforms.Grayscale(num_output_channels=3),\n            torchvision.transforms.ToTensor(),\n            torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], \n                                           std=[0.229, 0.224, 0.225])\n        ])\n        \n        self.data = pd.read_csv(csv_file)\n        all_imgs = os.listdir(os.path.join(root, \"images/images\"))\n        self.imgs = sorted([f for f in all_imgs if f in self.data[\"study_id\"].values])\n        \n        self.labels = []\n        for img_name in self.imgs:\n            row = self.data[self.data['study_id'] == img_name].iloc[0]\n            self.labels.append(1 if row['state'].lower() != 'non-nodule' else 0)\n\n    def __len__(self):\n        return len(self.imgs)\n\n    def __getitem__(self, idx):\n        image_name = self.imgs[idx]\n        img_path = os.path.join(self.root, \"images/images\", image_name)\n        \n        sitk_img = sitk.ReadImage(img_path)\n        arr = sitk.GetArrayFromImage(sitk_img).astype(np.float32)\n        arr = np.squeeze(arr)\n        arr -= arr.min()\n        if arr.max() > 0:\n            arr /= arr.max()\n        arr8 = (arr * 255).round().astype(np.uint8)\n        pil_img = Image.fromarray(arr8)\n        w, h = pil_img.size\n        \n        img_tensor = self.transform(pil_img)\n\n        row = self.data[self.data['study_id'] == image_name].iloc[0]\n        state = row['state'].lower()\n        \n        boxes = torch.empty((0,4), dtype=torch.float32)\n        labels = torch.empty((0,), dtype=torch.int64)\n        area = torch.empty((0,), dtype=torch.float32)\n        iscrowd = torch.empty((0,), dtype=torch.int64)\n\n        if state != 'non-nodule':\n            x_center = float(row['x'])\n            y_center = float(row['y'])\n            size_mm = float(row['size'])\n            \n            width_px = size_mm / self.PIXEL_SPACING\n            half_size = width_px / 2.0\n            \n            xmin = max(0, x_center - half_size)\n            ymin = max(0, y_center - half_size)\n            xmax = min(w, x_center + half_size)\n            ymax = min(h, y_center + half_size)\n            \n            boxes = torch.tensor([[xmin, ymin, xmax, ymax]], dtype=torch.float32)\n            labels = torch.ones((1,), dtype=torch.int64)\n            area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0])\n            iscrowd = torch.zeros((1,), dtype=torch.int64)\n\n        target = {\n            'boxes': boxes,\n            'labels': labels,\n            'image_id': torch.tensor([idx]),\n            'area': area,\n            'iscrowd': iscrowd\n        }\n\n        if self.debug and len(boxes) > 0:\n            self._debug_plot(pil_img, boxes[0], image_name)\n\n        return img_tensor, target, image_name\n\n    def _debug_plot(self, img, box, title):\n        fig, ax = plt.subplots(1, figsize=(6,6))\n        ax.imshow(img, cmap='gray')\n        rect = patches.Rectangle(\n            (box[0], box[1]),\n            box[2] - box[0],\n            box[3] - box[1],\n            linewidth=2, edgecolor='r', facecolor='none'\n        )\n        ax.add_patch(rect)\n        ax.set_title(f\"{title}: [{box[0]:.1f}, {box[1]:.1f}, {box[2]:.1f}, {box[3]:.1f}]\")\n        ax.axis('off')\n        plt.show()\n\nclass NoduleDetection:\n    def __init__(self, data_root):\n        self.data_root = data_root\n        self.transform = torchvision.transforms.Compose([\n            torchvision.transforms.Grayscale(num_output_channels=3),\n            torchvision.transforms.ToTensor(),\n            torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n        self.model = self._init_model()\n        self.metric = MeanAveragePrecision()\n        \n    def _init_model(self):\n        model = torchvision.models.detection.fasterrcnn_resnet50_fpn(\n            weights=torchvision.models.detection.FasterRCNN_ResNet50_FPN_Weights.DEFAULT\n        )\n        \n        for name, param in model.backbone.named_parameters():\n            if 'layer4' not in name and 'fpn' not in name:\n                param.requires_grad = False\n                \n        in_features = model.roi_heads.box_predictor.cls_score.in_features\n        model.roi_heads.box_predictor = FastRCNNPredictor(in_features, 2)\n        \n        return model.to(device)\n    \n    def train(self, num_epochs=50):\n        dataset = JSRTDataset(self.data_root, \n                            os.path.join(self.data_root, 'jsrt_metadata.csv'))\n        \n        skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n        \n        for fold, (train_idx, val_idx) in enumerate(skf.split(dataset, dataset.labels)):\n            print(f\"\\n--- Training Fold {fold+1}/5 ---\")\n            \n            model = self._init_model()\n            optimizer = torch.optim.AdamW(\n                [p for p in model.parameters() if p.requires_grad],\n                lr=1e-4,\n                weight_decay=0.01\n            )\n            lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n                optimizer,\n                mode='max',\n                patience=3,\n                factor=0.1,\n                verbose=True\n            )\n            train_loader, val_loader = self._create_loaders(dataset, train_idx, val_idx)\n            best_map = 0\n            \n            for epoch in range(num_epochs):\n                model.train()\n                train_loss = 0.0\n                \n                for images, targets, _ in train_loader:\n                    images = [img.to(device) for img in images]\n                    targets = [{k: v.to(device) for k,v in t.items()} for t in targets]\n                    \n                    loss_dict = model(images, targets)\n                    losses = sum(loss for loss in loss_dict.values())\n\n                    print(losses)\n                    optimizer.zero_grad()\n                    losses.backward()\n                    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n                    optimizer.step()\n                    \n                    train_loss += losses.item()\n                \n                val_map = self._validate(model, val_loader)\n                \n                lr_scheduler.step(val_map)\n                \n                print(f\"Epoch {epoch+1}/{num_epochs}\")\n                print(f\"Train Loss: {train_loss/len(train_loader):.4f}\")\n                print(f\"Val mAP: {val_map:.4f}\")\n                print(f\"Current LR: {optimizer.param_groups[0]['lr']:.2e}\")\n                \n                if val_map > best_map:\n                    best_map = val_map\n                    print(epoch)\n                    torch.save(model.state_dict(), f\"best_fold{fold}{epoch}.pth\")\n              \n            torch.save(model.state_dict(), f\"best_fold{fold}final.pth\")      \n            print(f\"Fold {fold+1} Best mAP: {best_map:.4f}\")\n\n    def _create_loaders(self, dataset, train_idx, val_idx):\n        train_sampler = torch.utils.data.SubsetRandomSampler(train_idx)\n        val_sampler = torch.utils.data.SubsetRandomSampler(val_idx)\n        \n        train_loader = torch.utils.data.DataLoader(\n            dataset,\n            batch_size=2,\n            sampler=train_sampler,\n            num_workers=0,\n            collate_fn=collate_fn,\n            pin_memory=True\n        )\n        \n        val_loader = torch.utils.data.DataLoader(\n            dataset,\n            batch_size=2,\n            sampler=val_sampler,\n            num_workers=0,\n            collate_fn=collate_fn,\n            pin_memory=True\n        )\n        \n        return train_loader, val_loader\n    \n    def _validate(self, model, val_loader):\n        model.eval()\n        metric = MeanAveragePrecision().to(device)\n        \n        with torch.no_grad():\n            for images, targets, _ in val_loader:\n                images = [img.to(device) for img in images]\n                targets = [{k: v.to(device) for k,v in t.items()} for t in targets]\n                \n                preds = model(images)\n                metric.update(preds, targets)\n        \n        return metric.compute()['map'].item()\n    \n    def predict(self, image_path):\n        self.model.eval()\n        sitk_img = sitk.ReadImage(image_path)\n        arr = sitk.GetArrayFromImage(sitk_img).astype(np.float32)\n        arr = np.squeeze(arr)\n        arr -= arr.min()\n        if arr.max() > 0:\n            arr /= arr.max()\n        pil_img = Image.fromarray((arr * 255).round().astype(np.uint8))\n        W, H = pil_img.size\n        \n        img_tensor, flipped = self._augment(pil_img)\n        with torch.no_grad():\n            pred = self.model([img_tensor.to(device)])[0]\n        if flipped:\n            pred['boxes'] = self._flip_boxes(pred['boxes'], W)\n    \n        boxes = pred['boxes']\n        scores = pred['scores']\n        keep = torchvision.ops.nms(boxes, scores, 0.5)\n        \n        return {\n            'boxes': boxes[keep].cpu().numpy(),\n            'scores': scores[keep].cpu().numpy()\n        }\n\n    def _augment(self, pil_img):\n        img = self.transform(pil_img)\n        flip = False\n        if random.random() > 0.5:\n            img = torch.flip(img, [-1])\n            flip = True\n        return img, flip\n    \n    def _flip_boxes(self, boxes, img_width):\n        boxes = boxes.clone()\n        boxes[:, [0, 2]] = img_width - boxes[:, [2, 0]]\n        return boxes\n\ndetector = NoduleDetection(\"/kaggle/input/nodules-in-chest-xrays-jsrt\")\n#detector.train(num_epochs=50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-01T14:12:45.270901Z","iopub.execute_input":"2025-05-01T14:12:45.271579Z","iopub.status.idle":"2025-05-01T14:12:46.005523Z","shell.execute_reply.started":"2025-05-01T14:12:45.271557Z","shell.execute_reply":"2025-05-01T14:12:46.004909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom PIL import Image\nimport numpy as np\nimport pandas as pd\nimport torch\n\nimage_path    = \"/kaggle/input/nodules-in-chest-xrays-jsrt/images/images/JPCNN093.png\"\ncsv_path      = \"/kaggle/input/nodules-in-chest-xrays-jsrt/jsrt_metadata.csv\"\nPIXEL_SPACING = 0.175\ngt_color      = \"lime\"\npred_color    = \"red\"\nlinewidth     = 1\n\ndf   = pd.read_csv(csv_path)\nrow  = df[df[\"study_id\"] == image_path.split(\"/\")[-1]].iloc[0]\nstate = row['state'].lower()\n\npil = Image.open(image_path)\nw, h = pil.size\n\ngt_box = None\nif state != 'non-nodule':\n    x_center = float(row['x'])\n    y_center = float(row['y'])\n    size_mm  = float(row['size'])\n    width_px = size_mm / PIXEL_SPACING\n    half_sz  = width_px / 2.0\n\n    xmin_gt = max(0, x_center - half_sz)\n    ymin_gt = max(0, y_center - half_sz)\n    xmax_gt = min(w, x_center + half_sz)\n    ymax_gt = min(h, y_center + half_sz)\n    gt_box = [xmin_gt, ymin_gt, xmax_gt, ymax_gt]\n\nresult      = detector.predict(image_path)\nboxes_pred  = result['boxes']\nscores_pred = result['scores']\n\nif len(scores_pred) > 0:\n    best_idx   = scores_pred.argmax()\n    best_box   = boxes_pred[best_idx]\n    best_score = scores_pred[best_idx]\nelse:\n    best_box = None\n\niou = None\nif gt_box is not None and best_box is not None:\n    if isinstance(best_box, torch.Tensor):\n        best_box = best_box.cpu().numpy()\n    \n    gt_x1, gt_y1, gt_x2, gt_y2 = gt_box\n    pred_x1, pred_y1, pred_x2, pred_y2 = best_box\n    print(gt_x1, gt_y1, gt_x2, gt_y2)\n    print(pred_x1, pred_y1, pred_x2, pred_y2)\n    x_left = max(gt_x1, pred_x1)\n    y_top = max(gt_y1, pred_y1)\n    x_right = min(gt_x2, pred_x2)\n    y_bottom = min(gt_y2, pred_y2)\n    \n    if x_right < x_left or y_bottom < y_top:\n        area_inter = 0.0\n    else:\n        area_inter = (x_right - x_left) * (y_bottom - y_top)\n    \n    area_gt = (gt_x2 - gt_x1) * (gt_y2 - gt_y1)\n    area_pred = (pred_x2 - pred_x1) * (pred_y2 - pred_y1)\n    area_union = area_gt + area_pred - area_inter\n    print(area_gt)\n    print(area_pred)\n    print(area_inter)\n    print(area_union)\n    iou = area_inter / area_union if area_union != 0 else 0.0\n\nimg_arr = np.array(pil.convert(\"L\"))\nfig, ax = plt.subplots(figsize=(8, 8))\nax.imshow(img_arr, cmap='gray')\nax.set_title(\"Predictions  \" + (\"|  Ground Truth\" if gt_box else \"\"))\nax.axis('off')\n\nif gt_box is not None:\n    xmin, ymin, xmax, ymax = gt_box\n    w_gt = xmax - xmin\n    h_gt = ymax - ymin\n    rect_gt = plt.Rectangle(\n        (xmin, ymin), w_gt, h_gt,\n        fill=False, edgecolor=gt_color, linewidth=linewidth,\n        label=\"Ground Truth\"\n    )\n    ax.add_patch(rect_gt)\n\nif best_box is not None:\n    x1, y1, x2, y2 = best_box\n    w_p = x2 - x1\n    h_p = y2 - y1\n    rect_p = plt.Rectangle(\n        (x1, y1), w_p, h_p,\n        fill=False, edgecolor=pred_color, linewidth=linewidth,\n        label=f\"Pred (score={best_score:.2f})\"\n    )\n    ax.add_patch(rect_p)\n    ax.text(\n        x1, y1 - 5, f\"{best_score:.2f}\",\n        color=pred_color, fontsize=8,\n        backgroundcolor='white', alpha=0.7\n    )\n\nif iou is not None:\n    ax.text(\n        0.5, 0.95, f\"IoU: {iou:.2f}\",\n        transform=ax.transAxes,\n        fontsize=12, color='white', ha='center', va='top',\n        bbox=dict(facecolor='black', alpha=0.5)\n    )\n\nhandles, labels = ax.get_legend_handles_labels()\nif handles:\n    ax.legend(handles, labels, loc='upper right')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-01T16:25:34.215663Z","iopub.execute_input":"2025-05-01T16:25:34.216409Z","iopub.status.idle":"2025-05-01T16:25:35.197509Z","shell.execute_reply.started":"2025-05-01T16:25:34.216372Z","shell.execute_reply":"2025-05-01T16:25:35.19683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom PIL import Image\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom tqdm import tqdm\n\ncsv_path      = \"/kaggle/input/nodules-in-chest-xrays-jsrt/jsrt_metadata.csv\"\nimage_folder  = \"/kaggle/input/nodules-in-chest-xrays-jsrt/images/images/\"\nPIXEL_SPACING = 0.175\ngt_color      = \"lime\"\npred_color    = \"red\"\nlinewidth     = 1\nresults = []\n\ndf = pd.read_csv(csv_path)\n\nfor _, row in tqdm(df.iterrows(), total=len(df)):\n    image_path = f\"{image_folder}{row['study_id']}\"\n    \n    try:\n        pil = Image.open(image_path)\n        w, h = pil.size\n        \n        gt_box = None\n        state = row['state'].lower()\n        if state != 'non-nodule':\n            x_center = float(row['x'])\n            y_center = float(row['y'])\n            size_mm  = float(row['size'])\n            width_px = size_mm / PIXEL_SPACING\n            half_sz  = width_px / 2.0\n\n            xmin_gt = max(0, x_center - half_sz)\n            ymin_gt = max(0, y_center - half_sz)\n            xmax_gt = min(w, x_center + half_sz)\n            ymax_gt = min(h, y_center + half_sz)\n            gt_box = [xmin_gt, ymin_gt, xmax_gt, ymax_gt]\n\n        result = detector.predict(image_path)\n        boxes_pred = result['boxes']\n        scores_pred = result['scores']\n        \n        best_box = None\n        best_score = None\n        if len(scores_pred) > 0:\n            best_idx = scores_pred.argmax()\n            best_box = boxes_pred[best_idx]\n            best_score = scores_pred[best_idx].item()\n\n        iou = None\n        has_nodule = state != 'non-nodule'\n        detected = best_box is not None\n            \n        if gt_box is not None and best_box is not None:\n            if isinstance(best_box, torch.Tensor):\n                best_box = best_box.cpu().numpy()\n            \n            gt_x1, gt_y1, gt_x2, gt_y2 = gt_box\n            pred_x1, pred_y1, pred_x2, pred_y2 = best_box\n\n            x_left = max(gt_x1, pred_x1)\n            y_top = max(gt_y1, pred_y1)\n            x_right = min(gt_x2, pred_x2)\n            y_bottom = min(gt_y2, pred_y2)\n            \n            area_inter = 0.0\n            if x_right > x_left and y_bottom > y_top:\n                area_inter = (x_right - x_left) * (y_bottom - y_top)\n            \n            area_gt = (gt_x2 - gt_x1) * (gt_y2 - gt_y1)\n            area_pred = (pred_x2 - pred_x1) * (pred_y2 - pred_y1)\n            area_union = area_gt + area_pred - area_inter\n            \n            iou = area_inter / area_union if area_union != 0 else 0.0\n\n        results.append({\n            'image_id': row['study_id'],\n            'has_nodule': has_nodule,\n            'detected': detected,\n            'iou': iou if iou is not None else np.nan,\n            'confidence': best_score\n        })\n\n    except Exception as e:\n        print(f\"Error processing {row['study_id']}: {str(e)}\")\n        continue\n\nresults_df = pd.DataFrame(results)\n\nnodule_cases = results_df[results_df['has_nodule']]\nnon_nodule_cases = results_df[~results_df['has_nodule']]\n\nprint(\"\\n=== Evaluation Metrics ===\")\nprint(f\"Total images: {len(results_df)}\")\nprint(f\"Nodule cases: {len(nodule_cases)}\")\nprint(f\"Non-nodule cases: {len(non_nodule_cases)}\\n\")\n\nprint(\"For nodule cases:\")\nprint(f\"- Detection rate: {nodule_cases['detected'].mean():.2%}\")\nprint(f\"- Average IoU: {nodule_cases['iou'].mean():.2f}\")\nprint(f\"- Median confidence: {nodule_cases['confidence'].median():.2f}\\n\")\n\nprint(\"For non-nodule cases:\")\nprint(f\"- False positive rate: {non_nodule_cases['detected'].mean():.2%}\")\nprint(f\"- Average confidence (when detected): \", end=\"\")\nif non_nodule_cases['detected'].sum() > 0:\n    print(f\"{non_nodule_cases[non_nodule_cases['detected']]['confidence'].mean():.2f}\")\nelse:\n    print(\"N/A\")\n\nresults_df.to_csv('detection_results.csv', index=False)\nprint(\"\\nResults saved to detection_results.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-01T16:20:33.708565Z","iopub.execute_input":"2025-05-01T16:20:33.708985Z","iopub.status.idle":"2025-05-01T16:22:04.421199Z","shell.execute_reply.started":"2025-05-01T16:20:33.708961Z","shell.execute_reply":"2025-05-01T16:22:04.420442Z"}},"outputs":[],"execution_count":null}]}