{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n\"\"\"\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\"\"\"\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-18T20:05:32.405091Z","iopub.execute_input":"2026-08-18T20:05:32.40549Z","iopub.status.idle":"2026-08-18T20:05:33.247519Z","shell.execute_reply.started":"2026-08-18T20:05:32.405464Z","shell.execute_reply":"2026-08-18T20:05:33.246444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install pydicom","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-18T20:13:28.636911Z","iopub.execute_input":"2026-08-18T20:13:28.637162Z","iopub.status.idle":"2026-08-18T20:13:33.270183Z","shell.execute_reply.started":"2026-08-18T20:13:28.637143Z","shell.execute_reply":"2026-08-18T20:13:33.269051Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport pandas as pd\nimport numpy as np\nimport pydicom\nfrom glob import glob\nfrom PIL import Image\nfrom torchvision import transforms\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm.notebook import tqdm\nfrom transformers import AutoModel\n\n# Configurar el dispositivo\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Usando dispositivo: {device}\")\n\n# Opciones de configuración\nmodel_path = \"/kaggle/input/models/metaresearch/dinov2/pytorch/small/1\" \ncsv_path = '/kaggle/input/datasets/antoniolopezrios/lables1/labels_probabilidad.csv'\n# dicom_train_dir = '/kaggle/input/tu-dataset/train_images' # Ajustar ruta\ndicom_test_dir = '/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series'   # Ajustar ruta\nsubmission_path = '/kaggle/input/tu-dataset/sample_submission.csv' # Ajustar ruta","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-18T20:18:09.013445Z","iopub.execute_input":"2026-08-18T20:18:09.013757Z","iopub.status.idle":"2026-08-18T20:18:34.747072Z","shell.execute_reply.started":"2026-08-18T20:18:09.013732Z","shell.execute_reply":"2026-08-18T20:18:34.746151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nfrom glob import glob\n\n# 1. Leer el CSV con las etiquetas\ncsv_path = '/kaggle/input/datasets/antoniolopezrios/lables1/labels_probabilidad.csv'\ndf_labels = pd.read_csv(csv_path)\n\n# Asumimos que la primera columna es el ID del estudio\nid_column = df_labels.columns[0]\ntarget_cols=[\"ACL_probability\",\"MCL_probability\",\"Medial Meniscus_probability\",\"Lateral Meniscus_probability\",\"Medial OA_probability\", \"Lateral OA_probability\",\"PF OA_probability\",\"Effusion_probability\",\"Synovitis_probability\",\"Baker's_probability\",\"Contusion_probability\",\"Fracture_probability\"]\n\n#columnas_necesarias = [id_column] + target_cols\n#df_labels = df_labels[columnas_necesarias]\n\n#target_cols = df_labels[name_cols].to_list()\n\nprint(f\"Columna de ID: {id_column}\")\nprint(f\"Número de targets: {len(df_labels)} (Debe ser 12)\")\n\n# 2. Buscar todas las imágenes asociadas a cada study_id\ndicom_base_dir = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series' # Ajustar ruta\nstudy_paths_dict = {}\n\n# Suponiendo que la estructura es: train_images/study_id/series_id/imagen.dcm\nfor study_id in df_labels[id_column]:\n    # Busca recursivamente todos los .dcm bajo la carpeta del study_id\n    search_path = os.path.join(dicom_base_dir, str(study_id), '**', '*.dcm')\n    dicom_files = glob(search_path, recursive=True)\n    \n    if len(dicom_files) > 0:\n        study_paths_dict[study_id] = dicom_files\n\n# 3. Filtrar estudios que no tengan imágenes en el directorio\n# Mantenemos las 13 columnas intactas en el DataFrame\ndf_final = df_labels[df_labels[id_column].isin(study_paths_dict.keys())].reset_index(drop=True)\n\nprint(f\"Total de estudios listos para entrenar: {len(df_final)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-18T20:18:38.888285Z","iopub.execute_input":"2026-08-18T20:18:38.889161Z","iopub.status.idle":"2026-08-18T20:23:04.933385Z","shell.execute_reply.started":"2026-08-18T20:18:38.889126Z","shell.execute_reply":"2026-08-18T20:23:04.932272Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Transformaciones de DINOv2\ndinov2_transforms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\ndef process_dicom(path):\n    try:\n        dicom = pydicom.dcmread(path)\n        data = dicom.pixel_array.astype(np.float32)\n    \n        data_min, data_max = np.min(data), np.max(data)\n        if data_max != data_min:\n            data = (data - data_min) / (data_max - data_min)\n        else:\n            data = np.zeros_like(data)\n        \n        data = (data * 255).astype(np.uint8)\n    \n        if getattr(dicom, 'PhotometricInterpretation', None) == 'MONOCHROME1':\n            data = np.amax(data) - data\n        \n        data = np.stack([data, data, data], axis=-1)\n        return Image.fromarray(data)\n    except Exception as e:\n        # Si el DICOM está corrupto, atrapamos el error y devolvemos None\n        # Opcional: puedes comentar el print si no quieres que llene tu pantalla de texto\n        print(f\"⚠️ DICOM corrupto saltado: {path}\")\n        return None\n\nclass StudyDicomDataset(Dataset):\n    def __init__(self, df, study_paths_dict, id_column, target_cols, transform=None):\n        self.df = df\n        self.study_paths_dict = study_paths_dict\n        self.id_column = id_column\n        self.target_cols = target_cols\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        study_id = self.df.iloc[idx][self.id_column]\n        dicom_paths = self.study_paths_dict[study_id]\n        \n        images = []\n        for path in dicom_paths:\n            img = process_dicom(path)\n            if self.transform:\n                img = self.transform(img)\n            images.append(img)\n            \n        if len(images) == 0:\n            # Retornamos un tensor de ceros para no crashear el modelo\n            # Ajusta el (3, 224, 224) al tamaño exacto que tu modelo espera\n            final_image_tensor = torch.zeros((3, 224, 224), dtype=torch.float32)\n            return final_image_tensor, torch.tensor(labels)\n        else:\n            # ... [AQUÍ VA TU LÓGICA ORIGINAL PARA APILAR / TRANSFORMAR LAS IMÁGENES] ...\n            # Ej: final_image_tensor = self.transform(images) \n            final_image_tensor = torch.stack(images) \n            labels = torch.tensor(self.df.iloc[idx][self.target_cols].values.astype(float), dtype=torch.float32)\n            return final_image_tensor,labels\n            pass \n            \n        \n        \n\nclass TestStudyDicomDataset(Dataset):\n    def __init__(self, df, study_paths_dict, id_column, transform=None):\n        self.df = df\n        self.study_paths_dict = study_paths_dict\n        self.id_column = id_column\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        study_id = self.df.iloc[idx][self.id_column]\n        dicom_paths = self.study_paths_dict.get(study_id, [])\n        \n        if len(dicom_paths) == 0:\n            return torch.zeros((3, 224, 224)).unsqueeze(0), study_id\n\n        images = []\n        for path in dicom_paths:\n            img = process_dicom(path)\n            if self.transform:\n                img = self.transform(img)\n            images.append(img)\n            \n        return torch.stack(images), study_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-18T21:06:00.517208Z","iopub.execute_input":"2026-08-18T21:06:00.518342Z","iopub.status.idle":"2026-08-18T21:06:00.53494Z","shell.execute_reply.started":"2026-08-18T21:06:00.5183Z","shell.execute_reply":"2026-08-18T21:06:00.534001Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PatchCNN(nn.Module):\n    def __init__(self, embed_dim=384, hidden_dim=256):\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_channels=embed_dim, out_channels=512, kernel_size=3, padding=1)\n        self.relu = nn.ReLU()\n        self.conv2 = nn.Conv1d(in_channels=512, out_channels=hidden_dim, kernel_size=3, padding=1)\n        self.pool = nn.AdaptiveAvgPool1d(1) \n\n    def forward(self, x):\n        x = x.permute(0, 2, 1)\n        x = self.relu(self.conv1(x))\n        x = self.relu(self.conv2(x))\n        x = self.pool(x).squeeze(-1)\n        return x\n\nclass Dinov2StudyModelLocal(nn.Module):\n    def __init__(self, model_path, num_classes=12):\n        super().__init__()\n        self.dinov2 = AutoModel.from_pretrained(model_path)\n        \n        for param in self.dinov2.parameters():\n            param.requires_grad = False\n            \n        embed_dim = 384 # Dimensión de dinov2_vits14\n        \n        self.patch_aggregator = PatchCNN(embed_dim=embed_dim, hidden_dim=256)\n        \n        self.classifier = nn.Sequential(\n            nn.Linear(256, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes),\n            nn.Sigmoid()\n        )\n\n    def forward(self, x):\n        if x.dim() == 5:\n            x = x.squeeze(0) # (1, N, 3, 224, 224) -> (N, 3, 224, 224)\n            \n        with torch.no_grad():\n            outputs = self.dinov2(pixel_values=x)\n            \n        # Extraer tokens de parche e ignorar el token [CLS]\n        patch_tokens = outputs.last_hidden_state[:, 1:, :]\n        \n        # Reducir espacialmente por imagen y promediar temporalmente por estudio\n        image_vectors = self.patch_aggregator(patch_tokens)\n        study_vector = image_vectors.mean(dim=0, keepdim=True) \n        \n        output = self.classifier(study_vector) \n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-18T20:23:35.973152Z","iopub.execute_input":"2026-08-18T20:23:35.973447Z","iopub.status.idle":"2026-08-18T20:23:35.985056Z","shell.execute_reply.started":"2026-08-18T20:23:35.973423Z","shell.execute_reply":"2026-08-18T20:23:35.98412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 5\nNUM_CLASSES = len(target_cols) # <- Esto detectará automáticamente que son 15 y ajustará la capa final\nGRADIENT_ACCUMULATION_STEPS = 32 \n\n# DataLoader con batch_size=1\ntrain_dataset = StudyDicomDataset(\n    df=df_final[1:], \n    study_paths_dict=study_paths_dict, \n    id_column=id_column, \n    target_cols=target_cols, \n    transform=dinov2_transforms\n)\ntrain_loader = DataLoader(train_dataset, batch_size=1, shuffle=True, num_workers=2)\n\n# Aquí pasamos el NUM_CLASSES correcto (15) para que coincida con tu CSV\nmodel = Dinov2StudyModelLocal(model_path=model_path, num_classes=NUM_CLASSES).to(device)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4)\n\n# Bucle de Entrenamiento\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    progress_bar = tqdm(enumerate(train_loader), total=len(train_loader), desc=f\"Época {epoch+1}/{EPOCHS}\")\n    \n    for i, (images, labels) in progress_bar:\n        images, labels = images.to(device), labels.to(device)\n        \n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        loss = loss / GRADIENT_ACCUMULATION_STEPS\n        loss.backward()\n        \n        if (i + 1) % GRADIENT_ACCUMULATION_STEPS == 0 or (i + 1) == len(train_loader):\n            optimizer.step()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * GRADIENT_ACCUMULATION_STEPS\n        progress_bar.set_postfix({'loss': loss.item() * GRADIENT_ACCUMULATION_STEPS})\n        \n    epoch_loss = running_loss / len(train_loader.dataset)\n    print(f\"Resumen Época {epoch+1} | Pérdida Media: {epoch_loss:.4f}\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-18T22:05:51.644125Z","iopub.execute_input":"2026-08-18T22:05:51.644634Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Cargar CSV de prueba y mapear rutas\ndf_test = pd.read_csv(submission_path)\ntest_paths_dict = {}\n\nfor study_id in tqdm(df_test[id_column], desc=\"Mapeando imágenes de prueba\"):\n    search_path = os.path.join(dicom_test_dir, str(study_id), '**', '*.dcm')\n    dicom_files = glob(search_path, recursive=True)\n    if len(dicom_files) > 0:\n        test_paths_dict[study_id] = dicom_files\n    else:\n        test_paths_dict[study_id] = []\n\n# 2. Configurar DataLoader de prueba\ntest_dataset = TestStudyDicomDataset(\n    df=df_test, \n    study_paths_dict=test_paths_dict, \n    id_column=id_column, \n    transform=dinov2_transforms\n)\ntest_loader = DataLoader(test_dataset, batch_size=1, shuffle=False, num_workers=2)\n\n# 3. Bucle de Inferencia\nmodel.eval()\npredictions = []\nstudy_ids = []\n\nwith torch.no_grad():\n    for images, study_id in tqdm(test_loader, desc=\"Generando Predicciones\"):\n        images = images.to(device)\n        outputs = model(images)\n        \n        probs = outputs.cpu().numpy().squeeze()\n        predictions.append(probs)\n        study_ids.append(study_id[0])\n\n# 4. Formatear y exportar resultados\npredictions = np.array(predictions)\nif predictions.ndim == 1:\n    predictions = np.expand_dims(predictions, axis=0)\n\nsubmission_df = pd.DataFrame(predictions, columns=target_cols)\nsubmission_df.insert(0, id_column, study_ids)\n\nsubmission_df.to_csv('submission.csv', index=False)\nprint(\"¡Archivo submission.csv guardado y listo!\")\ndisplay(submission_df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}