{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14407528,"sourceType":"datasetVersion","datasetId":9153851},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"header","cell_type":"markdown","source":"# 🔬 Scientific Image Forgery Detection - FINAL\n## Pretrained DINOv2 with Optimized Post-Processing\n\n**Key Improvements from baseline (0.303):**\n- 95th percentile threshold (from top solutions)\n- TTA enabled\n- Optimized area/confidence thresholds","metadata":{}},{"id":"imports","cell_type":"code","source":"import os, cv2, json, math, random, torch, io\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\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\nseed_everything(42)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T06:54:26.175680Z","iopub.execute_input":"2026-01-07T06:54:26.176474Z","iopub.status.idle":"2026-01-07T06:54:26.183866Z","shell.execute_reply.started":"2026-01-07T06:54:26.176441Z","shell.execute_reply":"2026-01-07T06:54:26.183152Z"}},"outputs":[],"execution_count":null},{"id":"config","cell_type":"code","source":"# ═══════════════════════════════════════════════════════════════════════════\n# CONFIGURATION - OPTIMIZED FOR FINAL SUBMISSION\n# ═══════════════════════════════════════════════════════════════════════════\nBASE_DIR = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nTEST_DIR = f\"{BASE_DIR}/test_images\"\nDINO_PATH = \"/kaggle/input/dinov2/pytorch/base/1\"\n\n# Pretrained model - THIS IS CRITICAL! Must use existing weights\nMODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-U52/CNNDINOv2-U52/model_seg_final.pt'\n\nIMG_SIZE = 518\n\n# OPTIMIZED INFERENCE PARAMETERS (from top solutions)\nUSE_TTA = True              # Enable TTA\nPERCENTILE_THR = 95         # 95th percentile (from handoff)\nMIN_THR = 0.5               # Minimum threshold floor\nAREA_THR = 300              # From reference notebooks\nMEAN_THR = 0.25             # Slightly higher than baseline\n\nprint(f\"TTA: {USE_TTA}\")\nprint(f\"Threshold: {PERCENTILE_THR}th percentile (min {MIN_THR})\")\nprint(f\"Area threshold: {AREA_THR}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T06:54:26.185284Z","iopub.execute_input":"2026-01-07T06:54:26.185600Z","iopub.status.idle":"2026-01-07T06:54:26.198708Z","shell.execute_reply.started":"2026-01-07T06:54:26.185578Z","shell.execute_reply":"2026-01-07T06:54:26.198070Z"}},"outputs":[],"execution_count":null},{"id":"model","cell_type":"code","source":"# ═══════════════════════════════════════════════════════════════════════════\n# MODEL DEFINITION - Must match pretrained architecture\n# ═══════════════════════════════════════════════════════════════════════════\nclass DinoTinyDecoder(nn.Module):\n    def __init__(self, in_ch=768, out_ch=1):\n        super().__init__()\n        self.block1 = nn.Sequential(\n            nn.Conv2d(in_ch, 384, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1)\n        )\n        self.block2 = nn.Sequential(\n            nn.Conv2d(384, 192, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1)\n        )\n        self.block3 = nn.Sequential(\n            nn.Conv2d(192, 96, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True)\n        )\n        self.conv_out = nn.Conv2d(96, out_ch, kernel_size=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        x = F.interpolate(x, size=target_size, mode='bilinear', align_corners=False)\n        return x\n\nclass DinoSegmenter(nn.Module):\n    def __init__(self, encoder, processor):\n        super().__init__()\n        self.encoder = encoder\n        self.processor = processor\n        for p in self.encoder.parameters():\n            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        fmap = feats[:, 1:, :].permute(0, 2, 1).reshape(B, C, s, s)\n        return fmap\n        \n    def forward(self, x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap, (IMG_SIZE, IMG_SIZE))\n\n# Load encoder and processor\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)\n\n# Initialize and load pretrained model\nmodel = DinoSegmenter(encoder, processor).to(device)\nmodel.load_state_dict(torch.load(MODEL_LOC, map_location=device))\nmodel.eval()\nprint(f\"✅ Loaded pretrained model from: {MODEL_LOC}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T06:54:26.199498Z","iopub.execute_input":"2026-01-07T06:54:26.199795Z","iopub.status.idle":"2026-01-07T06:54:29.598946Z","shell.execute_reply.started":"2026-01-07T06:54:26.199761Z","shell.execute_reply":"2026-01-07T06:54:29.598297Z"}},"outputs":[],"execution_count":null},{"id":"inference","cell_type":"code","source":"# ═══════════════════════════════════════════════════════════════════════════\n# INFERENCE FUNCTIONS\n# ═══════════════════════════════════════════════════════════════════════════\ndef rle_encode(mask):\n    \"\"\"Run-length encoding for submission.\"\"\"\n    pixels = mask.T.flatten()\n    dots = np.where(pixels == 1)[0]\n    if len(dots) == 0:\n        return \"authentic\"\n    run_lengths = []\n    prev = -2\n    for b in dots:\n        if b > prev + 1:\n            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\n@torch.no_grad()\ndef predict_single(model, img_tensor):\n    \"\"\"Single prediction without TTA.\"\"\"\n    return torch.sigmoid(model(img_tensor))[0, 0].cpu().numpy()\n\n@torch.no_grad()\ndef predict_with_tta(model, img_tensor):\n    \"\"\"Prediction with test-time augmentation (H+V flips).\"\"\"\n    predictions = []\n    \n    # Original\n    pred = torch.sigmoid(model(img_tensor))\n    predictions.append(pred)\n    \n    # Horizontal flip\n    pred_h = torch.sigmoid(model(torch.flip(img_tensor, dims=[3])))\n    predictions.append(torch.flip(pred_h, dims=[3]))\n    \n    # Vertical flip\n    pred_v = torch.sigmoid(model(torch.flip(img_tensor, dims=[2])))\n    predictions.append(torch.flip(pred_v, dims=[2]))\n    \n    # Average predictions\n    avg_pred = torch.stack(predictions).mean(0)\n    return avg_pred[0, 0].cpu().numpy()\n\ndef postprocess_optimized(prob, orig_size):\n    \"\"\"\n    Optimized post-processing using 95th percentile threshold.\n    Key improvement from baseline.\n    \"\"\"\n    # Dynamic threshold: 95th percentile (from handoff spec)\n    thresh = np.percentile(prob, PERCENTILE_THR)\n    thresh = max(MIN_THR, thresh)  # Minimum floor\n    \n    # Binarize\n    mask = (prob > thresh).astype(np.uint8)\n    \n    # Morphological operations (closing + opening)\n    kernel_close = np.ones((5, 5), np.uint8)\n    kernel_open = np.ones((3, 3), np.uint8)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_close)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open)\n    \n    # Resize to original size\n    mask = cv2.resize(mask, orig_size, interpolation=cv2.INTER_NEAREST)\n    \n    return mask, thresh\n\ndef predict_image(model, img_path):\n    \"\"\"Full prediction pipeline.\"\"\"\n    # Load image\n    pil = Image.open(img_path).convert(\"RGB\")\n    orig_size = pil.size  # (W, H)\n    \n    # Prepare tensor\n    img_resized = pil.resize((IMG_SIZE, IMG_SIZE))\n    img_arr = np.array(img_resized, np.float32) / 255.0\n    img_tensor = torch.from_numpy(img_arr).permute(2, 0, 1)[None].to(device)\n    \n    # Predict\n    if USE_TTA:\n        prob = predict_with_tta(model, img_tensor)\n    else:\n        prob = predict_single(model, img_tensor)\n    \n    # Post-process\n    mask, thresh = postprocess_optimized(prob, orig_size)\n    \n    # Calculate area and confidence\n    area = int(mask.sum())\n    if area > 0:\n        mask_small = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n        mean_conf = float(prob[mask_small == 1].mean())\n    else:\n        mean_conf = 0.0\n    \n    # Decision\n    if area < AREA_THR or mean_conf < MEAN_THR:\n        return \"authentic\", None, {\"area\": area, \"mean\": mean_conf, \"thr\": thresh}\n    \n    return \"forged\", mask, {\"area\": area, \"mean\": mean_conf, \"thr\": thresh}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T06:54:29.599704Z","iopub.execute_input":"2026-01-07T06:54:29.599910Z","iopub.status.idle":"2026-01-07T06:54:29.611785Z","shell.execute_reply.started":"2026-01-07T06:54:29.599890Z","shell.execute_reply":"2026-01-07T06:54:29.611133Z"}},"outputs":[],"execution_count":null},{"id":"submission","cell_type":"code","source":"# ═══════════════════════════════════════════════════════════════════════════\n# GENERATE SUBMISSION\n# ═══════════════════════════════════════════════════════════════════════════\nprint(\"\\n\" + \"=\"*60)\nprint(\"GENERATING SUBMISSION\")\nprint(\"=\"*60)\n\n# Get test files\ntest_files = sorted(Path(TEST_DIR).glob(\"*.png\"))\nprint(f\"Test images: {len(test_files)}\")\n\nresults = []\nforged_count = 0\nauthentic_count = 0\n\nfor img_path in tqdm(test_files, desc=\"Inference\"):\n    case_id = img_path.stem\n    \n    label, mask, info = predict_image(model, str(img_path))\n    \n    if label == \"authentic\":\n        annotation = \"authentic\"\n        authentic_count += 1\n    else:\n        annotation = rle_encode(mask.astype(np.uint8))\n        forged_count += 1\n    \n    results.append({\n        \"case_id\": case_id,\n        \"annotation\": annotation\n    })\n\nprint(f\"\\nResults: {forged_count} forged, {authentic_count} authentic\")\n\n# Create and save submission\nsubmission = pd.DataFrame(results)\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(f\"\\n✅ Submission saved: submission.csv\")\nprint(submission.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T06:54:29.613157Z","iopub.execute_input":"2026-01-07T06:54:29.613434Z","iopub.status.idle":"2026-01-07T06:54:30.579161Z","shell.execute_reply.started":"2026-01-07T06:54:29.613399Z","shell.execute_reply":"2026-01-07T06:54:30.578513Z"}},"outputs":[],"execution_count":null},{"id":"validate","cell_type":"code","source":"# Validation check\nprint(\"\\n\" + \"=\"*60)\nprint(\"VALIDATION\")\nprint(\"=\"*60)\nprint(f\"Shape: {submission.shape}\")\nprint(f\"Columns: {submission.columns.tolist()}\")\nprint(f\"Authentic: {(submission['annotation'] == 'authentic').sum()}\")\nprint(f\"Forged: {(submission['annotation'] != 'authentic').sum()}\")\nprint(\"\\n✅ Complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T06:54:30.580052Z","iopub.execute_input":"2026-01-07T06:54:30.580309Z","iopub.status.idle":"2026-01-07T06:54:30.585637Z","shell.execute_reply.started":"2026-01-07T06:54:30.580285Z","shell.execute_reply":"2026-01-07T06:54:30.585049Z"}},"outputs":[],"execution_count":null}]}