{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":51.085353,"end_time":"2025-11-16T16:15:41.162948","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-11-16T16:14:50.077595","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# IMPORTS","metadata":{}},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.173001Z","iopub.execute_input":"2025-12-16T18:55:40.173559Z","iopub.status.idle":"2025-12-16T18:55:40.177795Z","shell.execute_reply.started":"2025-12-16T18:55:40.173528Z","shell.execute_reply":"2025-12-16T18:55:40.176955Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset paths","metadata":{}},{"cell_type":"code","source":"DATA_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n\nTRAIN_FORGED = f\"{DATA_ROOT}/train_images/forged\"\nTRAIN_AUTH   = f\"{DATA_ROOT}/train_images/authentic\"\nTRAIN_MASKS  = f\"{DATA_ROOT}/train_masks\"\n\nTEST_IMAGES  = f\"{DATA_ROOT}/test_images\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.178942Z","iopub.execute_input":"2025-12-16T18:55:40.179146Z","iopub.status.idle":"2025-12-16T18:55:40.182827Z","shell.execute_reply.started":"2025-12-16T18:55:40.179130Z","shell.execute_reply":"2025-12-16T18:55:40.182028Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Unified Dataset (Forged + Authentic)","metadata":{}},{"cell_type":"code","source":"class ForgeryDataset(Dataset):\n    def __init__(self, forged_dir, authentic_dir, mask_dir, transform=None):\n        self.forged_dir = forged_dir\n        self.authentic_dir = authentic_dir\n        self.mask_dir = mask_dir\n        self.transform = transform\n\n        self.forged_images = sorted(os.listdir(forged_dir))\n        self.authentic_images = sorted(os.listdir(authentic_dir))\n\n        # label images internally\n        self.samples = (\n            [(img, \"forged\") for img in self.forged_images] +\n            [(img, \"authentic\") for img in self.authentic_images]\n        )\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        img_name, img_type = self.samples[idx]\n\n        if img_type == \"forged\":\n            img_path = os.path.join(self.forged_dir, img_name)\n        else:\n            img_path = os.path.join(self.authentic_dir, img_name)\n\n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n        if img_type == \"forged\":\n            raw_mask = np.load(\n                os.path.join(self.mask_dir, img_name.replace(\".png\", \".npy\"))\n            )\n            mask = (raw_mask.sum(axis=0) > 0).astype(np.float32)\n        else:\n            # authentic → empty mask\n            mask = np.zeros(image.shape[:2], dtype=np.float32)\n\n        if self.transform:\n            augmented = self.transform(image=image, mask=mask)\n            image = augmented[\"image\"]\n            mask = augmented[\"mask\"].unsqueeze(0)\n\n        return image, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.183462Z","iopub.execute_input":"2025-12-16T18:55:40.183706Z","iopub.status.idle":"2025-12-16T18:55:40.190585Z","shell.execute_reply.started":"2025-12-16T18:55:40.183684Z","shell.execute_reply":"2025-12-16T18:55:40.190047Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TRAINING TRANSFORMS","metadata":{}},{"cell_type":"code","source":"train_tfms = A.Compose([\n    A.RandomResizedCrop(\n        size=(320, 320),\n        scale=(0.75, 1.0),\n        ratio=(0.9, 1.1),\n        p=1.0\n    ),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.RandomRotate90(p=0.5),\n    A.Normalize(),\n    ToTensorV2()\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.191750Z","iopub.execute_input":"2025-12-16T18:55:40.192094Z","iopub.status.idle":"2025-12-16T18:55:40.197860Z","shell.execute_reply.started":"2025-12-16T18:55:40.192079Z","shell.execute_reply":"2025-12-16T18:55:40.197321Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# DATALOADER","metadata":{}},{"cell_type":"code","source":"train_dataset = ForgeryDataset(\n    forged_dir=TRAIN_FORGED,\n    authentic_dir=TRAIN_AUTH,\n    mask_dir=TRAIN_MASKS,\n    transform=train_tfms\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=6,       \n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.198447Z","iopub.execute_input":"2025-12-16T18:55:40.198625Z","iopub.status.idle":"2025-12-16T18:55:40.209836Z","shell.execute_reply.started":"2025-12-16T18:55:40.198612Z","shell.execute_reply":"2025-12-16T18:55:40.209167Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# SIMPLE CNN ENCODER (KAGGLE-SAFE)","metadata":{}},{"cell_type":"code","source":"class SimpleEncoder(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.layer1 = nn.Sequential(\n            nn.Conv2d(3, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True)\n        )\n\n        self.layer2 = nn.Sequential(\n            nn.MaxPool2d(2),\n            nn.Conv2d(64, 128, 3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(inplace=True)\n        )\n\n        self.layer3 = nn.Sequential(\n            nn.MaxPool2d(2),\n            nn.Conv2d(128, 256, 3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(inplace=True)\n        )\n\n        self.layer4 = nn.Sequential(\n            nn.MaxPool2d(2),\n            nn.Conv2d(256, 512, 3, padding=1),\n            nn.BatchNorm2d(512),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        f1 = self.layer1(x)     # 320x320\n        f2 = self.layer2(f1)    # 160x160\n        f3 = self.layer3(f2)    # 80x80\n        f4 = self.layer4(f3)    # 40x40\n        return [f1, f2, f3, f4]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.210559Z","iopub.execute_input":"2025-12-16T18:55:40.210769Z","iopub.status.idle":"2025-12-16T18:55:40.216203Z","shell.execute_reply.started":"2025-12-16T18:55:40.210749Z","shell.execute_reply":"2025-12-16T18:55:40.215583Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# UNET DECODER","metadata":{}},{"cell_type":"code","source":"class DecoderBlock(nn.Module):\n    def __init__(self, in_channels, skip_channels, out_channels):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_channels + skip_channels, out_channels, 3, padding=1)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n        self.relu = nn.ReLU(inplace=True)\n\n    def forward(self, x, skip):\n        x = F.interpolate(x, scale_factor=2, mode=\"bilinear\", align_corners=False)\n        x = torch.cat([x, skip], dim=1)\n        x = self.relu(self.bn1(self.conv1(x)))\n        x = self.relu(self.bn2(self.conv2(x)))\n        return x\n\n\nclass UNet(nn.Module):\n    def __init__(self, encoder):\n        super().__init__()\n        self.encoder = encoder\n\n        self.center = nn.Sequential(\n            nn.MaxPool2d(2),\n            nn.Conv2d(512, 512, 3, padding=1),\n            nn.BatchNorm2d(512),\n            nn.ReLU(inplace=True)\n        )\n\n        self.dec4 = DecoderBlock(512, 512, 256)\n        self.dec3 = DecoderBlock(256, 256, 128)\n        self.dec2 = DecoderBlock(128, 128, 64)\n        self.dec1 = DecoderBlock(64, 64, 32)\n\n        self.final = nn.Conv2d(32, 1, kernel_size=1)\n\n    def forward(self, x):\n        f1, f2, f3, f4 = self.encoder(x)\n\n        center = self.center(f4)\n\n        d4 = self.dec4(center, f4)\n        d3 = self.dec3(d4, f3)\n        d2 = self.dec2(d3, f2)\n        d1 = self.dec1(d2, f1)\n\n        return self.final(d1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.216859Z","iopub.execute_input":"2025-12-16T18:55:40.217086Z","iopub.status.idle":"2025-12-16T18:55:40.224195Z","shell.execute_reply.started":"2025-12-16T18:55:40.217065Z","shell.execute_reply":"2025-12-16T18:55:40.223717Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ========================\n# MODEL SANITY CHECK\n# ========================\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = UNet(SimpleEncoder()).to(device)\nx = torch.randn(1, 3, 320, 320).to(device)\n\ny = model(x)\nprint(\"Output shape:\", y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.326992Z","iopub.execute_input":"2025-12-16T18:55:40.327174Z","iopub.status.idle":"2025-12-16T18:55:40.398604Z","shell.execute_reply.started":"2025-12-16T18:55:40.327160Z","shell.execute_reply":"2025-12-16T18:55:40.397925Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# EXTREME LOSS FUNCTION","metadata":{}},{"cell_type":"code","source":"def dice_loss(pred, target, smooth=1.0):\n    pred = torch.sigmoid(pred)\n    intersection = (pred * target).sum()\n    union = pred.sum() + target.sum()\n    return 1 - (2. * intersection + smooth) / (union + smooth)\n\n\nclass FocalLoss(nn.Module):\n    def __init__(self, alpha=0.8, gamma=2.0):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n        self.bce = nn.BCEWithLogitsLoss(reduction=\"none\")\n\n    def forward(self, pred, target):\n        bce = self.bce(pred, target)\n        pt = torch.exp(-bce)\n        focal = self.alpha * (1 - pt) ** self.gamma * bce\n        return focal.mean()\n\n\ndef combined_loss(pred, target):\n    bce = nn.BCEWithLogitsLoss()(pred, target)\n    dice = dice_loss(pred, target)\n    focal = FocalLoss()(pred, target)\n    return bce + dice + focal","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.399754Z","iopub.execute_input":"2025-12-16T18:55:40.399937Z","iopub.status.idle":"2025-12-16T18:55:40.405820Z","shell.execute_reply.started":"2025-12-16T18:55:40.399922Z","shell.execute_reply":"2025-12-16T18:55:40.405240Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TRAINING SETUP","metadata":{}},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = UNet(SimpleEncoder()).to(device)\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=1e-3,\n    weight_decay=1e-4\n)\n\nEPOCHS = 50","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.406450Z","iopub.execute_input":"2025-12-16T18:55:40.406723Z","iopub.status.idle":"2025-12-16T18:55:40.474496Z","shell.execute_reply.started":"2025-12-16T18:55:40.406708Z","shell.execute_reply":"2025-12-16T18:55:40.473869Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TRAINING LOOP","metadata":{}},{"cell_type":"code","source":"for epoch in range(EPOCHS):\n    model.train()\n    running_loss = 0.0\n\n    for images, masks in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS}\"):\n        images = images.to(device)\n        masks = masks.to(device)\n\n        optimizer.zero_grad()\n\n        outputs = model(images)\n        loss = combined_loss(outputs, masks)\n\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item()\n\n    avg_loss = running_loss / len(train_loader)\n    print(f\"Epoch {epoch+1} - Avg Loss: {avg_loss:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T18:55:40.475233Z","iopub.execute_input":"2025-12-16T18:55:40.475482Z","iopub.status.idle":"2025-12-16T19:41:17.956264Z","shell.execute_reply.started":"2025-12-16T18:55:40.475466Z","shell.execute_reply":"2025-12-16T19:41:17.955370Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TEST TRANSFORMS (REQUIRED)","metadata":{}},{"cell_type":"code","source":"test_tfms = A.Compose([\n    A.Resize(320, 320),   # must match training size\n    A.Normalize(),\n    ToTensorV2()\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T19:48:17.896880Z","iopub.execute_input":"2025-12-16T19:48:17.897385Z","iopub.status.idle":"2025-12-16T19:48:17.902687Z","shell.execute_reply.started":"2025-12-16T19:48:17.897343Z","shell.execute_reply":"2025-12-16T19:48:17.902100Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# POST-PROCESSING FUNCTION","metadata":{}},{"cell_type":"code","source":"def post_process_mask(prob_mask, thresh, min_area):\n    binary = (prob_mask > thresh).astype(np.uint8)\n\n    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(\n        binary, connectivity=8\n    )\n\n    clean_mask = np.zeros_like(binary)\n\n    for i in range(1, num_labels):\n        if stats[i, cv2.CC_STAT_AREA] >= min_area:\n            clean_mask[labels == i] = 1\n\n    return clean_mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T19:49:56.196933Z","iopub.execute_input":"2025-12-16T19:49:56.197219Z","iopub.status.idle":"2025-12-16T19:49:56.201852Z","shell.execute_reply.started":"2025-12-16T19:49:56.197196Z","shell.execute_reply":"2025-12-16T19:49:56.201100Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# RLE ENCODING (REQUIRED)\n","metadata":{}},{"cell_type":"code","source":"def rle_encode(mask):\n    pixels = mask.flatten(order=\"F\")\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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T19:51:13.156515Z","iopub.execute_input":"2025-12-16T19:51:13.156784Z","iopub.status.idle":"2025-12-16T19:51:13.161141Z","shell.execute_reply.started":"2025-12-16T19:51:13.156764Z","shell.execute_reply":"2025-12-16T19:51:13.160446Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_sub = pd.read_csv(\n    \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/sample_submission.csv\"\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"THRESH = 0.35\nMIN_AREA = 80\nAUTH_AREA = 120\n\nmodel.eval()\n\n# There is exactly ONE test image\nimg_name = sorted(os.listdir(TEST_IMAGES))[0]\n\nimage = cv2.imread(os.path.join(TEST_IMAGES, img_name))\nimage = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\nwith torch.no_grad():\n    probs = []\n\n    for aug in [None, \"h\", \"v\"]:\n        img = image.copy()\n\n        if aug == \"h\":\n            img = np.fliplr(img)\n        elif aug == \"v\":\n            img = np.flipud(img)\n\n        augmented = test_tfms(image=img)\n        inp = augmented[\"image\"].unsqueeze(0).to(device)\n\n        pred = torch.sigmoid(model(inp)).cpu().numpy()[0, 0]\n\n        if aug == \"h\":\n            pred = np.fliplr(pred)\n        elif aug == \"v\":\n            pred = np.flipud(pred)\n\n        probs.append(pred)\n\n    prob = np.mean(probs, axis=0)\n\n# Post-process\nmask = post_process_mask(prob, thresh=THRESH, min_area=MIN_AREA)\n\nif mask.sum() < AUTH_AREA:\n    annotation = \"authentic\"\nelse:\n    rle = rle_encode(mask)\n    annotation = rle if rle.strip() != \"\" else \"authentic\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Fill ONLY the annotation column\nsample_sub.loc[:, \"annotation\"] = annotation","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_sub.to_csv(\"submission.csv\", index=False)\n\nprint(sample_sub)\nprint(sample_sub.dtypes)\nprint(len(sample_sub))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}