{"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":"gpu","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"},{"sourceId":14505551,"sourceType":"datasetVersion","datasetId":9262410},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":4803.853475,"end_time":"2026-01-04T03:07:58.189377","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-04T01:47:54.335902","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Notebook Overview: UNET-DINOv2-Hybrid \n\nThis notebook is adapted from the Scientific-Forensics-DINOv2-CNN model by Pankaj Gupta and CNN-DINOv2 Hybrid model by Abhishek Godara.It implements a deep learning model for scientific image forgery detection, whose core architecture is an encoder-decoder segmentation network based on a pre-trained Vision Transformer. Building on the Scientific-Forensics-DINOv2-CNN, the decoder is redesigned into a U-Net-style architecture with skip connections.The main architecture includes:\n\n- **Encoder-DINOv2:** DINOv2 is a powerful self-supervised learning Vision Transformer (ViT) model. It takes input images and outputs a series of visual features at different hierarchical levels.\n- **Decoder-DinoUnetDecoder:** Drawing on the design philosophy of U-Net, this decoder takes the multi-level features transmitted from the encoder and gradually restores the spatial resolution through upsampling operations and skip connections.\n\nThis model leverages the powerful feature extraction capability of DINOv2 and locates forged regions via an elaborately designed decoder with skip connections, which is ultimately applied to image classification.\n","metadata":{"papermill":{"duration":0.003455,"end_time":"2026-01-04T01:47:59.525301","exception":false,"start_time":"2026-01-04T01:47:59.521846","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"#  Step 1: Model Setup, Dataset Preparation, and Validation Scoring","metadata":{"papermill":{"duration":0.002727,"end_time":"2026-01-04T01:47:59.531345","exception":false,"start_time":"2026-01-04T01:47:59.528618","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os, cv2, json, math, random, torch\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom pathlib import Path\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom torch.utils.data import Dataset, DataLoader\nimport torch.nn as nn, torch.nn.functional as F, torch.optim as optim\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    # This forces CUDA to use deterministic algorithms (slower but consistent)\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\")\nprint(device)\nBASE_DIR  = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nAUTH_DIR  = f\"{BASE_DIR}/train_images/authentic\"\nFORG_DIR  = f\"{BASE_DIR}/train_images/forged\"\nMASK_DIR  = f\"{BASE_DIR}/train_masks\"\nTEST_DIR  = f\"{BASE_DIR}/test_images\"\nDINO_PATH = \"/kaggle/input/dinov2/pytorch/base/1\"\n\nIMG_SIZE = 512\nBATCH_SIZE = 2\nMODEL_LOC= '/kaggle/input/weightsweights/model_seg2/model_seg2.pt'\n# INFERENCE UTILS\nAREA_THR = 200\nMEAN_THR = 0.22\n\n\nclass ForgerySegDataset(Dataset):\n    def __init__(self, auth_paths, forg_paths, mask_dir, img_size=IMG_SIZE):\n        self.samples = []\n        for p in forg_paths:\n            m = os.path.join(mask_dir, Path(p).stem + \".npy\")\n            if os.path.exists(m):\n                self.samples.append((p, m))\n        for p in auth_paths:\n            self.samples.append((p, None))\n        self.img_size = img_size\n    def __len__(self): return len(self.samples)\n    def __getitem__(self, idx):\n        img_path, mask_path = self.samples[idx]\n        img = Image.open(img_path).convert(\"RGB\")\n        w, h = img.size\n        if mask_path is None:\n            mask = np.zeros((h, w), np.uint8)\n        else:\n            m = np.load(mask_path)\n            if m.ndim == 3: m = np.max(m, axis=0)\n            mask = (m > 0).astype(np.uint8)\n        img_r = img.resize((IMG_SIZE, IMG_SIZE))\n        mask_r = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n        img_t = torch.from_numpy(np.array(img_r, np.float32)/255.).permute(2,0,1)\n        mask_t = torch.from_numpy(mask_r[None, ...].astype(np.float32))\n        return img_t, mask_t\n\n\n#  MODEL (DINOv2 + Decoder)\n\nfrom transformers import AutoImageProcessor, AutoModel\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\nclass ConvBlock(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1)\n        )\n    def forward(self, x):\n        return self.conv(x)\n\nclass DinoUnetDecoder(nn.Module):\n    def __init__(self, in_ch=768, out_ch=1):\n        super().__init__()\n\n        self.dino3 = nn.Conv2d(768,128, 3,padding=1, bias=False)\n        # Dinov2 Stage2\n        self.dino2 = nn.Conv2d(768,256, 3,padding=1, bias=False)\n        # Dinov2 Stage1\n        self.dino1 = nn.Conv2d(in_ch,512, 3,padding=1, bias=False)\n\n        # Final Output: 96 -> 1\n        self.conv_out = nn.Conv2d(128, out_ch, kernel_size=1)\n\n        self.AfterCatBlock1 = nn.Sequential(\n            ConvBlock(1024, 512)\n        )\n        self.AfterCatBlock2 = nn.Sequential(\n            ConvBlock(512, 256)\n        )\n        self.AfterCatBlock3 = nn.Sequential(\n            nn.Conv2d(256, 128, 3, padding=1, bias=False),\n            nn.ReLU(inplace=True),\n        )\n\n        self.conv1 = nn.Conv2d(in_ch, 512, 3, padding=1, bias=False)\n        self.conv2 = nn.Conv2d(512, 256, 3, padding=1, bias=False)\n        self.conv3 = nn.Conv2d(256, 128, 3, padding=1, bias=False)\n\n\n    def forward(self, f):\n        up_f1 = f[-1]\n\n        up_f1 = F.interpolate(self.conv1(up_f1), size=(64, 64), mode='bilinear', align_corners=False)\n        e1 = F.interpolate(self.dino1(f[-2]), size=up_f1.shape[-2:], mode='bilinear', align_corners=False)\n        c1 = self.AfterCatBlock1(torch.cat([up_f1, e1], dim=1))\n\n        up_f2 = F.interpolate(self.conv2(c1), size=(128, 128), mode='bilinear', align_corners=False)\n        e2 = F.interpolate(self.dino2(f[-3]), size=up_f2.shape[-2:], mode='bilinear', align_corners=False)\n        c2 = self.AfterCatBlock2(torch.cat([up_f2, e2], dim=1))\n\n        up_f3 = F.interpolate(self.conv3(c2), size=(256, 256), mode='bilinear', align_corners=False)\n        e3 = F.interpolate(self.dino3(f[0]), size=up_f3.shape[-2:], mode='bilinear', align_corners=False)\n        c3 = self.AfterCatBlock3(torch.cat([up_f3, e3], dim=1))\n\n        x = F.interpolate(self.conv_out(c3), size=(IMG_SIZE, IMG_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, self.processor = encoder, processor\n        for p in self.encoder.parameters(): p.requires_grad = False\n        self.seg_head = DinoUnetDecoder(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        # with torch.no_grad():\n        #     feats = self.encoder(**inputs).last_hidden_state\n        # feats = self.encoder(**inputs).last_hidden_state\n        all_feats = self.encoder(**inputs, output_hidden_states=True, return_dict=True)\n        num_layers = self.encoder.config.num_hidden_layers  # DINOv2-base: 12层\n        B, N, C = all_feats.last_hidden_state.shape\n        s = int(math.sqrt(N - 1))\n\n        # DINOv2有12个隐藏层，索引从1到12，对应hidden_states[1]到hidden_states[12]\n        selected_layers = [4, 7, 10, 12]  # 按照要求的顺序\n        selected_feats = []\n\n        for layer_idx in selected_layers:          \n            layer_output = all_feats.hidden_states[layer_idx]\n            feat = layer_output[:, 1:, :].permute(0, 2, 1).reshape(B, C, s, s)\n            selected_feats.append(feat)\n\n        return selected_feats\n\n    def forward_seg(self, x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap)\n\n\nAREA_THR = 200\nMEAN_THR = 0.21\nUSE_TTA = True\n\nauth_imgs = sorted([str(Path(AUTH_DIR) / f) for f in os.listdir(AUTH_DIR)])\nforg_imgs = sorted([str(Path(FORG_DIR) / f) for f in os.listdir(FORG_DIR)])\ntrain_auth, val_auth = train_test_split(auth_imgs, test_size=0.2, random_state=42)\ntrain_forg, val_forg = train_test_split(forg_imgs, test_size=0.2, random_state=42)\n\ntrain_loader = DataLoader(ForgerySegDataset(train_auth, train_forg, MASK_DIR),\n                          batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(ForgerySegDataset(val_auth, val_forg, MASK_DIR),\n                        batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nmodel_seg = DinoSegmenter(encoder, processor).to(device)\n\n# Load pretrained weights if MODEL_LOC is specified\nif MODEL_LOC is not None and os.path.exists(MODEL_LOC):\n    model_seg.load_state_dict(torch.load(MODEL_LOC, map_location=device))\n    print(f\"✅ Loaded pretrained model from: {MODEL_LOC}\")\n    model_seg.eval()  # Set model to evaluation mode\n\n\n@torch.no_grad()\ndef segment_prob_map(pil):\n    x = torch.from_numpy(np.array(pil.resize((IMG_SIZE, IMG_SIZE)), np.float32) / 255.).permute(2, 0, 1)[None].to(\n        device)\n    prob = torch.sigmoid(model_seg.forward_seg(x))[0, 0].cpu().numpy()\n    return prob\n\n\n@torch.no_grad()\ndef segment_prob_map_with_tta(pil):\n    # 1. Preprocessing: Resize, Normalize, and move to Device\n    x = torch.from_numpy(np.array(pil.resize((IMG_SIZE, IMG_SIZE)), np.float32) / 255.).permute(2, 0, 1)[None].to(\n        device)\n\n    predictions = []\n\n    # 2. Original Prediction\n    pred_orig = torch.sigmoid(model_seg.forward_seg(x))\n    predictions.append(pred_orig)\n\n    # 3. Horizontal Flip TTA (dim 3)\n    # Flip input -> Predict -> Flip output back\n    pred_h = torch.sigmoid(model_seg.forward_seg(torch.flip(x, dims=[3])))\n    predictions.append(torch.flip(pred_h, dims=[3]))\n\n    # 4. Vertical Flip TTA (dim 2)\n    # Flip input -> Predict -> Flip output back\n    pred_v = torch.sigmoid(model_seg.forward_seg(torch.flip(x, dims=[2])))\n    predictions.append(torch.flip(pred_v, dims=[2]))\n\n    # 5. Average the predictions and format as numpy\n    # We stack the 3 predictions and take the mean across the stack dimension (0)\n    prob = torch.stack(predictions).mean(0)[0, 0].cpu().numpy()\n\n    return prob\n\n\ndef enhanced_adaptive_mask(prob, alpha_grad=0.45):\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_mag = np.sqrt(gx ** 2 + gy ** 2)\n    grad_norm = grad_mag / (grad_mag.max() + 1e-6)\n    enhanced = (1 - alpha_grad) * prob + alpha_grad * grad_norm\n    enhanced = cv2.GaussianBlur(enhanced, (3, 3), 0)\n    thr = np.mean(enhanced) + 0.3 * np.std(enhanced)\n    mask = (enhanced > thr).astype(np.uint8)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))\n    return mask, thr\n\n\ndef finalize_mask(prob, orig_size):\n    mask, thr = enhanced_adaptive_mask(prob)\n    mask = cv2.resize(mask, orig_size, interpolation=cv2.INTER_NEAREST)\n    return mask, thr\n\n\ndef pipeline_final(pil):\n    if USE_TTA:\n        prob = segment_prob_map_with_tta(pil)\n    else:\n        prob = segment_prob_map(pil)\n    mask, thr = finalize_mask(prob, pil.size)\n    area = int(mask.sum())\n    mean_inside = float(\n        prob[cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST) == 1].mean()) if area > 0 else 0.0\n    if area < AREA_THR or mean_inside < MEAN_THR:\n        return \"authentic\", None, {\"area\": area, \"mean_inside\": mean_inside, \"thr\": thr}\n    return \"forged\", mask, {\"area\": area, \"mean_inside\": mean_inside, \"thr\": thr}\n\n\nfrom sklearn.metrics import f1_score\n\nval_items = [(p, 1) for p in val_forg[:10]]\nresults = []\nfor p, _ in tqdm(val_items, desc=\"Validation forged-only\"):\n    pil = Image.open(p).convert(\"RGB\")\n    label, m_pred, dbg = pipeline_final(pil)\n    m_gt = np.load(Path(MASK_DIR) / f\"{Path(p).stem}.npy\")\n    if m_gt.ndim == 3: m_gt = np.max(m_gt, axis=0)\n    m_gt = (m_gt > 0).astype(np.uint8)\n    m_pred = (m_pred > 0).astype(np.uint8) if m_pred is not None else np.zeros_like(m_gt)\n    f1 = f1_score(m_gt.flatten(), m_pred.flatten(), zero_division=0)\n    results.append((Path(p).stem, f1, dbg))\nprint(\"\\n F1-score par image falsifiée:\\n\")\nfor cid, f1, dbg in results:\n    print(f\"{cid} — F1={f1:.4f} | area={dbg['area']} mean={dbg['mean_inside']:.3f} thr={dbg['thr']:.3f}\")\nprint(f\"\\n Moyenne F1 (falsifiées) = {np.mean([r[1] for r in results]):.4f}\")\n","metadata":{"execution":{"iopub.status.busy":"2026-01-15T11:12:23.648200Z","iopub.execute_input":"2026-01-15T11:12:23.648959Z","iopub.status.idle":"2026-01-15T11:13:03.146202Z","shell.execute_reply.started":"2026-01-15T11:12:23.648927Z","shell.execute_reply":"2026-01-15T11:13:03.145320Z"},"papermill":{"duration":4782.135448,"end_time":"2026-01-04T03:07:41.669605","exception":false,"start_time":"2026-01-04T01:47:59.534157","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os, json, cv2\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\n\ndef rle_encode(mask: np.ndarray, fg_val: int = 1) -> str:\n    pixels = mask.T.flatten()\n    dots = np.where(pixels == fg_val)[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# --- Paths ---\nTEST_DIR = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\"\nSAMPLE_SUB = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/sample_submission.csv\"\nOUT_PATH = \"submission.csv\"\n\nrows = []\nfor f in tqdm(sorted(os.listdir(TEST_DIR)), desc=\"Inference on Test Set\"):\n    pil = Image.open(Path(TEST_DIR)/f).convert(\"RGB\")\n    label, mask, dbg = pipeline_final(pil)  # utilise la version améliorée\n\n    # Sécurisation masque\n    if mask is None:\n        mask = np.zeros(pil.size[::-1], np.uint8)\n    else:\n        mask = np.array(mask, dtype=np.uint8)\n\n    # Annotation finale\n    if label == \"authentic\":\n        annot = \"authentic\"\n    else:\n        annot = rle_encode((mask > 0).astype(np.uint8))\n\n    rows.append({\n        \"case_id\": Path(f).stem,\n        \"annotation\": annot,\n        \"area\": int(dbg.get(\"area\", mask.sum())),\n        \"mean\": float(dbg.get(\"mean_inside\", 0.0)),\n        \"thr\": float(dbg.get(\"thr\", 0.0))\n    })\n\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)\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✅ Saved submission file: {OUT_PATH}\")\nprint(final.head(10))\n\n\nsample_files = sorted(os.listdir(TEST_DIR))[:5]\nfor f in sample_files:\n    pil = Image.open(Path(TEST_DIR)/f).convert(\"RGB\")\n    label, mask, dbg = pipeline_final(pil)\n    mask = np.array(mask, dtype=np.uint8) if mask is not None else np.zeros(pil.size[::-1], np.uint8)\n\n    print(f\"{'🔴' if label=='forged' else '🟢'} {f}: {label} | area={mask.sum()} mean={dbg.get('mean_inside', 0):.3f}\")\n\n    if label == \"authentic\":\n        plt.figure(figsize=(5,5))\n        plt.imshow(pil)\n        plt.title(f\"{f} — Authentic\")\n        plt.axis(\"off\")\n        plt.show()\n    else:\n        plt.figure(figsize=(10,5))\n        plt.subplot(1,2,1)\n        plt.imshow(pil)\n        plt.title(\"Original Image\")\n        plt.axis(\"off\")\n        plt.subplot(1,2,2)\n        plt.imshow(pil)\n        plt.imshow(mask, alpha=0.45, cmap=\"Reds\")\n        plt.title(f\"Predicted Forged Mask\\nArea={mask.sum()} | Mean={dbg.get('mean_inside', 0):.3f}\")\n        plt.axis(\"off\")\n        plt.show()","metadata":{"execution":{"iopub.status.busy":"2026-01-15T11:13:03.147713Z","iopub.execute_input":"2026-01-15T11:13:03.148350Z","iopub.status.idle":"2026-01-15T11:13:03.753116Z","shell.execute_reply.started":"2026-01-15T11:13:03.148321Z","shell.execute_reply":"2026-01-15T11:13:03.752511Z"},"papermill":{"duration":2.205312,"end_time":"2026-01-04T03:07:44.02708","exception":false,"start_time":"2026-01-04T03:07:41.821768","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 2: Hybrid Model — DINOv2 Feature Extraction & CNN Decoder Integration","metadata":{"papermill":{"duration":0.050199,"end_time":"2026-01-04T03:07:41.771527","exception":false,"start_time":"2026-01-04T03:07:41.721328","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"## 🔴 Visualizing Predicted Masks with the CNN–DINOv2 Hybrid Model\n","metadata":{"papermill":{"duration":0.053937,"end_time":"2026-01-04T03:07:44.136353","exception":false,"start_time":"2026-01-04T03:07:44.082416","status":"completed"},"tags":[]}},{"cell_type":"code","source":"\nimport torch, cv2, math, numpy as np, matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\n\nevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n# Use global IMG_SIZE from cell 2 (518)\n\n# 1️ Predict probability map (from model)\n@torch.no_grad()\ndef predict_prob_map(pil):\n    \"\"\"Return DINOv2 segmentation probability map [0,1].\"\"\"\n    img = pil.resize((IMG_SIZE, IMG_SIZE))\n    x = torch.from_numpy(np.array(img, np.float32) / 255.).permute(2, 0, 1)[None].to(device)\n    logits = model_seg.forward_seg(x)\n    prob = torch.sigmoid(logits)[0, 0].cpu().numpy()\n    return prob\n\n\n# 2️ Post-processing consistent with pipeline_final\ndef adaptive_mask(prob, alpha_grad=0.35):\n    \"\"\"Adaptive enhancement + morphological refinement.\"\"\"\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_mag = np.sqrt(gx**2 + gy**2)\n    grad_norm = grad_mag / (grad_mag.max() + 1e-6)\n\n    enhanced = (1 - alpha_grad) * prob + alpha_grad * grad_norm\n    enhanced = cv2.GaussianBlur(enhanced, (3, 3), 0)\n\n    thr = np.mean(enhanced) + 0.3 * np.std(enhanced)\n    mask = (enhanced > thr).astype(np.uint8)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))\n    return mask, float(thr)\n\n\n# 3️ Unified visualization pipeline (uses same filtering logic as pipeline_final)\ndef pipeline_visual(pil):\n    if USE_TTA:\n        prob = segment_prob_map_with_tta(pil)\n    else:\n        prob = predict_prob_map(pil)\n    mask, thr = adaptive_mask(prob)\n    area = int(mask.sum())\n    mean_inside = float(prob[mask == 1].mean()) if area > 0 else 0.0\n\n    # ✅ FIXED: Use same decision rule as pipeline_final for consistency\n    if area < AREA_THR or mean_inside < MEAN_THR:\n        label = \"authentic\"\n    else:\n        label = \"forged\"\n    return label, mask, thr, area, mean_inside\n\n\n# 4️ Visualization (for validation forged samples)\nsample_forged = val_forg[:5]\nn = len(sample_forged)\nfig, axes = plt.subplots(n, 3, figsize=(12, n * 3))\nif n == 1:\n    axes = np.expand_dims(axes, axis=0)\n\nfor i, p in enumerate(sample_forged):\n    pil = Image.open(p).convert(\"RGB\")\n    label, m_pred, thr, area, mean = pipeline_visual(pil)\n\n    # Ground Truth mask\n    m_gt = np.load(Path(MASK_DIR)/f\"{Path(p).stem}.npy\")\n    if m_gt.ndim == 3: m_gt = np.max(m_gt, axis=0)\n    m_gt = (m_gt > 0).astype(np.uint8)\n\n    # Resize all for consistency\n    img_disp = cv2.resize(np.array(pil), (IMG_SIZE, IMG_SIZE))\n    gt_disp  = cv2.resize(m_gt, (IMG_SIZE, IMG_SIZE))\n    pr_disp  = cv2.resize(m_pred, (IMG_SIZE, IMG_SIZE))\n\n    # === Column 1: Original ===\n    axes[i, 0].imshow(img_disp)\n    axes[i, 0].set_title(\"🖼️ Original Image\", fontsize=11, weight=\"bold\")\n    axes[i, 0].axis(\"off\")\n\n    # === Column 2: Ground Truth ===\n    axes[i, 1].imshow(gt_disp, cmap=\"gray\")\n    axes[i, 1].set_title(\"✅ Ground Truth\", fontsize=11, weight=\"bold\")\n    axes[i, 1].axis(\"off\")\n\n    # === Column 3: Predicted Mask ===\n    axes[i, 2].imshow(img_disp)\n    axes[i, 2].imshow(pr_disp, cmap=\"coolwarm\", alpha=0.45)\n    axes[i, 2].set_title(f\"🔮 Predicted ({label})\\nThr={thr:.3f} | Area={area} | Mean={mean:.3f}\",\n                         fontsize=10)\n    axes[i, 2].axis(\"off\")\n\nplt.subplots_adjust(top=0.92, hspace=0.35)\nfig.suptitle(\"🔍 Segmentation of Forged Samples\",\n             fontsize=16, fontweight=\"bold\", color=\"#b30000\")\n\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2026-01-15T11:13:03.754116Z","iopub.execute_input":"2026-01-15T11:13:03.754370Z","iopub.status.idle":"2026-01-15T11:13:06.072631Z","shell.execute_reply.started":"2026-01-15T11:13:03.754346Z","shell.execute_reply":"2026-01-15T11:13:06.071873Z"},"papermill":{"duration":5.877977,"end_time":"2026-01-04T03:07:50.069292","exception":false,"start_time":"2026-01-04T03:07:44.191315","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🟢 Visualization of Authentic Images (Hybrid DINOv2-based Detector)","metadata":{"papermill":{"duration":0.065261,"end_time":"2026-01-04T03:07:50.202781","exception":false,"start_time":"2026-01-04T03:07:50.13752","status":"completed"},"tags":[]}},{"cell_type":"code","source":"\nimport matplotlib.pyplot as plt\nimport cv2, numpy as np\nfrom pathlib import Path\nfrom PIL import Image\n\n# Select a few authentic examples\nsample_auth = val_auth[:5]\nn = len(sample_auth)\n\nfig, axes = plt.subplots(n, 2, figsize=(9, n * 3))\nif n == 1:\n    axes = np.expand_dims(axes, axis=0)\n\nfor i, p in enumerate(sample_auth):\n    pil = Image.open(p).convert(\"RGB\")\n\n    # ====== USE pipeline_visual from Cell 2 ======\n    label, m_pred, thr, area, mean = pipeline_visual(pil)  # Uses robust_pipeline_final internally\n    # =============================================\n\n    # Predicted mask (should be empty for authentic images)\n    m_pred = (m_pred > 0).astype(np.uint8) if m_pred is not None else np.zeros((IMG_SIZE, IMG_SIZE))\n\n    # Resize for consistent display\n    img_disp = cv2.resize(np.array(pil), (IMG_SIZE, IMG_SIZE))\n    pr_disp = cv2.resize(m_pred, (IMG_SIZE, IMG_SIZE))\n\n    # === Column 1: Original Image ===\n    axes[i, 0].imshow(img_disp)\n    axes[i, 0].set_title(\"🖼️ Original Image\", fontsize=11, weight=\"bold\")\n    axes[i, 0].axis(\"off\")\n\n    # === Column 2: Predicted Mask ===\n    axes[i, 1].imshow(img_disp)\n    axes[i, 1].imshow(pr_disp, cmap=\"coolwarm\", alpha=0.45)\n    axes[i, 1].set_title(\n        f\"🟢 Predicted: {label.upper()}\\nArea={area} | Mean={mean:.3f} | Thr={thr:.3f}\",\n        fontsize=10\n    )\n    axes[i, 1].axis(\"off\")\n\n    for j in range(2):\n        axes[i, j].set_aspect(\"equal\")\n\nplt.subplots_adjust(top=0.90, hspace=0.35)\nfig.suptitle(\"🟢 Segmentation of Authentic Images\",\n             fontsize=16, fontweight=\"bold\", color=\"#009933\")\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2026-01-15T11:13:06.074070Z","iopub.execute_input":"2026-01-15T11:13:06.074272Z","iopub.status.idle":"2026-01-15T11:13:07.458373Z","shell.execute_reply.started":"2026-01-15T11:13:06.074253Z","shell.execute_reply":"2026-01-15T11:13:07.457587Z"},"papermill":{"duration":4.818679,"end_time":"2026-01-04T03:07:55.088316","exception":false,"start_time":"2026-01-04T03:07:50.269637","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}