{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14167480,"sourceType":"datasetVersion","datasetId":9030673},{"sourceId":14407528,"sourceType":"datasetVersion","datasetId":9153851},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3326,"modelId":986},{"sourceId":686586,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":520737,"modelId":534998},{"sourceId":4535,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3327,"modelId":986}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🕵️‍♀️ Scientific Image Forgery Detection: DINOv2 High-Res\n### **Score: 0.332 (Public LB)**\n\nThis notebook implements a robust semantic segmentation pipeline for detecting copy-move and splicing forgeries in scientific imagery.\n\n### **Key Features:**\n* **Backbone:** `DINOv2 (Base)` - Frozen Vision Transformer features.\n* **Resolution:** **4500px** - Upgraded input size to preserve microscopic artifacts.\n* **Inference:** Sparse Sliding Window (`Stride=400`) + TTA.\n* **Post-Processing:** Strict probability thresholding (`0.19`) with adaptive cleaning.\n\n### **Architecture:**\nThe model uses a custom \"Tiny Decoder\" that upsamples DINOv2's `14x14` patch embeddings into a high-fidelity segmentation mask.","metadata":{}},{"cell_type":"markdown","source":"## 1. Configuration\nWe prioritize **High Fidelity** inputs (4500px) while maintaining speed using a sparse stride (400).\n* **MAX_IMG_SIZE = 4500:** Prevents downscaling artifacts on large scientific figures.\n* **Threshold = 0.19:** A proven \"hard floor\" that rejects background noise while catching faint forgeries.","metadata":{}},{"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\nfrom scipy.ndimage import binary_fill_holes\nimport time\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    \n    # 🚀 SPEED OPTIMIZATION\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True \n\nseed_everything(42)\n\nclass CONFIG:\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    \n    # Paths\n    BASE_DIR  = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n    TEST_DIR  = f\"{BASE_DIR}/test_images\"\n    DINO_PATH = \"/kaggle/input/dinov2/pytorch/base/1\"\n    MODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-U52/CNNDINOv2-U52/model_seg_final.pt'\n    MAX_IMG_SIZE = 3000     \n    window_size = 518       \n    stride = 300            \n    use_tta = True          \n    batch_size = 32         \n    min_mean_conf = 0.19    \n    alpha_grad = 0.50       \n    min_pixel_size = 50     \n    TIME_LIMIT_HOURS = 8.5 ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Model Architecture\nA lightweight CNN decoder is attached to the DINOv2 backbone. It progressively upsamples the feature maps to recover spatial resolution.","metadata":{}},{"cell_type":"code","source":"class 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        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        fmap = feats[:, 1:, :].permute(0, 2, 1)\n        s = int(math.sqrt(N - 1))\n        fmap = fmap.reshape(B, C, s, s)\n        return fmap\n        \n    def forward_seg(self, x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap, (CONFIG.window_size, CONFIG.window_size))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Sliding Window & TTA\nTo handle 4K+ resolution images without OOM errors, we use a sliding window approach with **Edge Padding**.\n* **TTA:** Standard 3-Way Test Time Augmentation (Flip H, Flip V).\n* **Fusion:** We combine Global context (resized image) with Local details (crops) using a 0.4/0.6 weighted average.","metadata":{}},{"cell_type":"code","source":"def load_model_safely():\n    try:\n        processor = AutoImageProcessor.from_pretrained(CONFIG.DINO_PATH, local_files_only=True, use_fast=False)\n        encoder = AutoModel.from_pretrained(CONFIG.DINO_PATH, local_files_only=True).eval().to(CONFIG.device)\n        model = DinoSegmenter(encoder, processor).to(CONFIG.device)\n        if os.path.exists(CONFIG.MODEL_LOC):\n            model.load_state_dict(torch.load(CONFIG.MODEL_LOC, map_location=CONFIG.device))\n            print(f\"✅ Loaded weights: {CONFIG.MODEL_LOC}\")\n        model.eval()\n        return model\n    except Exception as e:\n        print(f\"Fatal Error: {e}\")\n        return None\n\nmodel = load_model_safely()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Adaptive Mask Generation\nThe probability map is refined using:\n1.  **Sobel Edge Boosting:** Enhances boundaries of spliced regions.\n2.  **Morphological Cleaning:** A `7x7` Closing operation connects fragmented forgery blobs.\n3.  **Safety Filter:** Blobs smaller than 100px are removed to prevent False Positives.\n","metadata":{}},{"cell_type":"code","source":"@torch.no_grad()\ndef predict_batch(images_list, model):\n    batch_np = np.stack(images_list)\n    x = torch.from_numpy(batch_np).float().permute(0,3,1,2) / 255.0\n    x = x.to(CONFIG.device)\n    \n    # 1. Original\n    pred = torch.sigmoid(model.forward_seg(x))\n    \n    if CONFIG.use_tta:\n        # 2. Flip H\n        pred_h = torch.flip(torch.sigmoid(model.forward_seg(torch.flip(x, [3]))), [3])\n        # 3. Flip V\n        pred_v = torch.flip(torch.sigmoid(model.forward_seg(torch.flip(x, [2]))), [2])\n        # 4. Flip HV (Rotate 180) - Extra stability\n        pred_hv = torch.flip(torch.sigmoid(model.forward_seg(torch.flip(x, [2, 3]))), [2, 3])\n        \n        # 4-Way Average\n        pred = (pred + pred_h + pred_v + pred_hv) / 4.0\n        \n    return pred.cpu().numpy()[:, 0, :, :]\n\ndef sliding_window_inference(pil_img, model):\n    w_orig, h_orig = pil_img.size\n    \n    if w_orig <= CONFIG.window_size and h_orig <= CONFIG.window_size:\n        pad_w = max(0, CONFIG.window_size - w_orig)\n        pad_h = max(0, CONFIG.window_size - h_orig)\n        img_np = np.array(pil_img)\n        img_padded = np.pad(img_np, ((0, pad_h), (0, pad_w), (0, 0)), mode='edge')\n        pred = predict_batch([img_padded], model)[0]\n        return pred[:h_orig, :w_orig]\n\n    full_prob = np.zeros((h_orig, w_orig), dtype=np.float32)\n    count_map = np.zeros((h_orig, w_orig), dtype=np.float32)\n    img_np = np.array(pil_img)\n    \n    crops, coords = [], []\n    for y in range(0, h_orig, CONFIG.stride):\n        for x in range(0, w_orig, CONFIG.stride):\n            y_start = min(y, h_orig - CONFIG.window_size)\n            x_start = min(x, w_orig - CONFIG.window_size)\n            y_start = max(0, y_start); x_start = max(0, x_start)\n            \n            crop = img_np[y_start:y_start+CONFIG.window_size, x_start:x_start+CONFIG.window_size]\n            if crop.shape[0] != CONFIG.window_size or crop.shape[1] != CONFIG.window_size:\n                 pad_h = CONFIG.window_size - crop.shape[0]\n                 pad_w = CONFIG.window_size - crop.shape[1]\n                 crop = np.pad(crop, ((0, pad_h), (0, pad_w), (0, 0)), mode='edge')\n            crops.append(crop)\n            coords.append((y_start, x_start))\n            \n    for i in range(0, len(crops), CONFIG.batch_size):\n        batch_crops = crops[i:i+CONFIG.batch_size]\n        batch_coords = coords[i:i+CONFIG.batch_size]\n        batch_preds = predict_batch(batch_crops, model)\n        if len(batch_crops) == 1 and len(batch_preds.shape) == 3:\n             batch_preds = np.expand_dims(batch_preds, axis=0)\n        \n        for pred, (y, x) in zip(batch_preds, batch_coords):\n            h_actual = min(CONFIG.window_size, h_orig - y)\n            w_actual = min(CONFIG.window_size, w_orig - x)\n            full_prob[y:y+h_actual, x:x+w_actual] += pred[:h_actual, :w_actual]\n            count_map[y:y+h_actual, x:x+w_actual] += 1\n            \n    return full_prob / (count_map + 1e-6)\n\ndef enhanced_adaptive_mask(prob, alpha_grad=0.50):\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 = np.sqrt(gx**2 + gy**2)\n    grad_max = grad.max()\n    if grad_max > 0: grad /= grad_max\n    \n    enhanced = (1 - alpha_grad) * prob + alpha_grad * grad\n    enhanced = cv2.GaussianBlur(enhanced, (3,3), 0)\n    \n    # Hard Floor Threshold (0.19)\n    mask = (enhanced > CONFIG.min_mean_conf).astype(np.uint8)\n    \n    # 7x7 Close (Proven Best)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((7,7), np.uint8))\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3,3), np.uint8))\n    \n    return mask, enhanced\n\ndef pipeline_fusion(pil_image, model):\n    w_orig, h_orig = pil_image.size\n    \n    # 1. Global (Lanczos)\n    img_global = pil_image.resize((CONFIG.window_size, CONFIG.window_size), Image.LANCZOS)\n    prob_global_small = predict_batch([np.array(img_global)], model)[0]\n    prob_global = cv2.resize(prob_global_small, (w_orig, h_orig), interpolation=cv2.INTER_LINEAR)\n    \n    # 2. Local\n    if max(w_orig, h_orig) > CONFIG.window_size * 1.2:\n        prob_detail = sliding_window_inference(pil_image, model)\n        # Fixed 0.4/0.6 Fusion (Proven Stability)\n        final_prob = (0.4 * prob_global) + (0.6 * prob_detail)\n    else:\n        final_prob = prob_global\n        \n    # 3. Post-Process\n    mask, enhanced_prob = enhanced_adaptive_mask(final_prob, alpha_grad=CONFIG.alpha_grad)\n    \n    num, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)\n    final_mask = np.zeros_like(mask)\n    \n    for i in range(1, num):\n        area = stats[i, cv2.CC_STAT_AREA]\n        if area < CONFIG.min_pixel_size: continue\n        \n        blob_mean = enhanced_prob[labels == i].mean()\n        if blob_mean > CONFIG.min_mean_conf:\n            final_mask[labels == i] = 1\n            \n    final_mask = binary_fill_holes(final_mask).astype(np.uint8)\n    \n    if final_mask.sum() == 0:\n        return \"authentic\", final_mask\n    return \"forged\", final_mask\n\ndef rle_encode(mask):\n    pixels = mask.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    if len(runs) == 0: return \"authentic\"\n    return json.dumps([int(x) for x in runs])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Main Loop\nProcessing test images with a time limit safety check.","metadata":{}},{"cell_type":"code","source":"def run_inference():\n    rows = []\n    test_files = sorted(os.listdir(CONFIG.TEST_DIR))\n    total_files = len(test_files)\n    \n    START_TIME = time.time()\n    LIMIT_SECONDS = CONFIG.TIME_LIMIT_HOURS * 3600\n    \n    print(f\"🚀 Running Forecasting Robust: 3000px + Stride 300 (Dense) + 4-Way TTA\")\n    \n    for i, f in tqdm(enumerate(test_files), total=total_files):\n        if time.time() - START_TIME > LIMIT_SECONDS:\n            print(f\"⚠️ Limit reached (8.5h). Filling remaining {total_files - i}.\")\n            for r_f in test_files[i:]:\n                 rows.append({\"case_id\": Path(r_f).stem, \"annotation\": \"authentic\"})\n            break\n            \n        try:\n            pil = Image.open(Path(CONFIG.TEST_DIR)/f).convert(\"RGB\")\n            \n            w, h = pil.size\n            scale = 1.0\n            if max(w, h) > CONFIG.MAX_IMG_SIZE:\n                scale = CONFIG.MAX_IMG_SIZE / max(w, h)\n                pil = pil.resize((int(w*scale), int(h*scale)), Image.BILINEAR)\n            \n            label, mask = pipeline_fusion(pil, model)\n            \n            if scale != 1.0:\n                mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)\n            \n            if label == \"authentic\":\n                annot = \"authentic\"\n            else:\n                annot = rle_encode(mask)\n        except Exception as e:\n            print(f\"Error {f}: {e}\")\n            annot = \"authentic\"\n            \n        rows.append({\"case_id\": Path(f).stem, \"annotation\": annot})\n        \n    sub = pd.DataFrame(rows)\n    sample = pd.read_csv(CONFIG.BASE_DIR + \"/sample_submission.csv\")\n    sample[\"case_id\"] = sample[\"case_id\"].astype(str)\n    final = sample[[\"case_id\"]].merge(sub, on=\"case_id\", how=\"left\").fillna(\"authentic\")\n    final.to_csv(\"submission.csv\", index=False)\n    print(\"✅ Submission saved.\")\n\nif __name__ == \"__main__\":\n    run_inference()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}