{"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":"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-11-03T01:43:06.903035Z","iopub.execute_input":"2025-11-03T01:43:06.903365Z","iopub.status.idle":"2025-11-03T01:54:16.592452Z","shell.execute_reply.started":"2025-11-03T01:43:06.903342Z","shell.execute_reply":"2025-11-03T01:54:16.591464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# Double convolution block used in UNet\nclass DoubleConv(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super(DoubleConv, self).__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        return self.conv(x)\n\n# UNet Model\nclass UNet(nn.Module):\n    def __init__(self, in_ch=3, out_ch=1):\n        super(UNet, self).__init__()\n        self.dconv_down1 = DoubleConv(in_ch, 64)\n        self.dconv_down2 = DoubleConv(64, 128)\n        self.dconv_down3 = DoubleConv(128, 256)\n        self.dconv_down4 = DoubleConv(256, 512)\n        self.maxpool = nn.MaxPool2d(2)\n        self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        self.dconv_up3 = DoubleConv(256 + 512, 256)\n        self.dconv_up2 = DoubleConv(128 + 256, 128)\n        self.dconv_up1 = DoubleConv(128 + 64, 64)\n        self.conv_last = nn.Conv2d(64, out_ch, 1)\n\n    def forward(self, x):\n        conv1 = self.dconv_down1(x)\n        conv2 = self.dconv_down2(self.maxpool(conv1))\n        conv3 = self.dconv_down3(self.maxpool(conv2))\n        conv4 = self.dconv_down4(self.maxpool(conv3))\n\n        x = self.upsample(conv4)\n        x = torch.cat([x, conv3], dim=1)\n        x = self.dconv_up3(x)\n\n        x = self.upsample(x)\n        x = torch.cat([x, conv2], dim=1)\n        x = self.dconv_up2(x)\n\n        x = self.upsample(x)\n        x = torch.cat([x, conv1], dim=1)\n        x = self.dconv_up1(x)\n\n        out = self.conv_last(x)\n        return out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:00:52.526119Z","iopub.execute_input":"2025-11-03T02:00:52.526659Z","iopub.status.idle":"2025-11-03T02:00:52.535686Z","shell.execute_reply.started":"2025-11-03T02:00:52.526633Z","shell.execute_reply":"2025-11-03T02:00:52.534805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\n# Assuming UNet is already defined (from previous code)\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n\n# Create UNet instance\nmodel = UNet(in_ch=3, out_ch=1).to(DEVICE)\n\n# Create a dummy input image batch: batch_size=1, 3 channels, 256x256 image\ndummy_input = torch.randn(1, 3, 256, 256).to(DEVICE)\n\n# Forward pass\noutput = model(dummy_input)\n\n# Print output shape\nprint(\"Output shape:\", output.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:01:02.795293Z","iopub.execute_input":"2025-11-03T02:01:02.796242Z","iopub.status.idle":"2025-11-03T02:01:02.880688Z","shell.execute_reply.started":"2025-11-03T02:01:02.796207Z","shell.execute_reply":"2025-11-03T02:01:02.879861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset\nimport numpy as np\nfrom PIL import Image\nimport torch\n\nclass ForgeryDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_path = self.df.iloc[idx]['img_path']\n        mask_path = self.df.iloc[idx]['mask_path']\n\n        # Load image and mask\n        image = np.array(Image.open(img_path).convert(\"RGB\"))\n        mask = np.load(mask_path)  # assuming mask is saved as .npy\n\n        # Normalize image to [0,1]\n        image = image / 255.0\n        mask = mask.astype(np.float32)\n\n        # Convert to CHW format for PyTorch\n        image = torch.tensor(image.transpose(2,0,1), dtype=torch.float)\n        mask = torch.tensor(mask[np.newaxis, :, :], dtype=torch.float)  # add channel dim\n\n        if self.transform:\n            image = self.transform(image)\n            mask = self.transform(mask)\n\n        return image, mask\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:01:13.388774Z","iopub.execute_input":"2025-11-03T02:01:13.389528Z","iopub.status.idle":"2025-11-03T02:01:13.395724Z","shell.execute_reply.started":"2025-11-03T02:01:13.389502Z","shell.execute_reply":"2025-11-03T02:01:13.394874Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import DataLoader\n\nBATCH_SIZE = 4  # you can adjust depending on GPU\ntrain_dataset = ForgeryDataset(train_df)\nval_dataset = ForgeryDataset(val_df)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:01:27.158141Z","iopub.execute_input":"2025-11-03T02:01:27.158826Z","iopub.status.idle":"2025-11-03T02:01:27.163966Z","shell.execute_reply.started":"2025-11-03T02:01:27.158798Z","shell.execute_reply":"2025-11-03T02:01:27.162940Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\n\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n\nmodel = UNet(in_ch=3, out_ch=1).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = optim.Adam(model.parameters(), lr=1e-4)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:01:34.049601Z","iopub.execute_input":"2025-11-03T02:01:34.050335Z","iopub.status.idle":"2025-11-03T02:01:34.120539Z","shell.execute_reply.started":"2025-11-03T02:01:34.050310Z","shell.execute_reply":"2025-11-03T02:01:34.119956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_multi_ela(img_path, scales=[5, 10, 15]):\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:\n            return np.zeros((TARGET_SIZE, TARGET_SIZE, len(scales)+3), dtype=np.float32)\n    img_resized = cv2.resize(img, (TARGET_SIZE, TARGET_SIZE))\n    ela_channels = []\n    for scale in scales:\n        temp_path = f\"/tmp/temp_{scale}_{time.time()}.jpg\"\n        cv2.imwrite(temp_path, img_resized, [cv2.IMWRITE_JPEG_QUALITY, 95])\n        compressed_img = cv2.imread(temp_path)\n        if compressed_img is None: compressed_img = np.zeros_like(img_resized)\n        error = np.abs(img_resized.astype(np.float32) - compressed_img.astype(np.float32))\n        ela_feature = np.mean(error, axis=2) * scale\n        ela_channels.append(ela_feature)\n        os.remove(temp_path)\n    # Combine RGB + ELA channels\n    combined = np.concatenate([img_resized.astype(np.float32)/255.] + [c[...,None]/255. for c in ela_channels], axis=2)\n    return cv2.resize(combined, (TARGET_SIZE, TARGET_SIZE), interpolation=cv2.INTER_LINEAR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:02:01.447953Z","iopub.execute_input":"2025-11-03T02:02:01.448276Z","iopub.status.idle":"2025-11-03T02:02:01.456437Z","shell.execute_reply.started":"2025-11-03T02:02:01.448255Z","shell.execute_reply":"2025-11-03T02:02:01.455432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def post_process_mask(mask, threshold=0.45, min_area=64):\n    mask_bin = (mask > threshold).astype(np.uint8)\n    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_bin, connectivity=8)\n    final_mask = np.zeros_like(mask_bin)\n    for i in range(1, num_labels):\n        if stats[i, cv2.CC_STAT_AREA] >= min_area:\n            final_mask[labels == i] = 1\n    return final_mask\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:02:10.966863Z","iopub.execute_input":"2025-11-03T02:02:10.967344Z","iopub.status.idle":"2025-11-03T02:02:10.972315Z","shell.execute_reply.started":"2025-11-03T02:02:10.967321Z","shell.execute_reply":"2025-11-03T02:02:10.971446Z"}},"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-11-03T02:02:11.431313Z","iopub.execute_input":"2025-11-03T02:02:11.432240Z","iopub.status.idle":"2025-11-03T02:02:12.172227Z","shell.execute_reply.started":"2025-11-03T02:02:11.432203Z","shell.execute_reply":"2025-11-03T02:02:12.171026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class UNetEnhanced(nn.Module):\n    def __init__(self, in_channels=6, out_channels=1):\n        super().__init__()\n        self.enc1 = nn.Sequential(nn.Conv2d(in_channels, 64, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(64))\n        self.enc2 = nn.Sequential(nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(128))\n        self.pool = nn.MaxPool2d(2)\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        self.dec1 = nn.Sequential(nn.Conv2d(128, 64, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(64))\n        self.final = nn.Conv2d(64, out_channels, 1)\n    def forward(self, x):\n        e1 = self.enc1(x)\n        e2 = self.enc2(self.pool(e1))\n        d1 = self.up(e2)\n        d1 = self.dec1(d1 + e1)  # skip connection\n        return torch.sigmoid(self.final(d1))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:02:30.280789Z","iopub.execute_input":"2025-11-03T02:02:30.281318Z","iopub.status.idle":"2025-11-03T02:02:30.287222Z","shell.execute_reply.started":"2025-11-03T02:02:30.281295Z","shell.execute_reply":"2025-11-03T02:02:30.286409Z"}},"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-11-03T02:02:33.897626Z","iopub.execute_input":"2025-11-03T02:02:33.898369Z","iopub.status.idle":"2025-11-03T02:02:34.127871Z","shell.execute_reply.started":"2025-11-03T02:02:33.898341Z","shell.execute_reply":"2025-11-03T02:02:34.126652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cat submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T02:02:50.942534Z","iopub.execute_input":"2025-11-03T02:02:50.942801Z","iopub.status.idle":"2025-11-03T02:02:51.096646Z","shell.execute_reply.started":"2025-11-03T02:02:50.942785Z","shell.execute_reply":"2025-11-03T02:02:51.095750Z"}},"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-11-03T02:02:52.893625Z","iopub.execute_input":"2025-11-03T02:02:52.893954Z","iopub.status.idle":"2025-11-03T02:02:52.900614Z","shell.execute_reply.started":"2025-11-03T02:02:52.893930Z","shell.execute_reply":"2025-11-03T02:02:52.899752Z"}},"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-03T02:02:55.201332Z","iopub.execute_input":"2025-11-03T02:02:55.201951Z","iopub.status.idle":"2025-11-03T02:02:55.211484Z","shell.execute_reply.started":"2025-11-03T02:02:55.201926Z","shell.execute_reply":"2025-11-03T02:02:55.210747Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}