{"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":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:44:28.564231Z","iopub.execute_input":"2025-10-26T14:44:28.564509Z","iopub.status.idle":"2025-10-26T14:44:38.006957Z","shell.execute_reply.started":"2025-10-26T14:44:28.564487Z","shell.execute_reply":"2025-10-26T14:44:38.006149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# We force numpy to a version < 2.0 to fix compatibility issues\n!pip install -q \"numpy<2.0\"\n\n!pip install -q albumentations\n!pip install -q segmentation-models-pytorch\n!pip install -q tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:44:38.008209Z","iopub.execute_input":"2025-10-26T14:44:38.008779Z","iopub.status.idle":"2025-10-26T14:46:15.390576Z","shell.execute_reply.started":"2025-10-26T14:44:38.008757Z","shell.execute_reply":"2025-10-26T14:46:15.389546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset\nimport cv2\nimport numpy as np\nimport os\nimport matplotlib.pyplot as plt\n\n# --- 1. Define the Dataset Class ---\n\nclass ForgeryDataset(Dataset):\n    \"\"\"\n    A PyTorch Dataset class to load forged images and their .npy masks.\n    \"\"\"\n    def __init__(self, images_dir, masks_dir):\n        self.images_dir = images_dir\n        self.masks_dir = masks_dir\n        \n        # Get all the image filenames (e.g., '47513.png')\n        self.image_files = os.listdir(images_dir)\n\n    def __len__(self):\n        # This returns the total number of samples\n        return len(self.image_files)\n\n    def __getitem__(self, idx):\n        # This loads one sample (image + mask)\n        \n        # 1. Get the filename\n        image_filename = self.image_files[idx]\n        file_stem = image_filename.split('.')[0]\n        mask_filename = file_stem + \".npy\"\n\n        # 2. Define the full paths\n        img_path = os.path.join(self.images_dir, image_filename)\n        mask_path = os.path.join(self.masks_dir, mask_filename)\n\n        # 3. Load the image\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        # 4. Load the mask\n        mask = np.load(mask_path)\n        mask = np.squeeze(mask) # Squeeze from (1, H, W) to (H, W)\n        \n        # In deep learning, masks should be (Height, Width, Channels)\n        # We need to add a channel dimension at the end: (H, W) -> (H, W, 1)\n        mask = np.expand_dims(mask, axis=-1)\n\n        # --- IMPORTANT ---\n        # Normalize image values from [0, 255] to [0.0, 1.0]\n        img = img.astype(np.float32) / 255.0\n        # Normalize mask values from [0, 255] to [0.0, 1.0] (for segmentation)\n        mask = mask.astype(np.float32) / 255.0\n        \n        # Convert to PyTorch tensors\n        # (H, W, C) -> (C, H, W) as PyTorch expects\n        img_tensor = torch.from_numpy(img).permute(2, 0, 1)\n        mask_tensor = torch.from_numpy(mask).permute(2, 0, 1)\n\n        return img_tensor, mask_tensor\n\n# --- 2. Test the Dataset Class ---\n\n# Define your paths again\nBASE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nFORGED_IMG_PATH = os.path.join(BASE_PATH, \"train_images\", \"forged\")\nTRAIN_MASK_PATH = os.path.join(BASE_PATH, \"train_masks\")\n\n# Create an instance of your dataset\ntrain_dataset = ForgeryDataset(images_dir=FORGED_IMG_PATH, masks_dir=TRAIN_MASK_PATH)\n\n# Let's load the 5th item (index 4) from the dataset\nimg, mask = train_dataset[4]\n\nprint(f\"Total images in dataset: {len(train_dataset)}\")\nprint(f\"Loaded image tensor shape: {img.shape}\")  # Should be [3, H, W]\nprint(f\"Loaded mask tensor shape:  {mask.shape}\") # Should be [1, H, W]\n\n# --- 3. Visualize the Tensors ---\n# We need to convert them back to (H, W, C) for plotting\nimg_to_show = img.permute(1, 2, 0).numpy()\nmask_to_show = mask.permute(1, 2, 0).squeeze().numpy() # .squeeze() to remove the 1\n\nplt.figure(figsize=(15, 7))\nplt.subplot(1, 2, 1)\nplt.imshow(img_to_show)\nplt.title(\"Loaded Image (as Tensor)\")\nplt.axis('off')\n\nplt.subplot(1, 2, 2)\nplt.imshow(mask_to_show, cmap='gray')\nplt.title(\"Loaded Mask (as Tensor)\")\nplt.axis('off')\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:46:15.391756Z","iopub.execute_input":"2025-10-26T14:46:15.392043Z","iopub.status.idle":"2025-10-26T14:46:21.799456Z","shell.execute_reply.started":"2025-10-26T14:46:15.392022Z","shell.execute_reply":"2025-10-26T14:46:21.798536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport matplotlib.pyplot as plt\n\n# 1. Define your data paths\nBASE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nFORGED_IMG_PATH = os.path.join(BASE_PATH, \"train_images\", \"forged\")\nTRAIN_MASK_PATH = os.path.join(BASE_PATH, \"train_masks\")\n\n# 2. Get a list of all FORGED images\nimage_files = os.listdir(FORGED_IMG_PATH)\nprint(f\"Found {len(image_files)} forged images.\")\nprint(\"---\")\n\n# Let's try a different image (e.g., the 10th one)\nimage_to_check = image_files[10] \nprint(f\"Attempting to load image: {image_to_check}\")\n\n# 3. Define the full paths\nimg_path = os.path.join(FORGED_IMG_PATH, image_to_check)\nmask_path = os.path.join(TRAIN_MASK_PATH, image_to_check)\n\n# --- DEBUGGING CHECK ---\n# Let's check if the mask file path we built is correct.\nif not os.path.exists(mask_path):\n    print(f\"ERROR: The assumed mask path does NOT exist:\")\n    print(f\"  {mask_path}\")\n    print(\"\\nThis means the mask filenames do not match the image filenames.\")\n    \n    # Let's see what the mask filenames actually look like:\n    print(\"\\n--- Listing first 5 files in train_masks: ---\")\n    mask_files_list = os.listdir(TRAIN_MASK_PATH)\n    print(mask_files_list[:5])\n    print(\"-------------------------------------------------\")\n    print(\"Compare the list above to the image name. You may need to change the filename.\")\n\nelse:\n    # 4. Load the image and its mask\n    print(\"SUCCESS: Found matching mask file!\")\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n\n    mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)\n\n    if mask is None:\n        print(f\"ERROR: Mask file at {mask_path} exists, but cv2.imread failed. File may be corrupt.\")\n    else:\n        # 5. Display them side-by-side\n        plt.figure(figsize=(15, 7))\n\n        plt.subplot(1, 2, 1)\n        plt.imshow(img)\n        plt.title(f\"Original Forged Image\\n({image_to_check})\")\n        plt.axis('off')\n\n        plt.subplot(1, 2, 2)\n        plt.imshow(mask, cmap='gray')\n        plt.title(f\"Forgery Mask\\n({image_to_check})\")\n        plt.axis('off')\n\n        plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:46:21.801104Z","iopub.execute_input":"2025-10-26T14:46:21.801694Z","iopub.status.idle":"2025-10-26T14:46:21.815697Z","shell.execute_reply.started":"2025-10-26T14:46:21.801671Z","shell.execute_reply":"2025-10-26T14:46:21.815004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# 1. Define your data paths\nBASE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nFORGED_IMG_PATH = os.path.join(BASE_PATH, \"train_images\", \"forged\")\nTRAIN_MASK_PATH = os.path.join(BASE_PATH, \"train_masks\")\n\n# 2. Get a list of all FORGED images\nimage_files = os.listdir(FORGED_IMG_PATH)\nprint(f\"Found {len(image_files)} forged images.\")\nprint(\"---\")\n\n# Let's pick an image to check (e.g., the 10th one)\nimage_filename = image_files[10] # This will be like '47513.png'\n\n# 3. Build the correct .npy filename\nfile_stem = image_filename.split('.')[0]\nmask_filename = file_stem + \".npy\"\n\nprint(f\"Loading image: {image_filename}\")\nprint(f\"Loading mask:  {mask_filename}\")\n\n# 4. Define the full paths\nimg_path = os.path.join(FORGED_IMG_PATH, image_filename)\nmask_path = os.path.join(TRAIN_MASK_PATH, mask_filename)\n\n# 5. Load the image and its mask\n# Load the image with cv2\nimg = cv2.imread(img_path)\nimg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n\n# Load the .npy file\nmask = np.load(mask_path)\nprint(f\"Original mask shape: {mask.shape}\")\n\n# --- THIS IS THE FIX ---\n# Squeeze the extra '1' dimension out\nmask = np.squeeze(mask)\nprint(f\"Squeezed mask shape: {mask.shape}\") # This will now be (68, 875)\n# ----------------------\n\n# 6. Display them side-by-side\nplt.figure(figsize=(15, 7))\n\n# Subplot for the Original Image\nplt.subplot(1, 2, 1)\nplt.imshow(img)\nplt.title(f\"Original Forged Image\\n({image_filename})\")\nplt.axis('off')\n\n# Subplot for the Forgery Mask\nplt.subplot(1, 2, 2)\nplt.imshow(mask, cmap='gray') # This will now work\nplt.title(f\"Forgery Mask (The 'Answer')\\n({mask_filename})\")\nplt.axis('off')\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:46:21.816545Z","iopub.execute_input":"2025-10-26T14:46:21.816784Z","iopub.status.idle":"2025-10-26T14:46:22.075508Z","shell.execute_reply.started":"2025-10-26T14:46:21.816768Z","shell.execute_reply":"2025-10-26T14:46:22.074679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 1. Install required libraries ---\n!pip install -q albumentations\n!pip install -q segmentation-models-pytorch\n!pip install -q tqdm  # For a progress bar\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, random_split\nimport cv2\nimport numpy as np\nimport os\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport segmentation_models_pytorch as smp\nfrom tqdm import tqdm # Import tqdm\n\n# --- 2. Update the Dataset Class (The Fix is Here) ---\nclass ForgeryDataset(Dataset):\n    def __init__(self, images_dir, masks_dir, augmentations=None):\n        self.images_dir = images_dir\n        self.masks_dir = masks_dir\n        self.augmentations = augmentations\n        \n        # --- DATA INTEGRITY CHECK ---\n        # We will check all files and only keep the ones where\n        # image and mask shapes match.\n        print(\"Running data integrity check...\")\n        all_image_files = os.listdir(images_dir)\n        self.image_files = [] # This will be our \"clean\" list\n        \n        for image_filename in tqdm(all_image_files): # Use tqdm for a progress bar\n            try:\n                # Get paths\n                file_stem = image_filename.split('.')[0]\n                mask_filename = file_stem + \".npy\"\n                img_path = os.path.join(self.images_dir, image_filename)\n                mask_path = os.path.join(self.masks_dir, mask_filename)\n\n                # Check if mask file exists\n                if not os.path.exists(mask_path):\n                    continue # Skip if no matching mask\n\n                # Load shapes\n                img = cv2.imread(img_path)\n                img_shape = img.shape[:2] # (H, W)\n                \n                mask = np.load(mask_path)\n                mask_shape = np.squeeze(mask).shape # (H, W)\n                \n                # Check for match\n                if img_shape == mask_shape:\n                    self.image_files.append(image_filename) # Add to our clean list\n            except Exception as e:\n                # Catch any other loading errors (like corrupt files)\n                print(f\"\\nWarning: Skipping {image_filename} due to error: {e}\")\n        \n        print(f\"Integrity check complete. Found {len(self.image_files)} valid image/mask pairs.\")\n\n    def __len__(self):\n        # This will now return the count of *clean* files\n        return len(self.image_files)\n\n    def __getitem__(self, idx):\n        # 1. Get filenames (from our clean list)\n        image_filename = self.image_files[idx]\n        file_stem = image_filename.split('.')[0]\n        mask_filename = file_stem + \".npy\"\n\n        # 2. Define paths\n        img_path = os.path.join(self.images_dir, image_filename)\n        mask_path = os.path.join(self.masks_dir, mask_filename)\n\n        # 3. Load image and mask (as NumPy arrays)\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        mask = np.load(mask_path)\n        mask = np.squeeze(mask) # Squeeze from (1, H, W) to (H, W)\n        \n        # 4. Apply Augmentations\n        if self.augmentations:\n            augmented = self.augmentations(image=img, mask=mask)\n            img = augmented['image']\n            mask = augmented['mask']\n        \n        # 5. Process Mask (using PyTorch operations)\n        mask = mask.to(torch.float32) / 255.0\n        mask = mask.unsqueeze(0)\n        \n        return img, mask\n\n# --- 3. Define Augmentations ---\nIMG_SIZE = 384\nBATCH_SIZE = 8 \n\ntrain_augs = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.HorizontalFlip(p=0.5),\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2() \n])\n\nval_augs = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\n# --- 4. Create and Split the Datasets ---\nBASE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nFORGED_IMG_PATH = os.path.join(BASE_PATH, \"train_images\", \"forged\")\nTRAIN_MASK_PATH = os.path.join(BASE_PATH, \"train_masks\")\n\n# This will now run the integrity check\nfull_dataset = ForgeryDataset(FORGED_IMG_PATH, TRAIN_MASK_PATH)\n\n# Split it into 90% training, 10% validation\ntrain_size = int(0.9 * len(full_dataset))\nval_size = len(full_dataset) - train_size\ntrain_data, val_data = random_split(full_dataset, [train_size, val_size])\n\n# Apply the different augmentations to the two splits\ntrain_data.dataset.augmentations = train_augs\nval_data.dataset.augmentations = val_augs\n\n# --- 5. Create DataLoaders ---\ntrain_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_data, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nprint(f\"Total valid images: {len(full_dataset)}\")\nprint(f\"Training images: {len(train_data)}\")\nprint(f\"Validation images: {len(val_data)}\")\nprint(f\"Batch size: {BATCH_SIZE}\")\n\n# --- 6. Test the DataLoader ---\n# This should now work perfectly!\nimages, masks = next(iter(train_loader))\n\nprint(\"\\n--- Test Batch Shapes ---\")\nprint(f\"Images batch shape: {images.shape}\")\nprint(f\"Masks batch shape:  {masks.shape}\")\n\nprint(\"\\nData pipeline is 100% complete and ready for training! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:46:22.076368Z","iopub.execute_input":"2025-10-26T14:46:22.076626Z","iopub.status.idle":"2025-10-26T14:49:22.194630Z","shell.execute_reply.started":"2025-10-26T14:46:22.076600Z","shell.execute_reply":"2025-10-26T14:49:22.193785Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch  # <-- THIS IS THE FIX\nimport segmentation_models_pytorch as smp\nimport torch.nn as nn\nimport torch.optim as optim\nfrom tqdm import tqdm\n\n# --- 1. Define Model, Loss, and Optimizer ---\n\n# Set the device to GPU if available\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Using device: {DEVICE}\")\n\n# Our \"Winning Edge\" Model\n# U-Net with a pre-trained EfficientNet-B4 backbone\nmodel = smp.Unet(\n    encoder_name=\"efficientnet-b4\",\n    encoder_weights=\"imagenet\",\n    in_channels=3,\n    classes=1, # We output 1 channel (the mask)\n).to(DEVICE)\n\n# Our \"Winning Edge\" Loss Function\n# A combo of BCE (pixel-level) and Dice (blob-level)\nclass ComboLoss(nn.Module):\n    def __init__(self, bce_weight=0.5, dice_weight=0.5):\n        super().__init__()\n        self.bce = nn.BCEWithLogitsLoss()\n        self.dice = smp.losses.DiceLoss(mode='binary')\n        self.bce_weight = bce_weight\n        self.dice_weight = dice_weight\n\n    def forward(self, inputs, targets):\n        bce_loss = self.bce(inputs, targets)\n        dice_loss = self.dice(inputs, targets)\n        return (self.bce_weight * bce_loss) + (self.dice_weight * dice_loss)\n\nloss_fn = ComboLoss().to(DEVICE)\noptimizer = optim.AdamW(model.parameters(), lr=1e-4) # AdamW is a great default\nscaler = torch.amp.GradScaler('cuda') # Use the updated torch.amp syntax\n\n# --- 2. Define the Training & Validation Functions ---\n\ndef train_fn(loader, model, optimizer, loss_fn, scaler):\n    \"\"\" Trains the model for one epoch. \"\"\"\n    model.train() # Set the model to training mode\n    loop = tqdm(loader, desc=\"Training\")\n    \n    for images, masks in loop:\n        # Move data to the GPU\n        images = images.to(DEVICE, dtype=torch.float32)\n        masks = masks.to(DEVICE, dtype=torch.float32)\n        \n        # Forward pass with mixed precision\n        with torch.amp.autocast('cuda'):\n            predictions = model(images)\n            loss = loss_fn(predictions, masks)\n            \n        # Backward pass\n        optimizer.zero_grad()\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        loop.set_postfix(loss=loss.item())\n\ndef val_fn(loader, model, loss_fn):\n    \"\"\" Validates the model. \"\"\"\n    model.eval() # Set the model to evaluation mode\n    loop = tqdm(loader, desc=\"Validation\")\n    val_loss = 0\n    \n    with torch.no_grad(): # No gradients needed for validation\n        for images, masks in loop:\n            images = images.to(DEVICE, dtype=torch.float32)\n            masks = masks.to(DEVICE, dtype=torch.float32)\n            \n            with torch.amp.autocast('cuda'):\n                predictions = model(images)\n                loss = loss_fn(predictions, masks)\n                \n            val_loss += loss.item()\n            loop.set_postfix(loss=loss.item())\n            \n    avg_loss = val_loss / len(loader)\n    print(f\"Average Validation Loss: {avg_loss:.4f}\")\n    return avg_loss\n\n# --- 3. The Training Loop ---\n\nNUM_EPOCHS = 5 # Start with 5 epochs to see how it goes\nbest_val_loss = float(\"inf\")\n\nfor epoch in range(NUM_EPOCHS):\n    print(f\"\\n--- Epoch {epoch+1}/{NUM_EPOCHS} ---\")\n    \n    train_fn(train_loader, model, optimizer, loss_fn, scaler)\n    val_loss = val_fn(val_loader, model, loss_fn)\n    \n    # Save the model if it's the best one so far\n    if val_loss < best_val_loss:\n        print(f\"Validation loss improved! Saving model...\")\n        best_val_loss = val_loss\n        torch.save(model.state_dict(), \"best_model.pth\")\n\nprint(\"\\n--- Training Complete ---\")\nprint(f\"Best Validation Loss: {best_val_loss:.4f}\")\nprint(\"Model saved to 'best_model.pth'\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:49:22.195794Z","iopub.execute_input":"2025-10-26T14:49:22.196122Z","iopub.status.idle":"2025-10-26T14:54:12.016626Z","shell.execute_reply.started":"2025-10-26T14:49:22.196092Z","shell.execute_reply":"2025-10-26T14:54:12.015890Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport json\nimport numba\nimport numpy.typing as npt\nimport torchvision.transforms.functional as F # This is for transforms, NOT interpolate\nimport torch.nn.functional as F_nn           # <-- THIS IS THE FIX (Import)\nfrom torch.utils.data import Dataset, DataLoader\n\n# --- 1. Copy the RLE Encoding Functions ---\n@numba.jit(nopython=True)\ndef _rle_encode_jit(x: npt.NDArray, fg_val: int = 1) -> list[int]:\n    \"\"\"Numba-jitted RLE encoder.\"\"\"\n    dots = np.where(x.T.flatten() == fg_val)[0]\n    run_lengths = []\n    prev = -2\n    for b in dots:\n        if b > prev + 1:\n            run_lengths.extend((b + 1, 0))\n        run_lengths[-1] += 1\n        prev = b\n    return run_lengths\n\ndef rle_encode(masks: list[npt.NDArray], fg_val: int = 1) -> str:\n    \"\"\"\n    Encodes a list of masks into a single RLE string.\n    We pass a list with one mask: [my_mask]\n    \"\"\"\n    return ';'.join([json.dumps(_rle_encode_jit(x, fg_val)) for x in masks])\n\n# --- 2. Create the Test Dataset ---\nclass TestDataset(Dataset):\n    def __init__(self, images_dir, augmentations):\n        self.images_dir = images_dir\n        self.image_files = os.listdir(images_dir)\n        self.augmentations = augmentations\n\n    def __len__(self):\n        return len(self.image_files)\n\n    def __getitem__(self, idx):\n        image_filename = self.image_files[idx]\n        image_id = image_filename.split('.')[0]\n        \n        img_path = os.path.join(self.images_dir, image_filename)\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        original_shape = img.shape[:2] # (H, W) - This is a tuple\n        \n        augmented = self.augmentations(image=img)\n        img_tensor = augmented['image']\n        \n        # Return the original shape as-is\n        return img_tensor, image_id, original_shape\n\n# --- 3. Define Inference Transforms ---\nTEST_IMG_SIZE = 384\ntest_augs = A.Compose([\n    A.Resize(TEST_IMG_SIZE, TEST_IMG_SIZE),\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\n# --- 4. Create Test DataLoader ---\n# Re-using BASE_PATH, BATCH_SIZE, and DEVICE from the previous cell\nTEST_IMG_PATH = os.path.join(BASE_PATH, \"test_images\")\ntest_dataset = TestDataset(TEST_IMG_PATH, test_augs)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\n# --- 5. Load Best Model ---\nprint(\"Loading best model from 'best_model.pth'...\")\nmodel = smp.Unet(\n    encoder_name=\"efficientnet-b4\",\n    encoder_weights=None,\n    in_channels=3,\n    classes=1,\n).to(DEVICE)\n\nmodel.load_state_dict(torch.load(\"best_model.pth\"))\nmodel.eval() \n\n# --- 6. The \"Winning Edge\" Post-Processing Rules ---\nPREDICTION_THRESHOLD = 0.5\nMIN_MASK_AREA = 100         \n\n# --- 7. Generate Predictions ---\nsubmission = [] \n\nwith torch.no_grad():\n    loop = tqdm(test_loader, desc=\"Generating Predictions\")\n    for img_tensor, image_ids, original_shapes in loop:\n        \n        img_tensor = img_tensor.to(DEVICE, dtype=torch.float32)\n        \n        with torch.amp.autocast('cuda'):\n            pred_masks_small = model(img_tensor)\n            \n        for i in range(pred_masks_small.shape[0]):\n            image_id = image_ids[i]\n            \n            original_h = original_shapes[0][i]\n            original_w = original_shapes[1][i]\n            \n            # --- THIS IS THE FIX (Call) ---\n            pred_mask = F_nn.interpolate(\n                pred_masks_small[i].unsqueeze(0), \n                size=(original_h.item(), original_w.item()),\n                mode='bilinear', \n                align_corners=False\n            ).squeeze()\n            \n            pred_mask = (torch.sigmoid(pred_mask) > PREDICTION_THRESHOLD).cpu().numpy().astype(np.uint8)\n\n            contours, _ = cv2.findContours(pred_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n            final_mask = np.zeros(pred_mask.shape, dtype=np.uint8)\n            for cnt in contours:\n                if cv2.contourArea(cnt) > MIN_MASK_AREA:\n                    cv2.drawContours(final_mask, [cnt], -1, 1, -1)\n            \n            if np.sum(final_mask) == 0:\n                prediction_string = \"authentic\"\n            else:\n                prediction_string = rle_encode([final_mask])\n                \n            submission.append({\n                'case_id': image_id,\n                'annotation': prediction_string\n            })\n\n# --- 8. Create and Save Submission File ---\nprint(\"\\nSaving submission file...\")\nsubmission_df = pd.DataFrame(submission)\nsubmission_df.to_csv(\"submission.csv\", index=False)\n\nprint(\"Done! 🎉 Your submission.csv is ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T14:59:15.251433Z","iopub.execute_input":"2025-10-26T14:59:15.252299Z","iopub.status.idle":"2025-10-26T14:59:16.057126Z","shell.execute_reply.started":"2025-10-26T14:59:15.252269Z","shell.execute_reply":"2025-10-26T14:59:16.056108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}