{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Scientific Image Forgery Detection ","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport timm\nimport numpy as np\nimport cv2\nimport pandas as pd\nfrom PIL import Image\nfrom torchvision import transforms\nfrom scipy import ndimage\nimport warnings\nimport gc\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom matplotlib.patches import Rectangle\nimport seaborn as sns\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:07:18.680081Z","iopub.execute_input":"2025-12-20T11:07:18.680309Z","iopub.status.idle":"2025-12-20T11:07:31.797013Z","shell.execute_reply.started":"2025-12-20T11:07:18.680265Z","shell.execute_reply":"2025-12-20T11:07:31.796238Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Configuration OPTIMIZED","metadata":{}},{"cell_type":"code","source":"class Config:\n    # Model settings - Using cached ViT model\n    model_name = \"vit_base_patch16_224\"\n    patch_size = 16\n    img_size = 224\n    \n    # Detection thresholds - Optimized for biomedical images\n    similarity_thresholds = [0.88, 0.85, 0.90]  # Multiple thresholds\n    min_similar_pairs = 4\n    \n    # Clustering settings\n    eps_values = [0.6, 0.8, 1.0]\n    min_samples = 2\n    \n    # Post-processing\n    kernel_sizes = [5, 9, 13]\n    min_area_ratio = 0.001\n    max_area_ratio = 0.5\n    \n    # Ensemble settings\n    ensemble_threshold = 2\n    \n    # Memory optimization\n    max_image_dim = 1024  # Resize large images for memory efficiency\n    batch_size = 1  # Process one image at a time\n    \n    # Visualization settings\n    vis_output_dir = \"/kaggle/working/visualizations\"\n    show_visualizations = True\n    save_visualizations = True\n    \nconfig = Config()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:07:37.314652Z","iopub.execute_input":"2025-12-20T11:07:37.314945Z","iopub.status.idle":"2025-12-20T11:07:37.320639Z","shell.execute_reply.started":"2025-12-20T11:07:37.314920Z","shell.execute_reply":"2025-12-20T11:07:37.319927Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Device Setup\n","metadata":{}},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:07:41.076595Z","iopub.execute_input":"2025-12-20T11:07:41.076882Z","iopub.status.idle":"2025-12-20T11:07:41.142925Z","shell.execute_reply.started":"2025-12-20T11:07:41.076859Z","shell.execute_reply":"2025-12-20T11:07:41.142244Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model Loading","metadata":{}},{"cell_type":"code","source":"\nprint(\"Loading model...\")\ntry:\n    model = timm.create_model(\n        config.model_name,\n        pretrained=True,\n        num_classes=0\n    )\n    model.to(device)\n    model.eval()\n    print(f\"Model loaded: {config.model_name}\")\nexcept Exception as e:\n    print(f\"Error loading model: {e}\")\n    print(\"Using fallback detection method...\")\n    model = None\n\n# Transform for ViT input\ntransform = transforms.Compose([\n    transforms.Resize((config.img_size, config.img_size)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Create visualization directory\nif config.save_visualizations:\n    os.makedirs(config.vis_output_dir, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:07:42.505146Z","iopub.execute_input":"2025-12-20T11:07:42.505666Z","iopub.status.idle":"2025-12-20T11:07:50.004683Z","shell.execute_reply.started":"2025-12-20T11:07:42.505640Z","shell.execute_reply":"2025-12-20T11:07:50.004027Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Core Functions\n","metadata":{}},{"cell_type":"code","source":"\ndef rle_encode(mask: np.ndarray) -> str:\n    \"\"\"\n    mask: (H, W), values {0,1}\n    Returns RLE string\n    \"\"\"\n    if mask.sum() == 0:\n        return \"authentic\"\n    \n    pixels = mask.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    runs = [str(int(x)) for x in runs]\n    return \"[\" + \",\".join(runs) + \"]\"\n\ndef resize_image_if_large(img_array, max_dim=1024):\n    \"\"\"\n    Resize image if it's too large to save memory\n    \"\"\"\n    h, w = img_array.shape[:2]\n    if max(h, w) > max_dim:\n        scale = max_dim / max(h, w)\n        new_h, new_w = int(h * scale), int(w * scale)\n        img_resized = cv2.resize(img_array, (new_w, new_h), interpolation=cv2.INTER_AREA)\n        return img_resized\n    return img_array\n\n@torch.no_grad()\ndef extract_patch_embeddings(img_tensor):\n    \"\"\"\n    Extract patch embeddings from ViT\n    \"\"\"\n    if model is None:\n        # Fallback: return dummy features\n        grid_size = config.img_size // config.patch_size\n        patches = torch.randn(1, grid_size * grid_size, 768).to(device)\n        return patches[0], grid_size\n    \n    feats = model.forward_features(img_tensor)\n    patches = feats[0, 1:]  # Remove cls token\n    grid_size = config.img_size // config.patch_size\n    return patches, grid_size\n\ndef compute_color_statistics(img_array):\n    \"\"\"\n    Compute basic color statistics for validation\n    \"\"\"\n    if len(img_array.shape) != 3:\n        return None\n    \n    # Convert to grayscale for intensity analysis\n    gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)\n    \n    # Basic statistics\n    mean_intensity = np.mean(gray)\n    std_intensity = np.std(gray)\n    \n    # Simple texture measure\n    sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)\n    sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)\n    gradient_magnitude = np.sqrt(sobel_x**2 + sobel_y**2)\n    texture_score = np.mean(gradient_magnitude)\n    \n    return {\n        'mean_intensity': mean_intensity,\n        'std_intensity': std_intensity,\n        'texture_score': texture_score\n    }\n\ndef detect_similar_patches(sim_matrix, threshold):\n    \"\"\"\n    Detect similar patches using given threshold\n    \"\"\"\n    sim_np = sim_matrix.cpu().numpy()\n    rows, cols = np.where(sim_np > threshold)\n    \n    pairs = []\n    for i, j in zip(rows, cols):\n        if i < j:\n            pairs.append((i, j, sim_np[i, j]))\n    \n    return pairs\n\ndef cluster_patches_simple(pairs, grid_size):\n    \"\"\"\n    Simple clustering for patches\n    \"\"\"\n    if len(pairs) < 2:\n        return None\n    \n    # Create simple adjacency\n    n_patches = grid_size * grid_size\n    adjacency = np.zeros((n_patches, n_patches), dtype=np.float32)\n    \n    for i, j, sim in pairs:\n        adjacency[i, j] = sim\n        adjacency[j, i] = sim\n    \n    # Convert to distance\n    distance = 1 - adjacency\n    \n    # Simple clustering\n    mask = np.zeros((grid_size, grid_size), dtype=np.uint8)\n    \n    # Mark all patches that appear in any pair\n    for i, j, _ in pairs:\n        x1, y1 = i // grid_size, i % grid_size\n        x2, y2 = j // grid_size, j % grid_size\n        mask[x1, y1] = 1\n        mask[x2, y2] = 1\n    \n    # Remove isolated points\n    if mask.sum() > 0:\n        labeled, num_features = ndimage.label(mask)\n        for i in range(1, num_features + 1):\n            component = (labeled == i).astype(np.uint8)\n            if component.sum() < 2:  # Remove single-point components\n                mask[labeled == i] = 0\n    \n    return mask if mask.sum() > 0 else None\n\ndef combine_masks(masks, original_size):\n    \"\"\"\n    Combine multiple detection masks\n    \"\"\"\n    if not masks:\n        return None\n    \n    # Resize masks to original size\n    resized_masks = []\n    for mask in masks:\n        if mask is not None and mask.sum() > 0:\n            resized = cv2.resize(\n                mask.astype(np.float32),\n                (original_size[0], original_size[1]),\n                interpolation=cv2.INTER_LINEAR\n            )\n            resized_masks.append(resized)\n    \n    if not resized_masks:\n        return None\n    \n    # Simple majority voting\n    combined = np.zeros_like(resized_masks[0])\n    for mask in resized_masks:\n        combined += (mask > 0.1).astype(np.float32)\n    \n    thresholded = (combined >= len(resized_masks) // 2).astype(np.uint8)\n    return thresholded if thresholded.sum() > 0 else None\n\ndef clean_mask(mask, original_size):\n    \"\"\"\n    Clean and validate mask\n    \"\"\"\n    if mask is None or mask.sum() == 0:\n        return None\n    \n    # Basic morphological cleaning\n    kernel = np.ones((7, 7), np.uint8)\n    cleaned = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)\n    cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel)\n    \n    # Remove small components\n    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(cleaned, connectivity=8)\n    \n    if num_labels <= 1:\n        return None\n    \n    # Filter by size\n    final_mask = np.zeros_like(cleaned)\n    min_area = original_size[0] * original_size[1] * config.min_area_ratio\n    max_area = original_size[0] * original_size[1] * config.max_area_ratio\n    \n    for i in range(1, num_labels):\n        area = stats[i, cv2.CC_STAT_AREA]\n        if min_area <= area <= max_area:\n            final_mask[labels == i] = 1\n    \n    return final_mask if final_mask.sum() > 0 else None\n\ndef validate_mask(mask, original_size):\n    \"\"\"\n    Validate if mask is reasonable\n    \"\"\"\n    if mask is None or mask.sum() == 0:\n        return False\n    \n    # Check coverage\n    coverage = mask.sum() / (original_size[0] * original_size[1])\n    if coverage < config.min_area_ratio or coverage > config.max_area_ratio:\n        return False\n    \n    # Check number of components\n    num_labels, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)\n    if num_labels <= 1:\n        return False\n    \n    # Check if any component is reasonably sized\n    areas = stats[1:, cv2.CC_STAT_AREA]\n    min_valid_area = original_size[0] * original_size[1] * config.min_area_ratio\n    return np.any(areas >= min_valid_area)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:00.608320Z","iopub.execute_input":"2025-12-20T11:08:00.608869Z","iopub.status.idle":"2025-12-20T11:08:00.627847Z","shell.execute_reply.started":"2025-12-20T11:08:00.608841Z","shell.execute_reply":"2025-12-20T11:08:00.626996Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# VISUALIZATION FUNCTIONS\n","metadata":{}},{"cell_type":"code","source":"def visualize_similarity_matrix(sim_matrix, image_name, output_dir):\n    \"\"\"\n    Visualize the patch similarity matrix\n    \"\"\"\n    plt.figure(figsize=(10, 8))\n    sim_np = sim_matrix.cpu().numpy()\n    \n    # Create heatmap\n    sns.heatmap(sim_np, cmap='viridis', \n                square=True, \n                cbar_kws={'label': 'Cosine Similarity'},\n                vmin=0, vmax=1)\n    \n    plt.title(f'Patch Similarity Matrix - {image_name}')\n    plt.xlabel('Patch Index')\n    plt.ylabel('Patch Index')\n    \n    # Save figure\n    if config.save_visualizations:\n        output_path = os.path.join(output_dir, f\"{image_name}_similarity_matrix.png\")\n        plt.savefig(output_path, dpi=150, bbox_inches='tight')\n    \n    if config.show_visualizations:\n        plt.show()\n    else:\n        plt.close()\n\ndef visualize_patch_detections(image_array, grid_size, similar_pairs, image_name, output_dir):\n    \"\"\"\n    Visualize detected similar patches on the image\n    \"\"\"\n    fig, axes = plt.subplots(1, 2, figsize=(16, 8))\n    \n    # Original image\n    axes[0].imshow(image_array)\n    axes[0].set_title(f'Original Image - {image_name}')\n    axes[0].axis('off')\n    \n    # Image with patch grid and similar patches highlighted\n    axes[1].imshow(image_array)\n    axes[1].set_title(f'Detected Similar Patches - {len(similar_pairs)} pairs')\n    axes[1].axis('off')\n    \n    # Calculate patch dimensions\n    h, w = image_array.shape[:2]\n    patch_h = h // grid_size\n    patch_w = w // grid_size\n    \n    # Draw grid\n    for i in range(grid_size + 1):\n        axes[0].axhline(y=i * patch_h, color='white', linestyle='-', linewidth=0.5, alpha=0.3)\n        axes[0].axvline(x=i * patch_w, color='white', linestyle='-', linewidth=0.5, alpha=0.3)\n        axes[1].axhline(y=i * patch_h, color='white', linestyle='-', linewidth=0.5, alpha=0.3)\n        axes[1].axvline(x=i * patch_w, color='white', linestyle='-', linewidth=0.5, alpha=0.3)\n    \n    # Highlight similar patches\n    colors = plt.cm.Set1(np.linspace(0, 1, 10))\n    color_idx = 0\n    \n    for i, j, similarity in similar_pairs[:20]:  # Show first 20 pairs for clarity\n        # Get patch coordinates\n        row_i, col_i = i // grid_size, i % grid_size\n        row_j, col_j = j // grid_size, j % grid_size\n        \n        # Create rectangles for patches\n        rect_i = Rectangle((col_i * patch_w, row_i * patch_h), patch_w, patch_h,\n                          linewidth=2, edgecolor=colors[color_idx % len(colors)], \n                          facecolor='none', alpha=0.7)\n        rect_j = Rectangle((col_j * patch_w, row_j * patch_h), patch_w, patch_h,\n                          linewidth=2, edgecolor=colors[color_idx % len(colors)], \n                          facecolor='none', alpha=0.7)\n        \n        axes[1].add_patch(rect_i)\n        axes[1].add_patch(rect_j)\n        \n        # Draw line connecting similar patches\n        center_i = (col_i * patch_w + patch_w/2, row_i * patch_h + patch_h/2)\n        center_j = (col_j * patch_w + patch_w/2, row_j * patch_h + patch_h/2)\n        axes[1].plot([center_i[0], center_j[0]], [center_i[1], center_j[1]], \n                    color=colors[color_idx % len(colors)], \n                    linewidth=1, alpha=0.5, linestyle='--')\n        \n        color_idx += 1\n    \n    plt.tight_layout()\n    \n    # Save figure\n    if config.save_visualizations:\n        output_path = os.path.join(output_dir, f\"{image_name}_patch_detections.png\")\n        plt.savefig(output_path, dpi=150, bbox_inches='tight')\n    \n    if config.show_visualizations:\n        plt.show()\n    else:\n        plt.close()\n\ndef visualize_detection_results(image_array, mask, image_name, prediction, output_dir):\n    \"\"\"\n    Visualize the final detection results with mask overlay\n    \"\"\"\n    fig, axes = plt.subplots(1, 4, figsize=(20, 5))\n    \n    # Original image\n    axes[0].imshow(image_array)\n    axes[0].set_title(f'Original Image\\n{image_name}')\n    axes[0].axis('off')\n    \n    # Detection mask\n    if mask is not None and mask.sum() > 0:\n        axes[1].imshow(mask, cmap='hot')\n        axes[1].set_title(f'Detection Mask\\n{int(mask.sum())} pixels detected')\n    else:\n        axes[1].imshow(np.zeros_like(image_array[:,:,0]), cmap='gray')\n        axes[1].set_title('No Forgery Detected')\n    axes[1].axis('off')\n    \n    # Overlay image\n    axes[2].imshow(image_array)\n    if mask is not None and mask.sum() > 0:\n        # Create overlay\n        overlay = image_array.copy()\n        overlay[mask == 1] = [255, 0, 0]  # Red overlay for detections\n        alpha = 0.3\n        axes[2].imshow(mask, cmap='Reds', alpha=alpha * (mask > 0))\n    axes[2].set_title('Detection Overlay')\n    axes[2].axis('off')\n    \n    # Binary mask with bounding boxes\n    axes[3].imshow(image_array)\n    if mask is not None and mask.sum() > 0:\n        # Find contours\n        contours, _ = cv2.findContours(mask.astype(np.uint8), \n                                      cv2.RETR_EXTERNAL, \n                                      cv2.CHAIN_APPROX_SIMPLE)\n        \n        # Draw bounding boxes\n        for idx, contour in enumerate(contours):\n            x, y, w, h = cv2.boundingRect(contour)\n            # Filter small detections\n            if w * h > image_array.shape[0] * image_array.shape[1] * config.min_area_ratio:\n                rect = Rectangle((x, y), w, h, \n                               linewidth=2, \n                               edgecolor='lime', \n                               facecolor='none')\n                axes[3].add_patch(rect)\n                # Add label\n                axes[3].text(x, y-5, f'Region {idx+1}', \n                           color='lime', fontweight='bold',\n                           bbox=dict(facecolor='black', alpha=0.7, edgecolor='none'))\n    \n    prediction_text = 'Forged' if prediction != 'authentic' else 'Authentic'\n    axes[3].set_title(f'Final Prediction: {prediction_text}')\n    axes[3].axis('off')\n    \n    plt.suptitle(f'Forgery Detection Results - {image_name}', fontsize=16, y=1.02)\n    plt.tight_layout()\n    \n    # Save figure\n    if config.save_visualizations:\n        output_path = os.path.join(output_dir, f\"{image_name}_detection_results.png\")\n        plt.savefig(output_path, dpi=150, bbox_inches='tight')\n    \n    if config.show_visualizations:\n        plt.show()\n    else:\n        plt.close()\n\ndef visualize_similarity_distribution(sim_matrix, image_name, output_dir):\n    \"\"\"\n    Visualize the distribution of patch similarities\n    \"\"\"\n    sim_np = sim_matrix.cpu().numpy()\n    # Get upper triangular values (excluding diagonal)\n    triu_indices = np.triu_indices_from(sim_np, k=1)\n    similarities = sim_np[triu_indices]\n    \n    fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n    \n    # Histogram\n    axes[0].hist(similarities, bins=50, edgecolor='black', alpha=0.7)\n    axes[0].axvline(x=0.85, color='red', linestyle='--', label='Threshold 0.85')\n    axes[0].axvline(x=0.88, color='orange', linestyle='--', label='Threshold 0.88')\n    axes[0].axvline(x=0.90, color='green', linestyle='--', label='Threshold 0.90')\n    axes[0].set_xlabel('Cosine Similarity')\n    axes[0].set_ylabel('Frequency')\n    axes[0].set_title('Distribution of Patch Similarities')\n    axes[0].legend()\n    axes[0].grid(True, alpha=0.3)\n    \n    # Box plot\n    axes[1].boxplot(similarities, vert=True)\n    axes[1].set_ylabel('Cosine Similarity')\n    axes[1].set_title('Similarity Statistics')\n    axes[1].grid(True, alpha=0.3)\n    \n    # Add statistics text\n    stats_text = f'Mean: {similarities.mean():.3f}\\nStd: {similarities.std():.3f}\\n'\n    stats_text += f'Min: {similarities.min():.3f}\\nMax: {similarities.max():.3f}\\n'\n    stats_text += f'Above 0.85: {(similarities > 0.85).sum()}'\n    axes[1].text(0.95, 0.05, stats_text, transform=axes[1].transAxes,\n                verticalalignment='bottom', horizontalalignment='right',\n                bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))\n    \n    plt.suptitle(f'Similarity Analysis - {image_name}', fontsize=14)\n    plt.tight_layout()\n    \n    # Save figure\n    if config.save_visualizations:\n        output_path = os.path.join(output_dir, f\"{image_name}_similarity_distribution.png\")\n        plt.savefig(output_path, dpi=150, bbox_inches='tight')\n    \n    if config.show_visualizations:\n        plt.show()\n    else:\n        plt.close()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:08.069859Z","iopub.execute_input":"2025-12-20T11:08:08.070401Z","iopub.status.idle":"2025-12-20T11:08:08.093538Z","shell.execute_reply.started":"2025-12-20T11:08:08.070373Z","shell.execute_reply":"2025-12-20T11:08:08.092781Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# MAIN PREDICTION FUNCTION\n","metadata":{}},{"cell_type":"code","source":"\ndef predict_image_offline_with_visualization(image_path, image_name=None):\n    \"\"\"\n    Main prediction function for offline submission with visualization\n    \"\"\"\n    try:\n        # Load and process image\n        img = Image.open(image_path).convert('RGB')\n        img_array = np.array(img)\n        original_size = img.size\n        \n        # Resize if too large (memory optimization)\n        if max(original_size) > config.max_image_dim:\n            img_array = resize_image_if_large(img_array, config.max_image_dim)\n            img = Image.fromarray(img_array)\n            original_size = img.size\n        \n        # Get color statistics for validation\n        color_stats = compute_color_statistics(img_array)\n        \n        # Transform for model\n        img_tensor = transform(img).unsqueeze(0).to(device)\n        \n        # Extract embeddings\n        patches, grid_size = extract_patch_embeddings(img_tensor)\n        \n        # Compute similarity matrix\n        patches_norm = torch.nn.functional.normalize(patches, dim=1)\n        sim_matrix = patches_norm @ patches_norm.T\n        sim_matrix.fill_diagonal_(0)\n        \n        # Get image name for saving\n        if image_name is None:\n            image_name = os.path.splitext(os.path.basename(image_path))[0]\n        \n        # Create image-specific visualization directory\n        img_vis_dir = os.path.join(config.vis_output_dir, image_name)\n        if config.save_visualizations:\n            os.makedirs(img_vis_dir, exist_ok=True)\n        \n        # Visualize similarity matrix\n        visualize_similarity_matrix(sim_matrix, image_name, img_vis_dir)\n        \n        # Visualize similarity distribution\n        visualize_similarity_distribution(sim_matrix, image_name, img_vis_dir)\n        \n        # Try multiple thresholds\n        detection_masks = []\n        all_pairs = []\n        \n        for threshold in config.similarity_thresholds:\n            pairs = detect_similar_patches(sim_matrix, threshold)\n            all_pairs.extend(pairs)\n            \n            if len(pairs) >= config.min_similar_pairs:\n                mask = cluster_patches_simple(pairs, grid_size)\n                if mask is not None:\n                    detection_masks.append(mask)\n        \n        # Visualize patch detections\n        visualize_patch_detections(img_array, grid_size, all_pairs[:50], image_name, img_vis_dir)\n        \n        # Combine masks\n        combined_mask = combine_masks(detection_masks, original_size)\n        \n        if combined_mask is None:\n            # Visualize result (authentic)\n            visualize_detection_results(img_array, None, image_name, \"authentic\", img_vis_dir)\n            return \"authentic\", None\n        \n        # Clean mask\n        cleaned_mask = clean_mask(combined_mask, original_size)\n        \n        if cleaned_mask is None:\n            # Visualize result (authentic)\n            visualize_detection_results(img_array, None, image_name, \"authentic\", img_vis_dir)\n            return \"authentic\", None\n        \n        # Validate mask\n        if not validate_mask(cleaned_mask, original_size):\n            # Visualize result (authentic)\n            visualize_detection_results(img_array, None, image_name, \"authentic\", img_vis_dir)\n            return \"authentic\", None\n        \n        # Additional validation based on color statistics\n        if color_stats is not None:\n            # If image has very low texture, be more strict\n            if color_stats['texture_score'] < 5:  # Very smooth image\n                coverage = cleaned_mask.sum() / (original_size[0] * original_size[1])\n                if coverage > 0.3:  # Too much coverage for smooth image\n                    # Visualize result (authentic)\n                    visualize_detection_results(img_array, None, image_name, \"authentic\", img_vis_dir)\n                    return \"authentic\", None\n        \n        # Convert to RLE\n        rle_string = rle_encode(cleaned_mask)\n        \n        # Visualize final results\n        visualize_detection_results(img_array, cleaned_mask, image_name, rle_string, img_vis_dir)\n        \n        return rle_string, cleaned_mask\n        \n    except Exception as e:\n        print(f\"Error processing image {image_path}: {e}\")\n        return \"authentic\", None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:22.298876Z","iopub.execute_input":"2025-12-20T11:08:22.299800Z","iopub.status.idle":"2025-12-20T11:08:22.309934Z","shell.execute_reply.started":"2025-12-20T11:08:22.299764Z","shell.execute_reply":"2025-12-20T11:08:22.309340Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Test Simple Image FUNCTION","metadata":{}},{"cell_type":"code","source":"\ndef test_single_image(image_path):\n    \"\"\"\n    Test and visualize a single image\n    \"\"\"\n    print(f\"\\n🔍 Testing single image: {os.path.basename(image_path)}\")\n    print(\"=\" * 50)\n    \n    # Make prediction with visualization\n    image_name = os.path.splitext(os.path.basename(image_path))[0]\n    prediction, mask = predict_image_offline_with_visualization(image_path, image_name)\n    \n    # Display results\n    print(f\"📊 Prediction: {'FORGED' if prediction != 'authentic' else 'AUTHENTIC'}\")\n    \n    if prediction != 'authentic':\n        try:\n            runs = eval(prediction)\n            num_runs = len(runs)\n            mask_pixels = sum(runs[1::2])\n            print(f\"📏 RLE Details: {num_runs} runs, {mask_pixels} pixels\")\n            print(f\"📝 RLE String: {prediction[:100]}...\" if len(prediction) > 100 else f\"📝 RLE String: {prediction}\")\n        except:\n            print(f\"📝 RLE String: {prediction}\")\n    \n    print(f\"📁 Visualizations saved to: {os.path.join(config.vis_output_dir, image_name)}\")\n    print(\"=\" * 50)\n    \n    return prediction, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:26.948182Z","iopub.execute_input":"2025-12-20T11:08:26.948881Z","iopub.status.idle":"2025-12-20T11:08:26.954535Z","shell.execute_reply.started":"2025-12-20T11:08:26.948845Z","shell.execute_reply":"2025-12-20T11:08:26.953917Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TEST SINGLE IMAGE FUNCTION\n","metadata":{}},{"cell_type":"code","source":"\ndef create_sample_submission_with_visualization(test_dir, num_samples=100):\n    \"\"\"\n    Create a sample submission file with visualization\n    \"\"\"\n    # Get test files\n    test_files = []\n    if os.path.exists(test_dir):\n        for fname in sorted(os.listdir(test_dir)):\n            if fname.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.bmp')):\n                img_path = os.path.join(test_dir, fname)\n                test_files.append((fname, img_path))\n    \n    # If no test files, create dummy submission\n    if not test_files:\n        print(\"No test images found. Creating sample submission...\")\n        results = []\n        for i in range(1, num_samples + 1):\n            # Mix of authentic and forged (20% forged)\n            if i % 5 == 0:\n                results.append((i, \"[1, 1000, 500, 800, 200, 600]\"))\n            else:\n                results.append((i, \"authentic\"))\n        \n        submission_df = pd.DataFrame(results, columns=[\"case_id\", \"annotation\"])\n        return submission_df\n    \n    # Process real test files\n    results = []\n    print(f\"Processing {len(test_files)} test images...\")\n    \n    for idx, (fname, img_path) in enumerate(test_files, 1):\n        try:\n            # Extract case_id from filename\n            import re\n            numbers = re.findall(r'\\d+', fname)\n            if numbers:\n                case_id = int(numbers[-1])\n            else:\n                # Use hash as fallback\n                case_id = hash(fname) % 100000\n            \n            # Make prediction with visualization\n            image_name = os.path.splitext(fname)[0]\n            annotation, mask = predict_image_offline_with_visualization(img_path, image_name)\n            results.append((case_id, annotation))\n            \n            print(f\"  [{idx}/{len(test_files)}] Processed: {fname} -> Case ID: {case_id}, Prediction: {'Forged' if annotation != 'authentic' else 'Authentic'}\")\n            \n        except Exception as e:\n            print(f\"Error processing {fname}: {e}\")\n            try:\n                numbers = re.findall(r'\\d+', fname)\n                case_id = int(numbers[0]) if numbers else hash(fname) % 100000\n                results.append((case_id, \"authentic\"))\n            except:\n                pass\n    \n    # Create DataFrame\n    submission_df = pd.DataFrame(results, columns=[\"case_id\", \"annotation\"])\n    \n    # Sort by case_id and remove duplicates\n    submission_df = submission_df.sort_values(\"case_id\").reset_index(drop=True)\n    submission_df = submission_df.drop_duplicates(subset=['case_id'], keep='first')\n    \n    return submission_df\n\ndef validate_submission(submission_df):\n    \"\"\"\n    Validate submission format\n    \"\"\"\n    if submission_df.empty:\n        print(\"❌ Submission is empty!\")\n        return False\n    \n    # Check required columns\n    required_cols = ['case_id', 'annotation']\n    if not all(col in submission_df.columns for col in required_cols):\n        print(f\"❌ Missing required columns. Expected: {required_cols}\")\n        return False\n    \n    # Check annotation format\n    invalid_count = 0\n    for idx, row in submission_df.iterrows():\n        annotation = row['annotation']\n        if annotation == 'authentic':\n            continue\n        elif isinstance(annotation, str) and annotation.startswith('[') and annotation.endswith(']'):\n            # Try to parse the RLE to validate\n            try:\n                rle_list = eval(annotation)\n                if not isinstance(rle_list, list) or len(rle_list) % 2 != 0:\n                    invalid_count += 1\n            except:\n                invalid_count += 1\n        else:\n            invalid_count += 1\n    \n    if invalid_count > 0:\n        print(f\"❌ Found {invalid_count} invalid annotations\")\n        return False\n    \n    # Check for duplicates\n    duplicates = submission_df['case_id'].duplicated().sum()\n    if duplicates > 0:\n        print(f\"⚠️  Found {duplicates} duplicate case_ids\")\n        # Remove duplicates\n        submission_df = submission_df.drop_duplicates(subset=['case_id'], keep='first')\n    \n    # Check for null values\n    null_count = submission_df.isnull().sum().sum()\n    if null_count > 0:\n        print(f\"⚠️  Found {null_count} null values\")\n    \n    print(f\"✅ Submission validation passed\")\n    print(f\"   Total predictions: {len(submission_df)}\")\n    print(f\"   Authentic: {(submission_df['annotation'] == 'authentic').sum()}\")\n    print(f\"   Forged: {len(submission_df) - (submission_df['annotation'] == 'authentic').sum()}\")\n    \n    return True\n\ndef visualize_submission_statistics(submission_df):\n    \"\"\"\n    Create visualization of submission statistics\n    \"\"\"\n    if submission_df.empty:\n        return\n    \n    # Calculate statistics\n    authentic_count = (submission_df['annotation'] == 'authentic').sum()\n    forgery_count = len(submission_df) - authentic_count\n    \n    # Create figure\n    fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n    \n    # Pie chart\n    labels = ['Authentic', 'Forged']\n    sizes = [authentic_count, forgery_count]\n    colors = ['lightgreen', 'lightcoral']\n    explode = (0.1, 0) if forgery_count > 0 else (0, 0)\n    \n    axes[0].pie(sizes, explode=explode, labels=labels, colors=colors,\n               autopct='%1.1f%%', shadow=True, startangle=90)\n    axes[0].set_title('Prediction Distribution')\n    axes[0].axis('equal')\n    \n    # Bar chart\n    axes[1].bar(labels, sizes, color=colors, edgecolor='black')\n    axes[1].set_ylabel('Number of Images')\n    axes[1].set_title('Prediction Counts')\n    axes[1].grid(axis='y', alpha=0.3)\n    \n    # Add value labels on bars\n    for i, v in enumerate(sizes):\n        axes[1].text(i, v + 0.5, str(v), ha='center', va='bottom', fontweight='bold')\n    \n    # RLE length distribution for forged images\n    if forgery_count > 0:\n        forged_df = submission_df[submission_df['annotation'] != 'authentic']\n        rle_lengths = []\n        \n        for annotation in forged_df['annotation']:\n            try:\n                rle_list = eval(annotation)\n                rle_lengths.append(len(rle_list))\n            except:\n                rle_lengths.append(0)\n        \n        axes[2].hist(rle_lengths, bins=20, edgecolor='black', alpha=0.7, color='lightcoral')\n        axes[2].set_xlabel('RLE Length (number of runs)')\n        axes[2].set_ylabel('Frequency')\n        axes[2].set_title('RLE Length Distribution for Forged Images')\n        axes[2].grid(True, alpha=0.3)\n    else:\n        axes[2].text(0.5, 0.5, 'No forged images detected', \n                    ha='center', va='center', transform=axes[2].transAxes,\n                    fontsize=12, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))\n        axes[2].set_title('RLE Length Distribution')\n        axes[2].axis('off')\n    \n    plt.suptitle('Submission Statistics', fontsize=16, y=1.05)\n    plt.tight_layout()\n    \n    # Save figure\n    if config.save_visualizations:\n        output_path = os.path.join(config.vis_output_dir, \"submission_statistics.png\")\n        plt.savefig(output_path, dpi=150, bbox_inches='tight')\n    \n    if config.show_visualizations:\n        plt.show()\n    else:\n        plt.close()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:32.163320Z","iopub.execute_input":"2025-12-20T11:08:32.163983Z","iopub.status.idle":"2025-12-20T11:08:32.182375Z","shell.execute_reply.started":"2025-12-20T11:08:32.163959Z","shell.execute_reply":"2025-12-20T11:08:32.181493Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Main Execution\n","metadata":{}},{"cell_type":"code","source":"def main():\n    print(\"=\" * 70)\n    print(\"SCIENTIFIC IMAGE FORGERY DETECTION - OFFLINE SUBMISSION WITH VISUALIZATION\")\n    print(\"=\" * 70)\n    print(\"This version is designed for offline Kaggle submission\")\n    print(\"No internet connection required during execution\")\n    print(f\"Visualizations will be saved to: {config.vis_output_dir}\")\n    print(\"=\" * 70)\n    \n    # Memory optimization\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    gc.collect()\n    \n    # Define test directory\n    test_dir = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\"\n    \n    # Option 1: Test a single image (uncomment to use)\n    # single_image_path = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images/45.png\"\n    # if os.path.exists(single_image_path):\n    #     test_single_image(single_image_path)\n    \n    # Option 2: Create full submission\n    print(\"\\n📁 Creating submission with visualization...\")\n    submission_df = create_sample_submission_with_visualization(test_dir)\n    \n    # Validate submission\n    print(\"\\n🔍 Validating submission...\")\n    is_valid = validate_submission(submission_df)\n    \n    if not is_valid:\n        print(\"❌ Submission validation failed. Creating fallback submission...\")\n        # Create simple fallback submission\n        fallback_data = []\n        for i in range(1, 101):\n            fallback_data.append((i, \"authentic\"))\n        submission_df = pd.DataFrame(fallback_data, columns=[\"case_id\", \"annotation\"])\n    \n    # Visualize submission statistics\n    print(\"\\n📊 Creating submission statistics visualization...\")\n    visualize_submission_statistics(submission_df)\n    \n    # Save submission\n    output_path = \"/kaggle/working/submission.csv\"\n    submission_df.to_csv(output_path, index=False)\n    \n    print(f\"\\n📄 Submission saved: {output_path}\")\n    \n    # Show preview\n    print(\"\\n👀 Submission Preview:\")\n    print(submission_df.head(10).to_string(index=False))\n    \n    # Calculate statistics\n    authentic_count = (submission_df['annotation'] == 'authentic').sum()\n    forgery_count = len(submission_df) - authentic_count\n    \n    print(f\"\\n📊 Final Statistics:\")\n    print(f\"   Total images: {len(submission_df)}\")\n    print(f\"   Authentic: {authentic_count} ({authentic_count/len(submission_df)*100:.1f}%)\")\n    print(f\"   Forged: {forgery_count} ({forgery_count/len(submission_df)*100:.1f}%)\")\n    \n    # Show RLE statistics for forged images\n    if forgery_count > 0:\n        forged_df = submission_df[submission_df['annotation'] != 'authentic']\n        print(f\"\\n🔍 Forged Image Details:\")\n        for _, row in forged_df.iterrows():\n            annotation = row['annotation']\n            if annotation != 'authentic':\n                try:\n                    runs = eval(annotation)\n                    num_runs = len(runs)\n                    mask_pixels = sum(runs[1::2])\n                    print(f\"   Case {row['case_id']}: {num_runs} runs, {mask_pixels:,} pixels\")\n                except:\n                    print(f\"   Case {row['case_id']}: RLE encoding\")\n    \n    print(f\"\\n✅ OFFLINE SUBMISSION WITH VISUALIZATION READY\")\n    print(f\"   Visualizations saved to: {config.vis_output_dir}\")\n    print(\"=\" * 70)\n    \n    return submission_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:46.590104Z","iopub.execute_input":"2025-12-20T11:08:46.590438Z","iopub.status.idle":"2025-12-20T11:08:46.599959Z","shell.execute_reply.started":"2025-12-20T11:08:46.590413Z","shell.execute_reply":"2025-12-20T11:08:46.599155Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Run with error handling","metadata":{}},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    try:\n        submission = main()\n    except Exception as e:\n        print(f\"\\n❌ Error in main execution: {e}\")\n        print(\"Creating emergency fallback submission...\")\n        \n        # Emergency fallback\n        try:\n            emergency_data = []\n            for i in range(1, 101):\n                emergency_data.append((i, \"authentic\"))\n            \n            emergency_df = pd.DataFrame(emergency_data, columns=[\"case_id\", \"annotation\"])\n            emergency_df.to_csv(\"/kaggle/working/submission.csv\", index=False)\n            print(\"✅ Emergency submission created\")\n        except:\n            print(\"❌ Could not create emergency submission\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-20T11:08:52.157603Z","iopub.execute_input":"2025-12-20T11:08:52.157903Z","iopub.status.idle":"2025-12-20T11:08:57.369417Z","shell.execute_reply.started":"2025-12-20T11:08:52.157880Z","shell.execute_reply":"2025-12-20T11:08:57.368678Z"}},"outputs":[],"execution_count":null}]}