{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"},{"sourceId":14366276,"sourceType":"datasetVersion","datasetId":9153851},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":63.129007,"end_time":"2025-12-30T04:03:10.929134","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-12-30T04:02:07.800127","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport json\nimport math\nimport random\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom pathlib import Path\nfrom PIL import Image\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom transformers import AutoImageProcessor, AutoModel\n\n# --- 1. Global Setup & Seeding ---\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True \n    torch.backends.cudnn.benchmark = False\n\nseed_everything(42)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nBASE_DIR  = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nDINO_PATH = \"/kaggle/input/dinov2/pytorch/base/1\"\nMODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-R69/model_seg_final.pt'\n\n# --- 2. Hyperparameters (The \"Recovery\" Tune) ---\nIMG_SIZE = 518\n# REVERT: Go back close to baseline to recover Recall\nAREA_THR = 145        # Compromise between 150 (safe) and 135 (risky)\nMEAN_THR = 0.19       # Back down from 0.24. Only filter VERY weak signals.\nSTD_COEFF = 0.30      # Back down from 0.60. 0.25 was baseline, 0.30 is a gentle nudge.\n\n# --- 3. Model Architecture ---\nclass DinoTinyDecoder(nn.Module):\n    def __init__(self, in_ch=768, out_ch=1):\n        super().__init__()\n        self.block1 = nn.Sequential(nn.Conv2d(in_ch, 384, 3, 1, 1), nn.ReLU(True), nn.Dropout2d(0.1))\n        self.block2 = nn.Sequential(nn.Conv2d(384, 192, 3, 1, 1), nn.ReLU(True), nn.Dropout2d(0.1))\n        self.block3 = nn.Sequential(nn.Conv2d(192, 96, 3, 1, 1), nn.ReLU(True))\n        self.conv_out = nn.Conv2d(96, out_ch, 1)\n\n    def forward(self, f, target_size):\n        x = F.interpolate(self.block1(f), size=(74, 74), mode='bilinear', align_corners=False)\n        x = F.interpolate(self.block2(x), size=(148, 148), mode='bilinear', align_corners=False)\n        x = F.interpolate(self.block3(x), size=(296, 296), mode='bilinear', align_corners=False)\n        x = self.conv_out(x)\n        return F.interpolate(x, size=target_size, mode='bilinear', align_corners=False)\n\nclass DinoSegmenter(nn.Module):\n    def __init__(self, encoder, processor):\n        super().__init__()\n        self.encoder, self.processor = encoder, processor\n        for p in self.encoder.parameters(): p.requires_grad = False\n        self.seg_head = DinoTinyDecoder(768, 1)\n        \n    def forward_features(self, x):\n        imgs = (x*255).clamp(0,255).byte().permute(0,2,3,1).cpu().numpy()\n        inputs = self.processor(images=list(imgs), return_tensors=\"pt\").to(x.device)\n        feats = self.encoder(**inputs).last_hidden_state\n        B, N, C = feats.shape\n        s = int(math.sqrt(N-1))\n        return feats[:,1:,:].permute(0,2,1).reshape(B, C, s, s)\n\n    def forward_seg(self, x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap, (IMG_SIZE, IMG_SIZE))\n\n# --- 4. Load Model ---\nprint(\"⏳ Loading Model...\")\nprocessor = AutoImageProcessor.from_pretrained(DINO_PATH, local_files_only=True, use_fast=False)\nencoder = AutoModel.from_pretrained(DINO_PATH, local_files_only=True).eval().to(device)\nmodel_seg = DinoSegmenter(encoder, processor).to(device)\n\nif os.path.exists(MODEL_LOC):\n    model_seg.load_state_dict(torch.load(MODEL_LOC, map_location=device))\n    print(f\"✅ Loaded weights from: {MODEL_LOC}\")\n\nmodel_seg.eval()\n\n# --- 5. Pipeline ---\n@torch.no_grad()\ndef segment_prob_map_multiscale(pil):\n    \"\"\"Run inference at 1.0x (Original & Flip) and 0.75x (Zoom Out).\"\"\"\n    img_tensor = torch.from_numpy(np.array(pil.resize((IMG_SIZE, IMG_SIZE)), np.float32)/255.).permute(2,0,1)[None].to(device)\n    \n    # 1. Scale 1.0x Original\n    prob_1 = torch.sigmoid(model_seg.forward_seg(img_tensor))[0,0]\n    \n    # 2. Scale 1.0x Flip\n    img_flip = torch.flip(img_tensor, [3])\n    prob_1_flip = torch.sigmoid(model_seg.forward_seg(img_flip))[0,0]\n    prob_1_flip = torch.flip(prob_1_flip, [1]) \n    \n    # 3. Scale 0.75x\n    size_small = (392, 392)\n    img_small = F.interpolate(img_tensor, size=size_small, mode='bilinear', align_corners=False)\n    prob_small = torch.sigmoid(model_seg.forward_seg(img_small))\n    prob_small = F.interpolate(prob_small, size=(IMG_SIZE, IMG_SIZE), mode='bilinear', align_corners=False)[0,0]\n\n    # REVERT: Balance weights back to be safer.\n    # 0.60 / 0.20 / 0.20 ensures we get enough signal from the zoom\n    prob_avg = (prob_1 * 0.60) + (prob_1_flip * 0.20) + (prob_small * 0.20)\n    \n    return prob_avg.cpu().numpy()\n\ndef enhanced_adaptive_mask(prob, alpha_grad=0.35):\n    # Edge enhancement\n    gx = cv2.Sobel(prob, cv2.CV_32F, 1, 0, ksize=3)\n    gy = cv2.Sobel(prob, cv2.CV_32F, 0, 1, ksize=3)\n    grad_norm = np.sqrt(gx**2 + gy**2)\n    grad_norm = grad_norm / (grad_norm.max() + 1e-6)\n    \n    enhanced = (1 - alpha_grad) * prob + alpha_grad * grad_norm\n    enhanced = cv2.GaussianBlur(enhanced, (3,3), 0)\n    \n    # Dynamic Thresholding (Gentle)\n    thr = np.mean(enhanced) + STD_COEFF * np.std(enhanced)\n    mask = (enhanced > thr).astype(np.uint8)\n    \n    # REVERT: Use (5,5) kernel. (3,3) was too fragmented.\n    # (5,5) closes gaps better, improving IoU on solid objects.\n    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5,5), np.uint8))\n    \n    # KEEP: This is the one safe \"aggressive\" change. \n    # It removes TTA artifacts at the very edge.\n    mask[:6, :] = 0\n    mask[-6:, :] = 0\n    mask[:, :6] = 0\n    mask[:, -6:] = 0\n    \n    return mask, thr\n\ndef pipeline_final(pil):\n    prob = segment_prob_map_multiscale(pil)\n    mask, thr = enhanced_adaptive_mask(prob)\n    mask = cv2.resize(mask, pil.size, interpolation=cv2.INTER_NEAREST)\n    \n    area = int(mask.sum())\n    \n    if area > 0:\n        mask_small = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n        if mask_small.sum() > 0:\n            mean_inside = float(prob[mask_small==1].mean())\n        else:\n            mean_inside = 0.0\n        max_conf = float(prob.max())\n    else:\n        mean_inside = 0.0\n        max_conf = 0.0\n\n    # REVERT: Relaxed \"Small but Strong\"\n    # If area < 300, we check for 0.75 confidence (was 0.80).\n    # This recovers the faint small dots we lost.\n    if area < 300:\n        if max_conf < 0.75 or mean_inside < 0.25:\n             return \"authentic\", None, {\"area\": area, \"mean\": mean_inside, \"thr\": thr}\n\n    # High confidence override\n    if max_conf > 0.95 and area > 50:\n        return \"forged\", mask, {\"area\": area, \"mean\": mean_inside, \"thr\": thr}\n\n    # Standard Filter\n    if area < AREA_THR or mean_inside < MEAN_THR:\n        return \"authentic\", None, {\"area\": area, \"mean\": mean_inside, \"thr\": thr}\n    \n    return \"forged\", mask, {\"area\": area, \"mean\": mean_inside, \"thr\": thr}\n\n# --- 6. RLE Encoder & Submission ---\ndef rle_encode(mask):\n    pixels = mask.T.flatten()\n    dots = np.where(pixels == 1)[0]\n    if len(dots) == 0: return \"authentic\"\n    run_lengths, prev = [], -2\n    for b in dots:\n        if b > prev + 1: run_lengths.extend((b + 1, 0))\n        run_lengths[-1] += 1\n        prev = b\n    return json.dumps([int(x) for x in run_lengths])\n\nTEST_DIR = f\"{BASE_DIR}/test_images\"\nSAMPLE_SUB = f\"{BASE_DIR}/sample_submission.csv\"\nOUT_PATH = \"submission.csv\"\n\nrows = []\ntest_files = sorted(os.listdir(TEST_DIR))\nprint(f\"🚀 Recovery Run on {len(test_files)} images...\")\n\nfor f in tqdm(test_files):\n    pil = Image.open(Path(TEST_DIR)/f).convert(\"RGB\")\n    label, mask, dbg = pipeline_final(pil)\n    \n    if label == \"authentic\" or mask is None:\n        annot = \"authentic\"\n    else:\n        annot = rle_encode((mask > 0).astype(np.uint8))\n        \n    rows.append({\"case_id\": Path(f).stem, \"annotation\": annot})\n\nsub = pd.DataFrame(rows)\nss = pd.read_csv(SAMPLE_SUB)\nss[\"case_id\"] = ss[\"case_id\"].astype(str)\nsub[\"case_id\"] = sub[\"case_id\"].astype(str)\n\nfinal = ss[[\"case_id\"]].merge(sub, on=\"case_id\", how=\"left\")\nfinal[\"annotation\"] = final[\"annotation\"].fillna(\"authentic\")\nfinal[[\"case_id\", \"annotation\"]].to_csv(OUT_PATH, index=False)\n\nprint(f\"\\n✅ Recovery Submission Saved: {OUT_PATH}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-02T13:25:00.643347Z","iopub.execute_input":"2026-01-02T13:25:00.643964Z","iopub.status.idle":"2026-01-02T13:25:01.604106Z","shell.execute_reply.started":"2026-01-02T13:25:00.643926Z","shell.execute_reply":"2026-01-02T13:25:01.603237Z"},"papermill":{"duration":0.706627,"end_time":"2025-12-30T04:03:04.172732","exception":false,"start_time":"2025-12-30T04:03:03.466105","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}