{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Setup \n- Thiis fixes an annoying numpy versioning bug later on in the notebook.","metadata":{}},{"cell_type":"code","source":"import os\n\n# 1. Force install exactly what we want\n!pip install --upgrade \"numpy<2.0\" \"scikit-learn<1.6.0\" --prefer-binary\n\n# 2. Kill the process to force a clean reload of the library into RAM\nprint(\"\\nEnvironment updated. RESTARTING RUNTIME NOW...\")\nos._exit(00)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:28:20.665292Z","iopub.execute_input":"2026-01-06T11:28:20.665622Z","execution_failed":"2026-01-06T11:28:26.623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install torch torchvision segmentation-models-pytorch albumentations","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:29:06.851538Z","iopub.execute_input":"2026-01-06T11:29:06.851864Z","iopub.status.idle":"2026-01-06T11:29:13.363356Z","shell.execute_reply.started":"2026-01-06T11:29:06.851839Z","shell.execute_reply":"2026-01-06T11:29:13.362467Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install --upgrade \"numpy<2.0\"\nimport numpy as np\nprint(f\"Success! Current version: {np.__version__}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:29:38.762530Z","iopub.execute_input":"2026-01-06T11:29:38.763103Z","iopub.status.idle":"2026-01-06T11:29:44.752398Z","shell.execute_reply.started":"2026-01-06T11:29:38.763073Z","shell.execute_reply":"2026-01-06T11:29:44.751513Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## We want 1.26.4","metadata":{}},{"cell_type":"code","source":"import warnings\n# Mute those annoying Pydantic metadata warnings\nwarnings.filterwarnings(\"ignore\", category=UserWarning, module=\"pydantic\")\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader, Dataset\nimport segmentation_models_pytorch as smp\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport cv2\nimport os\nimport numpy as np\n\nprint(f\"Verified NumPy: {np.__version__}\")\nprint(\"All systems go. No more version conflicts!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:29:49.115594Z","iopub.execute_input":"2026-01-06T11:29:49.115905Z","iopub.status.idle":"2026-01-06T11:29:57.930950Z","shell.execute_reply.started":"2026-01-06T11:29:49.115877Z","shell.execute_reply":"2026-01-06T11:29:57.930322Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Define the 'Forgery Dataset' with known images","metadata":{}},{"cell_type":"code","source":"class ForgeryDataset(Dataset):\n    def __init__(self, image_dir, mask_dir, transform=None):\n        self.image_dir = image_dir\n        self.mask_dir = mask_dir\n        # ensure we are matching the file names correctly\n        self.images = sorted(os.listdir(image_dir))\n        self.masks = sorted(os.listdir(mask_dir))\n        self.transform = transform\n\n    def __get_images(self):\n        return self.images\n    \n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img_path = os.path.join(self.image_dir, self.images[idx])\n        mask_path = os.path.join(self.mask_dir, self.masks[idx])\n        \n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        h, w, _ = image.shape # Get image dimensions\n\n        mask = np.load(mask_path)\n        \n        # fix channel dimensions (standardize to 2D)\n        if mask.ndim == 3:\n            mask = mask[:, :, 0]\n        \n        # force mask to match image dimensions\n        # if the mask is 512x512 and image is 1080x1920, this aligns them.\n        if mask.shape[0] != h or mask.shape[1] != w:\n            mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)\n\n        mask = mask.astype(np.uint8)\n\n        # Now Albumentations will be happy because shapes match perfectly\n        if self.transform:\n            augmented = self.transform(image=image, mask=mask)\n            image = augmented['image']\n            mask = augmented['mask']\n\n        mask = mask.float()\n        if mask.ndim == 2:\n            mask = mask.unsqueeze(0)\n            \n        if mask.max() > 1.0:\n            mask = mask / 255.0\n            \n        return image, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:01.458576Z","iopub.execute_input":"2026-01-06T11:30:01.459395Z","iopub.status.idle":"2026-01-06T11:30:01.467831Z","shell.execute_reply.started":"2026-01-06T11:30:01.459361Z","shell.execute_reply":"2026-01-06T11:30:01.466971Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## GPU acceleration","metadata":{}},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:11.073406Z","iopub.execute_input":"2026-01-06T11:30:11.073735Z","iopub.status.idle":"2026-01-06T11:30:11.164412Z","shell.execute_reply.started":"2026-01-06T11:30:11.073707Z","shell.execute_reply":"2026-01-06T11:30:11.163631Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Hyperparameters, potentially for sweeping later\n- Keeping learning rate at 1e-4, increasing epochs though","metadata":{"execution":{"iopub.status.busy":"2026-01-06T07:05:08.505896Z","iopub.execute_input":"2026-01-06T07:05:08.506827Z","iopub.status.idle":"2026-01-06T07:05:08.563121Z","shell.execute_reply.started":"2026-01-06T07:05:08.506799Z","shell.execute_reply":"2026-01-06T07:05:08.562544Z"}}},{"cell_type":"code","source":"learning_rate = 1e-4\nbatch_size = 8\nnum_epochs = 10\n\n# Updated transform using the new class name\ntrain_transform = A.Compose([\n    A.Resize(256, 256),\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])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:14.870423Z","iopub.execute_input":"2026-01-06T11:30:14.871202Z","iopub.status.idle":"2026-01-06T11:30:14.878031Z","shell.execute_reply.started":"2026-01-06T11:30:14.871170Z","shell.execute_reply":"2026-01-06T11:30:14.877209Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Initialize Model, loss, and optimizer\n- Using U-Net with a ResNet34 backbone","metadata":{}},{"cell_type":"code","source":"model = smp.Unet(\n    encoder_name=\"resnet34\",        \n    encoder_weights=\"imagenet\",     \n    in_channels=3,                  \n    classes=1, # Binary (forgery vs. authentic)\n).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:44.201323Z","iopub.execute_input":"2026-01-06T11:30:44.201961Z","iopub.status.idle":"2026-01-06T11:30:46.258756Z","shell.execute_reply.started":"2026-01-06T11:30:44.201933Z","shell.execute_reply":"2026-01-06T11:30:46.258134Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\"Dice loss quantifies the similarity between two sets. It is particularly useful in scenarios where the classes are imbalanced, such as in medical image segmentation\" - *that seems lucky*\n- Focal loss added afterwards ","metadata":{}},{"cell_type":"code","source":"import segmentation_models_pytorch as smp\n\n# Initialize the 'tools' individually\ndice_loss_fn = smp.losses.DiceLoss(mode='binary')\nfocal_loss_fn = smp.losses.FocalLoss(mode='binary')\noptimizer = optim.Adam(model.parameters(), lr=learning_rate)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:48.649308Z","iopub.execute_input":"2026-01-06T11:30:48.650078Z","iopub.status.idle":"2026-01-06T11:30:48.654975Z","shell.execute_reply.started":"2026-01-06T11:30:48.650045Z","shell.execute_reply":"2026-01-06T11:30:48.654213Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_fn(loader, model, optimizer, dice_loss_fn, focal_loss_fn, device):\n    model.train()\n    total_epoch_loss = 0\n    \n    for batch_idx, (data, targets) in enumerate(loader):\n        # 1. Move to GPU\n        data = data.to(device)\n        targets = targets.to(device)\n\n        # 2. Forward pass\n        # SMP models usually output (Batch, 1, H, W). We need to ensure targets match.\n        predictions = model(data)\n        \n        # 3. Calculate Hybrid Loss\n        # We calculate both and sum them. No '+' on the classes themselves!\n        l1 = dice_loss_fn(predictions, targets)\n        l2 = focal_loss_fn(predictions, targets)\n        loss = l1 + l2\n\n        # 4. Backward pass\n        optimizer.zero_grad() # Clean old gradients\n        loss.backward()       # Compute new gradients\n        optimizer.step()      # Update weights\n        \n        total_epoch_loss += loss.item()\n\n    return total_epoch_loss / len(loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:50.946742Z","iopub.execute_input":"2026-01-06T11:30:50.947513Z","iopub.status.idle":"2026-01-06T11:30:50.952467Z","shell.execute_reply.started":"2026-01-06T11:30:50.947483Z","shell.execute_reply":"2026-01-06T11:30:50.951790Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Helper function called during training","metadata":{}},{"cell_type":"code","source":"import os\ncwd = os.getcwd()\nprint(cwd)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:54.660953Z","iopub.execute_input":"2026-01-06T11:30:54.661493Z","iopub.status.idle":"2026-01-06T11:30:54.665632Z","shell.execute_reply.started":"2026-01-06T11:30:54.661463Z","shell.execute_reply":"2026-01-06T11:30:54.664763Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define the dataset and the data loader\n- changes to __get_item__ due to squeezing issues and mask confusion","metadata":{}},{"cell_type":"code","source":"train_ds = ForgeryDataset(\n    image_dir=\"../input/recodai-luc-scientific-image-forgery-detection/train_images/forged\", \n    mask_dir=\"../input/recodai-luc-scientific-image-forgery-detection/train_masks\", \n    transform=train_transform)\n\ntrain_loader = DataLoader(\n    train_ds, \n    batch_size=batch_size, \n    shuffle=True, \n    num_workers=4, # (2?) Performance improvement attempt\n    pin_memory=True # Speed up transfer from CPU to GPU\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:30:57.746333Z","iopub.execute_input":"2026-01-06T11:30:57.746763Z","iopub.status.idle":"2026-01-06T11:30:57.819012Z","shell.execute_reply.started":"2026-01-06T11:30:57.746733Z","shell.execute_reply":"2026-01-06T11:30:57.818461Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Check we actually loaded images\n*(directory structure confrmation)*","metadata":{}},{"cell_type":"code","source":"#for filename in train_ds.images:\n#    print(filename)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Looks good, we're loading the images into the dataset\n- all the images we expect got listed ","metadata":{}},{"cell_type":"markdown","source":"## Dataset and Loader defined, time to iterate\n- train_fn -> One iteration","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm \n\ndef train_fn(loader, model, optimizer, dice_loss_fn, focal_loss_fn, device):\n    model.train()\n    total_loss = 0\n    \n    for batch_idx, (data, targets) in enumerate(loader):\n        data = data.to(device)\n        targets = targets.to(device)\n\n        # Forward\n        predictions = model(data)\n        \n        # Calculate Hybrid Loss\n        loss_dice = dice_loss_fn(predictions, targets)\n        loss_focal = focal_loss_fn(predictions, targets)\n        \n        # Combining them 50/50 for a balanced start\n        combined_loss = loss_dice + loss_focal\n\n        # Backward\n        optimizer.zero_grad()\n        combined_loss.backward()\n        optimizer.step()\n        \n        total_loss += combined_loss.item()\n        \n    return total_loss / len(loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:31:02.945439Z","iopub.execute_input":"2026-01-06T11:31:02.946157Z","iopub.status.idle":"2026-01-06T11:31:02.950952Z","shell.execute_reply.started":"2026-01-06T11:31:02.946126Z","shell.execute_reply":"2026-01-06T11:31:02.950049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:31:08.753073Z","iopub.execute_input":"2026-01-06T11:31:08.753380Z","iopub.status.idle":"2026-01-06T11:31:08.762687Z","shell.execute_reply.started":"2026-01-06T11:31:08.753353Z","shell.execute_reply":"2026-01-06T11:31:08.762048Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**15 iterations defined.**","metadata":{}},{"cell_type":"code","source":"epochs = 5\n\nprint(f\"Starting training on {device}...\")\n\nincrement = 70\nfor epoch in range(epochs):\n    print(f\"--- Epoch {epoch+1}/{epochs} ---\")\n    avg_loss = train_fn(train_loader, model, optimizer, dice_loss_fn, focal_loss_fn, device)\n    print(f\"Average Loss for Epoch: {avg_loss:.4f} - {model}\")\n    # Save each after each run\n    torch.save(model.state_dict(), f\"model{increment}.pth\")\n    increment += 1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:31:18.100716Z","iopub.execute_input":"2026-01-06T11:31:18.101441Z","iopub.status.idle":"2026-01-06T11:35:48.906762Z","shell.execute_reply.started":"2026-01-06T11:31:18.101388Z","shell.execute_reply":"2026-01-06T11:35:48.905872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Mini-version of the F1 score evaluation logic\nmodel70 - 0.6747\n\n","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import f1_score\n\ndef evaluate_model(model_path, loader):\n    model.load_state_dict(torch.load(model_path))\n    model.eval()\n\n    # True positives, false positives, false negatives\n    tp, fp, fn = 0, 0, 0 \n    \n    with torch.no_grad():\n        for data, targets in loader:\n            data, targets = data.to(device), targets.to(device)\n            output = model(data)\n            preds = (torch.sigmoid(output) > 0.5).float()\n\n            # Calculate components for F1\n            tp += (preds * targets).sum().item()\n            fp += (preds * (1 - targets)).sum().item()\n            fn += ((1 - preds) * targets).sum().item()\n\n    # F1 Formula: 2*TP / (2*TP + FP + FN)\n    precision = tp / (tp + fp + 1e-7)\n    recall = tp / (tp + fn + 1e-7)\n    f1 = 2 * (precision * recall) / (precision + recall + 1e-7)\n    \n    return f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:39:15.268207Z","iopub.execute_input":"2026-01-06T11:39:15.268816Z","iopub.status.idle":"2026-01-06T11:39:15.274789Z","shell.execute_reply.started":"2026-01-06T11:39:15.268785Z","shell.execute_reply":"2026-01-06T11:39:15.274131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"evaluate_model('model70.pth', test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:52:27.257248Z","iopub.execute_input":"2026-01-06T11:52:27.258061Z","iopub.status.idle":"2026-01-06T11:52:27.435566Z","shell.execute_reply.started":"2026-01-06T11:52:27.258029Z","shell.execute_reply":"2026-01-06T11:52:27.434727Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## We should have at least 64 distinct models - evaluate each for F1 score\n- model 70 or 42 seems to have performed the best ","metadata":{}},{"cell_type":"code","source":"f1results = []\n\n# Get all .pth files in current directory\nmodel_files = [f for f in os.listdir('.') if f.endswith('.pth')]\n\nfor model_path in model_files:\n    print(f\"Evaluating: {model_path}\")\n    score = evaluate_model(model_path, train_loader)\n    \n    # Store as (score, path) tuple so we can sort by score and associate to the model \n    f1results.append((score, model_path))\n\n# Sort results: descending (highest F1 score first)\nf1results.sort(key=lambda x: x[0], reverse=True)\n\nprint(\"\\n--- Model Leaderboard ---\")\nfor score, path in f1results:\n    print(f\"{score:.4f} : {path}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model 24 *was* the best but the F1 score was only 0.22\n## Adding focal got us to >0.71\n- This could have been optimized to cease evaluating once it found a peak evaluation.","metadata":{}},{"cell_type":"code","source":"model_files = ['model65.pth']\n\nfor model_path in model_files:\n    print(f\"Evaluating: {model_path}\")\n    score = evaluate_model(model_path, train_loader)\n    print(score)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:42:11.368835Z","iopub.execute_input":"2026-01-06T11:42:11.369126Z","iopub.status.idle":"2026-01-06T11:42:55.899875Z","shell.execute_reply.started":"2026-01-06T11:42:11.369100Z","shell.execute_reply":"2026-01-06T11:42:55.898909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef visualize_prediction(model_path, dataset, index=0):\n    # Load the best model\n    model.load_state_dict(torch.load(model_path))\n    model.eval()\n    \n    # Get a sample\n    image_tensor, mask_tensor = dataset[index]\n    \n    with torch.no_grad():\n        # Add batch dimension and move to device\n        input_data = image_tensor.unsqueeze(0).to(device)\n        output = model(input_data)\n        prediction = (torch.sigmoid(output) > 0.5).float().cpu().squeeze()\n\n    # Convert image back to displayable format\n    # Un-normalize (using ImageNet stats used in transform)\n    inv_normalize = A.Normalize(\n        mean=[-0.485/0.229, -0.456/0.224, -0.406/0.225],\n        std=[1/0.229, 1/0.224, 1/0.225],\n        max_pixel_value=1.0\n    )\n    img = image_tensor.permute(1, 2, 0).cpu().numpy()\n    \n    # Plotting\n    plt.figure(figsize=(12, 4))\n    plt.subplot(1, 3, 1)\n    plt.title(\"Original Image\")\n    plt.imshow(img)\n    \n    plt.subplot(1, 3, 2)\n    plt.title(\"Ground Truth (NPY)\")\n    plt.imshow(mask_tensor.squeeze(), cmap='gray')\n    \n    plt.subplot(1, 3, 3)\n    plt.title(\"Model Prediction\")\n    plt.imshow(prediction, cmap='jet') # Jet shows confidence/heat\n    plt.show()\n\n# Run it for your top model\nvisualize_prediction(\"model65.pth\", train_ds, index=10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:42:55.901431Z","iopub.execute_input":"2026-01-06T11:42:55.901747Z","iopub.status.idle":"2026-01-06T11:42:56.603383Z","shell.execute_reply.started":"2026-01-06T11:42:55.901711Z","shell.execute_reply":"2026-01-06T11:42:56.602613Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"- Test forgery finding","metadata":{}},{"cell_type":"code","source":"for i in range(len(train_ds)):\n    _, mask = train_ds[i]\n    if mask.sum() > 0:\n        print(f\"Found a forgery at index: {i}\")\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:44:57.547841Z","iopub.execute_input":"2026-01-06T11:44:57.548326Z","iopub.status.idle":"2026-01-06T11:44:58.463746Z","shell.execute_reply.started":"2026-01-06T11:44:57.548297Z","shell.execute_reply":"2026-01-06T11:44:58.462804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\ndef rle_encode(mask):\n    \"\"\"\n    Converts a binary mask (0/1) into an RLE string or 'authentic'.\n    \"\"\"\n    pixels = mask.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    \n    if len(runs) == 0:\n        return \"authentic\"\n    # Joining the list of numbers into a single space-separated string\n    return ' '.join(str(x) for x in runs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:44:51.329075Z","iopub.execute_input":"2026-01-06T11:44:51.329367Z","iopub.status.idle":"2026-01-06T11:44:51.334418Z","shell.execute_reply.started":"2026-01-06T11:44:51.329339Z","shell.execute_reply":"2026-01-06T11:44:51.333837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\n\nclass ForgeryTestDataset(Dataset):\n    def __init__(self, image_dir, transform=None):\n        self.image_dir = image_dir\n        self.image_filenames = sorted(os.listdir(image_dir))\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.image_filenames)\n\n    def __getitem__(self, idx):\n        img_name = self.image_filenames[idx]\n        img_path = os.path.join(self.image_dir, img_name)\n        \n        # Load image\n        image = Image.open(img_path).convert(\"RGB\")\n        image_np = np.array(image)\n\n        if self.transform:\n            augmented = self.transform(image=image_np)\n            image_np = augmented['image']\n\n        # case_id is usually the filename without extension (e.g., '123' from '123.jpg')\n        case_id = os.path.splitext(img_name)[0]\n        \n        return image_np, case_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:44:48.583993Z","iopub.execute_input":"2026-01-06T11:44:48.584508Z","iopub.status.idle":"2026-01-06T11:44:48.590539Z","shell.execute_reply.started":"2026-01-06T11:44:48.584481Z","shell.execute_reply":"2026-01-06T11:44:48.589730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_transform = A.Compose([\n    A.Resize(256, 256), # Match your training size\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2(),\n])\n\n# Change 'path/to/test_images' to your actual test folder path\ntest_ds = ForgeryTestDataset(image_dir='../input/recodai-luc-scientific-image-forgery-detection/test_images/', transform=test_transform)\n\ntest_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:45:11.730592Z","iopub.execute_input":"2026-01-06T11:45:11.730900Z","iopub.status.idle":"2026-01-06T11:45:11.741743Z","shell.execute_reply.started":"2026-01-06T11:45:11.730872Z","shell.execute_reply":"2026-01-06T11:45:11.740933Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submission guidelines ","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nfrom tqdm import tqdm\n\ndef create_submission(model_path, test_loader, device):\n    model.load_state_dict(torch.load(model_path))\n    model.eval()\n    \n    submission_data = []\n\n    with torch.no_grad():\n        # Important: Ensure your loader returns (image, filename/case_id)\n        for images, case_ids in tqdm(test_loader, desc=\"Generating Submission\"):\n            images = images.to(device)\n            outputs = model(images)\n            \n            # Apply sigmoid and your BEST threshold found earlier (e.g., 0.3)\n            preds = (torch.sigmoid(outputs) > 0.3).cpu().numpy().astype(np.uint8)\n\n            for i in range(len(preds)):\n                mask = preds[i].squeeze()\n                rle_string = rle_encode(mask)\n                \n                submission_data.append({\n                    \"case_id\": case_ids[i],\n                    \"annotation\": rle_string\n                })\n\n    # Save to CSV\n    df = pd.DataFrame(submission_data)\n    df.to_csv(\"submission.csv\", index=False)\n    print(\"Submission file saved as submission.csv!\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:45:15.978683Z","iopub.execute_input":"2026-01-06T11:45:15.978973Z","iopub.status.idle":"2026-01-06T11:45:15.985074Z","shell.execute_reply.started":"2026-01-06T11:45:15.978949Z","shell.execute_reply":"2026-01-06T11:45:15.984273Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom tqdm import tqdm\n\ndef generate_final_submission(model, test_loader, device, threshold=0.3):\n    model.eval()\n    results = []\n\n    with torch.no_grad():\n        for images, case_ids in tqdm(test_loader):\n            images = images.to(device)\n            outputs = model(images)\n            \n            # Use the sigmoid + threshold to create the binary mask\n            preds = (torch.sigmoid(outputs) > threshold).cpu().numpy().astype(np.uint8)\n\n            for i in range(len(preds)):\n                mask = preds[i].squeeze() # Remove extra dimensions\n                rle_string = rle_encode(mask)\n                \n                results.append({\n                    \"case_id\": case_ids[i],\n                    \"annotation\": rle_string\n                })\n\n    # Create DataFrame and save\n    df = pd.DataFrame(results)\n    # Sort by case_id to keep it clean\n    df['case_id'] = pd.to_numeric(df['case_id'], errors='ignore')\n    df = df.sort_values('case_id')\n    \n    df.to_csv(\"submission.csv\", index=False)\n    print(\"\\nSubmission file 'submission.csv' is ready for upload!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:45:18.854088Z","iopub.execute_input":"2026-01-06T11:45:18.854909Z","iopub.status.idle":"2026-01-06T11:45:18.860724Z","shell.execute_reply.started":"2026-01-06T11:45:18.854876Z","shell.execute_reply":"2026-01-06T11:45:18.860099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from os import listdir\nfrom os.path import isfile, join\nfiles = os.listdir(\"../working/\")\nfor file in files:\n    print(file)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:48:38.510010Z","iopub.execute_input":"2026-01-06T11:48:38.510878Z","iopub.status.idle":"2026-01-06T11:48:38.516350Z","shell.execute_reply.started":"2026-01-06T11:48:38.510843Z","shell.execute_reply":"2026-01-06T11:48:38.515609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"create_submission(\"model70.pth\", test_loader, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:48:50.298495Z","iopub.execute_input":"2026-01-06T11:48:50.298817Z","iopub.status.idle":"2026-01-06T11:48:50.493153Z","shell.execute_reply.started":"2026-01-06T11:48:50.298791Z","shell.execute_reply":"2026-01-06T11:48:50.492480Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nprint(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:52:01.336952Z","iopub.execute_input":"2026-01-06T11:52:01.337734Z","iopub.status.idle":"2026-01-06T11:52:01.343029Z","shell.execute_reply.started":"2026-01-06T11:52:01.337701Z","shell.execute_reply":"2026-01-06T11:52:01.342241Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"generate_final_submission(model, test_loader, device, threshold=0.3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T11:53:12.441882Z","iopub.execute_input":"2026-01-06T11:53:12.442522Z","iopub.status.idle":"2026-01-06T11:53:12.507926Z","shell.execute_reply.started":"2026-01-06T11:53:12.442494Z","shell.execute_reply":"2026-01-06T11:53:12.507236Z"}},"outputs":[],"execution_count":null}]}