{"nbformat":4,"nbformat_minor":5,"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.12"}},"cells":[{"cell_type":"code","execution_count":null,"metadata":{},"source":"\"\"\"\nRSNA Knee Abnormality Detection: v32 INFERENCE\nLoads best_model.pth from v31 (EfficientNet-B0, 256px) and runs inference on ALL test studies.\nOutputs submission.csv to /kaggle/working.\nAuthor: Manus AI\n\"\"\"\nimport os\nimport sys\nimport glob\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\nimport pydicom\nimport cv2\n\nprint(f\"PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}\")\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nBASE = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\nTARGETS = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA',\n           'Lateral OA', 'PF OA', 'Effusion', 'Synovitis', \"Baker's\",\n           'Contusion', 'Fracture']\n\nclass CFG:\n    img_size = 256\n    num_slices = 3\n    num_planes = 2\n    num_classes = 12\n\ndef find_csv(name, root='/kaggle/input'):\n    for dirpath, _, files in os.walk(root):\n        if name in files:\n            return os.path.join(dirpath, name)\n    raise FileNotFoundError(name)\n\nWEIGHTS = find_csv('best_model.pth')\nprint('[Model] weights:', WEIGHTS)\n\n# ------------- Model (same as training v31) -------------\nclass KneeModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        from torchvision.models import efficientnet_b0\n        base = efficientnet_b0(weights=None)\n        base.classifier = nn.Identity()\n        self.backbone = base\n        self.att = nn.Sequential(nn.Linear(1280, 128), nn.ReLU())\n        self.gate = nn.Linear(128, 1)\n        self.head = nn.Sequential(\n            nn.Linear(1280, 512), nn.ReLU(), nn.Dropout(0.3),\n            nn.Linear(512, CFG.num_classes))\n\n    def forward(self, x):\n        x = x.float()\n        B = x.size(0)\n        x = x.view(B, -1, 3, x.size(-2), x.size(-1))\n        K = x.size(1)\n        s = x.view(B * K, 3, x.size(-2), x.size(-1))\n        feats = self.backbone(s).view(B, K, -1)\n        a = self.att(feats)\n        g = torch.softmax(self.gate(a), dim=1)\n        fused = (feats * g).sum(dim=1)\n        return self.head(fused)\n\n# ------------- Image loading -------------\ndef prep_stack(files, max_slices=CFG.num_slices, img_size=CFG.img_size):\n    imgs = []\n    for f in files:\n        try:\n            dcm = pydicom.dcmread(f, stop_before_pixels=False, force=True)\n            px = dcm.pixel_array.astype(np.float32)\n            px = apply_voi_lut(px, dcm)\n            if px.max() > px.min():\n                px = (px - px.min()) / (px.max() - px.min())\n            else:\n                continue\n            px = np.clip(px, 0, 1)\n            px = cv2.resize(px, (img_size, img_size), interpolation=cv2.INTER_AREA)\n            imgs.append(px)\n        except Exception:\n            continue\n    if not imgs:\n        return None\n    n = len(imgs)\n    idx = np.linspace(0, n - 1, max_slices, dtype=int) if n >= max_slices else np.arange(n)\n    stack = np.stack([imgs[i] for i in idx], axis=0)\n    rgb = np.repeat(stack[:, None, :, :], 3, axis=1)\n    return torch.from_numpy(rgb.astype(np.float32))\n\n# ------------- Dataset -------------\nclass KneeDataset(Dataset):\n    def __init__(self, df, series_df, img_dir):\n        self.ids = df['StudyInstanceUID'].astype(str).str.strip().values\n        self.img_dir = img_dir\n        self.series_map = {}\n        self.fluid = {}\n        for _, r in series_df.iterrows():\n            s_id = str(r.iloc[0]).strip()\n            ser = str(r.iloc[1]).strip()\n            self.series_map.setdefault(s_id, [])\n            if ser not in self.series_map[s_id]:\n                self.series_map[s_id].append(ser)\n            self.fluid[ser] = int(r.iloc[2]) if 'Fluid_Sensitive' in series_df.columns else 0\n        print(f'[Dataset] {len(self.ids)} studies, {len(self.series_map)} mapped')\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, i):\n        sid = str(self.ids[i]).strip()\n        series_list = self.series_map.get(sid, [])\n        fluid = [s for s in series_list if self.fluid.get(s, 0) == 1]\n        non_fluid = [s for s in series_list if self.fluid.get(s, 0) == 0]\n        selected = []\n        for pool in [fluid, non_fluid]:\n            for s in pool:\n                if len(selected) >= CFG.num_planes:\n                    break\n                selected.append(s)\n            if len(selected) >= CFG.num_planes:\n                break\n        if not selected:\n            selected = series_list[:CFG.num_planes]\n        stacks = []\n        zero = torch.zeros((CFG.num_slices, 3, CFG.img_size, CFG.img_size))\n        for s in selected:\n            path = os.path.join(self.img_dir, sid, s)\n            if not os.path.exists(path):\n                stacks.append(zero)\n                continue\n            files = sorted(glob.glob(os.path.join(path, '*.dcm')))\n            if not files:\n                stacks.append(zero)\n                continue\n            t = prep_stack(files)\n            stacks.append(t if t is not None else zero)\n        while len(stacks) < CFG.num_planes:\n            stacks.append(zero)\n        x = torch.stack(stacks[:CFG.num_planes], dim=0)\n        return x.view(-1, 3, CFG.img_size, CFG.img_size)\n\n# ------------- Main -------------\ndef main():\n    test_df = pd.read_csv(os.path.join(BASE, 'test.csv'))\n    print(f'[Data] test studies: {len(test_df)}')\n    img_dir = os.path.join(BASE, 'test_series')\n    test_series = pd.read_csv(find_csv('test_series.csv'))\n    print(f'[Data] test series rows: {len(test_series)}')\n\n    ds = KneeDataset(test_df, test_series, img_dir)\n    loader = DataLoader(ds, batch_size=16, shuffle=False, num_workers=0)\n\n    model = KneeModel().to(device)\n    model.load_state_dict(torch.load(WEIGHTS, map_location=device))\n    model.eval()\n\n    print('[Inference] running...')\n    preds = []\n    with torch.no_grad():\n        for x in loader:\n            x = x.to(device)\n            preds.append(torch.sigmoid(model(x)).cpu().numpy())\n    preds = np.vstack(preds)\n    print(f'[Inference] shape={preds.shape}, mean={preds.mean():.4f}')\n\n    sub = pd.DataFrame(preds, columns=TARGETS)\n    sub.insert(0, 'StudyInstanceUID', test_df['StudyInstanceUID'].astype(str).str.strip().values[:len(sub)])\n    sub.to_csv('/kaggle/working/submission.csv', index=False)\n    print('[Saved] /kaggle/working/submission.csv')\n    print(sub.to_string())\n    sys.stdout.flush()\n\nif __name__ == '__main__':\n    main()\n","outputs":[]}]}