{"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"},{"sourceId":13482618,"sourceType":"datasetVersion","datasetId":8559844}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"The Jupyter Notebook, `forgery-final-submission.ipynb`, implements a **deep learning pipeline for scientific image forgery detection and localization** using a **U-Net architecture** combined with **Error Level Analysis (ELA)** features.\n\nThe notebook is structured into three main phases:\n1.  **Environment Setup and Data Counts** (Cells 1-4)\n2.  **Model Training** (Cell 5)\n3.  **Inference and Submission Generation** (Cells 6-8)\n\n---\n\n## 1. Environment Setup and Data Counts\n\nThis initial section confirms the compute environment and checks the dataset's contents.\n\n* **Cell 1:** Executes `!nvidia-smi` to confirm that a **NVIDIA Tesla T4 GPU** is available and lists its specifications (CUDA Version 12.6, 16GB memory).\n* **Cell 2 & 3:** The output `2751` indicates that there are **2,751 forged training images** (`train_images/forged/*.png`) and a matching **2,751 ground truth mask files** (`train_masks/*.npy`).\n* **Cell 4:** The output `2377` indicates there are **2,377 authentic training images** (`train_images/authentic/*.png`). The code uses only the *forged* images for training, since authentic images lack the forgery masks essential to this segmentation task.\n\n---\n\n## 2. Model Training\n\nThis section defines the core components—utilities, model, and training loop—and then executes the training process.\n\n### Configuration and Utility Functions\n\n| Parameter | Value | Description |\n| :--- | :--- | :--- |\n| `TARGET_SIZE` | 256 | All images and masks are resized to $256 \\times 256$ for model input. |\n| `DEVICE` | 'cuda' | Uses the GPU for training if available. |\n| `BATCH_SIZE` | 8 | Number of samples per batch. |\n| `EPOCHS` | 2 | The model is trained for 2 epochs, with an early stopping mechanism based on validation loss. |\n| `LEARNING_RATE` | $1 \\times 10^{-4}$ | Initial learning rate for the Adam optimizer. |\n| `compute_ela` | Function | **Error Level Analysis (ELA):** This crucial function detects subtle differences in JPEG compression levels across an image, which is a strong indicator of forgery (e.g., copy-paste). It saves the image to a temporary file at a specific `quality` (95), re-reads it, calculates the absolute difference (error) between the original and re-compressed image, and returns the mean error (scaled by 10) as a feature. |\n| `DiceLoss` | Class | A common loss function for **image segmentation** tasks, which measures the overlap between the predicted mask and the ground truth mask. The goal is to minimize this loss. |\n\n### U-Net Model Architecture\n\nThe core of the solution is a custom **U-Net** model, a common architecture for biomedical image segmentation, adapted here for forgery localization.\n\n* **Input:** The model is initialized with `in_channels=4`. This is because the input tensor is a concatenation of **3 RGB channels** (the color image) and **1 ELA channel** (the forgery feature).\n* **Structure:** It follows the standard U-Net pattern: a **contracting path (encoder)** to capture context and a **symmetric expanding path (decoder)** to enable precise localization.\n* **Encoder/Decoder Block:** Each block consists of two convolutional layers, **ReLU activation**, and a **Dropout layer** (`p=0.2`). The Dropout is a regularization technique to prevent overfitting, which is important as the same Dropout is used in the inference phase.\n* **Skip Connections:** Tensors from the encoder are concatenated with the upsampled decoder features (`torch.cat`), allowing the decoder to access high-resolution features lost during downsampling.\n* **Output:** A final $1 \\times 1$ convolution produces a single-channel output, which is passed through a **Sigmoid activation** function to generate probability scores (0 to 1) for each pixel being part of a forgery.\n\n### Training Execution (`if __name__ == '__main__':`)\n\n1.  **Data Preparation:** It iterates through the input directories to create a DataFrame (`full_df`) of forged image path (`img_path`) and corresponding mask path (`mask_path`) pairs.\n2.  **Data Split:** The data is split into **90% for training** (`train_df`) and **10% for validation** (`val_df`).\n3.  **Data Loading:** Custom `ForgeryDataset` objects are created to handle reading, resizing (to `TARGET_SIZE`), ELA calculation, and tensor conversion for each image/mask pair. The input image is converted to a 4-channel tensor (RGB + ELA).\n4.  **Training Loop:** The `train_model` function orchestrates the process:\n    * It uses `DiceLoss` as the objective function.\n    * The **Adam optimizer** and a **ReduceLROnPlateau scheduler** (to reduce the learning rate if the validation loss plateaus) are used.\n    * The best model parameters (based on the lowest validation loss) are saved to `/tmp/model_new_scratch.pth`.\n5.  **Output:** The training ran for 2 epochs, with the best model saved after **Epoch 1** (Validation Loss: 0.6484).\n\n---\n\n## 3. Inference and Submission Generation\n\nThis section loads the trained model, processes the test images, and generates the final submission file.\n\n### Key Enhancements for Inference\n\nThe inference phase includes sophisticated post-processing steps to improve the quality of the predicted forgery mask:\n\n* **Fixed Threshold (`FIXED_THRESHOLD = 0.45`):** The model's raw probability map is binarized by considering any pixel with a probability greater than 0.45 as part of a forgery. This value was tuned to balance False Positives and False Negatives.\n* **Minimum Area Filtering (`MIN_FORGERY_AREA = 64`):** The mask is processed to remove small, isolated prediction regions that are likely noise. Only contiguous components with an area of at least 64 pixels (in the $256 \\times 256$ resized image) are kept. This reduces **False Positives**.\n* **RLE Encoding:** The resulting cleaned mask is resized back to its original dimensions and encoded into the **Run-Length Encoding (RLE)** format required for the submission. If the mask is entirely zero after post-processing, the RLE string is set to `'authentic'`.\n\n### Inference Execution\n\n1.  **Model Loading:** The U-Net model structure (including Dropout) is re-instantiated and the weights are loaded from the saved path (`/tmp/model_new_scratch.pth`).\n2.  **Data Preparation:** A DataFrame is created from the sample submission file, and image paths for the test cases are robustly located.\n3.  **Batch Processing (`run_inference_and_segment`):**\n    * Images are loaded, ELA is computed, and a 4-channel input tensor is created.\n    * Images are processed in batches (size 8) on the GPU.\n    * The model predicts a probability map.\n    * The **threshold (0.45)** and **Minimum Area Filter** are applied to clean the mask.\n    * The final mask is resized to the original image dimensions and converted to the **RLE string** format.\n4.  **Submission Generation:** A final `submission.csv` is created. For RLE strings, the format `\"[RLE_STRING]\"` is applied, while 'authentic' remains unbracketed, matching the required submission format.\n5.  **Output:** For the test image `45`, the inference shows a maximum forgery probability of `0.0000`, resulting in a final prediction of **`authentic`** in the `submission.csv`.","metadata":{}},{"cell_type":"code","source":"!nvidia-smi","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T07:52:37.634679Z","iopub.execute_input":"2025-10-28T07:52:37.635395Z","iopub.status.idle":"2025-10-28T07:52:37.861498Z","shell.execute_reply.started":"2025-10-28T07:52:37.635366Z","shell.execute_reply":"2025-10-28T07:52:37.860401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls -ltha /kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/forged/*.png |wc -l ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T07:52:43.052897Z","iopub.execute_input":"2025-10-28T07:52:43.053417Z","iopub.status.idle":"2025-10-28T07:52:47.611188Z","shell.execute_reply.started":"2025-10-28T07:52:43.053393Z","shell.execute_reply":"2025-10-28T07:52:47.610058Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls -ltha /kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks/*.npy |wc -l ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T07:52:53.303042Z","iopub.execute_input":"2025-10-28T07:52:53.303916Z","iopub.status.idle":"2025-10-28T07:52:56.879282Z","shell.execute_reply.started":"2025-10-28T07:52:53.303882Z","shell.execute_reply":"2025-10-28T07:52:56.878499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls /kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/authentic/*.png |wc -l ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T07:53:00.970106Z","iopub.execute_input":"2025-10-28T07:53:00.970988Z","iopub.status.idle":"2025-10-28T07:53:05.685113Z","shell.execute_reply.started":"2025-10-28T07:53:00.970955Z","shell.execute_reply":"2025-10-28T07:53:05.684346Z"}},"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\n\n# Suppress the specific UserWarning from the LR scheduler\nwarnings.filterwarnings(\"ignore\", category=UserWarning, module=\"torch.optim.lr_scheduler\")\n\nfrom warnings import filterwarnings\nfilterwarnings('ignore') \n\n# --- CONFIGURATION --\nTARGET_SIZE = 256\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nBATCH_SIZE = 8\n# FINAL OPTIMIZATION: 2 epochs is sufficient since the model saves after the first epoch's best val loss.\nEPOCHS = 2 \nLEARNING_RATE = 1e-4\n\n# --- PATHS ---\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\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# Model architecture includes Dropout to match inference cell\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), # Dropout layer \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    criterion = DiceLoss() \n    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)\n    \n    # Scheduler to reduce LR if val loss plateaus\n    scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=1) \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# --- MAIN EXECUTION BLOCK ---\n\nif __name__ == '__main__':\n    \n    print(\"Preparing training data paths...\")\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.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T07:53:11.938881Z","iopub.execute_input":"2025-10-28T07:53:11.939570Z","iopub.status.idle":"2025-10-28T08:04:55.610493Z","shell.execute_reply.started":"2025-10-28T07:53:11.939543Z","shell.execute_reply":"2025-10-28T08:04:55.609778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\nimport os\n\n# --- Configuration ---\n# Set the most probable path for the test image in the Kaggle environment\nIMAGE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images/45.png\" \n\n# --- Image Display Script ---\n\ntry:\n    # 1. Check if the file exists\n    if not os.path.exists(IMAGE_PATH):\n        # Fallback check, as the file extension might be .npy or .jpg\n        print(f\"🛑 ERROR: Image file '{IMAGE_PATH}' not found. Trying common alternatives...\")\n        \n        # Checking for common alternatives found in the dataset\n        base_dir = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\"\n        if os.path.exists(os.path.join(base_dir, '45.npy')):\n            IMAGE_PATH = os.path.join(base_dir, '45.npy')\n        elif os.path.exists(os.path.join(base_dir, '45.jpg')):\n             IMAGE_PATH = os.path.join(base_dir, '45.jpg')\n        else:\n             # If no path is found, raise the error\n            raise FileNotFoundError(f\"Image 45 not found at {base_dir} with .png, .npy, or .jpg extension.\")\n    \n    # 2. Load the image robustly (handling .npy if necessary)\n    img_bgr = cv2.imread(IMAGE_PATH)\n    \n    # If standard loading fails (e.g., it's a raw .npy file)\n    if img_bgr is None or img_bgr.size == 0:\n        try:\n            img_data = np.load(IMAGE_PATH)\n            if img_data.ndim == 3:\n                 # Assume RGB/BGR format is loaded\n                img_bgr = cv2.cvtColor(img_data, cv2.COLOR_RGB2BGR)\n            elif img_data.ndim == 2:\n                 # Grayscale to BGR\n                img_bgr = cv2.cvtColor(img_data, cv2.COLOR_GRAY2BGR)\n        except Exception:\n            raise RuntimeError(f\"Failed to load image data from '{IMAGE_PATH}' using both cv2.imread and np.load.\")\n\n    # 3. Convert from BGR to RGB for correct display in matplotlib\n    img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n\n    # 4. Display the image\n    plt.figure(figsize=(10, 10))\n    plt.imshow(img_rgb)\n    plt.title(f\"Image for Case ID: 45 ({os.path.basename(IMAGE_PATH)})\")\n    plt.axis('off') # Hide axis ticks and labels\n    plt.show()\n\nexcept Exception as e:\n    print(f\"An error occurred during image display: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T08:09:54.985171Z","iopub.execute_input":"2025-10-28T08:09:54.986049Z","iopub.status.idle":"2025-10-28T08:09:55.623770Z","shell.execute_reply.started":"2025-10-28T08:09:54.986022Z","shell.execute_reply":"2025-10-28T08:09:55.622824Z"}},"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 pandas as pd\nfrom tqdm import tqdm\nimport time\nimport csv\n\n# --- CONFIGURATION & PATHS ---\nTARGET_SIZE = 256\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nBATCH_SIZE = 8\n# ENHANCEMENT: Adjusted threshold to favor True Positives (from 0.5)\nFIXED_THRESHOLD = 0.45 \n# ENHANCEMENT: Minimum area filter for noise reduction (Tune this value on public LB)\nMIN_FORGERY_AREA = 64\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\"\n\n# Model path should point to the successfully trained model\nmodel_path = \"/tmp/model_new_scratch.pth\" \nOUTPUT_FILENAME = \"submission.csv\" \n\n# --- UTILITY FUNCTIONS (Must be defined here for the block to run) ---\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: 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:\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_{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\ndef create_test_df_robust(test_image_root, sample_submission_path):\n    master_df = pd.read_csv(sample_submission_path)\n    master_df['case_id'] = master_df['case_id'].astype(str)\n    present_files = {}\n    if os.path.exists(test_image_root):\n        # Use os.walk for robust search, accounting for .npy\n        for root, _, files in os.walk(test_image_root):\n            for f in files:\n                case_id = os.path.splitext(f)[0]\n                if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')):\n                    present_files[case_id] = os.path.join(root, f)\n                    \n    master_df['img_path'] = master_df['case_id'].map(present_files)\n    master_df['img_path'] = master_df['img_path'].fillna('MISSING_FILE')\n    return master_df[['case_id', 'img_path']]\n\ndef rle_encode(mask):\n    if mask.sum() == 0: return \"authentic\"\n    pixels = mask.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]]) \n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    return ', '.join(str(x) for x in runs)\n\n# --- MODEL ARCHITECTURE (FIXED: Includes Dropout to match trained model) ---\nclass UNet(nn.Module):\n    def __init__(self, in_channels=4, num_classes=1):\n        super().__init__()\n        # FIXED: Includes nn.Dropout(p=0.2)\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\n# --- INFERENCE FUNCTION WITH POST-PROCESSING ---\ndef run_inference_and_segment(unet_model, test_df):\n    results = [] \n    unet_model.eval()\n    images_to_process = []\n    \n    for index, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Processing\"):\n        case = row['case_id']\n        img_path = row['img_path']\n        \n        if img_path == 'MISSING_FILE' or img_path == 'NOT_FOUND':\n            results.append({'case_id': case, 'annotation': 'authentic'})\n            continue\n            \n        try:\n            # Robust image loading: Try cv2, then np.load for .npy\n            rgb_image = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n            if rgb_image is None or rgb_image.size == 0:\n                img_data = np.load(img_path)\n                if img_data.ndim == 3: rgb_image = img_data\n                elif img_data.ndim == 2: rgb_image = cv2.cvtColor(img_data, cv2.COLOR_GRAY2RGB)\n\n            if rgb_image is None or rgb_image.size == 0: raise ValueError(f\"Invalid image data for {case}\")\n                \n            original_shape = rgb_image.shape[:2]\n            ela_feature_2d = compute_ela(img_path)\n            \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            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            images_to_process.append((case, original_shape, stacked_input))\n            \n            # Process batch\n            if len(images_to_process) == BATCH_SIZE or index == len(test_df) - 1:\n                if images_to_process:\n                    batch_inputs = torch.stack([\n                        torch.tensor(img_data.transpose(2, 0, 1) / 255.0, dtype=torch.float32) \n                        for _, _, img_data in images_to_process\n                    ]).to(DEVICE)\n                    \n                    with torch.no_grad():\n                        outputs = unet_model(batch_inputs).detach().cpu().numpy()\n                        \n                    for i, output in enumerate(outputs):\n                        case_id_out, original_shape_out, _ = images_to_process[i]\n                        output_prob = output.squeeze()\n                        \n                        # --- LOG PROBABILITY HERE ---\n                        # RESTORED: Logging the max probability for debugging\n                        max_prob = np.max(output_prob)\n                        print(f\"|--- Case {case_id_out} Max Forgery Probability: {max_prob:.4f} ---|\")\n                        # ----------------------------\n\n                        # Apply Threshold (using 0.45)\n                        final_mask_resized = (output_prob > FIXED_THRESHOLD).astype(np.uint8)\n                        \n                        # ENHANCEMENT: Minimum Area Filtering (MAFilter)\n                        clean_mask_resized = np.zeros_like(final_mask_resized)\n                        \n                        # Find connected components \n                        num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(\n                            final_mask_resized, 4, cv2.CV_32S\n                        )\n                        \n                        # Iterate through each component (label 0 is the background)\n                        for label in range(1, num_labels):\n                            area = stats[label, cv2.CC_STAT_AREA]\n                            if area >= MIN_FORGERY_AREA:\n                                # Keep segments that meet the minimum size requirement\n                                clean_mask_resized[labels == label] = 1 \n                        \n                        # Resize the CLEANED mask back to the original size\n                        final_mask = cv2.resize(\n                            clean_mask_resized, \n                            (original_shape_out[1], original_shape_out[0]), \n                            interpolation=cv2.INTER_NEAREST\n                        )\n                        \n                        rle_annotation = rle_encode(final_mask)\n                        results.append({'case_id': case_id_out, 'annotation': rle_annotation})\n                        \n                    images_to_process = [] # Reset batch\n        except Exception as e:\n            print(f\"Error processing case {case}: {e}. Defaulting to authentic.\")\n            results.append({'case_id': case, 'annotation': 'authentic'})\n    return pd.DataFrame(results)\n\n# --- MAIN EXECUTION BLOCK ---\nif __name__ == \"__main__\":\n    \n    print(f\"--- Starting inference on {DEVICE} at {pd.Timestamp.now()} ---\")\n    \n    # 1. Load Model\n    model = None\n    try:\n        model = UNet(in_channels=4).to(DEVICE)\n        model.load_state_dict(torch.load(model_path, map_location=DEVICE))\n        model.eval() # Set model to evaluation mode\n        print(f\"Model loaded successfully from {model_path}\")\n    except Exception as e:\n        print(f\"Error loading model from {model_path}. Submitting 'authentic' for all cases. Error: {e}\")\n        model = None\n        \n    # 2. Prepare Data\n    test_df = create_test_df_robust(TEST_IMAGE_ROOT, SAMPLE_SUBMISSION_FILE)\n    test_df['case_id'] = test_df['case_id'].astype(str) \n    \n    # 3. Run Inference\n    if model:\n        results_df = run_inference_and_segment(model, test_df)\n    else:\n        results_df = test_df[['case_id']].assign(annotation='authentic')\n        \n    # 4. Finalize Submission DF\n    submission_df = test_df[['case_id']].copy().merge(results_df, on='case_id', how='left')\n    submission_df['annotation'] = submission_df['annotation'].fillna('authentic')\n    submission_df = submission_df[['case_id', 'annotation']].sort_values('case_id').reset_index(drop=True)\n    \n    # 5. Write CSV with Correct RLE Formatting\n    with open(OUTPUT_FILENAME, \"w\", newline='') as f:\n        writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)\n        writer.writerow(['case_id', 'annotation'])\n        \n        for _, row in submission_df.iterrows():\n            case_id = str(row['case_id']) \n            annotation = row['annotation']\n            \n            if annotation.lower() == 'authentic':\n                 writer.writerow([case_id, annotation])\n            else:\n                 # Create the full bracketed RLE string\n                 full_rle_string = f\"[{annotation}]\"\n                 writer.writerow([case_id, full_rle_string])\n    \n    print(f\"\\n✅ Created {OUTPUT_FILENAME} with {len(submission_df)} rows at {pd.Timestamp.now()}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T08:10:14.810899Z","iopub.execute_input":"2025-10-28T08:10:14.811587Z","iopub.status.idle":"2025-10-28T08:10:14.992547Z","shell.execute_reply.started":"2025-10-28T08:10:14.811561Z","shell.execute_reply":"2025-10-28T08:10:14.991623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cat submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T08:10:25.053152Z","iopub.execute_input":"2025-10-28T08:10:25.053700Z","iopub.status.idle":"2025-10-28T08:10:25.200860Z","shell.execute_reply.started":"2025-10-28T08:10:25.053679Z","shell.execute_reply":"2025-10-28T08:10:25.199812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate_and_print_rle(submission_df):\n    \"\"\"\n    Validates RLE output structure and prints debugging info.\n    Checks for: 1. Authentic/RLE count. 2. Even number of RLE elements.\n    \"\"\"\n    print(\"\\n--- RLE Output Validation Check ---\")\n    \n    # Analyze the annotations\n    authentic_count = submission_df['annotation'].apply(lambda x: x == 'authentic').sum()\n    rle_rows = submission_df[submission_df['annotation'] != 'authentic']\n    \n    print(f\"Total Submissions: {len(submission_df)}\")\n    print(f\"Authentic (No Forgery) Count: {authentic_count}\")\n    print(f\"RLE Annotated (Forged) Count: {len(rle_rows)}\")\n    \n    # CRITICAL CHECK: RLE strings must always have an even number of elements (start, length, start, length...)\n    rle_check = rle_rows['annotation'].apply(lambda x: len(x.split(' ')) % 2 == 0)\n    \n    if rle_check.all():\n        print(f\"✅ RLE Structure: All {len(rle_rows)} RLE strings contain an even number of elements.\")\n    else:\n        # Prints a warning if any RLE string has an odd number of elements (a common error)\n        bad_rle_count = len(rle_rows) - rle_check.sum()\n        print(f\"🛑 RLE ERROR: Found {bad_rle_count} RLE strings with an odd number of elements (Invalid pairing).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T08:10:30.992963Z","iopub.execute_input":"2025-10-28T08:10:30.993755Z","iopub.status.idle":"2025-10-28T08:10:30.999816Z","shell.execute_reply.started":"2025-10-28T08:10:30.993731Z","shell.execute_reply":"2025-10-28T08:10:30.999032Z"}},"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-10-28T08:10:35.248380Z","iopub.execute_input":"2025-10-28T08:10:35.248727Z","iopub.status.idle":"2025-10-28T08:10:35.260147Z","shell.execute_reply.started":"2025-10-28T08:10:35.248706Z","shell.execute_reply":"2025-10-28T08:10:35.259390Z"}},"outputs":[],"execution_count":null}]}