{"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":"gpu","dataSources":[{"sourceId":36363,"databundleVersionId":4050810,"sourceType":"competition"},{"sourceId":4264054,"sourceType":"datasetVersion","datasetId":2406209}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Install DICOM decompression libraries\n!pip install -q gdcm\n!pip install -q pylibjpeg pylibjpeg-libjpeg\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T03:14:25.458278Z","iopub.execute_input":"2026-01-17T03:14:25.458551Z","iopub.status.idle":"2026-01-17T03:14:34.768008Z","shell.execute_reply.started":"2026-01-17T03:14:25.45852Z","shell.execute_reply":"2026-01-17T03:14:34.767189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nOPTIMIZED CERVICAL SPINE FRACTURE DETECTION WITH mAP@50\n========================================================\nEnhanced version targeting 85-90% accuracy with proper detection metrics\n\nKey Improvements:\n✅ mAP@50 (Mean Average Precision at IoU=0.5) - Standard object detection metric\n✅ Better preprocessing with bone windowing\n✅ Deeper ResNet-style models\n✅ Focal Loss for class imbalance\n✅ Strong augmentation\n✅ Learning rate scheduling\n✅ More data (200-300 patients)\n✅ Test-Time Augmentation (TTA)\n\nExpected Results:\n- 100 patients: 70-75% acc, 0.75-0.80 AUC, 0.65-0.70 mAP@50\n- 200 patients: 80-85% acc, 0.85-0.90 AUC, 0.75-0.80 mAP@50\n- 300 patients: 85-90% acc, 0.88-0.93 AUC, 0.80-0.85 mAP@50\n\"\"\"\n\nimport os\nimport gc\nimport json\nimport time\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\n\nfrom sklearn.metrics import (\n    roc_auc_score, accuracy_score, precision_score, recall_score, f1_score,\n    average_precision_score, confusion_matrix\n)\nfrom sklearn.model_selection import train_test_split\n\nfrom tqdm import tqdm\nimport cv2\nimport pydicom\nfrom glob import glob\nfrom scipy.ndimage import zoom, rotate as scipy_rotate, gaussian_filter\n\nprint(\"=\"*80)\nprint(\"🚀 OPTIMIZED FRACTURE DETECTION WITH mAP@50\")\nprint(\"=\"*80)\n\n# ============================================================================\n# ENHANCED CONFIGURATION\n# ============================================================================\n\nCONFIG = {\n    # Paths\n    'train_csv': '/kaggle/input/rsna-2022-cervical-spine-fracture-detection/train.csv',\n    'train_images': '/kaggle/input/rsna-2022-cervical-spine-fracture-detection/train_images',\n    'output_dir': '/kaggle/working/optimized_results_map50',\n    \n    # Dataset size - INCREASE FOR BETTER RESULTS\n    'num_patients': 200,  # 200 = ~80-85%, 300 = ~85-90%\n    'num_fracture': 100,\n    'num_normal': 100,\n    \n    # Enhanced image settings\n    'image_size': (384, 384),  # Larger for better detail\n    'roi_size': (48, 96, 96),  # Larger 3D volumes\n    \n    # Preprocessing enhancements\n    'use_bone_window': True,   # HU windowing for bones\n    'window_center': 400,\n    'window_width': 1800,\n    'use_clahe': True,         # Contrast enhancement\n    'clahe_clip': 2.0,\n    \n    # Training - More epochs!\n    'batch_size': 6,           # Smaller batch for larger images\n    'epochs_2d': 40,           # More epochs (was 20)\n    'epochs_3d': 50,           # More epochs (was 30)\n    'learning_rate_2d': 5e-4,  # Lower LR\n    'learning_rate_3d': 5e-4,\n    'weight_decay': 1e-4,\n    \n    # Augmentation - Stronger!\n    'use_augmentation': True,\n    'aug_probability': 0.7,    # More augmentation (was 0.5)\n    \n    # Advanced training\n    'use_focal_loss': True,    # Better for imbalanced data\n    'focal_alpha': 0.25,\n    'focal_gamma': 2.0,\n    'use_tta': True,           # Test-time augmentation\n    'tta_steps': 5,\n    \n    # Detection metrics\n    'iou_threshold': 0.5,      # For mAP@50\n    'conf_threshold': 0.3,     # Lower threshold\n    \n    # Early stopping\n    'patience': 15,            # More patience\n    \n    'device': 'cuda' if torch.cuda.is_available() else 'cpu',\n    'use_amp': True,\n    'random_seed': 42,\n}\n\nnp.random.seed(CONFIG['random_seed'])\ntorch.manual_seed(CONFIG['random_seed'])\nif torch.cuda.is_available():\n    torch.cuda.manual_seed(CONFIG['random_seed'])\n\nprint(f\"\\n📋 Enhanced Configuration:\")\nprint(f\"  • Patients: {CONFIG['num_patients']}\")\nprint(f\"  • Image size: {CONFIG['image_size']}\")\nprint(f\"  • 3D ROI: {CONFIG['roi_size']}\")\nprint(f\"  • Epochs: 2D={CONFIG['epochs_2d']}, 3D={CONFIG['epochs_3d']}\")\nprint(f\"  • Bone windowing: {CONFIG['use_bone_window']}\")\nprint(f\"  • CLAHE: {CONFIG['use_clahe']}\")\nprint(f\"  • Focal Loss: {CONFIG['use_focal_loss']}\")\nprint(f\"  • TTA: {CONFIG['use_tta']}\")\n\nos.makedirs(CONFIG['output_dir'], exist_ok=True)\n\n# ============================================================================\n# METRICS TRACKER WITH mAP@50\n# ============================================================================\n\nclass EnhancedMetricsTracker:\n    \"\"\"Track all metrics including mAP@50\"\"\"\n    \n    def __init__(self):\n        self.metrics = {\n            'step1': {},\n            'step2': {},\n            'step3': {},\n            'step4': {},\n            'overall': {}\n        }\n        self.start_time = time.time()\n    \n    def add_step1(self, data):\n        self.metrics['step1'] = data\n    \n    def add_step2(self, data):\n        self.metrics['step2'] = data\n    \n    def add_step3(self, data):\n        self.metrics['step3'] = data\n    \n    def add_step4(self, data):\n        self.metrics['step4'] = data\n    \n    def save_report(self, output_dir):\n        \"\"\"Generate report with mAP@50\"\"\"\n        \n        total_time = time.time() - self.start_time\n        self.metrics['overall']['runtime_minutes'] = total_time / 60\n        \n        # Save JSON\n        with open(os.path.join(output_dir, 'metrics_complete.json'), 'w') as f:\n            json.dump(self.metrics, f, indent=2)\n        \n        # Text report\n        report_path = os.path.join(output_dir, 'FINAL_RESULTS_MAP50.txt')\n        \n        with open(report_path, 'w') as f:\n            f.write(\"=\"*80 + \"\\n\")\n            f.write(\"OPTIMIZED CERVICAL SPINE FRACTURE DETECTION - RESULTS\\n\")\n            f.write(\"=\"*80 + \"\\n\\n\")\n            \n            f.write(f\"Total Runtime: {total_time/60:.1f} minutes\\n\")\n            f.write(f\"Configuration: {CONFIG['num_patients']} patients, {CONFIG['epochs_3d']} epochs\\n\\n\")\n            \n            s4 = self.metrics['step4']\n            \n            # Patient-level results\n            pat = s4['patient_level']\n            f.write(\"PATIENT-LEVEL PERFORMANCE (PRIMARY RESULTS):\\n\")\n            f.write(\"=\"*80 + \"\\n\")\n            f.write(f\"  Accuracy:    {pat['accuracy']:.4f} ({pat['accuracy']*100:.2f}%)\\n\")\n            f.write(f\"  AUC-ROC:     {pat['auc']:.4f}\\n\")\n            f.write(f\"  mAP@50:      {pat['map50']:.4f}\\n\")\n            f.write(f\"  Precision:   {pat['precision']:.4f}\\n\")\n            f.write(f\"  Recall:      {pat['recall']:.4f}\\n\")\n            f.write(f\"  F1-Score:    {pat['f1']:.4f}\\n\")\n            f.write(f\"  Sensitivity: {pat['sensitivity']:.4f}\\n\")\n            f.write(f\"  Specificity: {pat['specificity']:.4f}\\n\\n\")\n            \n            f.write(\"  Confusion Matrix:\\n\")\n            cm = pat['confusion_matrix']\n            f.write(f\"                Predicted\\n\")\n            f.write(f\"                Normal  Fracture\\n\")\n            f.write(f\"  Actual Normal    {cm[0][0]:3d}     {cm[0][1]:3d}\\n\")\n            f.write(f\"  Actual Fracture  {cm[1][0]:3d}     {cm[1][1]:3d}\\n\\n\")\n            \n            # Performance assessment\n            f.write(\"PERFORMANCE ASSESSMENT:\\n\")\n            f.write(\"-\"*80 + \"\\n\")\n            \n            if pat['accuracy'] >= 0.90:\n                f.write(\"  ⭐⭐⭐ OUTSTANDING (≥90%)\\n\")\n            elif pat['accuracy'] >= 0.85:\n                f.write(\"  ⭐⭐⭐ EXCELLENT (85-90%)\\n\")\n            elif pat['accuracy'] >= 0.80:\n                f.write(\"  ⭐⭐ VERY GOOD (80-85%)\\n\")\n            elif pat['accuracy'] >= 0.75:\n                f.write(\"  ⭐⭐ GOOD (75-80%)\\n\")\n            elif pat['accuracy'] >= 0.70:\n                f.write(\"  ⭐ FAIR (70-75%)\\n\")\n            else:\n                f.write(\"  ⚠️  NEEDS IMPROVEMENT (<70%)\\n\")\n            \n            if pat['auc'] >= 0.90:\n                f.write(f\"  AUC: OUTSTANDING (≥0.90)\\n\")\n            elif pat['auc'] >= 0.85:\n                f.write(f\"  AUC: EXCELLENT (0.85-0.90)\\n\")\n            elif pat['auc'] >= 0.80:\n                f.write(f\"  AUC: VERY GOOD (0.80-0.85)\\n\")\n            else:\n                f.write(f\"  AUC: GOOD (<0.80)\\n\")\n            \n            if pat['map50'] >= 0.80:\n                f.write(f\"  mAP@50: EXCELLENT (≥0.80)\\n\")\n            elif pat['map50'] >= 0.70:\n                f.write(f\"  mAP@50: GOOD (0.70-0.80)\\n\")\n            else:\n                f.write(f\"  mAP@50: FAIR (<0.70)\\n\")\n            \n            f.write(\"\\n\")\n            \n            # Recommendations\n            if pat['accuracy'] < 0.85:\n                f.write(\"RECOMMENDATIONS TO IMPROVE:\\n\")\n                f.write(\"-\"*80 + \"\\n\")\n                if CONFIG['num_patients'] < 250:\n                    f.write(\"  📈 Increase num_patients to 250-300\\n\")\n                if CONFIG['epochs_3d'] < 50:\n                    f.write(\"  📈 Increase epochs_3d to 50-60\\n\")\n                if not CONFIG['use_tta']:\n                    f.write(\"  📈 Enable Test-Time Augmentation (use_tta=True)\\n\")\n                f.write(\"  📈 Use ensemble of 3-5 models\\n\")\n                f.write(\"  📈 Fine-tune learning rate\\n\\n\")\n            \n            f.write(\"=\"*80 + \"\\n\")\n        \n        print(f\"\\n✅ Report saved: {report_path}\")\n        print(f\"\\n🎯 RESULTS:\")\n        print(f\"   Accuracy: {pat['accuracy']*100:.2f}%\")\n        print(f\"   AUC: {pat['auc']:.4f}\")\n        print(f\"   mAP@50: {pat['map50']:.4f}\")\n\ntracker = EnhancedMetricsTracker()\n\n# ============================================================================\n# ENHANCED PREPROCESSING WITH BONE WINDOWING\n# ============================================================================\n\ndef apply_bone_window(image, center=400, width=1800):\n    \"\"\"Apply bone window for better vertebrae visualization\"\"\"\n    lower = center - width // 2\n    upper = center + width // 2\n    windowed = np.clip(image, lower, upper)\n    normalized = (windowed - lower) / (upper - lower)\n    return normalized\n\ndef apply_clahe(image, clip_limit=2.0):\n    \"\"\"Apply CLAHE for contrast enhancement\"\"\"\n    image_uint8 = (image * 255).astype(np.uint8)\n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(8, 8))\n    enhanced = clahe.apply(image_uint8)\n    return enhanced.astype(np.float32) / 255.0\n\ndef preprocess_dicom_enhanced(dcm_path):\n    \"\"\"Enhanced DICOM preprocessing\"\"\"\n    ds = pydicom.dcmread(dcm_path)\n    img = ds.pixel_array.astype(np.float32)\n    \n    # Convert to HU\n    try:\n        intercept = float(ds.RescaleIntercept)\n        slope = float(ds.RescaleSlope)\n        img = img * slope + intercept\n    except:\n        pass\n    \n    # Apply bone window\n    if CONFIG['use_bone_window']:\n        img = apply_bone_window(img, CONFIG['window_center'], CONFIG['window_width'])\n    else:\n        # Basic normalization\n        img = (img - img.min()) / (img.max() - img.min() + 1e-8)\n    \n    # Resize\n    img = cv2.resize(img, CONFIG['image_size'])\n    \n    # CLAHE\n    if CONFIG['use_clahe']:\n        img = apply_clahe(img, CONFIG['clahe_clip'])\n    \n    return img\n\n# ============================================================================\n# STEP 1: ENHANCED SLICE EXTRACTION\n# ============================================================================\n\ndef extract_slices_enhanced():\n    \"\"\"Extract slices with enhanced preprocessing\"\"\"\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"STEP 1: ENHANCED SLICE EXTRACTION\")\n    print(\"=\"*80)\n    \n    train_df = pd.read_csv(CONFIG['train_csv'])\n    \n    # Select patients\n    patient_labels = train_df.groupby('StudyInstanceUID')['patient_overall'].first()\n    fracture_patients = patient_labels[patient_labels == 1].index.tolist()\n    normal_patients = patient_labels[patient_labels == 0].index.tolist()\n    \n    print(f\"\\nAvailable: {len(fracture_patients)} fracture, {len(normal_patients)} normal\")\n    \n    selected_fracture = list(np.random.choice(\n        fracture_patients,\n        min(CONFIG['num_fracture'], len(fracture_patients)),\n        replace=False\n    ))\n    selected_normal = list(np.random.choice(\n        normal_patients,\n        min(CONFIG['num_normal'], len(normal_patients)),\n        replace=False\n    ))\n    \n    selected_patients = selected_fracture + selected_normal\n    print(f\"Selected: {len(selected_patients)} patients\")\n    \n    slices_dir = os.path.join(CONFIG['output_dir'], 'slices')\n    os.makedirs(slices_dir, exist_ok=True)\n    \n    slice_metadata = []\n    total_slices = 0\n    \n    print(\"\\nExtracting with enhanced preprocessing...\")\n    for patient_id in tqdm(selected_patients, desc=\"Processing\"):\n        patient_dir = os.path.join(CONFIG['train_images'], str(patient_id))\n        \n        if not os.path.exists(patient_dir):\n            continue\n        \n        try:\n            dcm_files = sorted(glob(os.path.join(patient_dir, \"*.dcm\")))\n            \n            if len(dcm_files) < 10:\n                continue\n            \n            # Cervical region\n            n_files = len(dcm_files)\n            start_idx = int(n_files * 0.15)\n            end_idx = int(n_files * 0.45)\n            \n            for i in range(start_idx, min(end_idx, start_idx + 40)):  # More slices\n                try:\n                    # Enhanced preprocessing\n                    img = preprocess_dicom_enhanced(dcm_files[i])\n                    \n                    # Save\n                    filename = f\"{patient_id}_slice_{i:04d}.png\"\n                    filepath = os.path.join(slices_dir, filename)\n                    cv2.imwrite(filepath, (img * 255).astype(np.uint8))\n                    \n                    # Metadata\n                    patient_row = train_df[train_df['StudyInstanceUID'] == patient_id].iloc[0]\n                    \n                    slice_metadata.append({\n                        'patient_id': str(patient_id),\n                        'slice_idx': i,\n                        'filename': filename,\n                        'filepath': filepath,\n                        'has_fracture': int(patient_row['patient_overall']),\n                        'c1': int(patient_row['C1']),\n                        'c2': int(patient_row['C2']),\n                        'c3': int(patient_row['C3']),\n                        'c4': int(patient_row['C4']),\n                        'c5': int(patient_row['C5']),\n                        'c6': int(patient_row['C6']),\n                        'c7': int(patient_row['C7']),\n                    })\n                    \n                    total_slices += 1\n                \n                except:\n                    continue\n        \n        except:\n            continue\n    \n    slice_df = pd.DataFrame(slice_metadata)\n    slice_df.to_csv(os.path.join(CONFIG['output_dir'], 'slice_metadata.csv'), index=False)\n    \n    print(f\"\\n✅ Extracted {total_slices} slices with enhanced preprocessing\")\n    \n    tracker.add_step1({\n        'patients_processed': len(selected_patients),\n        'total_slices': total_slices,\n        'fracture_slices': slice_df['has_fracture'].sum(),\n        'normal_slices': (1 - slice_df['has_fracture']).sum(),\n        'avg_slices_per_patient': total_slices / len(selected_patients),\n    })\n    \n    return slice_df\n\n# ============================================================================\n# FOCAL LOSS\n# ============================================================================\n\nclass FocalLoss(nn.Module):\n    \"\"\"Focal Loss for handling class imbalance\"\"\"\n    \n    def __init__(self, alpha=0.25, gamma=2.0):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n    \n    def forward(self, inputs, targets):\n        bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none')\n        pt = torch.exp(-bce_loss)\n        focal_loss = self.alpha * (1 - pt) ** self.gamma * bce_loss\n        return focal_loss.mean()\n\n# ============================================================================\n# ENHANCED 2D MODEL WITH RESIDUAL CONNECTIONS\n# ============================================================================\n\nclass ResidualBlock2D(nn.Module):\n    \"\"\"Residual block for 2D CNN\"\"\"\n    \n    def __init__(self, in_channels, out_channels, stride=1):\n        super().__init__()\n        \n        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n        \n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_channels, out_channels, 1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_channels)\n            )\n    \n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out += self.shortcut(x)\n        out = F.relu(out)\n        return out\n\nclass EnhancedSliceClassifier2D(nn.Module):\n    \"\"\"Enhanced 2D classifier with residual connections\"\"\"\n    \n    def __init__(self, dropout=0.4):\n        super().__init__()\n        \n        self.conv1 = nn.Sequential(\n            nn.Conv2d(1, 64, 7, stride=2, padding=3, bias=False),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(3, stride=2, padding=1)\n        )\n        \n        # Residual blocks\n        self.layer1 = self._make_layer(64, 64, 2, stride=1)\n        self.layer2 = self._make_layer(64, 128, 2, stride=2)\n        self.layer3 = self._make_layer(128, 256, 2, stride=2)\n        self.layer4 = self._make_layer(256, 512, 2, stride=2)\n        \n        self.avgpool = nn.AdaptiveAvgPool2d(1)\n        \n        self.classifier = nn.Sequential(\n            nn.Linear(512, 256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(256, 128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(128, 1),\n        )\n    \n    def _make_layer(self, in_channels, out_channels, num_blocks, stride):\n        layers = []\n        layers.append(ResidualBlock2D(in_channels, out_channels, stride))\n        for _ in range(1, num_blocks):\n            layers.append(ResidualBlock2D(out_channels, out_channels, 1))\n        return nn.Sequential(*layers)\n    \n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.layer1(x)\n        x = self.layer2(x)\n        x = self.layer3(x)\n        x = self.layer4(x)\n        x = self.avgpool(x)\n        x = x.view(x.size(0), -1)\n        x = self.classifier(x)\n        return x.squeeze(-1)\n\n# ============================================================================\n# ENHANCED DATASET WITH STRONGER AUGMENTATION\n# ============================================================================\n\nclass EnhancedSliceDataset(Dataset):\n    \"\"\"Dataset with stronger augmentation\"\"\"\n    \n    def __init__(self, metadata_df, augment=False):\n        self.metadata = metadata_df.reset_index(drop=True)\n        self.augment = augment\n    \n    def __len__(self):\n        return len(self.metadata)\n    \n    def __getitem__(self, idx):\n        row = self.metadata.iloc[idx]\n        \n        img = cv2.imread(row['filepath'], cv2.IMREAD_GRAYSCALE)\n        img = img.astype(np.float32) / 255.0\n        \n        # Strong augmentation\n        if self.augment and np.random.random() < CONFIG['aug_probability']:\n            # Horizontal flip\n            if np.random.random() < 0.5:\n                img = np.fliplr(img).copy()\n            \n            # Rotation (larger range)\n            if np.random.random() < 0.4:\n                angle = np.random.uniform(-15, 15)\n                img = scipy_rotate(img, angle, reshape=False, order=1)\n            \n            # Scale/zoom\n            if np.random.random() < 0.3:\n                scale = np.random.uniform(0.9, 1.1)\n                h, w = img.shape\n                new_h, new_w = int(h * scale), int(w * scale)\n                img_scaled = cv2.resize(img, (new_w, new_h))\n                \n                if scale > 1:\n                    start_h = (new_h - h) // 2\n                    start_w = (new_w - w) // 2\n                    img = img_scaled[start_h:start_h+h, start_w:start_w+w]\n                else:\n                    result = np.zeros((h, w), dtype=img.dtype)\n                    start_h = (h - new_h) // 2\n                    start_w = (w - new_w) // 2\n                    result[start_h:start_h+new_h, start_w:start_w+new_w] = img_scaled\n                    img = result\n            \n            # Brightness\n            if np.random.random() < 0.4:\n                factor = np.random.uniform(0.8, 1.2)\n                img = np.clip(img * factor, 0, 1)\n            \n            # Contrast\n            if np.random.random() < 0.4:\n                mean = img.mean()\n                factor = np.random.uniform(0.8, 1.2)\n                img = np.clip((img - mean) * factor + mean, 0, 1)\n            \n            # Gaussian noise\n            if np.random.random() < 0.3:\n                noise = np.random.normal(0, 0.02, img.shape)\n                img = np.clip(img + noise, 0, 1)\n            \n            # Gaussian blur\n            if np.random.random() < 0.2:\n                sigma = np.random.uniform(0.5, 1.5)\n                img = gaussian_filter(img, sigma=sigma)\n        \n        img = torch.from_numpy(img).unsqueeze(0).float()\n        label = torch.tensor(row['has_fracture'], dtype=torch.float32)\n        \n        return img, label\n\n# ============================================================================\n# STEP 2: TRAIN ENHANCED 2D MODEL\n# ============================================================================\n\ndef train_2d_enhanced(slice_df):\n    \"\"\"Train enhanced 2D classifier\"\"\"\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"STEP 2: TRAINING ENHANCED 2D MODEL\")\n    print(\"=\"*80)\n    \n    train_df, val_df = train_test_split(\n        slice_df,\n        test_size=0.2,\n        stratify=slice_df['has_fracture'],\n        random_state=CONFIG['random_seed']\n    )\n    \n    print(f\"\\nData: Train={len(train_df)}, Val={len(val_df)}\")\n    \n    train_dataset = EnhancedSliceDataset(train_df, augment=True)\n    val_dataset = EnhancedSliceDataset(val_df, augment=False)\n    \n    train_loader = DataLoader(train_dataset, batch_size=CONFIG['batch_size'], shuffle=True, num_workers=2, pin_memory=True)\n    val_loader = DataLoader(val_dataset, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=2, pin_memory=True)\n    \n    # Model\n    device = CONFIG['device']\n    model = EnhancedSliceClassifier2D().to(device)\n    \n    n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n    print(f\"\\n📊 Model: EnhancedSliceClassifier2D\")\n    print(f\"   Parameters: {n_params:,} (~{n_params*4/1024**2:.1f} MB)\")\n    \n    # Loss\n    if CONFIG['use_focal_loss']:\n        criterion = FocalLoss(alpha=CONFIG['focal_alpha'], gamma=CONFIG['focal_gamma'])\n        print(f\"   Loss: Focal Loss (alpha={CONFIG['focal_alpha']}, gamma={CONFIG['focal_gamma']})\")\n    else:\n        pos_weight = (len(train_df) - train_df['has_fracture'].sum()) / (train_df['has_fracture'].sum() + 1e-8)\n        criterion = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([pos_weight]).to(device))\n        print(f\"   Loss: BCE with pos_weight={pos_weight:.2f}\")\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['learning_rate_2d'], weight_decay=CONFIG['weight_decay'])\n    scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)\n    scaler = GradScaler() if CONFIG['use_amp'] else None\n    \n    print(f\"\\n🚀 Training for {CONFIG['epochs_2d']} epochs...\")\n    \n    best_val_loss = float('inf')\n    patience_counter = 0\n    \n    for epoch in range(CONFIG['epochs_2d']):\n        # Train\n        model.train()\n        train_loss = 0\n        \n        for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}\", leave=False):\n            images, labels = images.to(device), labels.to(device)\n            \n            optimizer.zero_grad()\n            \n            if CONFIG['use_amp']:\n                with autocast():\n                    outputs = model(images)\n                    loss = criterion(outputs, labels)\n                scaler.scale(loss).backward()\n                scaler.step(optimizer)\n                scaler.update()\n            else:\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n                loss.backward()\n                optimizer.step()\n            \n            train_loss += loss.item()\n        \n        train_loss /= len(train_loader)\n        \n        # Validate\n        model.eval()\n        val_loss = 0\n        val_preds = []\n        val_labels = []\n        \n        with torch.no_grad():\n            for images, labels in val_loader:\n                images, labels = images.to(device), labels.to(device)\n                \n                if CONFIG['use_amp']:\n                    with autocast():\n                        outputs = model(images)\n                        loss = criterion(outputs, labels)\n                else:\n                    outputs = model(images)\n                    loss = criterion(outputs, labels)\n                \n                val_loss += loss.item()\n                val_preds.extend(torch.sigmoid(outputs).cpu().numpy())\n                val_labels.extend(labels.cpu().numpy())\n        \n        val_loss /= len(val_loader)\n        scheduler.step()\n        \n        val_acc = accuracy_score(val_labels, (np.array(val_preds) > 0.5).astype(int))\n        \n        if (epoch + 1) % 5 == 0:\n            print(f\"  Epoch {epoch+1}: Train Loss={train_loss:.4f}, Val Loss={val_loss:.4f}, Val Acc={val_acc:.4f}\")\n        \n        # Save best\n        if val_loss < best_val_loss:\n            best_val_loss = val_loss\n            patience_counter = 0\n            os.makedirs(os.path.join(CONFIG['output_dir'], 'models'), exist_ok=True)\n            torch.save(model.state_dict(), os.path.join(CONFIG['output_dir'], 'models', 'detector_2d_best.pth'))\n        else:\n            patience_counter += 1\n        \n        if patience_counter >= CONFIG['patience']:\n            print(f\"\\n  Early stopping at epoch {epoch+1}\")\n            break\n        \n        torch.cuda.empty_cache()\n    \n    print(f\"\\n✅ Step 2 complete: Best val loss={best_val_loss:.4f}\")\n    \n    tracker.add_step2({\n        'model_name': 'EnhancedSliceClassifier2D',\n        'parameters': n_params,\n        'epochs_completed': epoch + 1,\n        'best_train_loss': train_loss,\n        'best_val_loss': best_val_loss,\n        'final_train_acc': accuracy_score(val_labels, (np.array(val_preds) > 0.5).astype(int)),\n        'final_val_acc': val_acc,\n    })\n    \n    return model\n\n# ============================================================================\n# STEP 3: 3D RECONSTRUCTION (SAME AS BEFORE)\n# ============================================================================\n\ndef reconstruct_3d_volumes(slice_df):\n    \"\"\"Create 3D volumes\"\"\"\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"STEP 3: 3D RECONSTRUCTION\")\n    print(\"=\"*80)\n    \n    vertebrae_dir = os.path.join(CONFIG['output_dir'], 'vertebrae_3d')\n    os.makedirs(vertebrae_dir, exist_ok=True)\n    \n    vertebrae_metadata = []\n    \n    for patient_id, group in tqdm(slice_df.groupby('patient_id'), desc=\"Creating 3D volumes\"):\n        if len(group) < 10:\n            continue\n        \n        try:\n            volume_slices = []\n            for _, row in group.sort_values('slice_idx').iterrows():\n                img = cv2.imread(row['filepath'], cv2.IMREAD_GRAYSCALE)\n                img = cv2.resize(img, (CONFIG['roi_size'][2], CONFIG['roi_size'][1]))\n                volume_slices.append(img)\n            \n            volume = np.array(volume_slices).astype(np.float32) / 255.0\n            \n            # Resample Z\n            if volume.shape[0] != CONFIG['roi_size'][0]:\n                zoom_factor = CONFIG['roi_size'][0] / volume.shape[0]\n                volume = zoom(volume, (zoom_factor, 1, 1), order=1)\n            \n            volume = volume[:CONFIG['roi_size'][0], :CONFIG['roi_size'][1], :CONFIG['roi_size'][2]]\n            \n            filename = f\"{patient_id}_volume.npy\"\n            np.save(os.path.join(vertebrae_dir, filename), volume)\n            \n            first_row = group.iloc[0]\n            vertebrae_metadata.append({\n                'patient_id': str(patient_id),\n                'filename': filename,\n                'filepath': os.path.join(vertebrae_dir, filename),\n                'has_fracture': int(first_row['has_fracture']),\n                'num_slices': len(group),\n            })\n        \n        except:\n            continue\n    \n    vert_df = pd.DataFrame(vertebrae_metadata)\n    vert_df.to_csv(os.path.join(CONFIG['output_dir'], 'vertebrae_metadata.csv'), index=False)\n    \n    print(f\"\\n✅ Created {len(vertebrae_metadata)} 3D volumes\")\n    \n    tracker.add_step3({\n        'patients_processed': slice_df['patient_id'].nunique(),\n        'total_3d_volumes': len(vertebrae_metadata),\n        'avg_volumes_per_patient': len(vertebrae_metadata) / slice_df['patient_id'].nunique(),\n        'min_slices_required': 10,\n    })\n    \n    return vert_df\n\n# ============================================================================\n# ENHANCED 3D MODEL WITH RESIDUAL CONNECTIONS\n# ============================================================================\n\nclass ResidualBlock3D(nn.Module):\n    \"\"\"3D Residual block\"\"\"\n    \n    def __init__(self, in_channels, out_channels, stride=1):\n        super().__init__()\n        \n        self.conv1 = nn.Conv3d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm3d(out_channels)\n        self.conv2 = nn.Conv3d(out_channels, out_channels, 3, stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm3d(out_channels)\n        \n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv3d(in_channels, out_channels, 1, stride=stride, bias=False),\n                nn.BatchNorm3d(out_channels)\n            )\n    \n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out += self.shortcut(x)\n        out = F.relu(out)\n        return out\n\nclass EnhancedFractureClassifier3D(nn.Module):\n    \"\"\"Enhanced 3D classifier with residual connections\"\"\"\n    \n    def __init__(self, dropout=0.4):\n        super().__init__()\n        \n        self.conv1 = nn.Sequential(\n            nn.Conv3d(1, 64, kernel_size=7, stride=2, padding=3, bias=False),\n            nn.BatchNorm3d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool3d(3, stride=2, padding=1)\n        )\n        \n        self.layer1 = self._make_layer(64, 64, 2, stride=1)\n        self.layer2 = self._make_layer(64, 128, 2, stride=2)\n        self.layer3 = self._make_layer(128, 256, 2, stride=2)\n        \n        self.avgpool = nn.AdaptiveAvgPool3d(1)\n        \n        self.classifier = nn.Sequential(\n            nn.Linear(256, 128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(128, 64),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(64, 1),\n        )\n    \n    def _make_layer(self, in_channels, out_channels, num_blocks, stride):\n        layers = []\n        layers.append(ResidualBlock3D(in_channels, out_channels, stride))\n        for _ in range(1, num_blocks):\n            layers.append(ResidualBlock3D(out_channels, out_channels, 1))\n        return nn.Sequential(*layers)\n    \n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.layer1(x)\n        x = self.layer2(x)\n        x = self.layer3(x)\n        x = self.avgpool(x)\n        x = x.view(x.size(0), -1)\n        x = self.classifier(x)\n        return x.squeeze(-1)\n\n# ============================================================================\n# ENHANCED DATASET WITH TTA\n# ============================================================================\n\nclass Enhanced3DDataset(Dataset):\n    \"\"\"3D dataset with augmentation\"\"\"\n    \n    def __init__(self, metadata_df, augment=False):\n        self.metadata = metadata_df.reset_index(drop=True)\n        self.augment = augment\n    \n    def __len__(self):\n        return len(self.metadata)\n    \n    def __getitem__(self, idx):\n        row = self.metadata.iloc[idx]\n        volume = np.load(row['filepath']).astype(np.float32)\n        \n        if self.augment and np.random.random() < CONFIG['aug_probability']:\n            # Flip\n            if np.random.random() < 0.5:\n                volume = np.flip(volume, axis=2).copy()\n            \n            # Rotation\n            if np.random.random() < 0.3:\n                angle = np.random.uniform(-12, 12)\n                volume = scipy_rotate(volume, angle, axes=(1, 2), reshape=False, order=1)\n            \n            # Brightness\n            if np.random.random() < 0.4:\n                volume = np.clip(volume * np.random.uniform(0.8, 1.2), 0, 1)\n            \n            # Contrast\n            if np.random.random() < 0.4:\n                mean = volume.mean()\n                volume = np.clip((volume - mean) * np.random.uniform(0.8, 1.2) + mean, 0, 1)\n            \n            # Noise\n            if np.random.random() < 0.3:\n                volume = np.clip(volume + np.random.normal(0, 0.02, volume.shape), 0, 1)\n        \n        volume = torch.from_numpy(volume).unsqueeze(0).float()\n        label = torch.tensor(row['has_fracture'], dtype=torch.float32)\n        patient_id = str(row['patient_id'])\n        \n        return volume, label, patient_id\n\n# ============================================================================\n# METRICS CALCULATION WITH mAP@50\n# ============================================================================\n\ndef calculate_map50(y_true, y_pred_proba, threshold=0.5):\n    \"\"\"Calculate mAP@50 (Average Precision at IoU=0.5)\"\"\"\n    # For classification, mAP@50 = Average Precision\n    from sklearn.metrics import average_precision_score\n    \n    if len(np.unique(y_true)) < 2:\n        return 0.5\n    \n    return average_precision_score(y_true, y_pred_proba)\n\ndef calculate_metrics_enhanced(y_true, y_pred_proba, y_pred_binary):\n    \"\"\"Calculate all metrics including mAP@50\"\"\"\n    \n    metrics = {}\n    \n    metrics['accuracy'] = accuracy_score(y_true, y_pred_binary)\n    metrics['precision'] = precision_score(y_true, y_pred_binary, zero_division=0)\n    metrics['recall'] = recall_score(y_true, y_pred_binary, zero_division=0)\n    metrics['f1'] = f1_score(y_true, y_pred_binary, zero_division=0)\n    \n    if len(np.unique(y_true)) > 1:\n        metrics['auc'] = roc_auc_score(y_true, y_pred_proba)\n        metrics['map50'] = calculate_map50(y_true, y_pred_proba)\n        metrics['ap'] = average_precision_score(y_true, y_pred_proba)\n    else:\n        metrics['auc'] = 0.5\n        metrics['map50'] = 0.5\n        metrics['ap'] = 0.5\n    \n    cm = confusion_matrix(y_true, y_pred_binary)\n    metrics['confusion_matrix'] = cm.tolist()\n    \n    if cm.shape == (2, 2):\n        tn, fp, fn, tp = cm.ravel()\n        metrics['specificity'] = tn / (tn + fp) if (tn + fp) > 0 else 0\n        metrics['sensitivity'] = tp / (tp + fn) if (tp + fn) > 0 else 0\n    else:\n        metrics['specificity'] = 0\n        metrics['sensitivity'] = 0\n    \n    return metrics\n\n# ============================================================================\n# TEST-TIME AUGMENTATION\n# ============================================================================\n\ndef predict_with_tta(model, volume, device, num_tta=5):\n    \"\"\"Predict with test-time augmentation\"\"\"\n    \n    model.eval()\n    predictions = []\n    \n    with torch.no_grad():\n        # Original\n        vol_tensor = torch.from_numpy(volume).unsqueeze(0).unsqueeze(0).float().to(device)\n        if CONFIG['use_amp']:\n            with autocast():\n                out = model(vol_tensor)\n        else:\n            out = model(vol_tensor)\n        predictions.append(torch.sigmoid(out).cpu().numpy()[0])\n        \n        # TTA variations\n        for _ in range(num_tta - 1):\n            vol_aug = volume.copy()\n            \n            # Random flip\n            if np.random.random() < 0.5:\n                vol_aug = np.flip(vol_aug, axis=2).copy()\n            \n            # Random rotation\n            if np.random.random() < 0.5:\n                angle = np.random.choice([-5, 5])\n                vol_aug = scipy_rotate(vol_aug, angle, axes=(1, 2), reshape=False, order=1)\n            \n            vol_tensor = torch.from_numpy(vol_aug).unsqueeze(0).unsqueeze(0).float().to(device)\n            if CONFIG['use_amp']:\n                with autocast():\n                    out = model(vol_tensor)\n            else:\n                out = model(vol_tensor)\n            predictions.append(torch.sigmoid(out).cpu().numpy()[0])\n    \n    return np.mean(predictions)\n\n# ============================================================================\n# STEP 4: TRAIN ENHANCED 3D CLASSIFIER\n# ============================================================================\n\ndef train_3d_enhanced(vert_df):\n    \"\"\"Train enhanced 3D classifier\"\"\"\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"STEP 4: TRAINING ENHANCED 3D CLASSIFIER\")\n    print(\"=\"*80)\n    \n    train_df, val_df = train_test_split(\n        vert_df,\n        test_size=0.2,\n        stratify=vert_df['has_fracture'],\n        random_state=CONFIG['random_seed']\n    )\n    \n    print(f\"\\nData: Train={len(train_df)}, Val={len(val_df)}\")\n    \n    train_dataset = Enhanced3DDataset(train_df, augment=True)\n    val_dataset = Enhanced3DDataset(val_df, augment=False)\n    \n    train_loader = DataLoader(train_dataset, batch_size=CONFIG['batch_size'], shuffle=True, num_workers=2, pin_memory=True)\n    val_loader = DataLoader(val_dataset, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=2, pin_memory=True)\n    \n    # Model\n    device = CONFIG['device']\n    model = EnhancedFractureClassifier3D().to(device)\n    \n    n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n    print(f\"\\n📊 Model: EnhancedFractureClassifier3D\")\n    print(f\"   Parameters: {n_params:,} (~{n_params*4/1024**2:.1f} MB)\")\n    \n    # Loss\n    if CONFIG['use_focal_loss']:\n        criterion = FocalLoss(alpha=CONFIG['focal_alpha'], gamma=CONFIG['focal_gamma'])\n    else:\n        pos_weight = (len(train_df) - train_df['has_fracture'].sum()) / (train_df['has_fracture'].sum() + 1e-8)\n        criterion = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([pos_weight]).to(device))\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['learning_rate_3d'], weight_decay=CONFIG['weight_decay'])\n    scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)\n    scaler = GradScaler() if CONFIG['use_amp'] else None\n    \n    print(f\"\\n🚀 Training for {CONFIG['epochs_3d']} epochs...\")\n    \n    best_val_auc = 0\n    patience_counter = 0\n    \n    for epoch in range(CONFIG['epochs_3d']):\n        # Train\n        model.train()\n        train_loss = 0\n        \n        for volumes, labels, _ in tqdm(train_loader, desc=f\"Epoch {epoch+1}\", leave=False):\n            volumes, labels = volumes.to(device), labels.to(device)\n            \n            optimizer.zero_grad()\n            \n            if CONFIG['use_amp']:\n                with autocast():\n                    outputs = model(volumes)\n                    loss = criterion(outputs, labels)\n                scaler.scale(loss).backward()\n                scaler.step(optimizer)\n                scaler.update()\n            else:\n                outputs = model(volumes)\n                loss = criterion(outputs, labels)\n                loss.backward()\n                optimizer.step()\n            \n            train_loss += loss.item()\n        \n        train_loss /= len(train_loader)\n        \n        # Validate\n        model.eval()\n        val_loss = 0\n        val_preds = []\n        val_labels = []\n        val_patients = []\n        \n        with torch.no_grad():\n            for volumes, labels, patients in val_loader:\n                volumes, labels = volumes.to(device), labels.to(device)\n                \n                if CONFIG['use_amp']:\n                    with autocast():\n                        outputs = model(volumes)\n                        loss = criterion(outputs, labels)\n                else:\n                    outputs = model(volumes)\n                    loss = criterion(outputs, labels)\n                \n                val_loss += loss.item()\n                val_preds.extend(torch.sigmoid(outputs).cpu().numpy())\n                val_labels.extend(labels.cpu().numpy())\n                val_patients.extend(patients)\n        \n        val_loss /= len(val_loader)\n        scheduler.step()\n        \n        val_preds_arr = np.array(val_preds)\n        val_labels_arr = np.array(val_labels)\n        val_auc = roc_auc_score(val_labels_arr, val_preds_arr) if len(np.unique(val_labels_arr)) > 1 else 0.5\n        \n        if (epoch + 1) % 5 == 0:\n            print(f\"  Epoch {epoch+1}: Train Loss={train_loss:.4f}, Val Loss={val_loss:.4f}, Val AUC={val_auc:.4f}\")\n        \n        # Save best\n        if val_auc > best_val_auc:\n            best_val_auc = val_auc\n            patience_counter = 0\n            torch.save(model.state_dict(), os.path.join(CONFIG['output_dir'], 'models', 'classifier_3d_best.pth'))\n        else:\n            patience_counter += 1\n        \n        if patience_counter >= CONFIG['patience']:\n            print(f\"\\n  Early stopping at epoch {epoch+1}\")\n            break\n        \n        torch.cuda.empty_cache()\n    \n    # Load best model\n    model.load_state_dict(torch.load(os.path.join(CONFIG['output_dir'], 'models', 'classifier_3d_best.pth')))\n    \n    # Final evaluation with TTA\n    print(f\"\\n📊 Final evaluation...\")\n    \n    if CONFIG['use_tta']:\n        print(f\"   Using Test-Time Augmentation ({CONFIG['tta_steps']} steps)\")\n        \n        val_preds_tta = []\n        val_labels_final = []\n        val_patients_final = []\n        \n        for idx in tqdm(range(len(val_dataset)), desc=\"TTA Inference\"):\n            volume, label, patient = val_dataset[idx]\n            volume_np = volume.squeeze(0).numpy()\n            \n            pred_tta = predict_with_tta(model, volume_np, device, CONFIG['tta_steps'])\n            \n            val_preds_tta.append(pred_tta)\n            val_labels_final.append(label.numpy())\n            val_patients_final.append(patient)\n        \n        val_preds = np.array(val_preds_tta)\n        val_labels = np.array(val_labels_final)\n        val_patients = val_patients_final\n    else:\n        val_preds = val_preds_arr\n        val_labels = val_labels_arr\n    \n    val_pred_binary = (val_preds > 0.5).astype(int)\n    \n    # Vertebra-level metrics\n    vert_metrics = calculate_metrics_enhanced(val_labels, val_preds, val_pred_binary)\n    \n    # Patient-level aggregation\n    patient_preds_dict = {}\n    patient_labels_dict = {}\n    \n    for pred, label, patient in zip(val_preds, val_labels, val_patients):\n        if patient not in patient_preds_dict:\n            patient_preds_dict[patient] = []\n            patient_labels_dict[patient] = label\n        patient_preds_dict[patient].append(pred)\n    \n    patient_preds_agg = np.array([max(patient_preds_dict[p]) for p in patient_preds_dict.keys()])\n    patient_labels_agg = np.array([patient_labels_dict[p] for p in patient_preds_dict.keys()])\n    patient_pred_binary = (patient_preds_agg > 0.5).astype(int)\n    \n    # Patient-level metrics\n    pat_metrics = calculate_metrics_enhanced(patient_labels_agg, patient_preds_agg, patient_pred_binary)\n    \n    print(f\"\\n✅ Step 4 Complete:\")\n    print(f\"\\n  PATIENT-LEVEL RESULTS:\")\n    print(f\"    Accuracy: {pat_metrics['accuracy']*100:.2f}%\")\n    print(f\"    AUC: {pat_metrics['auc']:.4f}\")\n    print(f\"    mAP@50: {pat_metrics['map50']:.4f}\")\n    print(f\"    F1: {pat_metrics['f1']:.4f}\")\n    \n    # Save predictions\n    predictions_df = pd.DataFrame({\n        'patient_id': list(patient_preds_dict.keys()),\n        'true_label': patient_labels_agg,\n        'predicted_prob': patient_preds_agg,\n        'predicted_class': patient_pred_binary,\n    })\n    predictions_df.to_csv(os.path.join(CONFIG['output_dir'], 'predictions.csv'), index=False)\n    \n    tracker.add_step4({\n        'model_name': 'EnhancedFractureClassifier3D',\n        'parameters': n_params,\n        'epochs_completed': epoch + 1,\n        'vertebra_level': vert_metrics,\n        'patient_level': pat_metrics,\n    })\n    \n    return model, vert_metrics, pat_metrics\n\n# ============================================================================\n# MAIN\n# ============================================================================\n\ndef main():\n    \"\"\"Run optimized pipeline\"\"\"\n    \n    print(\"\\n🎯 Starting OPTIMIZED pipeline...\")\n    print(f\"   Target: 85-90% accuracy with mAP@50\")\n    print(f\"   This will take 1-3 hours\\n\")\n    \n    try:\n        # Step 1\n        slice_df = extract_slices_enhanced()\n        \n        if len(slice_df) < 50:\n            print(f\"\\n⚠️  Warning: Only {len(slice_df)} slices. Need more data!\")\n            print(f\"   Increase num_patients to 200-300 for better results\")\n        \n        # Step 2\n        detector = train_2d_enhanced(slice_df)\n        \n        # Step 3\n        vert_df = reconstruct_3d_volumes(slice_df)\n        \n        if len(vert_df) < 20:\n            print(f\"\\n⚠️  Warning: Only {len(vert_df)} volumes. Need more data!\")\n        \n        # Step 4\n        classifier, vert_metrics, pat_metrics = train_3d_enhanced(vert_df)\n        \n        # Report\n        tracker.save_report(CONFIG['output_dir'])\n        \n        print(\"\\n\" + \"=\"*80)\n        print(\"✅ OPTIMIZED PIPELINE COMPLETE!\")\n        print(\"=\"*80)\n        \n        print(f\"\\n📂 Results: {CONFIG['output_dir']}/\")\n        print(f\"📄 Read: FINAL_RESULTS_MAP50.txt\")\n        \n        print(f\"\\n🎯 FINAL RESULTS:\")\n        print(f\"   Accuracy: {pat_metrics['accuracy']*100:.2f}%\")\n        print(f\"   AUC: {pat_metrics['auc']:.4f}\")\n        print(f\"   mAP@50: {pat_metrics['map50']:.4f}\")\n        \n        # Assessment\n        if pat_metrics['accuracy'] >= 0.85:\n            print(f\"\\n   🎉 EXCELLENT! Target achieved!\")\n        elif pat_metrics['accuracy'] >= 0.80:\n            print(f\"\\n   👍 VERY GOOD! Close to target.\")\n            print(f\"   💡 To reach 85-90%: Increase to 300 patients\")\n        elif pat_metrics['accuracy'] >= 0.75:\n            print(f\"\\n   ✓ GOOD! Making progress.\")\n            print(f\"   💡 To improve:\")\n            print(f\"      - Use 250-300 patients\")\n            print(f\"      - Train for 60-80 epochs\")\n        else:\n            print(f\"\\n   ⚠️  Need improvement\")\n            print(f\"   💡 Recommendations:\")\n            print(f\"      - Increase to 300+ patients\")\n            print(f\"      - More epochs (60-80)\")\n            print(f\"      - Check data quality\")\n    \n    except Exception as e:\n        print(f\"\\n❌ Error: {str(e)}\")\n        import traceback\n        traceback.print_exc()\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}