{"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":"gpu","dataSources":[{"sourceId":103103,"databundleVersionId":13042974,"sourceType":"competition"}],"dockerImageVersionId":31091,"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q ultralytics\n\nimport os\nimport sys\nimport time\nimport glob\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nimport yaml\nimport random\nfrom PIL import Image\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Disable wandb\nos.environ['WANDB_DISABLED'] = 'true'\n\n# Set random seeds for reproducibility\nrandom.seed(42)\nnp.random.seed(42)\n\n# Install required packages (uncomment when running in a fresh environment)\n# print(\"Installing required packages...\")\n# os.system('pip install -q ultralytics')\n\nimport torch\nfrom ultralytics import YOLO\n\n# Set deterministic behavior for PyTorch\ntorch.manual_seed(42)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed(42)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nprint(f'\\nPyTorch Version: {torch.__version__}')\nprint(f'CUDA Available: {torch.cuda.is_available()}')\nif torch.cuda.is_available():\n    try:\n        print(f'CUDA Device: {torch.cuda.get_device_name(0)}')\n    except Exception:\n        pass\n\n# Define paths (adjust if needed)\nBASE_PATH = '/kaggle/input/alpha-dent/AlphaDent'\nTRAIN_IMAGES_PATH = f'{BASE_PATH}/images/train'\nVALID_IMAGES_PATH = f'{BASE_PATH}/images/valid'\nTEST_IMAGES_PATH = f'{BASE_PATH}/images/test'\nTRAIN_LABELS_PATH = f'{BASE_PATH}/labels/train'\nVALID_LABELS_PATH = f'{BASE_PATH}/labels/valid'\n\n# Output paths\nOUTPUT_DIR = '/kaggle/working'\nWEIGHTS_DIR = f'{OUTPUT_DIR}/weights'\nos.makedirs(WEIGHTS_DIR, exist_ok=True)\n\n# Define class information\nCLASS_INFO = {\n    0: {'name': 'Abrasion', 'description': 'Teeth with mechanical wear of hard tissues'},\n    1: {'name': 'Filling', 'description': 'Dental fillings of various types'},\n    2: {'name': 'Crown', 'description': 'Dental crown (restoration)'},\n    3: {'name': 'Caries Class 1', 'description': 'Caries in fissures and pits'},\n    4: {'name': 'Caries Class 2', 'description': 'Caries on proximal surfaces of molars/premolars'},\n    5: {'name': 'Caries Class 3', 'description': 'Caries on proximal surfaces of incisors/canines without incisal edge'},\n    6: {'name': 'Caries Class 4', 'description': 'Caries on proximal surfaces of incisors/canines with incisal edge'},\n    7: {'name': 'Caries Class 5', 'description': 'Cervical caries (buccal/lingual surfaces)'},\n    8: {'name': 'Caries Class 6', 'description': 'Caries on incisal edges or cusps'}\n}\n\n# Create YAML configuration for YOLO (if needed)\nprint(\"\\n=== Creating YOLO Configuration ===\")\nyolo_config = {\n    'path': BASE_PATH,\n    'train': 'images/train',\n    'val': 'images/valid',\n    'test': 'images/test',\n    'nc': 9,\n    'names': [CLASS_INFO[i]['name'] for i in range(9)]\n}\n\nCUSTOM_YAML_PATH = f'{OUTPUT_DIR}/alphadent_config.yaml'\nwith open(CUSTOM_YAML_PATH, 'w') as f:\n    yaml.dump(yolo_config, f, default_flow_style=False)\nprint(f\"Created custom YAML config at: {CUSTOM_YAML_PATH}\")\n\n# Count images\ntrain_images = sorted(glob.glob(f'{TRAIN_IMAGES_PATH}/*.jpg'))\nvalid_images = sorted(glob.glob(f'{VALID_IMAGES_PATH}/*.jpg'))\ntest_images = sorted(glob.glob(f'{TEST_IMAGES_PATH}/*.jpg'))\n\nprint(f\"\\n=== Dataset Statistics ===\")\nprint(f\"Training images: {len(train_images)}\")\nprint(f\"Validation images: {len(valid_images)}\")\nprint(f\"Test images: {len(test_images)}\")\n\n# Analyze class distribution\ndef analyze_class_distribution(labels_path):\n    \"\"\"Analyze class distribution in dataset.\"\"\"\n    class_counts = {i: 0 for i in range(9)}\n    total_annotations = 0\n    \n    label_files = glob.glob(f'{labels_path}/*.txt')\n    \n    for label_file in tqdm(label_files, desc=\"Analyzing labels\", leave=False):\n        if os.path.exists(label_file) and os.path.getsize(label_file) > 0:\n            try:\n                with open(label_file, 'r') as f:\n                    lines = f.readlines()\n                    for line in lines:\n                        if line.strip():\n                            parts = line.strip().split()\n                            if parts:\n                                class_id = int(parts[0])\n                                if 0 <= class_id < 9:\n                                    class_counts[class_id] += 1\n                                    total_annotations += 1\n            except Exception:\n                continue\n    \n    return class_counts, total_annotations\n\nprint(\"\\n=== Analyzing Class Distribution ===\")\ntrain_class_counts, train_total = analyze_class_distribution(TRAIN_LABELS_PATH)\nvalid_class_counts, valid_total = analyze_class_distribution(VALID_LABELS_PATH)\n\nprint(f\"Training set: {train_total} total annotations\")\nprint(f\"Validation set: {valid_total} total annotations\")\n\n# Training configuration\nprint(\"\\n=== Model Training Configuration ===\")\nEPOCHS = 30\nIMAGE_SIZE = 640\nBATCH_SIZE = 8 if torch.cuda.is_available() else 4\nPATIENCE = 5\n\nprint(f\"Epochs: {EPOCHS}\")\nprint(f\"Image Size: {IMAGE_SIZE}\")\nprint(f\"Batch Size: {BATCH_SIZE}\")\nprint(f\"Early Stopping Patience: {PATIENCE}\")\n\n# Initialize model (you can change to yolov9-seg if available)\nprint(\"\\n=== Initializing Model ===\")\nmodel = YOLO('yolov8x-seg.pt')  # keep your base\n\n# (Optional) Training call (commented out if you only want inference)\n# results = model.train(...)\n\n# Loading best/last model if available\nbest_model_path = f'{OUTPUT_DIR}/alphadent_yolov8x/weights/best.pt'\nif os.path.exists(best_model_path):\n    model = YOLO(best_model_path)\n    print(f\"Loaded best model from: {best_model_path}\")\nelse:\n    last_model_path = f'{OUTPUT_DIR}/alphadent_yolov8x/weights/last.pt'\n    if os.path.exists(last_model_path):\n        model = YOLO(last_model_path)\n        print(f\"Loaded last model from: {last_model_path}\")\n    else:\n        print(\"Warning: No trained model found, using pretrained model\")\n\n# --- New post-processing helpers ---\ndef postprocess_mask_and_extract_polygon(raw_polygon_pts, orig_shape, approx_eps_ratio=0.01, morph_ratio=200, min_area_ratio=1e-4):\n    \"\"\"\n    raw_polygon_pts: Nx2 array-like polygon in absolute pixel coords (x,y) or list of points\n    orig_shape: (height, width)\n    approx_eps_ratio: epsilon for approxPolyDP as fraction of contour perimeter (0.01 -> 1%)\n    morph_ratio: divisor to compute morphological kernel size (kernel = max(3, min(h,w)//morph_ratio))\n    min_area_ratio: minimum area (as fraction of image area) to keep contour\n    Returns: list of normalized coords [x1, y1, x2, y2, ...] in [0,1] or None if fails\n    \"\"\"\n    try:\n        h, w = orig_shape\n        # Ensure polygon is Nx2 numpy array of ints\n        poly = np.array(raw_polygon_pts, dtype=np.int32)\n        if poly.ndim != 2 or poly.shape[1] != 2:\n            return None\n\n        # Create binary mask from polygon\n        mask = np.zeros((h, w), dtype=np.uint8)\n        cv2.fillPoly(mask, [poly], 255)\n\n        # Morphological cleanup: remove small noise and close holes\n        k = max(3, min(h, w) // morph_ratio)  # kernel size relative to image size\n        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))\n        # Opening then closing (remove small spots, then close small holes)\n        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)\n        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1)\n\n        # Find contours\n        contours_info = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        contours = contours_info[0] if len(contours_info) == 2 else contours_info[1]\n        if not contours:\n            return None\n\n        # Keep the largest contour by area\n        largest = max(contours, key=cv2.contourArea)\n        area = cv2.contourArea(largest)\n        if area < max(1.0, min_area_ratio * (h * w)):\n            # Too small: ignore\n            return None\n\n        peri = cv2.arcLength(largest, True)\n        eps = max(1.0, approx_eps_ratio * peri)\n        approx = cv2.approxPolyDP(largest, eps, True)  # simplified polygon\n        approx = approx.reshape(-1, 2)\n\n        if approx.shape[0] < 3:\n            # fallback to convex hull if approximation is degenerate\n            hull = cv2.convexHull(largest).reshape(-1, 2)\n            if hull.shape[0] < 3:\n                return None\n            coords = hull\n        else:\n            coords = approx\n\n        # Convert to normalized coords and clamp\n        normalized = []\n        for (x, y) in coords:\n            x_norm = float(x) / float(w)\n            y_norm = float(y) / float(h)\n            x_norm = max(0.0, min(1.0, x_norm))\n            y_norm = max(0.0, min(1.0, y_norm))\n            normalized.extend([x_norm, y_norm])\n\n        # Need at least 3 points (6 numbers)\n        if len(normalized) < 6:\n            return None\n\n        return normalized\n\n    except Exception as e:\n        # If anything goes wrong, skip this polygon\n        return None\n\ndef convert_to_submission_format(results, image_paths, min_polygon_points=3):\n    \"\"\"Convert YOLO results to competition submission format with polygon smoothing.\"\"\"\n    submission_rows = []\n\n    for idx, result in enumerate(results):\n        # Get image ID (filename without extension)\n        image_id = os.path.basename(image_paths[idx]).replace('.jpg', '')\n        \n        try:\n            h, w = result.orig_shape\n        except Exception:\n            # fallback: read image to know shape\n            img = cv2.imread(image_paths[idx])\n            if img is None:\n                continue\n            h, w = img.shape[:2]\n\n        # Check for masks in result (YOLOv8-seg returns result.masks)\n        masks = None\n        classes = []\n        confidences = []\n        try:\n            if hasattr(result, 'masks') and result.masks is not None:\n                # result.masks.xy usually a list of polygon arrays (in pixel coords)\n                try:\n                    masks = result.masks.xy  # list of numpy arrays, each shape (N,2)\n                except Exception:\n                    # fallback: some ultralytics versions may store differently\n                    masks = None\n            # boxes info (classes/conf)\n            if hasattr(result, 'boxes') and result.boxes is not None:\n                try:\n                    classes = result.boxes.cls.cpu().numpy().astype(int)\n                    confidences = result.boxes.conf.cpu().numpy()\n                except Exception:\n                    # fallback if boxes not on cpu\n                    try:\n                        classes = np.array(result.boxes.cls).astype(int)\n                        confidences = np.array(result.boxes.conf).astype(float)\n                    except Exception:\n                        classes = []\n                        confidences = []\n        except Exception:\n            masks = None\n\n        # If no mask polygons provided, try to get mask bitmasks and extract contours\n        alternate_masks_polygons = []\n        try:\n            if (masks is None) and hasattr(result, 'masks') and result.masks is not None:\n                # Some versions have result.masks.data or .masks.masks - attempt to convert\n                try:\n                    bitmasks = result.masks.data  # (N,H,W) boolean or similar\n                    for m in bitmasks:\n                        m_np = (m.cpu().numpy() if hasattr(m, 'cpu') else np.array(m)).astype(np.uint8) * 255\n                        contours_info = cv2.findContours(m_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n                        contours = contours_info[0] if len(contours_info) == 2 else contours_info[1]\n                        if contours:\n                            cnt = max(contours, key=cv2.contourArea)\n                            pts = cnt.reshape(-1, 2)\n                            alternate_masks_polygons.append(pts)\n                except Exception:\n                    alternate_masks_polygons = []\n        except Exception:\n            alternate_masks_polygons = []\n\n        # Choose which polygon source to use\n        polygon_sources = []\n        if masks:\n            # Convert each mask (which may be tensor-like or list) to numpy array\n            try:\n                for m in masks:\n                    arr = np.array(m, dtype=np.int32)\n                    if arr.ndim == 3 and arr.shape[1] == 2:\n                        arr2 = arr.reshape(-1, 2)\n                    elif arr.ndim == 2 and arr.shape[1] == 2:\n                        arr2 = arr\n                    else:\n                        # skip weird shape\n                        continue\n                    polygon_sources.append(arr2)\n            except Exception:\n                polygon_sources = []\n        if alternate_masks_polygons:\n            polygon_sources.extend(alternate_masks_polygons)\n\n        # If we still have no polygons, try extracting polygons from bounding boxes (as fallback)\n        if not polygon_sources:\n            try:\n                # result.boxes.xyxy gives bounding box corners\n                if hasattr(result.boxes, 'xyxy') and len(result.boxes.xyxy) > 0:\n                    boxes = np.array(result.boxes.xyxy.cpu()) if hasattr(result.boxes.xyxy, 'cpu') else np.array(result.boxes.xyxy)\n                    for b_idx, box in enumerate(boxes):\n                        x1, y1, x2, y2 = box[:4].astype(int)\n                        box_poly = np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype=np.int32)\n                        polygon_sources.append(box_poly)\n                        # ensure classes/confidences aligned\n            except Exception:\n                pass\n\n        # Now process polygon sources and map to classes/confidences if available\n        for mask_idx, poly_pts in enumerate(polygon_sources):\n            # Determine class/confidence for this mask if possible\n            cls = int(classes[mask_idx]) if (mask_idx < len(classes)) else 0\n            conf = float(confidences[mask_idx]) if (mask_idx < len(confidences)) else 0.0\n\n            # Postprocess and simplify polygon\n            norm_poly = postprocess_mask_and_extract_polygon(poly_pts, (h, w),\n                                                            approx_eps_ratio=0.01,\n                                                            morph_ratio=200,\n                                                            min_area_ratio=1e-5)\n            if norm_poly is None:\n                # if smoothing removed polygon (too small), try a coarse fallback: use raw polygon normalized\n                try:\n                    flat = []\n                    for (x, y) in poly_pts:\n                        flat.append(max(0.0, min(1.0, float(x) / float(w))))\n                        flat.append(max(0.0, min(1.0, float(y) / float(h))))\n                    if len(flat) >= 6:\n                        poly_str = ' '.join([f'{c:.6f}' for c in flat])\n                        submission_rows.append({\n                            'patient_id': image_id,\n                            'class_id': int(cls),\n                            'confidence': float(conf),\n                            'poly': poly_str\n                        })\n                except Exception:\n                    continue\n            else:\n                poly_str = ' '.join([f'{c:.6f}' for c in norm_poly])\n                submission_rows.append({\n                    'patient_id': image_id,\n                    'class_id': int(cls),\n                    'confidence': float(conf),\n                    'poly': poly_str\n                })\n\n    return submission_rows\n\n# Inference on test set\nprint(\"\\n=== Running Inference on Test Set ===\")\n\ntest_images = sorted(glob.glob(f'{TEST_IMAGES_PATH}/*.jpg'))\nall_submission_rows = []\nINFERENCE_BATCH_SIZE = 8 if torch.cuda.is_available() else 4\n\nprint(f\"Processing {len(test_images)} test images...\")\n\nfor i in tqdm(range(0, len(test_images), INFERENCE_BATCH_SIZE)):\n    batch_images = test_images[i:i + INFERENCE_BATCH_SIZE]\n    try:\n        results = model.predict(\n            batch_images,\n            imgsz=IMAGE_SIZE,\n            conf=0.25,  # Confidence threshold\n            iou=0.45,   # NMS IoU threshold\n            max_det=300,\n            device=0 if torch.cuda.is_available() else 'cpu',\n            verbose=False,\n            agnostic_nms=True,\n            retina_masks=True,\n        )\n        batch_rows = convert_to_submission_format(results, batch_images)\n        all_submission_rows.extend(batch_rows)\n    except Exception as e:\n        print(f\"Error in batch {i//INFERENCE_BATCH_SIZE}: {e}\")\n        continue\n\nprint(f\"\\nGenerated {len(all_submission_rows)} predictions\")\n\n# Create submission DataFrame\nprint(\"\\n=== Creating Submission File ===\")\nsubmission_df = pd.DataFrame(all_submission_rows)\n\n# Ensure all test images have at least one prediction\nall_test_ids = [os.path.basename(img).replace('.jpg', '') for img in test_images]\nif len(submission_df) > 0:\n    predicted_ids = submission_df['patient_id'].unique()\n    missing_ids = set(all_test_ids) - set(predicted_ids)\nelse:\n    missing_ids = set(all_test_ids)\n\n# Add dummy predictions for images without detections\nif 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)\nsubmission_df = submission_df.sort_values(['patient_id', 'confidence'], ascending=[True, False])\n\n# Ensure correct column order\nsubmission_df = submission_df[['patient_id', 'class_id', 'confidence', 'poly']]\n\n# Save the main submission file\nsubmission_df.to_csv('submission.csv', index=False)\nprint(\"Main submission file created: submission.csv\")\n\n# Verify submission format\nprint(\"\\n=== Verifying Submission Format ===\")\nprint(f\"Total predictions: {len(submission_df)}\")\nprint(f\"Unique images: {submission_df['patient_id'].nunique()}\")\nprint(f\"All test images included: {submission_df['patient_id'].nunique() == len(test_images)}\")\n\n# Display first few rows\nprint(\"\\nFirst 5 rows of submission:\")\nprint(submission_df.head())\n\n# Check for any potential issues\nprint(\"\\n=== Checking for Potential Issues ===\")\n\n# Check for missing test images\nmissing_in_submission = set(all_test_ids) - set(submission_df['patient_id'].unique())\nif missing_in_submission:\n    print(f\"WARNING: Missing images in submission: {missing_in_submission}\")\nelse:\n    print(\"✓ All test images have predictions\")\n\n# Check class distribution\nprint(\"\\nPredictions per class:\")\nclass_dist = submission_df['class_id'].value_counts().sort_index()\nfor class_id, count in class_dist.items():\n    if 0 <= class_id < 9:\n        print(f\"  Class {class_id} ({CLASS_INFO[class_id]['name']}): {count}\")\n\n# Check confidence distribution\nprint(f\"\\nConfidence statistics:\")\nprint(f\"  Min: {submission_df['confidence'].min():.4f}\")\nprint(f\"  Max: {submission_df['confidence'].max():.4f}\")\nprint(f\"  Mean: {submission_df['confidence'].mean():.4f}\")\nprint(f\"  Median: {submission_df['confidence'].median():.4f}\")\n\n# Create alternative submission with higher confidence threshold\nprint(\"\\n=== Creating Alternative Submission (Higher Confidence) ===\")\nhigh_conf_df = submission_df[submission_df['confidence'] >= 0.3].copy()\n\n# Ensure all images still have at least one prediction\nhigh_conf_ids = high_conf_df['patient_id'].unique()\nmissing_high_conf = set(all_test_ids) - set(high_conf_ids)\n\nif missing_high_conf:\n    # Add the highest confidence prediction for each missing image\n    for img_id in missing_high_conf:\n        img_preds = submission_df[submission_df['patient_id'] == img_id]\n        if len(img_preds) > 0:\n            # Add the highest confidence prediction\n            high_conf_df = pd.concat([high_conf_df, img_preds.head(1)], ignore_index=True)\n        else:\n            # Add dummy prediction\n            dummy_row = pd.DataFrame([{\n                'patient_id': img_id,\n                'class_id': 0,\n                'confidence': 0.01,\n                'poly': '0.1 0.1 0.1 0.2 0.2 0.2 0.2 0.1'\n            }])\n            high_conf_df = pd.concat([high_conf_df, dummy_row], ignore_index=True)\n\nhigh_conf_df = high_conf_df.sort_values(['patient_id', 'confidence'], ascending=[True, False])\nhigh_conf_df.to_csv('submission_high_conf.csv', index=False)\nprint(f\"Created high confidence submission with {len(high_conf_df)} predictions\")\n\nprint(\"\\n=== Pipeline Completed Successfully! ===\")\nprint(\"Submission files created:\")\nprint(\"  - submission.csv (main submission)\")\nprint(\"  - submission_high_conf.csv (alternative with higher confidence threshold)\")\nprint(\"\\nReady to submit to Kaggle!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-05T21:31:54.834968Z","iopub.execute_input":"2025-09-05T21:31:54.835805Z","iopub.status.idle":"2025-09-05T21:35:56.352595Z","shell.execute_reply.started":"2025-09-05T21:31:54.835768Z","shell.execute_reply":"2025-09-05T21:35:56.351715Z"}},"outputs":[],"execution_count":null}]}