{"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":"This is a comprehensive scientific image forgery detection pipeline. Let me break down the code and results in full detail:\n\nCODE OVERVIEW\n\nThis is a complete deep learning pipeline for detecting copy-move forgery in scientific images, with these key components:\n\n1. Problem Context\n\n    Detects manipulated regions in scientific images where content has been copied and pasted\n\n    Handles both classification (authentic vs forged) and segmentation (localizing forged regions)\n\n    Designed for a Kaggle competition on scientific image forgery detection\n\n2. Architecture Components\n\n    Memory Management: Real-time monitoring to prevent OOM errors\n\n    Evaluation Metrics: Custom F1 score with Hungarian algorithm for mask matching\n\n    Data Pipeline: Efficient chunked loading for large datasets\n\n    Feature Engineering: Handcrafted texture, color, and copy-move features\n\n    Deep Learning Model: Hybrid CNN with attention mechanism for both classification and segmentation\n\n    Training Pipeline: With early stopping, learning rate scheduling\n\n    Visualization: Comprehensive EDA and result analysis\n\n    Submission Generation: Creates competition-ready output\n\n3. Key Technical Features\n\n    Dual-head network: Classifies image AND segments forged regions\n\n    Attention mechanisms: Focuses on suspicious areas\n\n    Feature fusion: Combines learned and handcrafted features\n\n    Memory-efficient processing: Critical for large image datasets\n\n    Comprehensive evaluation: Multiple metrics and visualizations\n\nEXECUTION RESULTS ANALYSIS\nData Loading (Step 1)\ntext\n\nTotal images: 5176\nForged: 2799 (54.1%)\nAuthentic: 2377 (45.9%)\n\n    Dataset has good balance between authentic and forged images\n\n    Supplemental data adds 48 additional forged images\n\nTraining Results (Step 5)\n\nThe model trained for only 14 epochs before early stopping triggered:\n\n    Best accuracy: 53.96% (epoch 6)\n\n    Final validation accuracy: 51.83%\n\n    Final F1 score: 0.419\n\nKey Observations from Training:\n\n    Poor Performance: Accuracy is only slightly above random guessing (50%)\n\n    Early Stopping: Model stopped improving after 6 epochs\n\n    Low F1 Score: 0.419 indicates poor balance between precision and recall\n\nCritical Issue Identified\n\nThe EDA revealed a major problem:\ntext\n\n3. Mask Statistics:\n  Average mask area: 0.0 pixels\n  Min mask area: 0.0 pixels\n  Max mask area: 0.0 pixels\n\nThis means the masks are all empty! The model has no ground truth segmentation masks to learn from during training.\nSubmission Results (Step 7)\ntext\n\nSubmission Statistics:\n  Total images: 5177\n  Authentic predictions: 5177 (100%)\n  Forged predictions: 0\n\nThe model predicted ALL images as authentic! This happens because:\n\n    Without meaningful masks, the segmentation loss doesn't work properly\n\n    The classification loss alone leads to predicting the majority class\n\n    The model defaults to \"authentic\" for all predictions\n\nROOT CAUSE ANALYSIS\nPrimary Issues:\n\n    Data Loading Problem: The masks are either:\n\n        Not in the expected .npy format\n\n        Empty or corrupted\n\n        Not being loaded from the correct paths\n\n    Incorrect Data Split: The EDA sample (first 500 images) contained only forged images, which is unusual and suggests data loading issues.\n\n    Mask Path Resolution: The code expects masks in specific directories but they might not exist or be structured differently.\n\nTechnical Problems in the Code:\n\n    Data Discovery Logic:\n\npython\n\n# The code looks for masks in specific patterns:\nmask_path = train_masks_dir / f\"{img_id}.npy\"\n\nBut if masks are in different format or location, they won't be found.\n\n    Mask Loading:\n\npython\n\nmask_data = np.load(self.mask_paths[i])\n\nIf .npy files don't exist or contain zeros, masks remain empty.\n\n    Training Imbalance: With empty masks, the segmentation loss becomes ineffective:\n\npython\n\nloss_seg = self.criterion_seg(seg_pred, masks) * forged_mask\n# If masks are all zeros, this loss doesn't provide useful gradients\n\nWHY THE PIPELINE CLAIMS \"SUCCESS\"\n\nThe final message shows a logical error:\npython\n\nif trainer.best_accuracy > 0.95:  # BUG: Should be > 95.0 or > 0.9596\n    print(\" SUCCESS: Model achieved high accuracy!\")\n\nSince best_accuracy is in percentage (53.96), the condition > 0.95 is always true (53.96 > 0.95), causing incorrect success message.\nRECOMMENDATIONS FOR FIX\nImmediate Fixes:\n\n    Debug Mask Loading:\n\n        Check actual mask file formats and paths\n\n        Add debug prints to see if masks are being found\n\n        Verify mask contents with visual inspection\n\n    Fix Data Splitting:\n\n        Ensure stratified sampling works correctly\n\n        Verify label distribution in train/val splits\n\n    Correct Success Condition:\n\npython\n\nif trainer.best_accuracy > 95.0:  # 95% accuracy\n\nArchitecture Improvements:\n\n    Add Mask Validation: Check mask quality during loading\n\n    Better Error Handling: Graceful degradation when masks missing\n\n    Alternative Approaches:\n\n        Two-stage training (classification first, then segmentation)\n\n        Weakly-supervised learning if masks are poor quality\n\n        Data augmentation specifically for forgery patterns\n\nDataset Verification Steps:\n\n    Check actual competition dataset structure\n\n    Verify mask file extensions and formats\n\n    Test loading a few masks manually\n\n    Plot sample masks to verify they contain meaningful data\n\nCONCLUSION\n\nThis is a well-structured pipeline that failed due to data loading issues rather than algorithmic problems. The architecture is sound but:\n\n    Major bug: Masks aren't being loaded correctly, making segmentation impossible\n\n    Secondary bug: Success condition logic error\n\n    The pipeline executed technically correctly but on invalid data\n\nThe real value: The code structure is excellent and reusable. Once the data loading issues are fixed, this could be a strong solution for image forgery detection tasks.\n\nThe pipeline demonstrates good practices in:\n\n    Memory management for large-scale image processing\n\n    Comprehensive model evaluation\n\n    Production-ready submission generation\n\n    Modular, maintainable code structure","metadata":{}},{"cell_type":"code","source":"# ==============================================\n# COMPREHENSIVE SCIENTIFIC IMAGE FORGERY DETECTION PIPELINE\n# ==============================================\n\nimport os\nimport gc\nimport json\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom typing import Tuple, List, Dict, Optional, Any\nimport numpy as np\nimport pandas as pd\nimport numba\nfrom numba import types\nimport numpy.typing as npt\nimport scipy.optimize\nfrom scipy import ndimage\nimport cv2\nfrom tqdm.auto import tqdm\nimport matplotlib.pyplot as plt\nimport matplotlib\nmatplotlib.use('Agg')  # Non-interactive backend for memory efficiency\nimport seaborn as sns\nfrom sklearn.model_selection import train_test_split, StratifiedKFold\nfrom sklearn.metrics import confusion_matrix, roc_curve, auc, precision_recall_curve\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms, models\nimport torch.optim as optim\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nimport psutil\nimport time\nimport random\nfrom collections import defaultdict, Counter\nimport logging\n\n# Set random seeds for reproducibility\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(42)\n\n# ==============================================\n# MEMORY MANAGEMENT & MONITORING\n# ==============================================\n\nclass MemoryMonitor:\n    \"\"\"Real-time memory usage monitoring\"\"\"\n    def __init__(self, memory_limit_gb=8):\n        self.memory_limit = memory_limit_gb * 1024**3  # Convert to bytes\n        self.peak_memory = 0\n        \n    def get_memory_usage(self):\n        \"\"\"Get current memory usage in GB\"\"\"\n        process = psutil.Process(os.getpid())\n        memory_gb = process.memory_info().rss / 1024**3\n        self.peak_memory = max(self.peak_memory, memory_gb)\n        return memory_gb\n    \n    def check_memory_safe(self, required_gb=1):\n        \"\"\"Check if there's enough memory for operation\"\"\"\n        current = self.get_memory_usage()\n        available = self.memory_limit / 1024**3 - current\n        return available > required_gb\n    \n    def force_cleanup(self):\n        \"\"\"Force garbage collection and clear caches\"\"\"\n        gc.collect()\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        return self.get_memory_usage()\n    \n    def log_memory_status(self, operation_name=\"\"):\n        \"\"\"Log current memory status\"\"\"\n        current = self.get_memory_usage()\n        status = f\"{operation_name} - Memory: {current:.2f}GB (Peak: {self.peak_memory:.2f}GB)\"\n        return status\n\nmem_monitor = MemoryMonitor(memory_limit_gb=10)\n\n# ==============================================\n# EVALUATION METRICS (Provided by User)\n# ==============================================\n\nclass ParticipantVisibleError(Exception):\n    pass\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\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\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    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\ndef calculate_f1_score(pred_mask: npt.NDArray, gt_mask: npt.NDArray):\n    pred_flat = pred_mask.flatten()\n    gt_flat = gt_mask.flatten()\n\n    tp = np.sum((pred_flat == 1) & (gt_flat == 1))\n    fp = np.sum((pred_flat == 1) & (gt_flat == 0))\n    fn = np.sum((pred_flat == 0) & (gt_flat == 1))\n\n    precision = tp / (tp + fp) if (tp + fp) > 0 else 0\n    recall = tp / (tp + fn) if (tp + fn) > 0 else 0\n\n    if (precision + recall) > 0:\n        return 2 * (precision * recall) / (precision + recall)\n    else:\n        return 0\n\ndef calculate_f1_matrix(pred_masks: list[npt.NDArray], gt_masks: list[npt.NDArray]):\n    \"\"\"\n    Parameters:\n    pred_masks (np.ndarray):\n            First dimension is the number of predicted instances.\n            Each instance is a binary mask of shape (height, width).\n    gt_masks (np.ndarray):\n            First dimension is the number of ground truth instances.\n            Each instance is a binary mask of shape (height, width).\n    \"\"\"\n    num_instances_pred = len(pred_masks)\n    num_instances_gt = len(gt_masks)\n    f1_matrix = np.zeros((num_instances_pred, num_instances_gt))\n\n    # Calculate F1 scores for each pair of predicted and ground truth masks\n    for i in range(num_instances_pred):\n        for j in range(num_instances_gt):\n            f1_matrix[i, j] = calculate_f1_score(pred_masks[i], gt_masks[j])\n\n    if f1_matrix.shape[0] < len(gt_masks):\n        # Add a row of zeros to the matrix if the number of predicted instances is less than ground truth instances\n        f1_matrix = np.vstack((f1_matrix, np.zeros((len(gt_masks) - len(f1_matrix), num_instances_gt))))\n\n    return f1_matrix\n\ndef oF1_score(pred_masks: list[npt.NDArray], gt_masks: list[npt.NDArray]):\n    \"\"\"\n    Calculate the optimal F1 score for a set of predicted masks against\n    ground truth masks which considers the optimal F1 score matching.\n    This function uses the Hungarian algorithm to find the optimal assignment\n    of predicted masks to ground truth masks based on the F1 score matrix.\n    If the number of predicted masks is less than the number of ground truth masks,\n    it will add a row of zeros to the F1 score matrix to ensure that the dimensions match.\n\n    Parameters:\n    pred_masks (list of np.ndarray): List of predicted binary masks.\n    gt_masks (np.ndarray): Array of ground truth binary masks.\n    Returns:\n    float: Optimal F1 score.\n    \"\"\"\n    f1_matrix = calculate_f1_matrix(pred_masks, gt_masks)\n\n    # Find the best matching between predicted and ground truth masks\n    row_ind, col_ind = scipy.optimize.linear_sum_assignment(-f1_matrix)\n    # The linear_sum_assignment discards excess predictions so we need a separate penalty.\n    excess_predictions_penalty = len(gt_masks) / max(len(pred_masks), len(gt_masks))\n    return np.mean(f1_matrix[row_ind, col_ind]) * excess_predictions_penalty\n\ndef evaluate_single_image(label_rles: str, prediction_rles: str, shape_str: str) -> float:\n    shape = json.loads(shape_str)\n    label_rles = [rle_decode(x, shape=shape) for x in label_rles.split(';')]\n    prediction_rles = [rle_decode(x, shape=shape) for x in prediction_rles.split(';')]\n    return oF1_score(prediction_rles, label_rles)\n\ndef score(solution: pd.DataFrame, submission: pd.DataFrame, row_id_column_name: str) -> float:\n    \"\"\"\n    Official scoring function\n    \"\"\"\n    df = solution\n    df = df.rename(columns={'annotation': 'label'})\n\n    df['prediction'] = submission['annotation']\n    # Check for correct 'authentic' label\n    authentic_indices = (df['label'] == 'authentic') | (df['prediction'] == 'authentic')\n    df['image_score'] = ((df['label'] == df['prediction']) & authentic_indices).astype(float)\n\n    df.loc[~authentic_indices, 'image_score'] = df.loc[~authentic_indices].apply(\n        lambda row: evaluate_single_image(row['label'], row['prediction'], row['shape']), axis=1\n    )\n    return float(np.mean(df['image_score']))\n\n# ==============================================\n# DATA LOADING & PREPROCESSING\n# ==============================================\n\nclass ScientificForgeryDataset:\n    \"\"\"Memory-efficient dataset loader for scientific images\"\"\"\n    \n    def __init__(self, root_path: str, max_images_per_chunk: int = 1000, mode: str = 'train'):\n        self.root_path = Path(root_path)\n        self.max_images_per_chunk = max_images_per_chunk\n        self.memory_monitor = MemoryMonitor()\n        self.image_paths = []\n        self.mask_paths = []\n        self.labels = []  # 0 for authentic, 1 for forged\n        self.image_shapes = {}\n        self.mode = mode  # 'train', 'val', or 'test'\n        \n    def discover_files(self):\n        \"\"\"Discover all image and mask files recursively\"\"\"\n        print(\"Discovering files...\")\n        \n        # Train images (forged and authentic)\n        train_forged_dir = self.root_path / \"train_images\" / \"forged\"\n        train_authentic_dir = self.root_path / \"train_images\" / \"authentic\"\n        train_masks_dir = self.root_path / \"train_masks\"\n        supplemental_images_dir = self.root_path / \"supplemental_images\"\n        supplemental_masks_dir = self.root_path / \"supplemental_masks\"\n        test_images_dir = self.root_path / \"test_images\"\n        \n        # Collect train forged images\n        if train_forged_dir.exists():\n            for img_path in tqdm(list(train_forged_dir.glob(\"*.png\")), desc=\"Forged images\"):\n                img_id = img_path.stem\n                mask_path = train_masks_dir / f\"{img_id}.npy\"\n                if mask_path.exists():\n                    self.image_paths.append(str(img_path))\n                    self.mask_paths.append(str(mask_path))\n                    self.labels.append(1)  # Forged\n                    self.image_shapes[img_id] = cv2.imread(str(img_path)).shape[:2]\n        \n        # Collect train authentic images\n        if train_authentic_dir.exists():\n            for img_path in tqdm(list(train_authentic_dir.glob(\"*.png\")), desc=\"Authentic images\"):\n                img_id = img_path.stem\n                self.image_paths.append(str(img_path))\n                self.mask_paths.append(None)  # No masks for authentic\n                self.labels.append(0)  # Authentic\n                self.image_shapes[img_id] = cv2.imread(str(img_path)).shape[:2]\n        \n        # Collect supplemental data\n        if supplemental_images_dir.exists() and supplemental_masks_dir.exists():\n            for img_path in tqdm(list(supplemental_images_dir.glob(\"*.png\")), desc=\"Supplemental images\"):\n                img_id = img_path.stem\n                mask_path = supplemental_masks_dir / f\"{img_id}.npy\"\n                if mask_path.exists():\n                    self.image_paths.append(str(img_path))\n                    self.mask_paths.append(str(mask_path))\n                    self.labels.append(1)  # Forged\n                    self.image_shapes[img_id] = cv2.imread(str(img_path)).shape[:2]\n        \n        # Collect test images - only if in test mode\n        if self.mode == 'test' and test_images_dir.exists():\n            for img_path in tqdm(list(test_images_dir.glob(\"*.png\")), desc=\"Test images\"):\n                img_id = img_path.stem\n                self.image_paths.append(str(img_path))\n                self.mask_paths.append(None)\n                self.labels.append(0)  # Default label for test\n                self.image_shapes[img_id] = cv2.imread(str(img_path)).shape[:2]\n        \n        print(f\"Total images: {len(self.image_paths)}\")\n        if self.mode != 'test':\n            print(f\"Forged: {sum(1 for l in self.labels if l == 1)}\")\n            print(f\"Authentic: {sum(1 for l in self.labels if l == 0)}\")\n        \n        return self\n    \n    def load_chunk(self, chunk_idx: int, transform=None):\n        \"\"\"Load a chunk of images and masks\"\"\"\n        start_idx = chunk_idx * self.max_images_per_chunk\n        end_idx = min(start_idx + self.max_images_per_chunk, len(self.image_paths))\n        \n        images = []\n        masks = []\n        labels_chunk = []\n        ids_chunk = []\n        \n        for i in tqdm(range(start_idx, end_idx), desc=f\"Loading chunk {chunk_idx}\"):\n            if not self.memory_monitor.check_memory_safe(0.5):\n                print(\"Memory limit reached, stopping chunk loading\")\n                break\n                \n            # Load image\n            img_path = self.image_paths[i]\n            img = cv2.imread(img_path)\n            if img is None:\n                continue\n                \n            # Resize for consistency (maintain aspect ratio)\n            target_size = (256, 256)\n            img = cv2.resize(img, target_size)\n            img = img.astype(np.float32) / 255.0\n            \n            # Load mask if exists\n            mask = None\n            if self.mask_paths[i] is not None and os.path.exists(self.mask_paths[i]):\n                mask_data = np.load(self.mask_paths[i])\n                if mask_data.ndim == 2:\n                    mask = cv2.resize(mask_data.astype(np.float32), target_size)\n                    mask = (mask > 0.5).astype(np.float32)\n                else:\n                    mask = np.zeros(target_size, dtype=np.float32)\n            else:\n                mask = np.zeros(target_size, dtype=np.float32)\n            \n            # Apply transforms if provided\n            if transform:\n                # Convert to tensor format first\n                img_tensor = torch.from_numpy(img).permute(2, 0, 1).float()\n                img_tensor = transform(img_tensor)\n                img = img_tensor.permute(1, 2, 0).numpy()\n                \n                if mask is not None:\n                    mask_tensor = torch.from_numpy(mask).unsqueeze(0).float()\n                    mask_tensor = transform(mask_tensor)\n                    mask = mask_tensor.squeeze(0).numpy()\n            \n            images.append(img)\n            masks.append(mask)\n            labels_chunk.append(self.labels[i])\n            ids_chunk.append(Path(img_path).stem)\n        \n        return {\n            'images': np.array(images),\n            'masks': np.array(masks),\n            'labels': np.array(labels_chunk),\n            'ids': ids_chunk\n        }\n    \n    def get_data_iterator(self, batch_size=32, transform=None, indices=None):\n        \"\"\"Create a memory-efficient data iterator\"\"\"\n        if indices is not None:\n            # Use specific indices if provided\n            selected_indices = indices\n        else:\n            selected_indices = list(range(len(self.image_paths)))\n        \n        num_chunks = (len(selected_indices) + self.max_images_per_chunk - 1) // self.max_images_per_chunk\n        \n        for chunk_idx in range(num_chunks):\n            chunk_data = self.load_chunk(chunk_idx, transform)\n            \n            # Create batches within chunk\n            num_samples = len(chunk_data['images'])\n            for batch_start in range(0, num_samples, batch_size):\n                batch_end = min(batch_start + batch_size, num_samples)\n                \n                batch = {\n                    'images': chunk_data['images'][batch_start:batch_end],\n                    'masks': chunk_data['masks'][batch_start:batch_end],\n                    'labels': chunk_data['labels'][batch_start:batch_end],\n                    'ids': chunk_data['ids'][batch_start:batch_end]\n                }\n                \n                yield batch\n            \n            # Cleanup after chunk\n            mem_monitor.force_cleanup()\n\n# ==============================================\n# EXPLORATORY DATA ANALYSIS (EDA)\n# ==============================================\n\ndef perform_eda(dataset: ScientificForgeryDataset):\n    \"\"\"Perform comprehensive EDA on the dataset\"\"\"\n    print(\"Performing Exploratory Data Analysis...\")\n    \n    # Load a sample chunk for analysis\n    sample_data = dataset.load_chunk(0)\n    \n    # 1. Image Statistics\n    print(\"\\n1. Image Statistics:\")\n    print(f\"  Sample size: {len(sample_data['images'])}\")\n    print(f\"  Image shape: {sample_data['images'][0].shape}\")\n    print(f\"  Mask shape: {sample_data['masks'][0].shape}\")\n    print(f\"  Data type: {sample_data['images'][0].dtype}\")\n    \n    # 2. Label Distribution\n    label_counts = Counter(sample_data['labels'])\n    print(f\"\\n2. Label Distribution:\")\n    for label, count in label_counts.items():\n        label_name = \"Forged\" if label == 1 else \"Authentic\" if label == 0 else \"Test\"\n        print(f\"  {label_name}: {count} ({count/len(sample_data['labels'])*100:.1f}%)\")\n    \n    # 3. Mask Statistics (for forged images)\n    forged_indices = np.where(sample_data['labels'] == 1)[0]\n    if len(forged_indices) > 0:\n        mask_areas = []\n        for idx in forged_indices[:100]:  # Sample 100 forged images\n            mask_area = np.sum(sample_data['masks'][idx] > 0.5)\n            mask_areas.append(mask_area)\n        \n        print(f\"\\n3. Mask Statistics:\")\n        print(f\"  Average mask area: {np.mean(mask_areas):.1f} pixels\")\n        print(f\"  Min mask area: {np.min(mask_areas):.1f} pixels\")\n        print(f\"  Max mask area: {np.max(mask_areas):.1f} pixels\")\n    \n    # 4. Create EDA Visualizations\n    fig, axes = plt.subplots(3, 4, figsize=(15, 10))\n    axes = axes.flatten()\n    \n    # Show sample images\n    for i in range(min(12, len(sample_data['images']))):\n        ax = axes[i]\n        img = sample_data['images'][i]\n        label = sample_data['labels'][i]\n        mask = sample_data['masks'][i]\n        \n        # Show image\n        ax.imshow(img)\n        \n        # Overlay mask if forged\n        if label == 1 and np.sum(mask) > 0:\n            mask_overlay = np.ma.masked_where(mask == 0, mask)\n            ax.imshow(mask_overlay, alpha=0.5, cmap='Reds')\n        \n        ax.set_title(f\"{'Forged' if label == 1 else 'Authentic'}\")\n        ax.axis('off')\n    \n    plt.suptitle(\"Sample Images with Masks (Red = Forged Regions)\", fontsize=16)\n    plt.tight_layout()\n    plt.savefig('eda_sample_images.png', dpi=150, bbox_inches='tight')\n    plt.close()\n    \n    # 5. Image Intensity Distribution\n    fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n    \n    # Authentic images intensity\n    authentic_indices = np.where(sample_data['labels'] == 0)[0]\n    if len(authentic_indices) > 0:\n        authentic_intensities = [sample_data['images'][i].mean() for i in authentic_indices[:100]]\n        axes[0].hist(authentic_intensities, bins=30, alpha=0.7, color='blue', label='Authentic')\n    \n    # Forged images intensity\n    if len(forged_indices) > 0:\n        forged_intensities = [sample_data['images'][i].mean() for i in forged_indices[:100]]\n        axes[0].hist(forged_intensities, bins=30, alpha=0.7, color='red', label='Forged')\n    \n    axes[0].set_xlabel('Mean Pixel Intensity')\n    axes[0].set_ylabel('Frequency')\n    axes[0].set_title('Image Intensity Distribution')\n    axes[0].legend()\n    axes[0].grid(True, alpha=0.3)\n    \n    # 6. Class Distribution Pie Chart\n    labels = ['Authentic', 'Forged']\n    sizes = [\n        label_counts.get(0, 0),\n        label_counts.get(1, 0)\n    ]\n    colors = ['lightblue', 'lightcoral']\n    \n    axes[1].pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)\n    axes[1].set_title('Class Distribution')\n    \n    plt.tight_layout()\n    plt.savefig('eda_statistics.png', dpi=150, bbox_inches='tight')\n    plt.close()\n    \n    print(\"\\nEDA visualizations saved as 'eda_sample_images.png' and 'eda_statistics.png'\")\n\n# ==============================================\n# FEATURE ENGINEERING\n# ==============================================\n\nclass FeatureEngineer:\n    \"\"\"Extract handcrafted features for copy-move forgery detection\"\"\"\n    \n    @staticmethod\n    def extract_texture_features(image: np.ndarray) -> Dict[str, float]:\n        \"\"\"Extract texture-based features\"\"\"\n        if len(image.shape) == 3:\n            gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n        else:\n            gray = image\n        \n        features = {}\n        \n        # 1. GLCM features\n        glcm = cv2.calcHist([gray], [0], None, [256], [0, 256])\n        glcm = glcm / glcm.sum()\n        \n        # Energy (Uniformity)\n        features['energy'] = np.sum(glcm**2)\n        \n        # Entropy\n        features['entropy'] = -np.sum(glcm * np.log2(glcm + 1e-10))\n        \n        # 2. Edge density\n        edges = cv2.Canny((gray * 255).astype(np.uint8), 100, 200)\n        features['edge_density'] = np.sum(edges > 0) / edges.size\n        \n        # 3. Local binary patterns (simplified)\n        lbp = cv2.calcHist([gray], [0], None, [8], [0, 256])\n        lbp = lbp / lbp.sum()\n        features['lbp_entropy'] = -np.sum(lbp * np.log2(lbp + 1e-10))\n        \n        # 4. Image moments\n        moments = cv2.moments(gray)\n        if moments['mu00'] != 0:\n            features['hu_moment_1'] = moments['nu20'] + moments['nu02']\n        else:\n            features['hu_moment_1'] = 0\n        \n        return features\n    \n    @staticmethod\n    def extract_color_features(image: np.ndarray) -> Dict[str, float]:\n        \"\"\"Extract color-based features\"\"\"\n        if len(image.shape) == 2:\n            return {}\n        \n        features = {}\n        \n        # Color channel statistics\n        for i, color in enumerate(['blue', 'green', 'red']):\n            channel = image[:, :, i]\n            features[f'{color}_mean'] = np.mean(channel)\n            features[f'{color}_std'] = np.std(channel)\n            features[f'{color}_skew'] = np.mean((channel - np.mean(channel))**3) / (np.std(channel)**3 + 1e-10)\n        \n        # Color correlations\n        features['rg_correlation'] = np.corrcoef(image[:, :, 2].flatten(), image[:, :, 1].flatten())[0, 1]\n        features['rb_correlation'] = np.corrcoef(image[:, :, 2].flatten(), image[:, :, 0].flatten())[0, 1]\n        features['gb_correlation'] = np.corrcoef(image[:, :, 1].flatten(), image[:, :, 0].flatten())[0, 1]\n        \n        return features\n    \n    @staticmethod\n    def extract_copy_move_features(image: np.ndarray) -> Dict[str, float]:\n        \"\"\"Extract features specific to copy-move detection\"\"\"\n        features = {}\n        \n        if len(image.shape) == 3:\n            gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n        else:\n            gray = image\n        \n        # Block-based analysis for copy-move detection\n        block_size = 32\n        h, w = gray.shape\n        num_blocks_h = h // block_size\n        num_blocks_w = w // block_size\n        \n        block_features = []\n        for i in range(num_blocks_h):\n            for j in range(num_blocks_w):\n                block = gray[i*block_size:(i+1)*block_size, j*block_size:(j+1)*block_size]\n                block_mean = np.mean(block)\n                block_std = np.std(block)\n                block_features.append([block_mean, block_std])\n        \n        block_features = np.array(block_features)\n        \n        # Feature 1: Block similarity (potential copy-move)\n        if len(block_features) > 1:\n            # Calculate pairwise distances between blocks\n            from scipy.spatial.distance import pdist, squareform\n            distances = pdist(block_features, 'euclidean')\n            \n            # Look for very similar blocks (potential copy-move)\n            similarity_threshold = np.percentile(distances, 5)  # Most similar 5%\n            similar_pairs = np.sum(distances < similarity_threshold)\n            features['similar_block_pairs'] = similar_pairs / len(distances) if len(distances) > 0 else 0\n        \n        # Feature 2: Frequency domain analysis\n        f_transform = np.fft.fft2(gray)\n        f_shift = np.fft.fftshift(f_transform)\n        magnitude_spectrum = np.log(np.abs(f_shift) + 1)\n        \n        # Center region (low frequency)\n        center_h, center_w = h//2, w//2\n        center_region = magnitude_spectrum[center_h-16:center_h+16, center_w-16:center_w+16]\n        edge_region = magnitude_spectrum.copy()\n        edge_region[center_h-32:center_h+32, center_w-32:center_w+32] = 0\n        \n        features['center_freq_ratio'] = np.mean(center_region) / (np.mean(edge_region) + 1e-10)\n        \n        return features\n    \n    @staticmethod\n    def extract_all_features(image: np.ndarray) -> Dict[str, float]:\n        \"\"\"Extract all handcrafted features\"\"\"\n        features = {}\n        \n        # Texture features\n        texture_feats = FeatureEngineer.extract_texture_features(image)\n        features.update(texture_feats)\n        \n        # Color features\n        color_feats = FeatureEngineer.extract_color_features(image)\n        features.update(color_feats)\n        \n        # Copy-move specific features\n        copy_move_feats = FeatureEngineer.extract_copy_move_features(image)\n        features.update(copy_move_feats)\n        \n        return features\n\n# ==============================================\n# DEEP LEARNING MODEL ARCHITECTURE\n# ==============================================\n\nclass AttentionBlock(nn.Module):\n    \"\"\"Attention mechanism for focusing on suspicious regions\"\"\"\n    def __init__(self, in_channels):\n        super(AttentionBlock, self).__init__()\n        self.query_conv = nn.Conv2d(in_channels, in_channels // 8, kernel_size=1)\n        self.key_conv = nn.Conv2d(in_channels, in_channels // 8, kernel_size=1)\n        self.value_conv = nn.Conv2d(in_channels, in_channels, kernel_size=1)\n        self.gamma = nn.Parameter(torch.zeros(1))\n        self.softmax = nn.Softmax(dim=-1)\n        \n    def forward(self, x):\n        batch_size, C, width, height = x.size()\n        \n        # Generate query, key, value\n        proj_query = self.query_conv(x).view(batch_size, -1, width * height).permute(0, 2, 1)\n        proj_key = self.key_conv(x).view(batch_size, -1, width * height)\n        energy = torch.bmm(proj_query, proj_key)\n        attention = self.softmax(energy)\n        proj_value = self.value_conv(x).view(batch_size, -1, width * height)\n        \n        out = torch.bmm(proj_value, attention.permute(0, 2, 1))\n        out = out.view(batch_size, C, width, height)\n        out = self.gamma * out + x\n        \n        return out\n\nclass CopyMoveDetector(nn.Module):\n    \"\"\"Hybrid CNN model for copy-move forgery detection and segmentation\"\"\"\n    \n    def __init__(self, num_classes=2):\n        super(CopyMoveDetector, self).__init__()\n        \n        # Encoder (Feature Extraction)\n        self.encoder = nn.Sequential(\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n            \n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n            \n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n            \n            AttentionBlock(128),\n            \n            nn.Conv2d(128, 256, kernel_size=3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2),\n        )\n        \n        # Classification head\n        self.classifier = nn.Sequential(\n            nn.AdaptiveAvgPool2d((1, 1)),\n            nn.Flatten(),\n            nn.Linear(256, 128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n            nn.Linear(128, num_classes)\n        )\n        \n        # Segmentation head (for mask prediction)\n        self.segmentation_head = nn.Sequential(\n            nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2),\n            nn.BatchNorm2d(128),\n            nn.ReLU(inplace=True),\n            \n            nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            \n            nn.ConvTranspose2d(64, 32, kernel_size=2, stride=2),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            \n            nn.ConvTranspose2d(32, 16, kernel_size=2, stride=2),\n            nn.BatchNorm2d(16),\n            nn.ReLU(inplace=True),\n            \n            nn.Conv2d(16, 1, kernel_size=1),\n            nn.Sigmoid()\n        )\n        \n        # Feature fusion for handcrafted features\n        self.feature_fusion = nn.Sequential(\n            nn.Linear(256 + 15, 128),  # 15 handcrafted features\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.3),\n            nn.Linear(128, 64),\n            nn.ReLU(inplace=True),\n            nn.Linear(64, num_classes)\n        )\n    \n    def forward(self, x, handcrafted_features=None):\n        # Extract CNN features\n        features = self.encoder(x)\n        \n        # Classification\n        cls_features = self.classifier[:2](features)  # Adaptive pooling + flatten\n        classification = self.classifier[2:](cls_features)\n        \n        # Segmentation mask\n        segmentation = self.segmentation_head(features)\n        \n        # Feature fusion if handcrafted features provided\n        if handcrafted_features is not None:\n            fused_features = torch.cat([cls_features, handcrafted_features], dim=1)\n            classification = self.feature_fusion(fused_features)\n        \n        return classification, segmentation\n\n# ==============================================\n# DATA AUGMENTATION\n# ==============================================\n\nclass Augmentation:\n    \"\"\"Data augmentation for training\"\"\"\n    \n    @staticmethod\n    def get_train_transforms():\n        return transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.RandomVerticalFlip(p=0.5),\n            transforms.RandomRotation(degrees=10),\n            transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),\n            transforms.RandomAffine(degrees=0, translate=(0.1, 0.1), scale=(0.9, 1.1)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    \n    @staticmethod\n    def get_val_transforms():\n        return transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n\n# ==============================================\n# CUSTOM DATASET CLASS\n# ==============================================\n\nclass ForgeryDataset(Dataset):\n    \"\"\"PyTorch Dataset for scientific image forgery detection\"\"\"\n    \n    def __init__(self, image_paths, mask_paths, labels, transform=None):\n        self.image_paths = image_paths\n        self.mask_paths = mask_paths\n        self.labels = labels\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.image_paths)\n    \n    def __getitem__(self, idx):\n        # Load image\n        img_path = self.image_paths[idx]\n        img = cv2.imread(img_path)\n        if img is None:\n            # Return a dummy image if loading fails\n            img = np.zeros((256, 256, 3), dtype=np.uint8)\n        \n        # Resize\n        img = cv2.resize(img, (256, 256))\n        img = img.astype(np.float32) / 255.0\n        \n        # Load mask if exists\n        if self.mask_paths[idx] is not None and os.path.exists(self.mask_paths[idx]):\n            mask_data = np.load(self.mask_paths[idx])\n            if mask_data.ndim == 2:\n                mask = cv2.resize(mask_data.astype(np.float32), (256, 256))\n                mask = (mask > 0.5).astype(np.float32)\n            else:\n                mask = np.zeros((256, 256), dtype=np.float32)\n        else:\n            mask = np.zeros((256, 256), dtype=np.float32)\n        \n        label = self.labels[idx]\n        \n        # Apply transforms\n        if self.transform:\n            img_tensor = self.transform(torch.from_numpy(img).permute(2, 0, 1).float())\n        else:\n            img_tensor = torch.from_numpy(img).permute(2, 0, 1).float()\n        \n        mask_tensor = torch.from_numpy(mask).unsqueeze(0).float()\n        \n        return {\n            'image': img_tensor,\n            'mask': mask_tensor,\n            'label': torch.tensor(label, dtype=torch.long),\n            'id': Path(img_path).stem\n        }\n\n# ==============================================\n# MODEL TRAINING PIPELINE\n# ==============================================\n\nclass ModelTrainer:\n    \"\"\"Comprehensive model training with memory management\"\"\"\n    \n    def __init__(self, model, device='cuda' if torch.cuda.is_available() else 'cpu'):\n        self.model = model.to(device)\n        self.device = device\n        self.criterion_cls = nn.CrossEntropyLoss()\n        self.criterion_seg = nn.BCELoss()\n        self.optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5)\n        self.scheduler = ReduceLROnPlateau(self.optimizer, mode='max', patience=5, factor=0.5)\n        self.best_accuracy = 0.0\n        self.best_model_state = None\n        self.train_history = {\n            'train_loss': [], 'val_loss': [],\n            'train_acc': [], 'val_acc': [],\n            'train_f1': [], 'val_f1': []\n        }\n        \n    def train_epoch(self, train_loader, epoch, num_epochs):\n        \"\"\"Train for one epoch\"\"\"\n        self.model.train()\n        total_loss = 0\n        correct = 0\n        total = 0\n        \n        pbar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} [Train]')\n        for batch_idx, batch in enumerate(pbar):\n            # Memory check\n            if batch_idx % 50 == 0:\n                mem_monitor.force_cleanup()\n            \n            # Get data\n            images = batch['image'].to(self.device)\n            labels = batch['label'].to(self.device)\n            masks = batch['mask'].to(self.device)\n            \n            # Forward pass\n            self.optimizer.zero_grad()\n            cls_pred, seg_pred = self.model(images)\n            \n            # Calculate losses\n            loss_cls = self.criterion_cls(cls_pred, labels)\n            \n            # Only apply segmentation loss to forged images (label == 1)\n            forged_mask = (labels == 1).float().view(-1, 1, 1, 1)\n            loss_seg = self.criterion_seg(seg_pred, masks) * forged_mask\n            loss_seg = loss_seg.sum() / (forged_mask.sum() + 1e-8)\n            \n            loss = loss_cls + 0.5 * loss_seg\n            \n            # Backward pass\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)\n            self.optimizer.step()\n            \n            # Statistics\n            total_loss += loss.item()\n            _, predicted = cls_pred.max(1)\n            total += labels.size(0)\n            correct += predicted.eq(labels).sum().item()\n            \n            # Update progress bar\n            pbar.set_postfix({\n                'loss': loss.item(),\n                'acc': 100. * correct / total,\n                'mem': f\"{mem_monitor.get_memory_usage():.1f}GB\"\n            })\n        \n        avg_loss = total_loss / len(train_loader)\n        accuracy = 100. * correct / total\n        \n        return avg_loss, accuracy\n    \n    def validate(self, val_loader, epoch):\n        \"\"\"Validate the model\"\"\"\n        self.model.eval()\n        total_loss = 0\n        correct = 0\n        total = 0\n        all_preds = []\n        all_labels = []\n        \n        with torch.no_grad():\n            pbar = tqdm(val_loader, desc=f'Epoch {epoch+1} [Val]')\n            for batch_idx, batch in enumerate(pbar):\n                # Get data\n                images = batch['image'].to(self.device)\n                labels = batch['label'].to(self.device)\n                masks = batch['mask'].to(self.device)\n                \n                # Forward pass\n                cls_pred, seg_pred = self.model(images)\n                \n                # Calculate loss\n                loss_cls = self.criterion_cls(cls_pred, labels)\n                \n                # Only apply segmentation loss to forged images\n                forged_mask = (labels == 1).float().view(-1, 1, 1, 1)\n                loss_seg = self.criterion_seg(seg_pred, masks) * forged_mask\n                loss_seg = loss_seg.sum() / (forged_mask.sum() + 1e-8)\n                \n                loss = loss_cls + 0.5 * loss_seg\n                \n                # Statistics\n                total_loss += loss.item()\n                _, predicted = cls_pred.max(1)\n                total += labels.size(0)\n                correct += predicted.eq(labels).sum().item()\n                \n                all_preds.extend(predicted.cpu().numpy())\n                all_labels.extend(labels.cpu().numpy())\n                \n                pbar.set_postfix({\n                    'loss': loss.item(),\n                    'acc': 100. * correct / total\n                })\n        \n        avg_loss = total_loss / len(val_loader)\n        accuracy = 100. * correct / total\n        \n        # Calculate F1 score\n        from sklearn.metrics import f1_score\n        f1 = f1_score(all_labels, all_preds, average='macro')\n        \n        return avg_loss, accuracy, f1\n    \n    def train(self, train_loader, val_loader, num_epochs=50, patience=10):\n        \"\"\"Main training loop with early stopping\"\"\"\n        print(f\"Starting training on {self.device}...\")\n        \n        no_improve = 0\n        best_epoch = 0\n        \n        for epoch in range(num_epochs):\n            # Train\n            train_loss, train_acc = self.train_epoch(train_loader, epoch, num_epochs)\n            \n            # Validate\n            val_loss, val_acc, val_f1 = self.validate(val_loader, epoch)\n            \n            # Update learning rate\n            self.scheduler.step(val_acc)\n            \n            # Save history\n            self.train_history['train_loss'].append(train_loss)\n            self.train_history['val_loss'].append(val_loss)\n            self.train_history['train_acc'].append(train_acc)\n            self.train_history['val_acc'].append(val_acc)\n            self.train_history['val_f1'].append(val_f1)\n            \n            # Save best model\n            if val_acc > self.best_accuracy:\n                self.best_accuracy = val_acc\n                self.best_model_state = self.model.state_dict().copy()\n                best_epoch = epoch\n                no_improve = 0\n                \n                # Save model checkpoint\n                torch.save({\n                    'epoch': epoch,\n                    'model_state_dict': self.model.state_dict(),\n                    'optimizer_state_dict': self.optimizer.state_dict(),\n                    'accuracy': val_acc,\n                    'loss': val_loss,\n                }, 'best_model_checkpoint.pth')\n                print(f\"New best model saved with accuracy: {val_acc:.2f}%\")\n            else:\n                no_improve += 1\n            \n            # Early stopping\n            if no_improve >= patience:\n                print(f\"Early stopping triggered after {epoch + 1} epochs\")\n                break\n            \n            # Memory cleanup\n            mem_monitor.force_cleanup()\n        \n        # Load best model\n        if self.best_model_state is not None:\n            self.model.load_state_dict(self.best_model_state)\n        \n        print(f\"\\nTraining completed. Best accuracy: {self.best_accuracy:.2f}% at epoch {best_epoch + 1}\")\n        \n        return self.train_history\n\n# ==============================================\n# EVALUATION & VISUALIZATION\n# ==============================================\n\nclass ModelEvaluator:\n    \"\"\"Comprehensive model evaluation and visualization\"\"\"\n    \n    @staticmethod\n    def plot_training_history(history):\n        \"\"\"Plot training and validation metrics\"\"\"\n        fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n        \n        # Loss curve\n        axes[0, 0].plot(history['train_loss'], label='Train Loss')\n        axes[0, 0].plot(history['val_loss'], label='Val Loss')\n        axes[0, 0].set_xlabel('Epoch')\n        axes[0, 0].set_ylabel('Loss')\n        axes[0, 0].set_title('Training and Validation Loss')\n        axes[0, 0].legend()\n        axes[0, 0].grid(True, alpha=0.3)\n        \n        # Accuracy curve\n        axes[0, 1].plot(history['train_acc'], label='Train Accuracy')\n        axes[0, 1].plot(history['val_acc'], label='Val Accuracy')\n        axes[0, 1].set_xlabel('Epoch')\n        axes[0, 1].set_ylabel('Accuracy (%)')\n        axes[0, 1].set_title('Training and Validation Accuracy')\n        axes[0, 1].legend()\n        axes[0, 1].grid(True, alpha=0.3)\n        \n        # F1 score curve\n        axes[0, 2].plot(history['val_f1'], label='Val F1 Score', color='green')\n        axes[0, 2].set_xlabel('Epoch')\n        axes[0, 2].set_ylabel('F1 Score')\n        axes[0, 2].set_title('Validation F1 Score')\n        axes[0, 2].legend()\n        axes[0, 2].grid(True, alpha=0.3)\n        \n        # Learning rate curve (if available)\n        if 'lr' in history:\n            axes[1, 0].plot(history['lr'], label='Learning Rate')\n            axes[1, 0].set_xlabel('Epoch')\n            axes[1, 0].set_ylabel('Learning Rate')\n            axes[1, 0].set_title('Learning Rate Schedule')\n            axes[1, 0].legend()\n            axes[1, 0].grid(True, alpha=0.3)\n        \n        # Generalization gap\n        if len(history['train_acc']) > 0 and len(history['val_acc']) > 0:\n            gap = np.array(history['train_acc']) - np.array(history['val_acc'])\n            axes[1, 1].plot(gap, label='Generalization Gap', color='purple')\n            axes[1, 1].axhline(y=0, color='r', linestyle='--', alpha=0.3)\n            axes[1, 1].set_xlabel('Epoch')\n            axes[1, 1].set_ylabel('Accuracy Gap (%)')\n            axes[1, 1].set_title('Generalization Gap (Train - Val)')\n            axes[1, 1].legend()\n            axes[1, 1].grid(True, alpha=0.3)\n        \n        # Memory usage over time\n        axes[1, 2].hist([mem_monitor.get_memory_usage() for _ in range(10)], bins=20, alpha=0.7, color='orange')\n        axes[1, 2].set_xlabel('Memory Usage (GB)')\n        axes[1, 2].set_ylabel('Frequency')\n        axes[1, 2].set_title('Memory Usage Distribution')\n        axes[1, 2].grid(True, alpha=0.3)\n        \n        plt.tight_layout()\n        plt.savefig('training_history.png', dpi=150, bbox_inches='tight')\n        plt.close()\n        print(\"Training history plot saved as 'training_history.png'\")\n    \n    @staticmethod\n    def plot_confusion_matrix(y_true, y_pred, classes):\n        \"\"\"Plot confusion matrix\"\"\"\n        cm = confusion_matrix(y_true, y_pred)\n        \n        plt.figure(figsize=(8, 6))\n        sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n                   xticklabels=classes, yticklabels=classes)\n        plt.xlabel('Predicted')\n        plt.ylabel('True')\n        plt.title('Confusion Matrix')\n        plt.tight_layout()\n        plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')\n        plt.close()\n        print(\"Confusion matrix saved as 'confusion_matrix.png'\")\n    \n    @staticmethod\n    def plot_roc_curve(y_true, y_scores):\n        \"\"\"Plot ROC curve\"\"\"\n        fpr, tpr, _ = roc_curve(y_true, y_scores)\n        roc_auc = auc(fpr, tpr)\n        \n        plt.figure(figsize=(8, 6))\n        plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})')\n        plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n        plt.xlim([0.0, 1.0])\n        plt.ylim([0.0, 1.05])\n        plt.xlabel('False Positive Rate')\n        plt.ylabel('True Positive Rate')\n        plt.title('Receiver Operating Characteristic (ROC) Curve')\n        plt.legend(loc=\"lower right\")\n        plt.grid(True, alpha=0.3)\n        plt.tight_layout()\n        plt.savefig('roc_curve.png', dpi=150, bbox_inches='tight')\n        plt.close()\n        print(f\"ROC curve saved as 'roc_curve.png' (AUC: {roc_auc:.3f})\")\n    \n    @staticmethod\n    def visualize_predictions(model, test_loader, device, num_samples=12):\n        \"\"\"Visualize model predictions on test samples\"\"\"\n        model.eval()\n        \n        fig, axes = plt.subplots(3, 4, figsize=(15, 12))\n        axes = axes.flatten()\n        \n        with torch.no_grad():\n            for batch in test_loader:\n                images = batch['image'].to(device)\n                labels = batch['label'].cpu().numpy()\n                ids = batch['id']\n                \n                cls_pred, seg_pred = model(images)\n                _, predicted = cls_pred.max(1)\n                \n                for i in range(min(num_samples, len(images))):\n                    ax = axes[i]\n                    \n                    # Original image\n                    img = images[i].cpu().permute(1, 2, 0).numpy()\n                    img = (img - img.min()) / (img.max() - img.min())  # Normalize for display\n                    \n                    ax.imshow(img)\n                    \n                    # Ground truth mask if available\n                    if 'mask' in batch and batch['mask'][i].sum() > 0:\n                        mask = batch['mask'][i].squeeze().cpu().numpy()\n                        mask_overlay = np.ma.masked_where(mask == 0, mask)\n                        ax.imshow(mask_overlay, alpha=0.5, cmap='Reds', label='Ground Truth')\n                    \n                    # Predicted mask\n                    pred_mask = seg_pred[i, 0].cpu().numpy()\n                    pred_mask_thresh = (pred_mask > 0.5).astype(np.float32)\n                    \n                    if pred_mask_thresh.sum() > 0:\n                        pred_overlay = np.ma.masked_where(pred_mask_thresh == 0, pred_mask_thresh)\n                        ax.imshow(pred_overlay, alpha=0.5, cmap='Blues', label='Predicted')\n                    \n                    # Title with prediction info\n                    true_label = 'Forged' if labels[i] == 1 else 'Authentic'\n                    pred_label = 'Forged' if predicted[i] == 1 else 'Authentic'\n                    \n                    ax.set_title(f'ID: {ids[i]}\\nTrue: {true_label}, Pred: {pred_label}')\n                    ax.axis('off')\n                \n                break  # Only visualize first batch\n        \n        plt.suptitle('Model Predictions (Red=Ground Truth, Blue=Prediction)', fontsize=16)\n        plt.tight_layout()\n        plt.savefig('prediction_visualizations.png', dpi=150, bbox_inches='tight')\n        plt.close()\n        print(\"Prediction visualizations saved as 'prediction_visualizations.png'\")\n\n# ==============================================\n# SUBMISSION GENERATION\n# ==============================================\n\nclass SubmissionGenerator:\n    \"\"\"Generate submission file in required format\"\"\"\n    \n    def __init__(self, model, device):\n        self.model = model.to(device)\n        self.device = device\n        self.model.eval()\n    \n    def generate_predictions(self, test_loader):\n        \"\"\"Generate predictions for test images\"\"\"\n        predictions = []\n        image_ids = []\n        \n        with torch.no_grad():\n            pbar = tqdm(test_loader, desc=\"Generating predictions\")\n            for batch in pbar:\n                images = batch['image'].to(self.device)\n                ids = batch['id']\n                \n                # Get predictions\n                cls_pred, seg_pred = self.model(images)\n                _, predicted = cls_pred.max(1)\n                \n                for i in range(len(images)):\n                    img_id = ids[i]\n                    is_forged = predicted[i].item() == 1\n                    \n                    if is_forged:\n                        # Get mask and encode to RLE\n                        mask = seg_pred[i, 0].cpu().numpy()\n                        mask_binary = (mask > 0.5).astype(np.uint8)\n                        \n                        # Apply morphological operations to clean mask\n                        kernel = np.ones((3, 3), np.uint8)\n                        mask_binary = cv2.morphologyEx(mask_binary, cv2.MORPH_CLOSE, kernel)\n                        mask_binary = cv2.morphologyEx(mask_binary, cv2.MORPH_OPEN, kernel)\n                        \n                        # Find connected components\n                        num_labels, labels = cv2.connectedComponents(mask_binary)\n                        \n                        if num_labels > 1:  # Has at least one connected component\n                            # Get masks for each component\n                            component_masks = []\n                            for label in range(1, num_labels):\n                                component_mask = (labels == label).astype(np.uint8)\n                                component_masks.append(component_mask)\n                            \n                            # Encode all masks\n                            rle_str = rle_encode(component_masks)\n                            predictions.append(rle_str)\n                        else:\n                            predictions.append(\"authentic\")\n                    else:\n                        predictions.append(\"authentic\")\n                    \n                    image_ids.append(img_id)\n                \n                # Memory cleanup\n                if pbar.n % 10 == 0:\n                    mem_monitor.force_cleanup()\n        \n        return image_ids, predictions\n    \n    def create_submission_file(self, test_loader, output_path='submission.csv'):\n        \"\"\"Create final submission CSV file\"\"\"\n        print(\"Creating submission file...\")\n        \n        image_ids, predictions = self.generate_predictions(test_loader)\n        \n        # Create DataFrame\n        submission_df = pd.DataFrame({\n            'case_id': image_ids,\n            'annotation': predictions\n        })\n        \n        # Save to CSV\n        submission_df.to_csv(output_path, index=False)\n        print(f\"Submission file saved as '{output_path}'\")\n        \n        # Print some statistics\n        authentic_count = (submission_df['annotation'] == 'authentic').sum()\n        forged_count = len(submission_df) - authentic_count\n        \n        print(f\"\\nSubmission Statistics:\")\n        print(f\"  Total images: {len(submission_df)}\")\n        print(f\"  Authentic predictions: {authentic_count}\")\n        print(f\"  Forged predictions: {forged_count}\")\n        \n        return submission_df\n\n# ==============================================\n# MAIN PIPELINE EXECUTION\n# ==============================================\n\ndef main():\n    \"\"\"Main execution pipeline\"\"\"\n    print(\"=\" * 70)\n    print(\"SCIENTIFIC IMAGE FORGERY DETECTION PIPELINE\")\n    print(\"=\" * 70)\n    \n    # Initialize memory monitor\n    global mem_monitor\n    mem_monitor = MemoryMonitor(memory_limit_gb=10)\n    print(f\"Initial memory: {mem_monitor.get_memory_usage():.2f}GB\")\n    \n    # Step 1: Data Loading\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 1: DATA LOADING & PREPROCESSING\")\n    print(\"=\" * 50)\n    \n    root_path = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n    \n    # Load train dataset\n    train_dataset = ScientificForgeryDataset(root_path, max_images_per_chunk=500, mode='train')\n    train_dataset.discover_files()\n    \n    # Step 2: Exploratory Data Analysis\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 2: EXPLORATORY DATA ANALYSIS\")\n    print(\"=\" * 50)\n    \n    perform_eda(train_dataset)\n    \n    # Step 3: Prepare Data Loaders\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 3: PREPARING DATA LOADERS\")\n    print(\"=\" * 50)\n    \n    # Get all indices\n    all_indices = list(range(len(train_dataset.image_paths)))\n    \n    # Filter out invalid labels (keep only 0 and 1)\n    valid_indices = [i for i in all_indices if train_dataset.labels[i] in [0, 1]]\n    \n    # Split data into train and validation\n    train_indices, val_indices = train_test_split(\n        valid_indices, \n        test_size=0.2, \n        random_state=42,\n        stratify=[train_dataset.labels[i] for i in valid_indices]\n    )\n    \n    print(f\"Train samples: {len(train_indices)}\")\n    print(f\"Validation samples: {len(val_indices)}\")\n    \n    # Create PyTorch Datasets\n    train_transform = Augmentation.get_train_transforms()\n    val_transform = Augmentation.get_val_transforms()\n    \n    train_pytorch_dataset = ForgeryDataset(\n        [train_dataset.image_paths[i] for i in train_indices],\n        [train_dataset.mask_paths[i] for i in train_indices],\n        [train_dataset.labels[i] for i in train_indices],\n        transform=train_transform\n    )\n    \n    val_pytorch_dataset = ForgeryDataset(\n        [train_dataset.image_paths[i] for i in val_indices],\n        [train_dataset.mask_paths[i] for i in val_indices],\n        [train_dataset.labels[i] for i in val_indices],\n        transform=val_transform\n    )\n    \n    # Create DataLoaders\n    batch_size = 16\n    train_loader = DataLoader(\n        train_pytorch_dataset, \n        batch_size=batch_size, \n        shuffle=True,\n        num_workers=2,\n        pin_memory=True\n    )\n    \n    val_loader = DataLoader(\n        val_pytorch_dataset, \n        batch_size=batch_size, \n        shuffle=False,\n        num_workers=2,\n        pin_memory=True\n    )\n    \n    # Step 4: Model Initialization\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 4: MODEL INITIALIZATION\")\n    print(\"=\" * 50)\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"Using device: {device}\")\n    \n    model = CopyMoveDetector(num_classes=2)\n    print(f\"Model parameters: {sum(p.numel() for p in model.parameters()):,}\")\n    \n    # Step 5: Training\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 5: MODEL TRAINING\")\n    print(\"=\" * 50)\n    \n    trainer = ModelTrainer(model, device)\n    history = trainer.train(train_loader, val_loader, num_epochs=30, patience=8)\n    \n    # Step 6: Evaluation\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 6: MODEL EVALUATION & VISUALIZATION\")\n    print(\"=\" * 50)\n    \n    evaluator = ModelEvaluator()\n    \n    # Plot training history\n    evaluator.plot_training_history(history)\n    \n    # Generate final predictions on validation set\n    print(\"\\nEvaluating on validation set...\")\n    val_loss, val_acc, val_f1 = trainer.validate(val_loader, epoch=0)\n    print(f\"Final Validation Results:\")\n    print(f\"  Accuracy: {val_acc:.2f}%\")\n    print(f\"  F1 Score: {val_f1:.3f}\")\n    \n    # Visualize predictions\n    evaluator.visualize_predictions(model, val_loader, device)\n    \n    # Step 7: Generate Submission\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 7: GENERATING SUBMISSION FILE\")\n    print(\"=\" * 50)\n    \n    # Load test dataset\n    test_dataset = ScientificForgeryDataset(root_path, max_images_per_chunk=500, mode='test')\n    test_dataset.discover_files()\n    \n    test_pytorch_dataset = ForgeryDataset(\n        test_dataset.image_paths,\n        test_dataset.mask_paths,\n        test_dataset.labels,\n        transform=val_transform\n    )\n    \n    test_loader = DataLoader(\n        test_pytorch_dataset,\n        batch_size=batch_size,\n        shuffle=False,\n        num_workers=2,\n        pin_memory=True\n    )\n    \n    submission_gen = SubmissionGenerator(model, device)\n    submission_df = submission_gen.create_submission_file(test_loader)\n    \n    # Step 8: Final Memory Report\n    print(\"\\n\" + \"=\" * 50)\n    print(\"STEP 8: FINAL RESOURCES REPORT\")\n    print(\"=\" * 50)\n    \n    print(f\"Peak memory usage: {mem_monitor.peak_memory:.2f}GB\")\n    print(f\"Best model accuracy: {trainer.best_accuracy:.2f}%\")\n    \n    if trainer.best_accuracy > 0.95:\n        print(\"\\n SUCCESS: Model achieved high accuracy!\")\n    else:\n        print(\"\\n WARNING: Model accuracy could be improved.\")\n        print(\"Consider: More training data, hyperparameter tuning, or model architecture changes.\")\n    \n    print(\"\\n\" + \"=\" * 70)\n    print(\"PIPELINE EXECUTION COMPLETED SUCCESSFULLY!\")\n    print(\"=\" * 70)\n    \n    return model, submission_df, history\n\n# ==============================================\n# HYPERPARAMETER TUNING (Optional)\n# ==============================================\n\ndef hyperparameter_tuning():\n    \"\"\"Perform hyperparameter tuning using optuna or grid search\"\"\"\n    import optuna\n    \n    def objective(trial):\n        # Suggest hyperparameters\n        lr = trial.suggest_float('lr', 1e-5, 1e-3, log=True)\n        dropout_rate = trial.suggest_float('dropout', 0.2, 0.5)\n        batch_size = trial.suggest_categorical('batch_size', [8, 16, 32])\n        \n        # Create model with suggested hyperparameters\n        model = CopyMoveDetector(num_classes=2)\n        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n        model.to(device)\n        \n        # Create trainer with suggested hyperparameters\n        trainer = ModelTrainer(model, device)\n        trainer.optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-5)\n        \n        # Train and validate (simplified)\n        # Note: In practice, you'd use a subset of data for faster tuning\n        try:\n            # Load small subset for tuning\n            dataset = ScientificForgeryDataset(\"/kaggle/input/recodai-luc-scientific-image-forgery-detection\", max_images_per_chunk=100)\n            dataset.discover_files()\n            \n            train_loader = dataset.get_data_iterator(batch_size=batch_size)\n            val_loader = dataset.get_data_iterator(batch_size=batch_size)\n            \n            # Train for fewer epochs during tuning\n            history = trainer.train(train_loader, val_loader, num_epochs=10, patience=3)\n            \n            # Return best validation accuracy\n            return max(history['val_acc'])\n        \n        except Exception as e:\n            print(f\"Trial failed: {e}\")\n            return 0.0\n    \n    # Create study and optimize\n    study = optuna.create_study(direction='maximize')\n    study.optimize(objective, n_trials=20, n_jobs=1)\n    \n    print(f\"Best trial:\")\n    trial = study.best_trial\n    print(f\"  Value (Accuracy): {trial.value:.2f}%\")\n    print(f\"  Params: {trial.params}\")\n    \n    return study.best_params\n\n# ==============================================\n# ENSEMBLE MODEL (Optional Enhancement)\n# ==============================================\n\nclass EnsembleModel:\n    \"\"\"Ensemble of multiple models for better performance\"\"\"\n    \n    def __init__(self, model_paths, device):\n        self.models = []\n        for path in model_paths:\n            model = CopyMoveDetector(num_classes=2)\n            checkpoint = torch.load(path, map_location=device)\n            model.load_state_dict(checkpoint['model_state_dict'])\n            model.to(device)\n            model.eval()\n            self.models.append(model)\n        \n        self.device = device\n    \n    def predict(self, images):\n        \"\"\"Combine predictions from all models\"\"\"\n        all_cls_preds = []\n        all_seg_preds = []\n        \n        with torch.no_grad():\n            for model in self.models:\n                cls_pred, seg_pred = model(images)\n                all_cls_preds.append(cls_pred.softmax(dim=1))\n                all_seg_preds.append(seg_pred)\n        \n        # Average predictions\n        avg_cls = torch.stack(all_cls_preds).mean(dim=0)\n        avg_seg = torch.stack(all_seg_preds).mean(dim=0)\n        \n        return avg_cls, avg_seg\n\n# ==============================================\n# EXECUTION\n# ==============================================\n\nif __name__ == \"__main__\":\n    try:\n        # Execute main pipeline\n        final_model, submission, training_history = main()\n        \n        # Optional: Hyperparameter tuning (uncomment if needed)\n        # print(\"\\n\" + \"=\" * 50)\n        # print(\"OPTIONAL: HYPERPARAMETER TUNING\")\n        # print(\"=\" * 50)\n        # best_params = hyperparameter_tuning()\n        # print(f\"Best hyperparameters: {best_params}\")\n        \n        # Optional: Create ensemble (uncomment if needed)\n        # print(\"\\n\" + \"=\" * 50)\n        # print(\"OPTIONAL: CREATING ENSEMBLE MODEL\")\n        # print(\"=\" * 50)\n        # ensemble = EnsembleModel(['best_model_checkpoint.pth'], device)\n        \n    except Exception as e:\n        print(f\"\\n ERROR in pipeline execution: {e}\")\n        print(\"Traceback:\")\n        import traceback\n        traceback.print_exc()\n        \n        # Attempt to save any partial results\n        try:\n            if 'submission' in locals():\n                submission.to_csv('submission_partial.csv', index=False)\n                print(\"Partial submission saved as 'submission_partial.csv'\")\n        except:\n            pass","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null}]}