{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"## 📊 Exploratory Data Analysis (EDA)\n\nimport os\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport cv2\nfrom tqdm import tqdm\n\n# --- CONFIGURATION (from the original notebook) ---\nTRAIN_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images\"\nMASK_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\"\n\nprint(\"--- Starting Basic EDA ---\")\n\n# 1. Prepare Data List (same logic as in the notebook)\ndata_list = []\nfor root, _, files in os.walk(TRAIN_ROOT):\n    for f in files:\n        valid_extensions = ('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')\n        if f.lower().endswith(valid_extensions):\n            if 'forged' in root.lower():\n                case_id = os.path.splitext(f)[0]\n                img_path = os.path.join(root, f)\n                mask_path = os.path.join(MASK_ROOT, f\"{case_id}.npy\")\n                data_list.append({\n                    'case_id': case_id,\n                    'img_path': img_path,\n                    'mask_path': mask_path\n                })\nfull_df = pd.DataFrame(data_list)\n\n# Filter for images with existing masks\nif not full_df.empty:\n    full_df['mask_exists'] = full_df['mask_path'].apply(os.path.exists)\n    eda_df = full_df[full_df['mask_exists']].drop(columns=['mask_exists']).reset_index(drop=True)\nelse:\n    eda_df = pd.DataFrame(columns=['case_id', 'img_path', 'mask_path'])\n\n\n# 2. File and Case Counts\nprint(f\"\\nTotal potential images found in TRAIN_ROOT: {len(data_list)}\")\nprint(f\"Total valid image/mask pairs for training (Forged Cases): {len(eda_df)}\")\n\n# 3. Image and Mask Size Distribution\nif not eda_df.empty:\n    img_heights, img_widths = [], []\n    mask_pixels = [] # Count of non-zero pixels in the mask\n    \n    print(\"\\nAnalyzing image and mask dimensions/forgery area...\")\n    for index, row in tqdm(eda_df.iterrows(), total=len(eda_df)):\n        try:\n            # Read Image\n            img = cv2.imread(row['img_path'])\n            if img is not None and img.size > 0:\n                h, w = img.shape[:2]\n                img_heights.append(h)\n                img_widths.append(w)\n\n            # Read Mask (npy file)\n            mask = np.load(row['mask_path'])\n            # Assuming the forgery area is represented by non-zero pixels\n            mask_pixels.append(np.sum(mask > 0)) \n\n        except Exception as e:\n            # Handle files that can't be read (e.g., non-image .npy files not handled by cv2.imread)\n            pass\n\n    print(\"\\n--- Image Dimension Stats ---\")\n    print(f\"Unique Image Widths: {sorted(list(set(img_widths)))[:5]}{'...' if len(set(img_widths)) > 5 else ''}\")\n    print(f\"Unique Image Heights: {sorted(list(set(img_heights)))[:5]}{'...' if len(set(img_heights)) > 5 else ''}\")\n    print(f\"Mean Image Dimensions (H x W): {np.mean(img_heights):.0f} x {np.mean(img_widths):.0f}\")\n\n    # 4. Forgery Area Analysis\n    mask_pixels = np.array(mask_pixels)\n    forged_cases_with_area = np.sum(mask_pixels > 0)\n    total_forgery_area = np.sum(mask_pixels)\n    \n    print(\"\\n--- Forgery Area Stats ---\")\n    print(f\"Total cases with non-zero forgery area: {forged_cases_with_area} / {len(eda_df)}\")\n    print(f\"Mean Forgery Pixel Count per forged image: {np.mean(mask_pixels[mask_pixels > 0]):.0f} (pixels)\")\n    print(f\"Maximum Forgery Pixel Count: {np.max(mask_pixels)} (pixels)\")\n\n    # 5. Visualization: Forgery Area Distribution (First 100 cases for quick view)\n    plt.figure(figsize=(12, 5))\n    plt.bar(range(min(100, len(mask_pixels))), mask_pixels[:100])\n    plt.title('Forgery Pixel Count (First 100 Forged Samples)')\n    plt.xlabel('Sample Index')\n    plt.ylabel('Forged Pixel Count (Area)')\n    plt.show()\n\nelse:\n    print(\"🛑 EDA Skipped: The dataframe is empty. Check TRAIN_ROOT and MASK_ROOT paths.\")\n\nprint(\"\\n--- EDA Complete ---\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-02T00:43:44.640721Z","iopub.execute_input":"2025-11-02T00:43:44.640981Z","iopub.status.idle":"2025-11-02T00:46:11.956094Z","shell.execute_reply.started":"2025-11-02T00:43:44.640961Z","shell.execute_reply":"2025-11-02T00:46:11.955308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nimport warnings\nfrom warnings import filterwarnings\n\nfilterwarnings('ignore') # Suppress warnings\n\n# --- CONFIGURATION (from the original notebook) ---\nTARGET_SIZE = 256\nTRAIN_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images\"\nMASK_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\"\n\n# Replicate compute_ela for feature analysis\ndef compute_ela(img_path, quality=95, scale=10):\n    # ... (omitted for brevity, assume the original function is available)\n    # The original notebook's ELA function is used here.\n    img = cv2.imread(img_path)\n    if img is None or img.size == 0:\n        try:\n            img_data = np.load(img_path)\n            if img_data.ndim == 3: img = cv2.cvtColor(img_data, cv2.COLOR_RGB2BGR)\n            elif img_data.ndim == 2: img = cv2.cvtColor(img_data, cv2.COLOR_GRAY2BGR)\n        except Exception: return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    if img is None or img.size == 0:\n        return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    img_resized = cv2.resize(img, (TARGET_SIZE, TARGET_SIZE))\n    temp_path = f\"/tmp/temp_ela_{os.path.basename(img_path)}.jpg\" # Simplified temp_path\n    try:\n        # Use a consistent quality setting (95)\n        cv2.imwrite(temp_path, img_resized, [cv2.IMWRITE_JPEG_QUALITY, quality]) \n        compressed_img = cv2.imread(temp_path)\n        if compressed_img is None: return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n        error = np.abs(img_resized.astype(np.float32) - compressed_img.astype(np.float32))\n        ela_feature_2d = np.mean(error, axis=2) * scale # Scale by 10 as in the notebook\n    finally:\n        if os.path.exists(temp_path): os.remove(temp_path)\n    return cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE), interpolation=cv2.INTER_LINEAR).astype(np.float32)\n\n# Load the filtered DataFrame (assuming the prior EDA cell's 'eda_df' is available or recreate it)\ndata_list = []\nfor root, _, files in os.walk(TRAIN_ROOT):\n    for f in files:\n        valid_extensions = ('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')\n        if f.lower().endswith(valid_extensions) and 'forged' in root.lower():\n            case_id = os.path.splitext(f)[0]\n            mask_path = os.path.join(MASK_ROOT, f\"{case_id}.npy\")\n            if os.path.exists(mask_path):\n                data_list.append({'img_path': os.path.join(root, f), 'mask_path': mask_path})\neda_df = pd.DataFrame(data_list)\n\nprint(\"--- Starting Advanced EDA (Imbalance & Feature Check) ---\")\n\nif eda_df.empty:\n    print(\"🛑 EDA Skipped: Data frame is empty.\")\nelse:\n    total_pixels = 0\n    forgery_pixels = 0\n    ela_values, rgb_means = [], []\n\n    # Process only the first 50 images to speed up ELA computation for EDA\n    for index, row in tqdm(eda_df.head(50).iterrows(), total=len(eda_df.head(50)), desc=\"Processing samples\"):\n        try:\n            # 1. Image and Mask Load\n            rgb_image = cv2.cvtColor(cv2.imread(row['img_path']), cv2.COLOR_BGR2RGB)\n            if rgb_image is None or rgb_image.size == 0: continue\n                \n            mask = np.load(row['mask_path'])\n            if mask.ndim > 2: mask = mask[:, :, 0]\n            \n            # 2. Imbalance Check (Use original sizes for best estimate)\n            h, w = rgb_image.shape[:2]\n            total_pixels += h * w\n            forgery_pixels += np.sum(mask > 0)\n            \n            # 3. ELA Feature Check (Use 256x256 resized data)\n            ela_feature = compute_ela(row['img_path'])\n            ela_values.extend(ela_feature.flatten())\n            \n            # RGB feature check (resize/normalize similar to training)\n            rgb_resized = cv2.resize(rgb_image, (TARGET_SIZE, TARGET_SIZE)) / 255.0\n            rgb_means.extend(rgb_resized.mean(axis=2).flatten())\n\n        except Exception as e:\n            # print(f\"Warning: Could not process {row['img_path']}: {e}\")\n            continue\n\n    # --- Analysis 1: Imbalance Ratio ---\n    if total_pixels > 0:\n        imbalance_ratio = (forgery_pixels / total_pixels) * 100\n        print(f\"\\n--- Imbalance Ratio (Forged Pixels) ---\")\n        print(f\"Total Pixels Sampled: {total_pixels:,}\")\n        print(f\"Forged Pixels Sampled: {forgery_pixels:,}\")\n        print(f\"Forgery Imbalance Ratio: **{imbalance_ratio:.2f}%** (Positive Class)\")\n    \n    # --- Analysis 2: ELA Feature Distribution vs. RGB ---\n    if ela_values:\n        ela_values = np.array(ela_values)\n        rgb_means = np.array(rgb_means)\n\n        print(f\"\\n--- ELA Feature Distribution (Scaled by 10) ---\")\n        print(f\"ELA Feature Mean: {np.mean(ela_values):.4f}\")\n        print(f\"ELA Feature Std Dev: {np.std(ela_values):.4f}\")\n        print(f\"RGB Mean (Normalized): {np.mean(rgb_means):.4f}\")\n\n        plt.figure(figsize=(12, 5))\n        plt.hist(ela_values, bins=50, alpha=0.6, label='ELA Feature (Scaled)', color='red')\n        plt.title('Distribution of ELA Feature Values')\n        plt.xlabel('ELA Value (0 to ~2550)')\n        plt.ylabel('Frequency')\n        plt.legend()\n        plt.show()\n        \n        # This histogram helps visualize if ELA is predominantly zero or clustered.\n\nprint(\"\\n--- Advanced EDA Complete ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T00:46:20.750036Z","iopub.execute_input":"2025-11-02T00:46:20.750646Z","iopub.status.idle":"2025-11-02T00:46:29.493613Z","shell.execute_reply.started":"2025-11-02T00:46:20.750621Z","shell.execute_reply":"2025-11-02T00:46:29.492842Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport cv2\nimport os\n\n# Define the file path\nTEST_IMAGE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images/45.png\"\n\nprint(f\"Attempting to load image: {TEST_IMAGE_PATH}\")\n\nif not os.path.exists(TEST_IMAGE_PATH):\n    print(\"🛑 ERROR: The file path was not found. Please ensure the Kaggle competition data is mounted correctly.\")\nelse:\n    # Load the image using OpenCV (loads as BGR)\n    img = cv2.imread(TEST_IMAGE_PATH)\n    \n    if img is None:\n        print(\"🛑 ERROR: Could not read the image file.\")\n    else:\n        # Convert the image from BGR (OpenCV default) to RGB (Matplotlib default)\n        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        # Plot the image\n        plt.figure(figsize=(10, 8))\n        plt.imshow(img_rgb)\n        plt.title(f\"Test Image 45 (Dimensions: {img.shape[0]}x{img.shape[1]})\")\n        plt.axis('off') # Hide axes for a cleaner image view\n        plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T00:46:44.473266Z","iopub.execute_input":"2025-11-02T00:46:44.474027Z","iopub.status.idle":"2025-11-02T00:46:45.002618Z","shell.execute_reply.started":"2025-11-02T00:46:44.473998Z","shell.execute_reply":"2025-11-02T00:46:45.001631Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport cv2\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm import tqdm\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nimport time\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nimport warnings\nfrom warnings import filterwarnings\n\n# Suppress the specific UserWarning from the LR scheduler\nwarnings.filterwarnings(\"ignore\", category=UserWarning, module=\"torch.optim.lr_scheduler\")\nfilterwarnings('ignore')\n\n# --- CONFIGURATION --\nTARGET_SIZE = 256\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nBATCH_SIZE = 8\n# REDUCED EPOCHS to 3 (was 10)\nEPOCHS = 2\nLEARNING_RATE = 1e-4\n\n# --- PATHS (CORRECTED FOR KAGGLEHUB CACHE) ---\nTRAIN_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images\"\nMASK_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\"\nMODEL_SAVE_PATH = \"/tmp/model_new_scratch.pth\"\n\n# --- UTILITY FUNCTIONS ---\ndef compute_ela(img_path, quality=95, scale=10):\n    img = cv2.imread(img_path)\n    if img is None or img.size == 0:\n        try:\n            img_data = np.load(img_path)\n            if img_data.ndim == 3:\n                img = cv2.cvtColor(img_data, cv2.COLOR_RGB2BGR)\n            elif img_data.ndim == 2:\n                img = cv2.cvtColor(img_data, cv2.COLOR_GRAY2BGR)\n        except Exception:\n            return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    if img is None or img.size == 0:\n        return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    img_resized = cv2.resize(img, (TARGET_SIZE, TARGET_SIZE))\n    temp_path = f\"/tmp/temp_ela_{os.path.basename(img_path)}_{time.time()}.jpg\"\n    try:\n        cv2.imwrite(temp_path, img_resized, [cv2.IMWRITE_JPEG_QUALITY, quality])\n        compressed_img = cv2.imread(temp_path)\n        if compressed_img is None: return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n        error = np.abs(img_resized.astype(np.float32) - compressed_img.astype(np.float32))\n        ela_feature_2d = np.mean(error, axis=2) * scale\n    finally:\n        if os.path.exists(temp_path): os.remove(temp_path)\n    return cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE),\n                      interpolation=cv2.INTER_LINEAR).astype(np.float32)\n\nclass DiceLoss(nn.Module):\n    def __init__(self, smooth=1.0):\n        super(DiceLoss, self).__init__()\n        self.smooth = smooth\n    def forward(self, pred, target):\n        pred = pred.contiguous().view(-1)\n        target = target.contiguous().view(-1)\n        intersection = (pred * target).sum()\n        dice = (2. * intersection + self.smooth) / (pred.sum() + target.sum() + self.smooth)\n        return 1 - dice\n\n# Hybrid Loss combining Dice and BCE for stable training\nclass HybridLoss(nn.Module):\n    def __init__(self, dice_weight=0.5):\n        super(HybridLoss, self).__init__()\n        self.dice_loss = DiceLoss()\n        self.bce_loss = nn.BCELoss()\n        self.dice_weight = dice_weight\n\n    def forward(self, pred, target):\n        dice = self.dice_loss(pred, target)\n        bce = self.bce_loss(pred, target)\n        return self.dice_weight * dice + (1 - self.dice_weight) * bce\n\n# U-Net architecture\nclass UNet(nn.Module):\n    def __init__(self, in_channels=4, num_classes=1):\n        super().__init__()\n        def block(in_c, out_c):\n            return nn.Sequential(\n                nn.Conv2d(in_c, out_c, 3, 1, 1), nn.ReLU(),\n                nn.Dropout(p=0.2),\n                nn.Conv2d(out_c, out_c, 3, 1, 1), nn.ReLU()\n            )\n\n        self.enc1 = block(in_channels, 64)\n        self.enc2 = block(64, 128)\n        self.bottleneck = block(128, 256)\n        self.upconv2 = nn.ConvTranspose2d(256, 128, 2, 2)\n        self.dec2 = block(128 + 128, 128)\n        self.upconv1 = nn.ConvTranspose2d(128, 64, 2, 2)\n        self.dec1 = block(64 + 64, 64)\n        self.final_conv = nn.Conv2d(64, num_classes, 1)\n\n    def forward(self, x):\n        e1 = self.enc1(x)\n        p1 = F.max_pool2d(e1, 2)\n        e2 = self.enc2(p1)\n        p2 = F.max_pool2d(e2, 2)\n        b = self.bottleneck(p2)\n        d2 = self.upconv2(b)\n        d2 = torch.cat((d2, e2), dim=1)\n        d2 = self.dec2(d2)\n        d1 = self.upconv1(d2)\n        d1 = torch.cat((d1, e1), dim=1)\n        d1 = self.dec1(d1)\n        return torch.sigmoid(self.final_conv(d1))\n\nclass ForgeryDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row['img_path']\n        mask_path = row['mask_path']\n\n        # --- Load Image ---\n        rgb_image = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n\n        if rgb_image is None or rgb_image.size == 0:\n            try:\n                img_data = np.load(img_path)\n                if img_data.ndim == 3:\n                    rgb_image = img_data\n                elif img_data.ndim == 2:\n                    rgb_image = cv2.cvtColor(img_data, cv2.COLOR_GRAY2RGB)\n            except Exception as e:\n                raise RuntimeError(f\"Failed to load image from {img_path}: {e}\")\n\n        # --- Load Mask ---\n        try:\n            mask = np.load(mask_path)\n            if mask.ndim > 2:\n                mask = mask[:, :, 0]\n        except Exception as e:\n            raise RuntimeError(f\"Failed to load mask from {mask_path}: {e}\")\n\n        ela_feature_2d = compute_ela(img_path)\n\n        # Resize all features\n        rgb_image_resized = cv2.resize(rgb_image, (TARGET_SIZE, TARGET_SIZE))\n        ela_feature_resized = cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE))\n\n        # Use INTER_NEAREST for binary mask resizing\n        mask_resized = cv2.resize(mask.astype(np.uint8), (TARGET_SIZE, TARGET_SIZE), interpolation=cv2.INTER_NEAREST)\n\n        # Stack RGB (3) and ELA (1) for a 4-channel input\n        ela_feature_3d = np.expand_dims(ela_feature_resized, axis=-1)\n        stacked_input = np.concatenate([rgb_image_resized, ela_feature_3d], axis=-1)\n\n        # Convert to PyTorch tensors and normalize\n        image = torch.tensor(stacked_input.transpose(2, 0, 1) / 255.0, dtype=torch.float32)\n        mask = torch.tensor(mask_resized / 255.0, dtype=torch.float32).unsqueeze(0)\n\n        return image, mask\n\ndef train_model(model, train_loader, val_loader, epochs=EPOCHS, save_path=MODEL_SAVE_PATH):\n    # Use HybridLoss\n    criterion = HybridLoss(dice_weight=0.5)\n    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)\n\n    # Scheduler with patience=2 (unchanged)\n    scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\n    best_val_loss = float('inf')\n\n    model.to(DEVICE)\n    print(f\"Starting training on {DEVICE} for {epochs} epochs...\")\n\n    for epoch in range(epochs):\n        model.train()\n        train_loss_sum = 0\n\n        for inputs, targets in tqdm(train_loader, desc=f\"Epoch {epoch+1} Training\"):\n            inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)\n            optimizer.zero_grad()\n            outputs = model(inputs)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n            train_loss_sum += loss.item()\n\n        avg_train_loss = train_loss_sum / len(train_loader)\n\n        # Validation Phase\n        model.eval()\n        val_loss_sum = 0\n        with torch.no_grad():\n            for inputs, targets in val_loader:\n                inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)\n                outputs = model(inputs)\n                loss = criterion(outputs, targets)\n                val_loss_sum += loss.item()\n\n        avg_val_loss = val_loss_sum / len(val_loader)\n\n        # Scheduler Step\n        scheduler.step(avg_val_loss)\n\n        print(f\"Epoch {epoch+1} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}\")\n\n        if avg_val_loss < best_val_loss:\n            best_val_loss = avg_val_loss\n            torch.save(model.state_dict(), save_path)\n            print(f\"Model saved successfully to {save_path}. (New best Val Loss: {best_val_loss:.4f})\\n\")\n\n        current_lr = optimizer.param_groups[0]['lr']\n        print(f\"Current Learning Rate: {current_lr:.6f}\")\n\n\n# --- MAIN EXECUTION BLOCK ---\nif __name__ == '__main__':\n\n    print(\"Preparing training data paths...\\n\")\n\n    data_list = []\n\n    # Recursively walk through the TRAIN_ROOT to find all image files\n    for root, _, files in os.walk(TRAIN_ROOT):\n        for f in files:\n            valid_extensions = ('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')\n\n            if f.lower().endswith(valid_extensions):\n                # Only process files in the 'forged' subdirectory, as only they have masks\n                if 'forged' in root.lower():\n                    case_id = os.path.splitext(f)[0]\n                    img_path = os.path.join(root, f)\n\n                    # Use .npy for the mask extension\n                    mask_path = os.path.join(MASK_ROOT, f\"{case_id}.npy\")\n\n                    data_list.append({\n                        'case_id': case_id,\n                        'img_path': img_path,\n                        'mask_path': mask_path\n                    })\n\n    if not data_list:\n        full_df = pd.DataFrame(columns=['case_id', 'img_path', 'mask_path'])\n    else:\n        full_df = pd.DataFrame(data_list)\n\n    if not full_df.empty:\n        # Final check: Keep only images that have a corresponding mask file\n        full_df['mask_exists'] = full_df['mask_path'].apply(os.path.exists)\n        full_df = full_df[full_df['mask_exists']].drop(columns=['mask_exists']).reset_index(drop=True)\n\n    if full_df.empty:\n        print(\"🛑 FATAL ERROR: No valid image/mask pairs found in the input paths. Cannot train. (Check file extensions/paths again)\")\n    else:\n        print(f\"✅ Found {len(full_df)} valid forged samples for training.\")\n\n        # Split data\n        train_df, val_df = train_test_split(full_df, test_size=0.1, random_state=42)\n\n        # Create DataLoaders\n        train_dataset = ForgeryDataset(train_df)\n        val_dataset = ForgeryDataset(val_df)\n        train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\n        val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\n        # Instantiate model (4 input channels: 3 RGB + 1 ELA)\n        model = UNet(in_channels=4)\n\n        # START TRAINING\n        train_model(model, train_loader, val_loader)\n\n        print(\"\\n✅ TRAINING COMPLETE. The trained model is saved and ready for inference.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T00:46:48.464302Z","iopub.execute_input":"2025-11-02T00:46:48.464587Z","iopub.status.idle":"2025-11-02T00:56:30.613508Z","shell.execute_reply.started":"2025-11-02T00:46:48.464567Z","shell.execute_reply":"2025-11-02T00:56:30.612670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# efficientnet_ela_inference_offline.py (Kaggle-friendly)\n# ============================================================\n\nimport os\nimport time\nimport numpy as np\nimport pandas as pd\nimport cv2\nfrom tqdm import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torchvision import models\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ---------------- CONFIG ----------------\nTARGET_SIZE = 256\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nBATCH_SIZE = 8\nFIXED_THRESHOLD = 0.45\nMIN_FORGERY_AREA = 32\n\nTEST_IMAGE_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\"\nSAMPLE_SUBMISSION_FILE = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/sample_submission.csv\"\nMODEL_PATH = \"/tmp/model_new_scratch.pth\"   # change if you have a checkpoint\nOUTPUT_FILENAME = \"submission.csv\"\n\n# ============================================================\n# ---------------- UTIL: ELA ----------------\n# ============================================================\ndef compute_ela(img_path, quality=95, scale=10):\n    \"\"\"\n    Returns a TARGET_SIZE x TARGET_SIZE single-channel ELA image (float32).\n    Robust to jpg/png/npy; returns zeros if unreadable.\n    \"\"\"\n    img = None\n    try:\n        img = cv2.imread(img_path)\n    except Exception:\n        img = None\n\n    if img is None or img.size == 0:\n        try:\n            arr = np.load(img_path)\n            if arr.ndim == 3 and arr.shape[2] == 3:\n                img = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_RGB2BGR)\n            elif arr.ndim == 2:\n                img = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_GRAY2BGR)\n        except Exception:\n            img = None\n\n    if img is None or img.size == 0:\n        return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    img_resized = cv2.resize(img, (TARGET_SIZE, TARGET_SIZE))\n    tmp_jpg = f\"/tmp/ela_tmp_{os.getpid()}_{time.time()}.jpg\"\n    try:\n        cv2.imwrite(tmp_jpg, img_resized, [int(cv2.IMWRITE_JPEG_QUALITY), quality])\n        comp = cv2.imread(tmp_jpg)\n        if comp is None:\n            return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n        error = np.abs(img_resized.astype(np.float32) - comp.astype(np.float32))\n        ela = np.mean(error, axis=2) * scale\n    finally:\n        if os.path.exists(tmp_jpg):\n            try:\n                os.remove(tmp_jpg)\n            except Exception:\n                pass\n\n    ela_resized = cv2.resize(ela, (TARGET_SIZE, TARGET_SIZE), interpolation=cv2.INTER_LINEAR)\n    return ela_resized.astype(np.float32)\n\n# ============================================================\n# ---------------- DATAFRAME CREATION ----------------\n# ============================================================\ndef create_test_df_robust(test_image_root, sample_submission_path):\n    df = pd.read_csv(sample_submission_path)\n    df['case_id'] = df['case_id'].astype(str)\n    present = {}\n    if os.path.exists(test_image_root):\n        for root, _, files in os.walk(test_image_root):\n            for f in files:\n                if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')):\n                    present[os.path.splitext(f)[0]] = os.path.join(root, f)\n    df['img_path'] = df['case_id'].map(present)\n    df['img_path'] = df['img_path'].fillna('MISSING_FILE')\n    return df[['case_id', 'img_path']]\n\n# ============================================================\n# ---------------- RLE ENCODING ----------------\n# ============================================================\ndef rle_encode(mask):\n    \"\"\"Run-length encoding using column-major (Fortran) order — Kaggle standard.\"\"\"\n    if mask.sum() == 0:\n        return \"authentic\"\n    pixels = mask.astype(np.uint8).flatten(order='F')\n    padded = np.pad(pixels, (1,1), mode='constant', constant_values=0)\n    changes = np.where(padded[1:] != padded[:-1])[0]\n    runs = changes.reshape(-1,2)\n    starts = runs[:,0] + 1\n    lengths = runs[:,1] - runs[:,0]\n    return ' '.join(str(s) + ' ' + str(l) for s, l in zip(starts, lengths))\n\n# ============================================================\n# ---------------- MODEL DEFINITION ----------------\n# ============================================================\nclass EffNetB0_ELA_Segmenter(nn.Module):\n    def __init__(self, pretrained=False):  # <-- pretrained=False (offline safe)\n        super().__init__()\n        # Load EfficientNet without internet download\n        self.backbone = models.efficientnet_b0(weights=None)\n\n        # Modify first conv to accept 4 channels (RGB + ELA)\n        first_conv = self.backbone.features[0][0]\n        out_ch = first_conv.out_channels\n        k = first_conv.kernel_size\n        new_conv = nn.Conv2d(4, out_ch, kernel_size=k, stride=first_conv.stride,\n                             padding=first_conv.padding, bias=False)\n\n        with torch.no_grad():\n            old_w = first_conv.weight.data.clone()\n            new_w = torch.zeros((out_ch, 4, k[0], k[1]))\n            new_w[:, :3, :, :] = old_w\n            new_w[:, 3:4, :, :] = old_w.mean(dim=1, keepdim=True)\n            new_conv.weight.data.copy_(new_w)\n        self.backbone.features[0][0] = new_conv\n\n        encoder_out_channels = 1280\n        self.decoder = nn.Sequential(\n            nn.Conv2d(encoder_out_channels, 256, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False),\n            nn.Conv2d(256, 128, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False),\n            nn.Conv2d(128, 64, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(64, 1, kernel_size=1),\n            nn.Sigmoid()\n        )\n\n    def forward(self, x):\n        feats = self.backbone.features(x)\n        mask = self.decoder(feats)\n        mask_up = F.interpolate(mask, size=(TARGET_SIZE, TARGET_SIZE),\n                                mode='bilinear', align_corners=False)\n        return mask_up\n\n# ============================================================\n# ---------------- IMAGE TO TENSOR ----------------\n# ============================================================\ndef image_to_tensor_rgb_ela(img_path):\n    try:\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n    except Exception:\n        img = None\n    if img is None or img.size == 0:\n        try:\n            arr = np.load(img_path)\n            if arr.ndim == 2:\n                arr = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_GRAY2BGR)\n            elif arr.ndim == 3 and arr.shape[2] == 3:\n                arr = arr.astype(np.uint8)\n            img = arr[:, :, ::-1]\n        except Exception:\n            rgb = np.zeros((TARGET_SIZE, TARGET_SIZE, 3), dtype=np.uint8)\n            ela = np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n            tensor = np.concatenate([rgb.transpose(2,0,1).astype(np.float32)/255.0,\n                                     ela[np.newaxis, :]], axis=0)\n            return torch.from_numpy(tensor).float()\n\n    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img_rgb = cv2.resize(img_rgb, (TARGET_SIZE, TARGET_SIZE))\n    ela = compute_ela(img_path)\n\n    rgb_norm = img_rgb.astype(np.float32) / 255.0\n    ela_norm = ela.astype(np.float32)\n    if ela_norm.max() > 0:\n        ela_norm = (ela_norm - ela_norm.min()) / (ela_norm.max() - ela_norm.min() + 1e-8)\n    stacked = np.concatenate([rgb_norm.transpose(2,0,1), ela_norm[np.newaxis, ...]], axis=0)\n    return torch.from_numpy(stacked).float()\n\n# ============================================================\n# ---------------- INFERENCE ----------------\n# ============================================================\ndef run_inference(model_path=None, tta=True):\n    device = DEVICE\n    model = EffNetB0_ELA_Segmenter(pretrained=False).to(device)\n\n    # Try loading weights if available\n    if model_path and os.path.exists(model_path):\n        state = torch.load(model_path, map_location=device)\n        model_state = model.state_dict()\n        compatible_state = {k: v for k, v in state.items()\n                            if k in model_state and v.shape == model_state[k].shape}\n        model_state.update(compatible_state)\n        model.load_state_dict(model_state, strict=False)\n        print(f\"✅ Loaded compatible weights ({len(compatible_state)} matched layers).\")\n    else:\n        print(\"⚠️ No checkpoint found or incompatible — using random weights.\")\n\n    model.eval()\n    test_df = create_test_df_robust(TEST_IMAGE_ROOT, SAMPLE_SUBMISSION_FILE)\n    results = []\n\n    batch_imgs = []\n    case_ids = []\n\n    def flush_batch(batch_imgs, case_ids):\n        if len(batch_imgs) == 0:\n            return []\n        x = torch.stack(batch_imgs, dim=0).to(device)\n        with torch.no_grad():\n            preds = model(x)\n            if tta:\n                x_flip = torch.flip(x, dims=[3])\n                preds_flip = model(x_flip)\n                preds_flip = torch.flip(preds_flip, dims=[3])\n                preds = (preds + preds_flip) / 2.0\n            preds_np = preds.squeeze(1).cpu().numpy()\n        encs = []\n        for p in preds_np:\n            mask = (p > FIXED_THRESHOLD).astype(np.uint8)\n            if mask.sum() < MIN_FORGERY_AREA:\n                mask[:] = 0\n            encs.append(rle_encode(mask))\n        return encs\n\n    for _, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Inference\"):\n        cid = row['case_id']\n        path = row['img_path']\n        case_ids.append(cid)\n        if path == 'MISSING_FILE' or not os.path.exists(path):\n            results.append(\"authentic\")\n            continue\n        t = image_to_tensor_rgb_ela(path)\n        batch_imgs.append(t)\n        if len(batch_imgs) >= BATCH_SIZE:\n            results.extend(flush_batch(batch_imgs, case_ids))\n            batch_imgs, case_ids = [], []\n\n    if len(batch_imgs) > 0:\n        results.extend(flush_batch(batch_imgs, case_ids))\n\n    submission_df = pd.DataFrame({\n        'case_id': test_df['case_id'],\n        'encoded_pixels': results\n    })\n    submission_df.to_csv(OUTPUT_FILENAME, index=False)\n    print(f\"\\n✅ Created {OUTPUT_FILENAME} with {len(submission_df)} rows at {pd.Timestamp.now()}\")\n    return submission_df\n\n# ============================================================\n# ---------------- RUN ----------------\n# ============================================================\nif __name__ == \"__main__\":\n    submission = run_inference(model_path=MODEL_PATH, tta=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T01:03:32.623539Z","iopub.execute_input":"2025-11-02T01:03:32.623870Z","iopub.status.idle":"2025-11-02T01:03:33.114139Z","shell.execute_reply.started":"2025-11-02T01:03:32.623850Z","shell.execute_reply":"2025-11-02T01:03:33.113206Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cat submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T01:03:41.256220Z","iopub.execute_input":"2025-11-02T01:03:41.256758Z","iopub.status.idle":"2025-11-02T01:03:41.400613Z","shell.execute_reply.started":"2025-11-02T01:03:41.256737Z","shell.execute_reply":"2025-11-02T01:03:41.399812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate_and_print_rle(submission_df):\n    # Validate the submission format\n    print(\"Submission columns:\", submission_df.columns.tolist())\n    \n    authentic_count = submission_df['encoded_pixels'].apply(lambda x: x == 'authentic').sum()\n    rle_rows = submission_df[submission_df['encoded_pixels'] != 'authentic']\n    \n    print(f\"Total rows: {len(submission_df)}\")\n    print(f\"Authentic count: {authentic_count}\")\n    print(f\"Forgery (RLE) count: {len(rle_rows)}\")\n    \n    # Show first few RLE entries\n    print(\"\\nSample RLE rows:\")\n    print(rle_rows.head())\n\n# Use it\nsubmission_df = pd.read_csv(\"submission.csv\")\nvalidate_and_print_rle(submission_df)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T01:04:49.282209Z","iopub.execute_input":"2025-11-02T01:04:49.282829Z","iopub.status.idle":"2025-11-02T01:04:49.297025Z","shell.execute_reply.started":"2025-11-02T01:04:49.282805Z","shell.execute_reply":"2025-11-02T01:04:49.296311Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = pd.read_csv(\"submission.csv\")\nvalidate_and_print_rle(submission_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T01:04:52.265040Z","iopub.execute_input":"2025-11-02T01:04:52.265817Z","iopub.status.idle":"2025-11-02T01:04:52.274019Z","shell.execute_reply.started":"2025-11-02T01:04:52.265775Z","shell.execute_reply":"2025-11-02T01:04:52.273168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}