{"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":113558,"databundleVersionId":14878066,"sourceType":"competition"},{"sourceId":14213969,"sourceType":"datasetVersion","datasetId":8573348},{"sourceId":211097053,"sourceType":"kernelVersion"},{"sourceId":139474,"sourceType":"modelInstanceVersion","modelInstanceId":118113,"modelId":141350}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!tar xfvz /kaggle/input/ultralytics-for-offline-install/archive.tar.gz\n!pip install --no-index --find-links=./packages ultralytics\n!rm -rf ./packages","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T08:23:28.114876Z","iopub.execute_input":"2025-12-17T08:23:28.115079Z","iopub.status.idle":"2025-12-17T08:25:08.603870Z","shell.execute_reply.started":"2025-12-17T08:23:28.115061Z","shell.execute_reply":"2025-12-17T08:25:08.602705Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metric_file = '''\n#!/usr/bin/env python\n# coding: utf-8\n\n# In[ ]:\n\n\nimport json\n\nimport numba\nimport numpy as np\nfrom numba import types\nimport numpy.typing as npt\nimport pandas as pd\nimport scipy.optimize\n\n\nclass ParticipantVisibleError(Exception):\n    pass\n\n\n@numba.jit(nopython=True)\ndef _rle_encode_jit(x: npt.NDArray, fg_val: int = 1) -> list[int]:\n    \"\"\"Numba-jitted RLE encoder.\"\"\"\n    dots = np.where(x.T.flatten() == fg_val)[0]\n    run_lengths = []\n    prev = -2\n    for b in dots:\n        if b > prev + 1:\n            run_lengths.extend((b + 1, 0))\n        run_lengths[-1] += 1\n        prev = b\n    return run_lengths\n\n\ndef rle_encode(masks: list[npt.NDArray], fg_val: int = 1) -> str:\n    \"\"\"\n    Adapted from contrails RLE https://www.kaggle.com/code/inversion/contrails-rle-submission\n    Args:\n        masks: list of numpy array of shape (height, width), 1 - mask, 0 - background\n    Returns: run length encodings as a string, with each RLE JSON-encoded and separated by a semicolon.\n    \"\"\"\n    return ';'.join([json.dumps(_rle_encode_jit(x, fg_val)) for x in masks])\n\n@numba.njit\ndef _rle_decode_jit(mask_rle: npt.NDArray, height: int, width: int) -> npt.NDArray:\n    \"\"\"\n    s: numpy array of run-length encoding pairs (start, length)\n    shape: (height, width) of array to return\n    Returns numpy array, 1 - mask, 0 - background\n    \"\"\"\n    if len(mask_rle) % 2 != 0:\n        # Numba requires raising a standard exception.\n        raise ValueError('One or more rows has an odd number of values.')\n\n    starts, lengths = mask_rle[0::2], mask_rle[1::2]\n    starts -= 1\n    ends = starts + lengths\n    for i in range(len(starts) - 1):\n        if ends[i] > starts[i + 1]:\n            raise ValueError('Pixels must not be overlapping.')\n    img = np.zeros(height * width, dtype=np.bool_)\n    for lo, hi in zip(starts, ends):\n        img[lo:hi] = 1\n    return img\n\n\ndef rle_decode(mask_rle: str, shape: tuple[int, int]) -> npt.NDArray:\n    \"\"\"\n    mask_rle: run-length as string formatted (start length)\n              empty predictions need to be encoded with '-'\n    shape: (height, width) of array to return\n    Returns numpy array, 1 - mask, 0 - background\n    \"\"\"\n\n    mask_rle = json.loads(mask_rle)\n    mask_rle = np.asarray(mask_rle, dtype=np.int32)\n    starts = mask_rle[0::2]\n    if sorted(starts) != list(starts):\n        raise ParticipantVisibleError('Submitted values must be in ascending order.')\n    try:\n        return _rle_decode_jit(mask_rle, shape[0], shape[1]).reshape(shape, order='F')\n    except ValueError as e:\n        raise ParticipantVisibleError(str(e)) from e\n    '''\n\nwith open(\"metric.py\", 'w', encoding='utf-8') as file:\n    file.write(metric_file)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T08:25:08.605925Z","iopub.execute_input":"2025-12-17T08:25:08.606189Z","iopub.status.idle":"2025-12-17T08:25:08.612814Z","shell.execute_reply.started":"2025-12-17T08:25:08.606166Z","shell.execute_reply":"2025-12-17T08:25:08.611980Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Import libraries\nfrom ultralytics import YOLO\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom metric import rle_encode\nfrom scipy import ndimage\nfrom skimage.metrics import structural_similarity as ssim\nfrom typing import List, Tuple, Optional\nimport warnings\nimport matplotlib.pyplot as plt\n\nwarnings.filterwarnings('ignore')\n\nprint(\"All libraries imported successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T08:33:21.150273Z","iopub.execute_input":"2025-12-17T08:33:21.150596Z","iopub.status.idle":"2025-12-17T08:33:21.216110Z","shell.execute_reply.started":"2025-12-17T08:33:21.150575Z","shell.execute_reply":"2025-12-17T08:33:21.215421Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_dice_coefficient(mask1: np.ndarray, mask2: np.ndarray) -> float:\n    \"\"\"\n    Calculate Dice coefficient between two binary masks.\n    Dice = 2 * |A ∩ B| / (|A| + |B|)\n    \n    For copy-paste forgeries, this should be LOW (masks shouldn't overlap)\n    \"\"\"\n    intersection = np.logical_and(mask1, mask2).sum()\n    if mask1.sum() + mask2.sum() == 0:\n        return 0.0\n    \n    dice = 2.0 * intersection / (mask1.sum() + mask2.sum())\n    return dice\n\n\ndef calculate_iou(mask1: np.ndarray, mask2: np.ndarray) -> float:\n    \"\"\"\n    Calculate Intersection over Union between two binary masks.\n    For copy-paste forgeries, this should be LOW\n    \"\"\"\n    intersection = np.logical_and(mask1, mask2).sum()\n    union = np.logical_or(mask1, mask2).sum()\n    \n    if union == 0:\n        return 0.0\n    \n    return intersection / union\n\n\ndef extract_mask_region(image: np.ndarray, mask: np.ndarray, \n                        padding: int = 10) -> Tuple[np.ndarray, Tuple[int, int, int, int]]:\n    \"\"\"\n    Extract the bounding box region around a mask with padding.\n    \"\"\"\n    rows = np.any(mask, axis=1)\n    cols = np.any(mask, axis=0)\n    \n    if not rows.any() or not cols.any():\n        return None, None\n    \n    rmin, rmax = np.where(rows)[0][[0, -1]]\n    cmin, cmax = np.where(cols)[0][[0, -1]]\n    \n    h, w = image.shape[:2]\n    rmin = max(0, rmin - padding)\n    rmax = min(h, rmax + padding + 1)\n    cmin = max(0, cmin - padding)\n    cmax = min(w, cmax + padding + 1)\n    \n    region = image[rmin:rmax, cmin:cmax].copy()\n    bbox = (cmin, rmin, cmax - cmin, rmax - rmin)\n    \n    return region, bbox\n\n\ndef calculate_shape_similarity(mask1: np.ndarray, mask2: np.ndarray) -> float:\n    \"\"\"\n    Calculate shape similarity using Hu Moments.\n    For copy-paste forgeries, this should be HIGH\n    \"\"\"\n    try:\n        moments1 = cv2.moments(mask1.astype(np.uint8))\n        moments2 = cv2.moments(mask2.astype(np.uint8))\n        \n        hu1 = cv2.HuMoments(moments1).flatten()\n        hu2 = cv2.HuMoments(moments2).flatten()\n        \n        # Log transform for numerical stability\n        hu1 = -np.sign(hu1) * np.log10(np.abs(hu1) + 1e-10)\n        hu2 = -np.sign(hu2) * np.log10(np.abs(hu2) + 1e-10)\n        \n        distance = np.linalg.norm(hu1 - hu2)\n        similarity = 1.0 / (1.0 + distance)\n        \n        return similarity\n    except:\n        return 0.0\n\n\ndef calculate_area_ratio(mask1: np.ndarray, mask2: np.ndarray) -> float:\n    \"\"\"\n    Calculate area ratio between two masks.\n    For copy-paste forgeries, this should be HIGH (similar sizes)\n    \"\"\"\n    area1 = mask1.sum()\n    area2 = mask2.sum()\n    \n    if area1 == 0 or area2 == 0:\n        return 0.0\n    \n    return min(area1, area2) / max(area1, area2)\n\n\ndef calculate_texture_similarity(region1: np.ndarray, region2: np.ndarray) -> float:\n    \"\"\"\n    Calculate texture similarity using SSIM.\n    For copy-paste forgeries, this should be HIGH\n    \"\"\"\n    try:\n        # Resize to same dimensions\n        h1, w1 = region1.shape[:2]\n        h2, w2 = region2.shape[:2]\n        target_h = min(h1, h2)\n        target_w = min(w1, w2)\n        \n        if target_h < 7 or target_w < 7:\n            return 0.0\n        \n        region1_resized = cv2.resize(region1, (target_w, target_h))\n        region2_resized = cv2.resize(region2, (target_w, target_h))\n        \n        # Convert to grayscale\n        if len(region1_resized.shape) == 3:\n            region1_gray = cv2.cvtColor(region1_resized, cv2.COLOR_BGR2GRAY)\n        else:\n            region1_gray = region1_resized\n            \n        if len(region2_resized.shape) == 3:\n            region2_gray = cv2.cvtColor(region2_resized, cv2.COLOR_BGR2GRAY)\n        else:\n            region2_gray = region2_resized\n        \n        # Calculate SSIM\n        score, _ = ssim(region1_gray, region2_gray, full=True)\n        \n        return max(0.0, score)\n    except:\n        return 0.0\n\n\nprint(\"Similarity functions defined!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T08:33:49.710438Z","iopub.execute_input":"2025-12-17T08:33:49.710942Z","iopub.status.idle":"2025-12-17T08:33:49.724966Z","shell.execute_reply.started":"2025-12-17T08:33:49.710920Z","shell.execute_reply":"2025-12-17T08:33:49.724204Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def analyze_mask_pair_similarity(image: np.ndarray, \n                                 mask1: np.ndarray, \n                                 mask2: np.ndarray,\n                                 verbose: bool = False) -> dict:\n    \"\"\"\n    Comprehensive similarity analysis between two masks.\n    \n    Copy-paste detection logic:\n    - LOW overlap (Dice, IoU) → masks are in different locations\n    - HIGH similarity (shape, texture, area) → same object copied\n    \"\"\"\n    results = {\n        'dice_coefficient': 0.0,\n        'iou': 0.0,\n        'area_ratio': 0.0,\n        'shape_similarity': 0.0,\n        'texture_similarity': 0.0,\n        'is_copy_paste': False,\n        'confidence': 0.0\n    }\n    \n    # Calculate overlap metrics (should be LOW for copy-paste)\n    results['dice_coefficient'] = calculate_dice_coefficient(mask1, mask2)\n    results['iou'] = calculate_iou(mask1, mask2)\n    \n    # Calculate similarity metrics (should be HIGH for copy-paste)\n    results['area_ratio'] = calculate_area_ratio(mask1, mask2)\n    results['shape_similarity'] = calculate_shape_similarity(mask1, mask2)\n    \n    # Extract regions and calculate texture similarity\n    region1, bbox1 = extract_mask_region(image, mask1)\n    region2, bbox2 = extract_mask_region(image, mask2)\n    \n    if region1 is not None and region2 is not None:\n        results['texture_similarity'] = calculate_texture_similarity(region1, region2)\n    \n    # Decision logic\n    overlap_score = (results['dice_coefficient'] + results['iou']) / 2.0\n    similarity_score = (\n        results['area_ratio'] * 0.3 +\n        results['shape_similarity'] * 0.3 +\n        results['texture_similarity'] * 0.4\n    )\n    \n    # Copy-paste criteria:\n    # Low overlap (<0.3) AND high similarity (>0.5)\n    is_copy_paste = (overlap_score < 0.4) and (similarity_score > 0.7)\n    \n    results['is_copy_paste'] = is_copy_paste\n    results['confidence'] = similarity_score\n    \n    if verbose:\n        print(f\"  Overlap: {overlap_score:.3f}, Similarity: {similarity_score:.3f}\")\n        print(f\"  → Dice: {results['dice_coefficient']:.3f}, IoU: {results['iou']:.3f}\")\n        print(f\"  → Area: {results['area_ratio']:.3f}, Shape: {results['shape_similarity']:.3f}, Texture: {results['texture_similarity']:.3f}\")\n        print(f\"  → Copy-paste: {'YES' if is_copy_paste else 'NO'}\")\n    \n    return results\n\n\ndef validate_forgery_masks(image: np.ndarray, \n                          masks_list: List[np.ndarray],\n                          verbose: bool = False) -> Tuple[List[np.ndarray], bool]:\n    \"\"\"\n    Validate masks and determine if they represent copy-paste forgery.\n    \n    Returns:\n        valid_masks: List of validated masks\n        is_forgery: Whether copy-paste forgery was detected\n    \"\"\"\n    n_masks = len(masks_list)\n    \n    if verbose:\n        print(f\"  Validating {n_masks} masks...\")\n    \n    # RULE 1: Must have at least 2 masks\n    if n_masks < 2:\n        if verbose:\n            print(f\"  ✗ Rejected: Only {n_masks} mask(s) detected (need ≥2)\")\n        return [], False\n    \n    # RULE 2: Analyze all pairs for copy-paste similarity\n    copy_paste_found = False\n    max_confidence = 0.0\n    \n    for i in range(n_masks):\n        for j in range(i + 1, n_masks):\n            if verbose:\n                print(f\"\\n  Analyzing pair ({i}, {j}):\")\n            \n            analysis = analyze_mask_pair_similarity(\n                image, masks_list[i], masks_list[j], verbose=verbose\n            )\n            \n            if analysis['is_copy_paste']:\n                copy_paste_found = True\n                max_confidence = max(max_confidence, analysis['confidence'])\n    \n    if verbose:\n        if copy_paste_found:\n            print(f\"\\n  ✓ Copy-paste forgery detected (confidence: {max_confidence:.3f})\")\n        else:\n            print(f\"\\n  ✗ No copy-paste patterns found\")\n    \n    return masks_list if copy_paste_found else [], copy_paste_found\n\n\nprint(\"Validation functions defined!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:02.788205Z","iopub.execute_input":"2025-12-17T09:04:02.788860Z","iopub.status.idle":"2025-12-17T09:04:02.801943Z","shell.execute_reply.started":"2025-12-17T09:04:02.788835Z","shell.execute_reply":"2025-12-17T09:04:02.801165Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ============================================================================\n# # PREDICTION AND SUBMISSION GENERATION\n# # ============================================================================\n\n# def predict_and_create_submission_csv_file(\n#     model_path: str,\n#     test_images_dir: str = \"test_images\",\n#     output_csv: str = \"submission.csv\",\n#     conf_threshold: float = 0.25,\n#     iou_threshold: float = 0.45,\n#     min_area: int = 128,  # Add minimum area threshold like reference\n#     verbose: bool = True\n# ):\n#     \"\"\"\n#     Make predictions on test images and create submission CSV file.\n#     Enhanced with minimum area filtering.\n#     \"\"\"\n    \n#     if verbose:\n#         print(\"=\"*80)\n#         print(\"CREATING SUBMISSION FILE\")\n#         print(\"=\"*80)\n#         print(f\"\\nModel: {model_path}\")\n#         print(f\"Test images directory: {test_images_dir}\")\n#         print(f\"Confidence threshold: {conf_threshold}\")\n#         print(f\"IoU threshold: {iou_threshold}\")\n#         print(f\"Minimum area (pixels): {min_area}\")\n    \n#     # Load the trained model\n#     if verbose:\n#         print(f\"\\n[1/4] Loading model...\")\n#     model = YOLO(model_path)\n    \n#     # Get all test images\n#     test_dir = Path(test_images_dir)\n#     test_images = sorted(list(test_dir.glob('*.png')) + \n#                         list(test_dir.glob('*.jpg')) + \n#                         list(test_dir.glob('*.jpeg')) +\n#                         list(test_dir.glob('*.tif')) +\n#                         list(test_dir.glob('*.tiff')))\n    \n#     if verbose:\n#         print(f\"[2/4] Found {len(test_images)} test images\")\n    \n#     if len(test_images) == 0:\n#         print(f\"WARNING: No images found in {test_images_dir}\")\n#         return pd.DataFrame()\n    \n#     # Process each image\n#     if verbose:\n#         print(f\"[3/4] Making predictions...\")\n    \n#     submissions = []\n    \n#     for idx, img_path in enumerate(test_images):\n#         case_id = img_path.stem\n        \n#         # Read image to get original shape\n#         img = cv2.imread(str(img_path))\n#         if img is None:\n#             print(f\"WARNING: Could not read {img_path}\")\n#             submissions.append({\n#                 'case_id': case_id,\n#                 'annotation': 'authentic'\n#             })\n#             continue\n        \n#         original_h, original_w = img.shape[:2]\n        \n#         # Run prediction\n#         results = model.predict(\n#             source=str(img_path),\n#             conf=conf_threshold,\n#             iou=iou_threshold,\n#             verbose=False,\n#             save=True\n#         )\n        \n#         result = results[0]\n        \n#         # Check if any masks were detected\n#         if result.masks is None or len(result.masks) == 0:\n#             # No forgery detected - classify as authentic\n#             annotation = \"authentic\"\n#         else:\n#             # Extract all detected masks\n#             masks_list = []\n            \n#             for mask_data in result.masks.data:\n#                 # Convert mask tensor to numpy array\n#                 mask = mask_data.cpu().numpy()\n                \n#                 # Resize mask to original image dimensions\n#                 mask_resized = cv2.resize(\n#                     mask, \n#                     (original_w, original_h), \n#                     interpolation=cv2.INTER_NEAREST  # Use NEAREST for masks\n#                 )\n                \n#                 # Threshold to binary mask\n#                 binary_mask = (mask_resized > 0.5).astype(np.uint8)\n                \n#                 # Filter by minimum area (like reference implementation)\n#                 mask_area = binary_mask.sum()\n                \n#                 if mask_area >= min_area:\n#                     masks_list.append(binary_mask)\n#                     if verbose:\n#                         coverage = mask_area / (original_h * original_w)\n#                         print(f\"  {case_id}: Instance area={mask_area}, coverage={coverage:.4f}\")\n            \n#             # Encode masks to RLE format\n#             if len(masks_list) > 0:\n#                 annotation = rle_encode(masks_list, fg_val=1)\n#             else:\n#                 annotation = \"authentic\"\n        \n#         submissions.append({\n#             'case_id': case_id,\n#             'annotation': annotation\n#         })\n        \n#         # Progress update\n#         if verbose and (idx + 1) % 10 == 0:\n#             print(f\"  Processed {idx + 1}/{len(test_images)} images...\")\n    \n#     # Create DataFrame\n#     if verbose:\n#         print(f\"[4/4] Creating submission file...\")\n    \n#     submission_df = pd.DataFrame(submissions)\n    \n#     # Ensure case_id is string type\n#     submission_df['case_id'] = submission_df['case_id'].astype(str)\n    \n#     # Sort by case_id (convert to int if possible)\n#     try:\n#         submission_df['case_id_int'] = submission_df['case_id'].astype(int)\n#         submission_df = submission_df.sort_values('case_id_int').drop('case_id_int', axis=1)\n#     except:\n#         submission_df = submission_df.sort_values('case_id')\n    \n#     # Save to CSV\n#     submission_df.to_csv(output_csv, index=False)\n    \n#     # Print summary\n#     if verbose:\n#         print(f\"\\n{'='*80}\")\n#         print(\"SUBMISSION SUMMARY\")\n#         print('='*80)\n#         print(f\"Total images: {len(submission_df)}\")\n#         print(f\"Authentic: {(submission_df['annotation'] == 'authentic').sum()}\")\n#         print(f\"Forged: {(submission_df['annotation'] != 'authentic').sum()}\")\n#         print(f\"\\nSubmission saved to: {output_csv}\")\n#         print('='*80)\n        \n#         # Show sample predictions\n#         print(\"\\nSample predictions:\")\n#         print(submission_df.head(10).to_string(index=False))\n    \n#     return submission_df\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:05.094515Z","iopub.execute_input":"2025-12-17T09:04:05.095316Z","iopub.status.idle":"2025-12-17T09:04:05.101028Z","shell.execute_reply.started":"2025-12-17T09:04:05.095284Z","shell.execute_reply":"2025-12-17T09:04:05.100286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_and_create_submission_with_validation(\n    model_path: str,\n    test_images_dir: str = \"test_images\",\n    output_csv: str = \"submission.csv\",\n    conf_threshold: float = 0.25,\n    iou_threshold: float = 0.45,\n    min_area: int = 128,\n    enable_validation: bool = True,\n    verbose: bool = True\n):\n    \"\"\"\n    Enhanced prediction with structural similarity validation.\n    \n    Key features:\n    1. Filters out single-mask predictions\n    2. Validates copy-paste forgeries using similarity analysis\n    3. Only marks as forged if copy-paste is confirmed\n    \"\"\"\n    \n    if verbose:\n        print(\"=\"*80)\n        print(\"ENHANCED SUBMISSION WITH POST-PROCESSING VALIDATION\")\n        print(\"=\"*80)\n        print(f\"\\nModel: {model_path}\")\n        print(f\"Test images directory: {test_images_dir}\")\n        print(f\"Confidence threshold: {conf_threshold}\")\n        print(f\"IoU threshold: {iou_threshold}\")\n        print(f\"Minimum area (pixels): {min_area}\")\n        print(f\"Validation enabled: {enable_validation}\")\n    \n    # Load model\n    if verbose:\n        print(f\"\\n[1/4] Loading model...\")\n    model = YOLO(model_path)\n    \n    # Get test images\n    test_dir = Path(test_images_dir)\n    test_images = sorted(list(test_dir.glob('*.png')) + \n                        list(test_dir.glob('*.jpg')) + \n                        list(test_dir.glob('*.jpeg')) +\n                        list(test_dir.glob('*.tif')) +\n                        list(test_dir.glob('*.tiff')))\n    \n    if verbose:\n        print(f\"[2/4] Found {len(test_images)} test images\")\n    \n    if len(test_images) == 0:\n        print(f\"WARNING: No images found in {test_images_dir}\")\n        return pd.DataFrame()\n    \n    # Statistics\n    stats = {\n        'total': 0,\n        'authentic': 0,\n        'forged': 0,\n        'single_mask_rejected': 0,\n        'validation_rejected': 0,\n        'validation_passed': 0\n    }\n    \n    # Process each image\n    if verbose:\n        print(f\"[3/4] Making predictions with validation...\\n\")\n    \n    submissions = []\n    \n    for idx, img_path in enumerate(test_images):\n        case_id = img_path.stem\n        stats['total'] += 1\n        \n        # Read image\n        img = cv2.imread(str(img_path))\n        if img is None:\n            print(f\"WARNING: Could not read {img_path}\")\n            submissions.append({\n                'case_id': case_id,\n                'annotation': 'authentic'\n            })\n            stats['authentic'] += 1\n            continue\n        \n        original_h, original_w = img.shape[:2]\n        \n        # Run prediction\n        results = model.predict(\n            source=str(img_path),\n            conf=conf_threshold,\n            iou=iou_threshold,\n            verbose=False\n        )\n        \n        result = results[0]\n        \n        # Check if any masks detected\n        if result.masks is None or len(result.masks) == 0:\n            annotation = \"authentic\"\n            stats['authentic'] += 1\n        else:\n            # Extract all masks\n            masks_list = []\n            \n            for mask_data in result.masks.data:\n                mask = mask_data.cpu().numpy()\n                mask_resized = cv2.resize(\n                    mask, \n                    (original_w, original_h), \n                    interpolation=cv2.INTER_NEAREST\n                )\n                binary_mask = (mask_resized > 0.5).astype(np.uint8)\n                \n                # Filter by minimum area\n                mask_area = binary_mask.sum()\n                if mask_area >= min_area:\n                    masks_list.append(binary_mask)\n            \n            # POST-PROCESSING: Validate masks\n            if enable_validation:\n                if verbose:\n                    print(f\"\\n{case_id}: {len(masks_list)} masks detected\")\n                \n                valid_masks, is_forgery = validate_forgery_masks(\n                    img, masks_list, verbose=verbose\n                )\n                \n                if len(masks_list) < 2:\n                    stats['single_mask_rejected'] += 1\n                    annotation = \"authentic\"\n                elif not is_forgery:\n                    stats['validation_rejected'] += 1\n                    annotation = \"authentic\"\n                else:\n                    stats['validation_passed'] += 1\n                    annotation = rle_encode(valid_masks, fg_val=1)\n                    stats['forged'] += 1\n            else:\n                # Without validation\n                if len(masks_list) < 2:\n                    stats['single_mask_rejected'] += 1\n                    annotation = \"authentic\"\n                else:\n                    annotation = rle_encode(masks_list, fg_val=1)\n                    stats['forged'] += 1\n            \n            if annotation == \"authentic\":\n                stats['authentic'] += 1\n        \n        submissions.append({\n            'case_id': case_id,\n            'annotation': annotation\n        })\n        \n        # Progress\n        if verbose and (idx + 1) % 10 == 0:\n            print(f\"\\n{'='*60}\")\n            print(f\"Progress: {idx + 1}/{len(test_images)}\")\n            print(f\"Authentic: {stats['authentic']}, Forged: {stats['forged']}\")\n            print('='*60)\n    \n    # Create DataFrame\n    if verbose:\n        print(f\"\\n[4/4] Creating submission file...\")\n    \n    submission_df = pd.DataFrame(submissions)\n    submission_df['case_id'] = submission_df['case_id'].astype(str)\n    \n    # Sort by case_id\n    try:\n        submission_df['case_id_int'] = submission_df['case_id'].astype(int)\n        submission_df = submission_df.sort_values('case_id_int').drop('case_id_int', axis=1)\n    except:\n        submission_df = submission_df.sort_values('case_id')\n    \n    # Save\n    submission_df.to_csv(output_csv, index=False)\n    \n    # Print summary\n    if verbose:\n        print(f\"\\n{'='*80}\")\n        print(\"ENHANCED SUBMISSION SUMMARY\")\n        print('='*80)\n        print(f\"Total images: {stats['total']}\")\n        print(f\"Authentic: {stats['authentic']} ({stats['authentic']/stats['total']*100:.1f}%)\")\n        print(f\"Forged: {stats['forged']} ({stats['forged']/stats['total']*100:.1f}%)\")\n        print(f\"\\nPost-processing statistics:\")\n        print(f\"  Single mask rejected: {stats['single_mask_rejected']}\")\n        print(f\"  Validation rejected: {stats['validation_rejected']}\")\n        print(f\"  Validation passed: {stats['validation_passed']}\")\n        print(f\"\\nSubmission saved to: {output_csv}\")\n        print('='*80)\n        \n        print(\"\\nSample predictions:\")\n        print(submission_df.head(10).to_string(index=False))\n    \n    return submission_df\n\n\nprint(\"Enhanced prediction function defined!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:06.581796Z","iopub.execute_input":"2025-12-17T09:04:06.582362Z","iopub.status.idle":"2025-12-17T09:04:06.598662Z","shell.execute_reply.started":"2025-12-17T09:04:06.582338Z","shell.execute_reply":"2025-12-17T09:04:06.598048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Run with optimized settings\n# submission_df = predict_and_create_submission_csv_file(\n#     model_path=\"/kaggle/input/yolo-01-f0rgery/best.pt\",\n#     test_images_dir=\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\",\n#     output_csv=\"submission.csv\",\n#     conf_threshold=0.20,      # Adjust based on validation\n#     iou_threshold=0.40,       # Standard NMS threshold\n#     min_area=128,             # Filter small false positives\n#     verbose=True\n# )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:10.007612Z","iopub.execute_input":"2025-12-17T09:04:10.008065Z","iopub.status.idle":"2025-12-17T09:04:10.011566Z","shell.execute_reply.started":"2025-12-17T09:04:10.008043Z","shell.execute_reply":"2025-12-17T09:04:10.010926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run with validation enabled\nsubmission_df = predict_and_create_submission_with_validation(\n    model_path=\"/kaggle/input/yolo-01-f0rgery/best.pt\",\n    test_images_dir=\"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\",\n    output_csv=\"submission.csv\",\n    conf_threshold=0.40,\n    iou_threshold=0.40,\n    min_area=128,\n    enable_validation=True,  # Set to False to disable post-processing\n    verbose=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:11.942411Z","iopub.execute_input":"2025-12-17T09:04:11.942679Z","iopub.status.idle":"2025-12-17T09:04:12.565208Z","shell.execute_reply.started":"2025-12-17T09:04:11.942659Z","shell.execute_reply":"2025-12-17T09:04:12.564442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import matplotlib.pyplot as plt\n# def visualize_predictions(\n#     model_path: str,\n#     image_path: str,\n#     output_path: str = \"prediction_viz.png\",\n#     conf_threshold: float = 0.25,\n#     iou: float = 0.45\n# ):\n#     \"\"\"\n#     Runs YOLO prediction on a single image and saves the visualized output.\n#     \"\"\"\n\n#     # Load model\n#     model = YOLO(model_path)\n\n#     # Run prediction (no auto-saving)\n#     results = model.predict(\n#         source=image_path,\n#         conf=conf_threshold,\n#         iou=0.45,\n#         save=False,            # prevent YOLO from saving in runs/\n#         imgsz=640\n#     )\n\n#     # YOLO returns a list (usually of size 1 for one image)\n#     result = results[0]\n#     print(result)\n#     # Get annotated image from YOLO\n#     rendered = result.plot()    # numpy array (BGR)\n\n#     # Save the visualization\n#     cv2.imwrite(output_path, rendered)\n#     plt.imshow(rendered)\n#     plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:12.566617Z","iopub.execute_input":"2025-12-17T09:04:12.566914Z","iopub.status.idle":"2025-12-17T09:04:12.570647Z","shell.execute_reply.started":"2025-12-17T09:04:12.566897Z","shell.execute_reply":"2025-12-17T09:04:12.569919Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_with_validation(\n    model_path: str,\n    image_path: str,\n    conf_threshold: float = 0.25,\n    iou: float = 0.45,\n    min_area: int = 128\n):\n    \"\"\"\n    Visualize predictions with validation results.\n    \"\"\"\n    # Load model and image\n    model = YOLO(model_path)\n    img = cv2.imread(image_path)\n    \n    if img is None:\n        print(f\"Could not read {image_path}\")\n        return\n    \n    # Run prediction\n    results = model.predict(\n        source=image_path,\n        conf=conf_threshold,\n        iou=iou,\n        save=False\n    )\n    \n    result = results[0]\n    \n    # Extract masks\n    masks_list = []\n    if result.masks is not None:\n        for mask_data in result.masks.data:\n            mask = mask_data.cpu().numpy()\n            mask_resized = cv2.resize(\n                mask,\n                (img.shape[1], img.shape[0]),\n                interpolation=cv2.INTER_NEAREST\n            )\n            binary_mask = (mask_resized > 0.5).astype(np.uint8)\n            \n            if binary_mask.sum() >= min_area:\n                masks_list.append(binary_mask)\n    \n    # Validate\n    print(f\"\\n{'='*60}\")\n    print(f\"Image: {Path(image_path).name}\")\n    print(f\"Masks detected: {len(masks_list)}\")\n    \n    valid_masks, is_forgery = validate_forgery_masks(img, masks_list, verbose=True)\n    \n    print(f\"\\nFinal decision: {'FORGED' if is_forgery else 'AUTHENTIC'}\")\n    print('='*60)\n    \n    # Visualize\n    fig, axes = plt.subplots(1, 2, figsize=(15, 7))\n    \n    # Original with YOLO detections\n    rendered = result.plot()\n    axes[0].imshow(cv2.cvtColor(rendered, cv2.COLOR_BGR2RGB))\n    axes[0].set_title(f'YOLO Detection ({len(masks_list)} masks)')\n    axes[0].axis('off')\n    \n    # Validation result\n    overlay = img.copy()\n    if is_forgery and len(valid_masks) > 0:\n        colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]\n        for i, mask in enumerate(valid_masks):\n            color = colors[i % len(colors)]\n            overlay[mask > 0] = overlay[mask > 0] * 0.5 + np.array(color) * 0.5\n    \n    axes[1].imshow(cv2.cvtColor(overlay.astype(np.uint8), cv2.COLOR_BGR2RGB))\n    axes[1].set_title(f'After Validation: {\"FORGED\" if is_forgery else \"AUTHENTIC\"}')\n    axes[1].axis('off')\n    \n    plt.tight_layout()\n    plt.savefig('validation_result.png', dpi=150, bbox_inches='tight')\n    plt.show()\n    \n    return is_forgery, valid_masks\n\n\nprint(\"Visualization function defined!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:14.206565Z","iopub.execute_input":"2025-12-17T09:04:14.207022Z","iopub.status.idle":"2025-12-17T09:04:14.218625Z","shell.execute_reply.started":"2025-12-17T09:04:14.207000Z","shell.execute_reply":"2025-12-17T09:04:14.217912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# # Visualize some predictions\n# test_f = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/forged\"\n# best_model = \"/kaggle/input/yolo-01-f0rgery/best.pt\"\n\n# for path in os.listdir(test_f)[:1]:\n#     visualize_predictions(best_model, f\"{test_f}/{path}\",conf_threshold=0.20, iou = 0.40)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:04:15.984265Z","iopub.execute_input":"2025-12-17T09:04:15.984798Z","iopub.status.idle":"2025-12-17T09:04:15.988171Z","shell.execute_reply.started":"2025-12-17T09:04:15.984773Z","shell.execute_reply":"2025-12-17T09:04:15.987414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Test visualization on a sample image\nimport os\n\ntest_f = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/forged\"\nbest_model = \"/kaggle/input/yolo-01-f0rgery/best.pt\"\n\n# Visualize first forged image\nfor path in os.listdir(test_f)[:50]:\n    visualize_with_validation(\n        best_model, \n        f\"{test_f}/{path}\",\n        conf_threshold=0.15, \n        iou=0.40,\n        min_area=128\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T09:05:23.891370Z","iopub.execute_input":"2025-12-17T09:05:23.891854Z","execution_failed":"2025-12-17T09:06:00.532Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}