{"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":"# INSTALL DEPENDENCIES","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\nfrom torch.utils.data import Dataset, DataLoader\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nfrom torchvision.models.segmentation import deeplabv3_resnet50","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:58:30.446465Z","iopub.execute_input":"2025-12-16T05:58:30.446642Z","iopub.status.idle":"2025-12-16T05:59:12.269170Z","shell.execute_reply.started":"2025-12-16T05:58:30.446625Z","shell.execute_reply":"2025-12-16T05:59:12.268563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_FORGED = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/forged\"\nTRAIN_MASKS  = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\"\n\nTEST_IMAGES  = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.270585Z","iopub.execute_input":"2025-12-16T05:59:12.270933Z","iopub.status.idle":"2025-12-16T05:59:12.274585Z","shell.execute_reply.started":"2025-12-16T05:59:12.270915Z","shell.execute_reply":"2025-12-16T05:59:12.273898Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# DATA SANITY CHECK","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nimg_name = os.listdir(TRAIN_FORGED)[0]\n\nimage = cv2.imread(os.path.join(TRAIN_FORGED, img_name))\nimage = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\nraw_mask = np.load(os.path.join(TRAIN_MASKS, img_name.replace(\".png\", \".npy\")))\nmask = (raw_mask.sum(axis=0) > 0).astype(np.uint8)\n\nplt.figure(figsize=(12,4))\n\nplt.subplot(1,3,1)\nplt.title(\"Image\")\nplt.imshow(image)\nplt.axis(\"off\")\n\nplt.subplot(1,3,2)\nplt.title(\"Mask\")\nplt.imshow(mask, cmap=\"gray\")\nplt.axis(\"off\")\n\nplt.subplot(1,3,3)\nplt.title(\"Overlay\")\nplt.imshow(image)\nplt.imshow(mask, cmap=\"Reds\", alpha=0.4)\nplt.axis(\"off\")\n\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.275348Z","iopub.execute_input":"2025-12-16T05:59:12.275644Z","iopub.status.idle":"2025-12-16T05:59:12.765574Z","shell.execute_reply.started":"2025-12-16T05:59:12.275617Z","shell.execute_reply":"2025-12-16T05:59:12.765001Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_tfms = A.Compose([\n    A.Resize(256, 256),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.Normalize(),\n    ToTensorV2()\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.766273Z","iopub.execute_input":"2025-12-16T05:59:12.766483Z","iopub.status.idle":"2025-12-16T05:59:12.773710Z","shell.execute_reply.started":"2025-12-16T05:59:12.766465Z","shell.execute_reply":"2025-12-16T05:59:12.772987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ForgeryDataset(Dataset):\n    def __init__(self, img_dir, mask_dir=None, transform=None):\n        self.img_dir = img_dir\n        self.mask_dir = mask_dir\n        self.images = sorted(os.listdir(img_dir))\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img_name = self.images[idx]\n\n        image = cv2.imread(os.path.join(self.img_dir, img_name))\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n        if self.mask_dir:\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            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-16T05:59:12.774576Z","iopub.execute_input":"2025-12-16T05:59:12.774843Z","iopub.status.idle":"2025-12-16T05:59:12.789051Z","shell.execute_reply.started":"2025-12-16T05:59:12.774827Z","shell.execute_reply":"2025-12-16T05:59:12.788419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ds = ForgeryDataset(TRAIN_FORGED, TRAIN_MASKS, train_tfms)\nimg, mask = ds[0]\n\nprint(\"Image:\", img.shape)\nprint(\"Mask :\", mask.shape)\nprint(\"Mask values:\", torch.unique(mask))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.789724Z","iopub.execute_input":"2025-12-16T05:59:12.789933Z","iopub.status.idle":"2025-12-16T05:59:12.875670Z","shell.execute_reply.started":"2025-12-16T05:59:12.789909Z","shell.execute_reply":"2025-12-16T05:59:12.875111Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    ForgeryDataset(TRAIN_FORGED, TRAIN_MASKS, train_tfms),\n    batch_size=8,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.877469Z","iopub.execute_input":"2025-12-16T05:59:12.878079Z","iopub.status.idle":"2025-12-16T05:59:12.884006Z","shell.execute_reply.started":"2025-12-16T05:59:12.878053Z","shell.execute_reply":"2025-12-16T05:59:12.883444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def dice_loss(pred, target, smooth=1.0):\n    pred = torch.sigmoid(pred)\n    intersection = (pred * target).sum()\n    return 1 - (2. * intersection + smooth) / (\n        pred.sum() + target.sum() + smooth\n    )\n\ndef combined_loss(pred, target):\n    bce = nn.BCEWithLogitsLoss()(pred, target)\n    dice = dice_loss(pred, target)\n    return bce + dice\n\nloss_fn = combined_loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.884767Z","iopub.execute_input":"2025-12-16T05:59:12.884993Z","iopub.status.idle":"2025-12-16T05:59:12.896397Z","shell.execute_reply.started":"2025-12-16T05:59:12.884969Z","shell.execute_reply":"2025-12-16T05:59:12.895598Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision.models.segmentation import deeplabv3_resnet50\nimport torch.nn as nn\n\nmodel = deeplabv3_resnet50(\n    weights=None,              # no Deeplab pretrained weights\n    weights_backbone=None,     # no ResNet pretrained weights\n    num_classes=1              # directly set output channels\n)\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:12.897230Z","iopub.execute_input":"2025-12-16T05:59:12.897557Z","iopub.status.idle":"2025-12-16T05:59:13.717690Z","shell.execute_reply.started":"2025-12-16T05:59:12.897533Z","shell.execute_reply":"2025-12-16T05:59:13.717112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T05:59:13.718387Z","iopub.execute_input":"2025-12-16T05:59:13.718606Z","iopub.status.idle":"2025-12-16T05:59:13.722789Z","shell.execute_reply.started":"2025-12-16T05:59:13.718589Z","shell.execute_reply":"2025-12-16T05:59:13.722175Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 10\n\nfor epoch in range(EPOCHS):\n    model.train()\n    epoch_loss = 0\n\n    for imgs, masks in tqdm(train_loader):\n        imgs, masks = imgs.to(device), masks.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(imgs)[\"out\"]\n        loss = loss_fn(outputs, masks)\n\n        loss.backward()\n        optimizer.step()\n\n        epoch_loss += loss.item()\n\n    avg_loss = epoch_loss / len(train_loader)\n    print(f\"Epoch {epoch+1}/{EPOCHS} - Avg Loss: {avg_loss:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"THRESH = 0.4\nMIN_AREA = 100\nAUTH_AREA = 150\n\nprint(THRESH, MIN_AREA, AUTH_AREA)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\n\ndef 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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_with_tta(model, image):\n    preds = []\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)[\"out\"]).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        preds.append(pred)\n\n    return np.mean(preds, axis=0)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_tfms = A.Compose([\n    A.Resize(256, 256),\n    A.Normalize(),\n    ToTensorV2()\n])","metadata":{"trusted":true},"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)\nsample_sub","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T08:15:12.299676Z","iopub.execute_input":"2025-12-16T08:15:12.300195Z","iopub.status.idle":"2025-12-16T08:15:12.316566Z","shell.execute_reply.started":"2025-12-16T08:15:12.300164Z","shell.execute_reply":"2025-12-16T08:15:12.315879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\nwith torch.no_grad():\n    # There is only ONE test image in this competition\n    img_name = sorted(os.listdir(TEST_IMAGES))[0]\n\n    image = cv2.imread(os.path.join(TEST_IMAGES, img_name))\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n    prob = predict_with_tta(model, image)\n\n    mask = post_process_mask(\n        prob,\n        thresh=THRESH,\n        min_area=MIN_AREA\n    )\n\n    if mask.sum() < AUTH_AREA:\n        annotation = \"authentic\"\n    else:\n        rle = rle_encode(mask)\n        annotation = rle if rle.strip() != \"\" else \"authentic\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_sub[\"annotation\"] = annotation\nsample_sub","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_sub.to_csv(\"submission.csv\", index=False)\nprint(\"Saved submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T08:17:27.874342Z","iopub.execute_input":"2025-12-16T08:17:27.874667Z","iopub.status.idle":"2025-12-16T08:17:27.881801Z","shell.execute_reply.started":"2025-12-16T08:17:27.874643Z","shell.execute_reply":"2025-12-16T08:17:27.880886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(sample_sub.dtypes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T08:17:38.824541Z","iopub.execute_input":"2025-12-16T08:17:38.825186Z","iopub.status.idle":"2025-12-16T08:17:38.830029Z","shell.execute_reply.started":"2025-12-16T08:17:38.825152Z","shell.execute_reply":"2025-12-16T08:17:38.829270Z"}},"outputs":[],"execution_count":null}]}