{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":103103,"databundleVersionId":13042974,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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\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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-29T13:10:10.405642Z","iopub.execute_input":"2025-08-29T13:10:10.405853Z","iopub.status.idle":"2025-08-29T13:10:11.598949Z","shell.execute_reply.started":"2025-08-29T13:10:10.405829Z","shell.execute_reply":"2025-08-29T13:10:11.598253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hybrid Faster R-CNN + U-Net++ Pipeline for Dental Detection and Segmentation\n!pip install -q segmentation-models-pytorch==0.3.3 --no-deps\n!pip install -q pretrainedmodels==0.7.4 timm==0.9.2 efficientnet-pytorch==0.7.1\n\nimport torch\ntorch.cuda.empty_cache()\n\nimport os\nimport sys\nimport glob\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\nimport warnings\nfrom PIL import Image\nimport random\nfrom sklearn.model_selection import train_test_split\nimport yaml\nfrom collections import defaultdict\nimport logging\n\n\nwarnings.filterwarnings('ignore')\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n# Set seeds\nrandom.seed(42)\nnp.random.seed(42)\ntorch.manual_seed(42)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed(42)\n\nimport segmentation_models_pytorch as smp\nimport torchvision\nfrom torchvision.models.detection import FasterRCNN\nfrom torchvision.models.detection.backbone_utils import resnet_fpn_backbone\n\nprint(f'PyTorch Version: {torch.__version__}')\nprint(f'CUDA Available: {torch.cuda.is_available()}')\n\n# Configuration\nclass Config:\n    # Paths - adjust for your environment\n    BASE_PATH = '/kaggle/input/alpha-dent/AlphaDent'\n    TRAIN_IMAGES_PATH = f'{BASE_PATH}/images/train'\n    VALID_IMAGES_PATH = f'{BASE_PATH}/images/valid'\n    TEST_IMAGES_PATH = f'{BASE_PATH}/images/test'\n    TRAIN_LABELS_PATH = f'{BASE_PATH}/labels/train'\n    VALID_LABELS_PATH = f'{BASE_PATH}/labels/valid'\n    \n    YAML_PATH = f'{BASE_PATH}/yolo_seg_train.yaml'\n    \n    OUTPUT_DIR = '/kaggle/working/'\n    MODEL_DIR = f'{OUTPUT_DIR}/hybrid_model'\n    \n    IMAGE_SIZE = 640\n    BATCH_SIZE = 4\n    LEARNING_RATE = 1e-4\n    EPOCHS = 1\n    \n    # Detection parameters\n    DETECTION_THRESHOLD = 0.5\n    NMS_THRESHOLD = 0.3\n    \n    # Class information (from YAML file)\n    CLASS_NAMES = []\n    NUM_SEGMENTATION_CLASSES = 0\n\n# Load class names from YAML file\ndef load_class_names(yaml_path):\n    \"\"\"Load class names from YAML file.\"\"\"\n    try:\n        with open(yaml_path, 'r') as f:\n            data = yaml.safe_load(f)\n        class_names = data['names']\n        Config.CLASS_NAMES = class_names\n        Config.NUM_SEGMENTATION_CLASSES = len(class_names)\n        print(f\"Loaded {len(class_names)} classes: {class_names}\")\n        return class_names\n    except Exception as e:\n        print(f\"Error loading YAML file: {e}\")\n        # Default classes if YAML loading fails\n        Config.CLASS_NAMES = [\n            'Abrasion', 'Filling', 'Crown', 'Caries Class 1', 'Caries Class 2',\n            'Caries Class 3', 'Caries Class 4', 'Caries Class 5', 'Caries Class 6'\n        ]\n        Config.NUM_SEGMENTATION_CLASSES = len(Config.CLASS_NAMES)\n        return Config.CLASS_NAMES\n\n# Load class names\nclass_names = load_class_names(Config.YAML_PATH)\nos.makedirs(Config.MODEL_DIR, exist_ok=True)\n\n# Enhanced Dataset for both Detection and Segmentation\nclass HybridDentalDataset(Dataset):\n    \"\"\"Dataset that provides both detection boxes and segmentation masks.\"\"\"\n    \n    def __init__(self, image_paths, labels_path, transform=None, \n                 image_size=640, mode='segmentation'):\n        self.image_paths = image_paths\n        self.labels_path = labels_path\n        self.transform = transform\n        self.image_size = image_size\n        self.mode = mode  # 'detection', 'segmentation', or 'hybrid'\n        \n        # Filter valid images\n        self.valid_images = []\n        for img_path in image_paths:\n            base_name = os.path.splitext(os.path.basename(img_path))[0]\n            label_file = os.path.join(labels_path, f\"{base_name}.txt\")\n            if os.path.exists(label_file):\n                self.valid_images.append(img_path)\n        \n        print(f\"Found {len(self.valid_images)} images with labels\")\n    \n    def __len__(self):\n        return len(self.valid_images)\n    \n    def __getitem__(self, idx):\n        img_path = self.valid_images[idx]\n        base_name = os.path.splitext(os.path.basename(img_path))[0]\n        \n        # Load image\n        image = cv2.imread(img_path)\n        if image is None:\n            image = np.zeros((self.image_size, self.image_size, 3), dtype=np.uint8)\n        else:\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            image = cv2.resize(image, (self.image_size, self.image_size))\n        \n        # Load annotations\n        label_file = os.path.join(self.labels_path, f\"{base_name}.txt\")\n        \n        # For this task, we'll focus on segmentation only\n        mask = self.yolo_to_mask(label_file, (self.image_size, self.image_size))\n        \n        # Apply transforms\n        if self.transform:\n            augmented = self.transform(image=image, mask=mask)\n            image = augmented['image']\n            mask = augmented['mask']\n        \n        return image, mask.long()\n    \n    def yolo_to_mask(self, label_path, target_size):\n        \"\"\"Convert YOLO polygon annotations to segmentation mask.\"\"\"\n        h, w = target_size\n        mask = np.zeros((h, w), dtype=np.uint8)\n        \n        if os.path.exists(label_path):\n            try:\n                with open(label_path, 'r') as f:\n                    lines = f.readlines()\n                \n                for line in lines:\n                    parts = line.strip().split()\n                    if len(parts) < 6:\n                        continue\n                    \n                    class_id = int(parts[0]) + 1  # +1 for background\n                    if class_id >= Config.NUM_SEGMENTATION_CLASSES:\n                        continue\n                    \n                    coords = list(map(float, parts[1:]))\n                    if len(coords) % 2 != 0:\n                        continue\n                    \n                    points = []\n                    for i in range(0, len(coords), 2):\n                        x = int(coords[i] * w)\n                        y = int(coords[i+1] * h)\n                        points.append([x, y])\n                    \n                    if len(points) >= 3:\n                        points = np.array(points, dtype=np.int32)\n                        cv2.fillPoly(mask, [points], class_id)\n            \n            except Exception as e:\n                print(f\"Error processing segmentation mask {label_path}: {e}\")\n        \n        return mask\n\n# Stage 3: U-Net++ for Detailed Segmentation (Modified for 9 classes)\nclass DentalSegmentationModel(nn.Module):\n    \"\"\"U-Net++ model for detailed dental condition segmentation.\"\"\"\n    \n    def __init__(self, encoder_name=\"efficientnet-b4\", \n                 num_classes=Config.NUM_SEGMENTATION_CLASSES):\n        super(DentalSegmentationModel, self).__init__()\n        \n        self.model = smp.UnetPlusPlus(\n            encoder_name=encoder_name,\n            encoder_weights=\"imagenet\",\n            in_channels=3,\n            classes=num_classes,\n            activation=None,\n        )\n    \n    def forward(self, x):\n        return self.model(x)\n\n# Hybrid Model combining all approaches\nclass HybridDentalModel(nn.Module):\n    \"\"\"Complete hybrid model combining jaw classification, detection, and segmentation.\"\"\"\n    \n    def __init__(self):\n        super(HybridDentalModel, self).__init__()\n        \n        self.segmentation_model = DentalSegmentationModel()\n    \n    def forward(self, x, mode='segmentation'):\n        \"\"\"\n        Forward pass with different modes:\n        - 'segmentation': Segment dental conditions\n        - 'inference': Complete pipeline\n        \"\"\"\n        \n        if mode == 'segmentation':\n            return self.segmentation_model(x)\n        \n        elif mode == 'inference':\n            # For inference, we'll focus on segmentation only\n            results = {}\n            \n            # Segment dental conditions\n            segmentation_results = self.segmentation_model(x)\n            results['segmentation'] = segmentation_results\n            \n            return results\n\n# Enhanced Loss Functions\nclass HybridLoss(nn.Module):\n    \"\"\"Combined loss for the hybrid model.\"\"\"\n    \n    def __init__(self, seg_weight=1.0):\n        super(HybridLoss, self).__init__()\n        self.seg_weight = seg_weight\n        \n        # Segmentation losses\n        self.ce_loss = nn.CrossEntropyLoss()\n        self.dice_loss = self.dice_loss_fn\n    \n    def dice_loss_fn(self, inputs, targets):\n        \"\"\"Dice loss for segmentation.\"\"\"\n        inputs = F.softmax(inputs, dim=1)\n        dice_loss = 0\n        \n        for i in range(1, inputs.shape[1]):  # Skip background class\n            input_flat = inputs[:, i].contiguous().view(-1)\n            target_flat = (targets == i).float().view(-1)\n            \n            intersection = (input_flat * target_flat).sum()\n            dice = (2. * intersection + 1) / (input_flat.sum() + target_flat.sum() + 1)\n            dice_loss += (1 - dice)\n        \n        return dice_loss / (inputs.shape[1] - 1)  # Exclude background\n    \n    def forward(self, predictions, targets):\n        total_loss = 0\n        \n        if 'segmentation' in predictions:\n            seg_pred = predictions['segmentation']\n            seg_target = targets['masks']\n            \n            ce_loss = self.ce_loss(seg_pred, seg_target)\n            dice_loss = self.dice_loss(seg_pred, seg_target)\n            seg_loss = ce_loss + dice_loss\n            \n            total_loss += self.seg_weight * seg_loss\n        \n        return total_loss\n\n# mAP@50 Evaluation Metric\nclass MAP50Evaluator:\n    \"\"\"Calculate mAP@50 for segmentation tasks.\"\"\"\n    \n    def __init__(self, num_classes, iou_threshold=0.5):\n        self.num_classes = num_classes\n        self.iou_threshold = iou_threshold\n        self.reset()\n    \n    def reset(self):\n        self.tp = [0] * self.num_classes\n        self.fp = [0] * self.num_classes\n        self.fn = [0] * self.num_classes\n    \n    def calculate_iou(self, mask1, mask2):\n        \"\"\"Calculate Intersection over Union for two binary masks.\"\"\"\n        intersection = np.logical_and(mask1, mask2).sum()\n        union = np.logical_or(mask1, mask2).sum()\n        return intersection / (union + 1e-10)\n    \n    def update(self, pred_masks, true_masks):\n        \"\"\"Update metrics with new batch of predictions and ground truth.\"\"\"\n        batch_size = pred_masks.shape[0]\n        \n        for i in range(batch_size):\n            pred_mask = pred_masks[i]\n            true_mask = true_masks[i]\n            \n            for class_id in range(1, self.num_classes):  # Skip background\n                pred_class_mask = (pred_mask == class_id)\n                true_class_mask = (true_mask == class_id)\n                \n                # If both have no instances, skip\n                if not np.any(pred_class_mask) and not np.any(true_class_mask):\n                    continue\n                \n                # If prediction exists but no ground truth - FP\n                if np.any(pred_class_mask) and not np.any(true_class_mask):\n                    self.fp[class_id] += 1\n                    continue\n                \n                # If ground truth exists but no prediction - FN\n                if not np.any(pred_class_mask) and np.any(true_class_mask):\n                    self.fn[class_id] += 1\n                    continue\n                \n                # Calculate IoU\n                iou = self.calculate_iou(pred_class_mask, true_class_mask)\n                \n                if iou >= self.iou_threshold:\n                    self.tp[class_id] += 1\n                else:\n                    self.fp[class_id] += 1\n    \n    def compute_map50(self):\n        \"\"\"Compute mAP@50 across all classes.\"\"\"\n        ap_scores = []\n        \n        for class_id in range(1, self.num_classes):  # Skip background\n            tp = self.tp[class_id]\n            fp = self.fp[class_id]\n            fn = self.fn[class_id]\n            \n            precision = tp / (tp + fp + 1e-10)\n            recall = tp / (tp + fn + 1e-10)\n            \n            # AP for this class (simplified - actual AP calculation is more complex)\n            ap = precision  # Simplified version for monitoring\n            ap_scores.append(ap)\n        \n        if len(ap_scores) == 0:\n            return 0.0\n        \n        return np.mean(ap_scores)\n    \n    def compute_class_metrics(self):\n        \"\"\"Compute precision, recall, and AP for each class.\"\"\"\n        metrics = {}\n        for class_id in range(1, self.num_classes):\n            tp = self.tp[class_id]\n            fp = self.fp[class_id]\n            fn = self.fn[class_id]\n            \n            precision = tp / (tp + fp + 1e-10)\n            recall = tp / (tp + fn + 1e-10)\n            f1 = 2 * (precision * recall) / (precision + recall + 1e-10)\n            \n            metrics[class_id] = {\n                'precision': precision,\n                'recall': recall,\n                'f1': f1,\n                'tp': tp,\n                'fp': fp,\n                'fn': fn\n            }\n        \n        return metrics\n\n# Training Pipeline\nclass HybridTrainer:\n    \"\"\"Training pipeline for the hybrid model.\"\"\"\n    \n    def __init__(self, model, device):\n        self.model = model\n        self.device = device\n        self.criterion = HybridLoss()\n        \n        # mAP@50 evaluator\n        self.evaluator = MAP50Evaluator(Config.NUM_SEGMENTATION_CLASSES)\n        \n        # Optimizer for segmentation\n        self.seg_optimizer = torch.optim.AdamW(\n            self.model.segmentation_model.parameters(), \n            lr=Config.LEARNING_RATE\n        )\n        \n        # Learning rate scheduler\n        self.seg_scheduler = torch.optim.lr_scheduler.StepLR(self.seg_optimizer, step_size=10, gamma=0.1)\n    \n    def train_stage(self, loader, epoch=0):\n        \"\"\"Train segmentation model.\"\"\"\n        self.model.train()\n        \n        total_loss = 0\n        num_batches = 0\n        \n        pbar = tqdm(loader, desc=f'Training segmentation')\n        \n        for images, masks in pbar:\n            images = images.to(self.device)\n            masks = masks.to(self.device)\n            \n            self.seg_optimizer.zero_grad()\n            outputs = self.model(images, mode='segmentation')\n            loss = self.criterion.ce_loss(outputs, masks) + self.criterion.dice_loss(outputs, masks)\n            \n            loss.backward()\n            self.seg_optimizer.step()\n            \n            total_loss += loss.item()\n            num_batches += 1\n            pbar.set_postfix({'loss': loss.item()})\n        \n        self.seg_scheduler.step()\n        return total_loss / num_batches\n    \n    def evaluate(self, loader, epoch=0):\n        \"\"\"Evaluate model and compute mAP@50.\"\"\"\n        self.model.eval()\n        self.evaluator.reset()\n        \n        total_loss = 0\n        num_batches = 0\n        \n        with torch.no_grad():\n            pbar = tqdm(loader, desc=f'Validation')\n            for images, masks in pbar:\n                images = images.to(self.device)\n                masks = masks.to(self.device)\n                \n                outputs = self.model(images, mode='segmentation')\n                loss = self.criterion.ce_loss(outputs, masks) + self.criterion.dice_loss(outputs, masks)\n                total_loss += loss.item()\n                num_batches += 1\n                \n                # Convert to numpy for mAP calculation\n                pred_masks = torch.argmax(F.softmax(outputs, dim=1), dim=1).cpu().numpy()\n                true_masks = masks.cpu().numpy()\n                \n                # Update mAP evaluator\n                self.evaluator.update(pred_masks, true_masks)\n                \n                pbar.set_postfix({'val_loss': loss.item()})\n        \n        # Compute metrics\n        val_loss = total_loss / num_batches\n        map50 = self.evaluator.compute_map50()\n        class_metrics = self.evaluator.compute_class_metrics()\n        \n        return val_loss, map50, class_metrics\n\n# Inference Pipeline\nclass HybridInference:\n    \"\"\"Complete inference pipeline combining all model outputs.\"\"\"\n    \n    def __init__(self, model, device):\n        self.model = model\n        self.device = device\n        self.model.eval()\n    \n    def predict(self, image_path):\n        \"\"\"Run complete inference pipeline on a single image.\"\"\"\n        # Load and preprocess image\n        image = cv2.imread(image_path)\n        if image is None:\n            return None\n        \n        original_image = image.copy()\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        original_h, original_w = image.shape[:2]\n        \n        # Transform for inference\n        transform = A.Compose([\n            A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),\n            A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n            ToTensorV2(),\n        ])\n        \n        augmented = transform(image=image)\n        image_tensor = augmented['image'].unsqueeze(0).to(self.device)\n        \n        with torch.no_grad():\n            # Run complete pipeline\n            results = self.model(image_tensor, mode='inference')\n            \n            # Process results\n            processed_results = {\n                'image_path': image_path,\n                'original_size': (original_w, original_h)\n            }\n            \n            # Segmentation\n            if 'segmentation' in results:\n                seg_probs = F.softmax(results['segmentation'], dim=1)\n                seg_mask = torch.argmax(seg_probs, dim=1).squeeze().cpu().numpy()\n                \n                # Resize back to original size\n                seg_mask_resized = cv2.resize(\n                    seg_mask.astype(np.uint8),\n                    (original_w, original_h),\n                    interpolation=cv2.INTER_NEAREST\n                )\n                \n                processed_results['segmentation'] = {\n                    'mask': seg_mask_resized,\n                    'probabilities': seg_probs.squeeze().cpu().numpy()\n                }\n        \n        return processed_results\n    \n    def extract_polygons_from_mask(self, mask, min_area=100):\n        \"\"\"Extract polygons from segmentation mask for each class.\"\"\"\n        polygons = []\n        \n        for class_id in range(1, Config.NUM_SEGMENTATION_CLASSES):  # Skip background\n            # Create binary mask for this class\n            class_mask = (mask == class_id).astype(np.uint8)\n            \n            # Find contours\n            contours, _ = cv2.findContours(class_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n            \n            for contour in contours:\n                # Filter by area\n                area = cv2.contourArea(contour)\n                if area < min_area:\n                    continue\n                \n                # Simplify polygon\n                epsilon = 0.002 * cv2.arcLength(contour, True)\n                approx = cv2.approxPolyDP(contour, epsilon, True)\n                \n                if len(approx) >= 3:  # Need at least 3 points for a polygon\n                    # Normalize coordinates to [0, 1]\n                    h, w = mask.shape\n                    normalized_coords = []\n                    for point in approx:\n                        x_norm = float(point[0][0]) / w\n                        y_norm = float(point[0][1]) / h\n                        # Ensure coordinates are within [0, 1]\n                        x_norm = max(0.0, min(1.0, x_norm))\n                        y_norm = max(0.0, min(1.0, y_norm))\n                        normalized_coords.extend([x_norm, y_norm])\n                    \n                    # Format polygon string\n                    poly_str = ' '.join([f'{coord:.6f}' for coord in normalized_coords])\n                    \n                    # Estimate confidence based on area (this is a simplification)\n                    confidence = min(1.0, area / (w * h) * 10)\n                    \n                    polygons.append({\n                        'class_id': class_id - 1,  # Convert back to 0-indexed\n                        'confidence': confidence,\n                        'poly': poly_str\n                    })\n        \n        return polygons\n    \n    def predict_test_set(self, test_images_path, output_dir):\n        \"\"\"Run inference on all test images and save results in Kaggle format.\"\"\"\n        test_images = sorted(glob.glob(f\"{test_images_path}/*.jpg\"))\n        os.makedirs(output_dir, exist_ok=True)\n        \n        results = []\n        for img_path in tqdm(test_images, desc=\"Processing test images\"):\n            result = self.predict(img_path)\n            if result and 'segmentation' in result:\n                # Get image ID (filename without extension)\n                image_id = os.path.splitext(os.path.basename(img_path))[0]\n                \n                # Extract polygons from mask\n                mask = result['segmentation']['mask']\n                polygons = self.extract_polygons_from_mask(mask)\n                \n                # Add to results\n                for poly in polygons:\n                    results.append({\n                        'patient_id': image_id,\n                        'class_id': poly['class_id'],\n                        'confidence': poly['confidence'],\n                        'poly': poly['poly']\n                    })\n        \n        # Create submission DataFrame\n        submission_df = pd.DataFrame(results)\n        \n        # Ensure all test images have at least one prediction\n        all_test_ids = [os.path.splitext(os.path.basename(img))[0] for img in test_images]\n        if len(submission_df) > 0:\n            predicted_ids = submission_df['patient_id'].unique()\n            missing_ids = set(all_test_ids) - set(predicted_ids)\n        else:\n            missing_ids = set(all_test_ids)\n\n        # Add dummy predictions for images without detections\n        if missing_ids:\n            print(f\"Adding dummy predictions for {len(missing_ids)} images without detections\")\n            dummy_rows = []\n            for img_id in missing_ids:\n                # Create a small dummy polygon\n                dummy_rows.append({\n                    'patient_id': img_id,\n                    'class_id': 0,  # Default to class 0 (Abrasion)\n                    'confidence': 0.01,  # Very low confidence\n                    'poly': '0.1 0.1 0.1 0.2 0.2 0.2 0.2 0.1'  # Small square polygon\n                })\n            \n            submission_df = pd.concat([submission_df, pd.DataFrame(dummy_rows)], ignore_index=True)\n\n        # Sort by patient_id and then by confidence (descending)\n        submission_df = submission_df.sort_values(['patient_id', 'confidence'], ascending=[True, False])\n\n        # Ensure correct column order\n        submission_df = submission_df[['patient_id', 'class_id', 'confidence', 'poly']]\n\n        # Save the main submission file\n        submission_df.to_csv(f'{output_dir}/submission.csv', index=False)\n        print(f\"Submission file created: {output_dir}/submission.csv\")\n        \n        return submission_df\n\n# Main training function\ndef train_hybrid_model():\n    \"\"\"Train the complete hybrid model.\"\"\"\n    print(\"=== Starting Hybrid Training Pipeline ===\")\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"Using device: {device}\")\n    \n    # Create datasets\n    train_images = sorted(glob.glob(f'{Config.TRAIN_IMAGES_PATH}/*.jpg'))\n    valid_images = sorted(glob.glob(f'{Config.VALID_IMAGES_PATH}/*.jpg'))\n    \n    print(f\"Training images: {len(train_images)}\")\n    print(f\"Validation images: {len(valid_images)}\")\n    \n    # Training model\n    model = HybridDentalModel().to(device)\n    trainer = HybridTrainer(model, device)\n    \n    # Train segmentation model\n    print(\"\\n=== Training Dental Segmentation ===\")\n    \n    seg_transform = A.Compose([\n        A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),\n        A.HorizontalFlip(p=0.5),\n        A.RandomBrightnessContrast(p=0.3),\n        A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n        ToTensorV2(),\n    ])\n    \n    seg_dataset = HybridDentalDataset(\n        train_images, \n        Config.TRAIN_LABELS_PATH, \n        transform=seg_transform,\n        mode='segmentation'\n    )\n    \n    seg_loader = DataLoader(\n        seg_dataset, \n        batch_size=Config.BATCH_SIZE, \n        shuffle=True, \n        num_workers=0\n    )\n    \n    # Validation dataset\n    val_dataset = HybridDentalDataset(\n        valid_images, \n        Config.VALID_LABELS_PATH, \n        transform=A.Compose([\n            A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),\n            A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n            ToTensorV2(),\n        ]),\n        mode='segmentation'\n    )\n    \n    val_loader = DataLoader(\n        val_dataset, \n        batch_size=Config.BATCH_SIZE, \n        shuffle=False, \n        num_workers=0\n    )\n    \n    best_val_loss = float('inf')\n    best_map50 = 0.0\n    \n    for epoch in range(Config.EPOCHS):\n        # Training\n        train_loss = trainer.train_stage(seg_loader, epoch)\n        \n        # Validation with mAP@50 calculation\n        val_loss, map50, class_metrics = trainer.evaluate(val_loader, epoch)\n        \n        print(f\"Epoch {epoch+1}/{Config.EPOCHS}\")\n        print(f\"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, mAP@50: {map50:.4f}\")\n        \n        # Print class-wise metrics\n        print(\"Class-wise metrics:\")\n        for class_id, metrics in class_metrics.items():\n            class_name = Config.CLASS_NAMES[class_id-1] if class_id-1 < len(Config.CLASS_NAMES) else f\"Class {class_id}\"\n            print(f\"  {class_name}: Precision={metrics['precision']:.3f}, Recall={metrics['recall']:.3f}, F1={metrics['f1']:.3f}\")\n        \n        # Save best model based on validation loss\n        if val_loss < best_val_loss:\n            best_val_loss = val_loss\n            torch.save(model.state_dict(), f\"{Config.MODEL_DIR}/best_segmentation_model.pth\")\n            print(f\"Saved best model with validation loss: {val_loss:.4f}\")\n        \n        # Also save best model based on mAP@50\n        if map50 > best_map50:\n            best_map50 = map50\n            torch.save(model.state_dict(), f\"{Config.MODEL_DIR}/best_map50_model.pth\")\n            print(f\"Saved best mAP@50 model with score: {map50:.4f}\")\n    \n    return model\n\n# Quick test\ndef test_hybrid_model():\n    \"\"\"Test the hybrid model setup.\"\"\"\n    print(\"=== Testing Hybrid Model ===\")\n    \n    try:\n        # Test model creation\n        model = HybridDentalModel()\n        print(\"✅ Hybrid model created successfully\")\n        \n        # Test forward pass\n        dummy_input = torch.randn(1, 3, Config.IMAGE_SIZE, Config.IMAGE_SIZE)\n        \n        # Test segmentation component\n        seg_out = model(dummy_input, mode='segmentation')\n        print(f\"✅ Segmentation output: {seg_out.shape}\")\n        \n        # Test mAP evaluator\n        evaluator = MAP50Evaluator(Config.NUM_SEGMENTATION_CLASSES)\n        print(\"✅ mAP evaluator created successfully\")\n        \n        # Test inference mode\n        model.eval()\n        results = model(dummy_input, mode='inference')\n        print(\"✅ Complete inference pipeline working\")\n        \n        return True\n        \n    except Exception as e:\n        print(f\"❌ Test failed: {e}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\nif __name__ == \"__main__\":\n    # Test the hybrid setup\n    if test_hybrid_model():\n        print(\"\\nHybrid model test passed! Ready for training.\")\n        # Train the model\n        model = train_hybrid_model()\n        \n        # Run inference on test set\n        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n        model.load_state_dict(torch.load(f\"{Config.MODEL_DIR}/best_segmentation_model.pth\"))\n        inference = HybridInference(model, device)\n        \n        # Create output directory for test results\n        test_output_dir = f\"{Config.OUTPUT_DIR}/test_results\"\n        submission_df = inference.predict_test_set(Config.TEST_IMAGES_PATH, test_output_dir)\n        \n        print(f\"Test results saved to {test_output_dir}\")\n        \n        # Display submission summary\n        print(\"\\n=== Submission Summary ===\")\n        print(f\"Total predictions: {len(submission_df)}\")\n        print(f\"Unique images: {submission_df['patient_id'].nunique()}\")\n        \n        # Check class distribution\n        print(\"\\nPredictions per class:\")\n        for class_id in range(Config.NUM_SEGMENTATION_CLASSES - 1):  # Exclude background\n            count = len(submission_df[submission_df['class_id'] == class_id])\n            print(f\"  Class {class_id} ({Config.CLASS_NAMES[class_id]}): {count}\")\n    else:\n        print(\"\\nPlease fix the issues before proceeding.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T21:37:52.595112Z","iopub.execute_input":"2025-09-02T21:37:52.595433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load best model and run inference\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel.load_state_dict(torch.load(f\"{Config.MODEL_DIR}/best_segmentation_model.pth\", map_location=device))\nmodel.to(device)\nmodel.eval()\n\ninference = HybridInference(model, device)\n\n# Run predictions on test set\nsubmission_df = inference.predict_test_set(Config.TEST_IMAGES_PATH, Config.OUTPUT_DIR)\n\n# ✅ Save to Kaggle submission path\nsubmission_path = \"/kaggle/working/submission.csv\"\nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"✅ Submission file saved at: {submission_path}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T21:13:37.949796Z","iopub.execute_input":"2025-09-02T21:13:37.950662Z","iopub.status.idle":"2025-09-02T21:14:20.951931Z","shell.execute_reply.started":"2025-09-02T21:13:37.950632Z","shell.execute_reply":"2025-09-02T21:14:20.951271Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nsubmission_path = \"/kaggle/working/submission.csv\"\nsubmission_df.to_csv(submission_path, index=False)\n\nprint(\"✅ submission saved:\", os.path.exists(submission_path))\nprint(\"📄 Location:\", submission_path)\nprint(submission_df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T21:22:38.411503Z","iopub.execute_input":"2025-09-02T21:22:38.412086Z","iopub.status.idle":"2025-09-02T21:22:38.461513Z","shell.execute_reply.started":"2025-09-02T21:22:38.412060Z","shell.execute_reply":"2025-09-02T21:22:38.460929Z"}},"outputs":[],"execution_count":null}]}