{"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":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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\n# ============================================================\n# CONFIG\n# ============================================================\nDATA_DIR = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train_images\")\nTRAIN_MASK_DIR = os.path.join(DATA_DIR, \"train_masks\")\nTEST_IMG_DIR = os.path.join(DATA_DIR, \"test_images\")\n\nIMG_SIZE = 256\nBATCH_SIZE = 8\nEPOCHS = 2\nLR = 1e-3\nTHRESHOLD = 0.3\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n# ============================================================\n# RLE\n# ============================================================\ndef 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 list(runs)\n\n# ============================================================\n# DATASET\n# ============================================================\nclass ForgeryDataset(Dataset):\n    def __init__(self, img_dir, mask_dir):\n        self.img_dir = img_dir\n        self.mask_dir = mask_dir\n        self.images = []\n\n        for root, _, files in os.walk(img_dir):\n            for f in files:\n                if f.lower().endswith((\".png\", \".jpg\", \".jpeg\", \".tif\", \".tiff\")):\n                    self.images.append(os.path.join(root, f))\n\n        print(f\"Loaded {len(self.images)} training images\")\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img_path = self.images[idx]\n        name = os.path.basename(img_path)\n\n        image = cv2.imread(img_path)\n        if image is None:\n            image = np.zeros((IMG_SIZE, IMG_SIZE, 3), dtype=np.uint8)\n\n        image = cv2.resize(image, (IMG_SIZE, IMG_SIZE))\n        image = image.transpose(2, 0, 1) / 255.0\n\n        mask = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.float32)\n        base = os.path.splitext(name)[0]\n\n        for f in os.listdir(self.mask_dir):\n            if f.startswith(base):\n                m = cv2.imread(os.path.join(self.mask_dir, f), 0)\n                if m is not None:\n                    m = cv2.resize(m, (IMG_SIZE, IMG_SIZE))\n                    mask = np.maximum(mask, m > 0)\n\n        return (\n            torch.tensor(image, dtype=torch.float32),\n            torch.tensor(mask[None], dtype=torch.float32),\n        )\n\n# ============================================================\n# TINY U-NET\n# ============================================================\nclass UNetSmall(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        def C(in_c, out_c):\n            return nn.Sequential(\n                nn.Conv2d(in_c, out_c, 3, padding=1),\n                nn.ReLU(inplace=True),\n                nn.Conv2d(out_c, out_c, 3, padding=1),\n                nn.ReLU(inplace=True),\n            )\n\n        self.d1 = C(3, 16)\n        self.p1 = nn.MaxPool2d(2)\n        self.d2 = C(16, 32)\n        self.p2 = nn.MaxPool2d(2)\n        self.mid = C(32, 64)\n\n        self.u2 = nn.ConvTranspose2d(64, 32, 2, 2)\n        self.c2 = C(64, 32)\n        self.u1 = nn.ConvTranspose2d(32, 16, 2, 2)\n        self.c1 = C(32, 16)\n        self.out = nn.Conv2d(16, 1, 1)\n\n    def forward(self, x):\n        d1 = self.d1(x)\n        d2 = self.d2(self.p1(d1))\n        m = self.mid(self.p2(d2))\n        x = self.c2(torch.cat([self.u2(m), d2], 1))\n        x = self.c1(torch.cat([self.u1(x), d1], 1))\n        return self.out(x)\n\n# ============================================================\n# TRAIN\n# ============================================================\ntrain_ds = ForgeryDataset(TRAIN_IMG_DIR, TRAIN_MASK_DIR)\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)\n\nmodel = UNetSmall().to(DEVICE)\noptimizer = torch.optim.Adam(model.parameters(), lr=LR)\ncriterion = nn.BCEWithLogitsLoss()\n\nmodel.train()\nfor imgs, masks in tqdm(train_loader):\n    imgs, masks = imgs.to(DEVICE), masks.to(DEVICE)\n    optimizer.zero_grad()\n    loss = criterion(model(imgs), masks)\n    loss.backward()\n    optimizer.step()\n\n# ============================================================\n# INFERENCE\n# ============================================================\nmodel.eval()\nresults = []\n\nfor fname in tqdm(sorted(os.listdir(TEST_IMG_DIR))):\n    if not fname.lower().endswith((\".png\", \".jpg\", \".jpeg\", \".tif\", \".tiff\")):\n        continue\n\n    case_id = int(os.path.splitext(fname)[0])\n    img = cv2.imread(os.path.join(TEST_IMG_DIR, fname))\n\n    if img is None:\n        results.append((case_id, \"authentic\"))\n        continue\n\n    img_r = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    x = torch.tensor(img_r.transpose(2, 0, 1) / 255.0).float().unsqueeze(0).to(DEVICE)\n\n    with torch.no_grad():\n        pred = torch.sigmoid(model(x))[0, 0].cpu().numpy()\n\n    mask = (pred > THRESHOLD).astype(np.uint8)\n    mask = cv2.resize(mask, (img.shape[1], img.shape[0]))\n\n    if mask.sum() < 30:\n        results.append((case_id, \"authentic\"))\n    else:\n        results.append((case_id, str(rle_encode(mask))))\n\n# ============================================================\n# SUBMISSION\n# ============================================================\npd.DataFrame(results, columns=[\"case_id\", \"annotation\"]).to_csv(\n    \"submission.csv\", index=False\n)\n\nprint(\"✅ submission.csv ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T15:26:11.932054Z","iopub.execute_input":"2026-01-11T15:26:11.932348Z","iopub.status.idle":"2026-01-11T15:29:24.487620Z","shell.execute_reply.started":"2026-01-11T15:26:11.932323Z","shell.execute_reply":"2026-01-11T15:29:24.486855Z"}},"outputs":[],"execution_count":null}]}