{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 🧠 DINOv2 Multi-Task Pipeline\n\nThis notebook implements a **multi-task architecture** based on **DINOv2** —  \na frozen vision backbone that powers two lightweight heads:  \n**Segmentation** (detecting manipulated regions) and  \n**Classification** (authentic vs forged).\n\n---\n\n### ⚙️ Architecture Overview\n- **Encoder** → DINOv2-Base (frozen feature extractor)  \n- **Segmentation Head** → compact Conv–ReLU–Conv decoder  \n- **Classification Head** → linear layer over the CLS token  \n- **Optimization** → `AdamW` + `BCEWithLogitsLoss` / `CrossEntropyLoss`  \n- **Metrics** → IoU, Dice, Pixel Accuracy  \n\n---\n\n### 🔄 Pipeline Steps\n1. **Dataset Loading** → authentic / forged images + mask files (`.npy`)  \n2. **Feature Extraction** → DINOv2 encoder  \n3. **Segmentation** → binary mask prediction of forged regions  \n4. **Classification** → authenticity decision (A / F)  \n5. **Evaluation** → IoU, Dice & Accuracy on validation set  \n\n---\n\n> 🧩 **Note:** The model runs in **Offline Kaggle** mode —  \n> DINOv2 is loaded from `/kaggle/input/dinov2/pytorch/base/1`.\n","metadata":{}},{"cell_type":"code","source":"import os, gc, json, math, random\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport cv2\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\n\n# Repro & device\n\ndef seed_everything(s=42):\n    random.seed(s); np.random.seed(s); torch.manual_seed(s); torch.cuda.manual_seed_all(s)\nseed_everything(42)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Paths (adapte DINO_PATH)\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\"\nSAMPLE_SUB = f\"{BASE_DIR}/sample_submission.csv\"\n\n# DINOv2 local folder (contient config.json, preprocessor_config.json, pytorch_model.bin)\nDINO_PATH = \"/kaggle/input/dinov2/pytorch/base/1\"\n\n# Hyperparams\nIMG_SIZE = 256\nBATCH_SEG = 4\nBATCH_CLS = 32\nEPOCHS_SEG = 1\nEPOCHS_CLS = 1\nLR_SEG = 3e-4\nLR_CLS = 1e-3\nWEIGHT_DECAY = 1e-4\n\n\n# Utils: metrics + RLE\ndef binarize(x, thr=0.5):\n    return (x > thr).astype(np.uint8)\n\ndef iou_score(pred, gt, eps=1e-7):\n    p = binarize(pred); g = binarize(gt)\n    inter = (p & g).sum()\n    union = (p | g).sum()\n    return float(inter) / (float(union) + eps)\n\ndef dice_score(pred, gt, eps=1e-7):\n    p = binarize(pred); g = binarize(gt)\n    inter = (p & g).sum()\n    return float(2*inter) / (p.sum() + g.sum() + eps)\n\ndef pixel_acc(pred, gt, thr=0.5):\n    p = binarize(pred, thr); g = binarize(gt, thr)\n    return float((p == g).sum()) / float(np.prod(g.shape))\n\ndef rle_encode_numpy(mask):\n    \"\"\"Kaggle-compatible RLE: mask: (H,W) uint8 {0,1}, column-major.\"\"\"\n    pixels = mask.T.flatten()\n    dots = np.where(pixels == 1)[0]\n    if len(dots) == 0:\n        return \"[]\"  # JSON empty list (competition variant allows JSON-encoded runs)\n    run_lengths, 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(run_lengths)\n\n# -------------------------\n# Dataset: segmentation\n# - For forged: charge .npy ; si ndim==3 -> max proj\n# - For authentic: masque vide\n# -------------------------\nclass ForgerySegDataset(Dataset):\n    def __init__(self, auth_paths, forg_paths, mask_dir, img_size=256):\n        self.samples = []\n        self.mask_dir = mask_dir\n        self.img_size = img_size\n\n        for p in auth_paths:\n            self.samples.append((p, None))  # None => masque vide\n\n        for p in forg_paths:\n            stem = Path(p).stem\n            m = os.path.join(mask_dir, stem + \".npy\")\n            self.samples.append((p, m if os.path.exists(m) else None))\n\n    def __len__(self):\n        return len(self.samples)\n\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\n        # mask\n        if (mask_path is None):\n            mask = np.zeros((h, w), dtype=np.uint8)\n        else:\n            m = np.load(mask_path)\n            if m.ndim == 3:\n                m = np.max(m, axis=0)\n            mask = m.astype(np.uint8)\n            if mask.shape != (h, w):\n                # si jamais (rare), on redimensionne\n                mask = np.array(Image.fromarray(mask).resize((w, h), Image.NEAREST), dtype=np.uint8)\n\n        # resize to common size\n        img_r = img.resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR)\n        mask_r = np.array(Image.fromarray(mask).resize((IMG_SIZE, IMG_SIZE), Image.NEAREST), dtype=np.uint8)\n\n        # to tensor\n        img_t = torch.from_numpy(np.array(img_r, dtype=np.float32)/255.).permute(2,0,1)  # [3,H,W]\n        mask_t = torch.from_numpy(mask_r[None, ...].astype(np.float32))                  # [1,H,W] in {0.,1.}\n        return img_t, mask_t\n\n\n# Liste des fichiers\n\nauth_imgs = sorted([str(Path(AUTH_DIR)/f) for f in os.listdir(AUTH_DIR) if f.lower().endswith((\".png\",\".jpg\",\".jpeg\",\".tif\"))])\nforg_imgs = sorted([str(Path(FORG_DIR)/f) for f in os.listdir(FORG_DIR) if f.lower().endswith((\".png\",\".jpg\",\".jpeg\",\".tif\"))])\n\n# Split identique pour seg/cls\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\n\n# DINOv2 local (offline)\n\nfrom transformers import AutoImageProcessor, AutoModel\ntry:\n    processor = AutoImageProcessor.from_pretrained(DINO_PATH, local_files_only=True)\n    dino_encoder = AutoModel.from_pretrained(DINO_PATH, local_files_only=True)\nexcept Exception as e:\n    # Fallback minimal (sans internet) si jamais AutoModel échoue — on stoppe proprement\n    raise RuntimeError(f\"Impossible de charger DINOv2 localement depuis {DINO_PATH}: {e}\")\n\ndino_encoder.eval().to(device)\n# Dimensions patch: on déduit la grille (H_p, W_p) depuis image_size & patch_size si dispo\ncfg = getattr(dino_encoder, \"config\", None)\npatch = getattr(cfg, \"patch_size\", 14) if cfg else 14  # typical ViT patch size\ninp = getattr(processor, \"size\", {\"shortest_edge\": 224})\nif isinstance(inp, dict):\n    # pour ViT/DINO processors, on force 224x224\n    proc_w = inp.get(\"width\", 224); proc_h = inp.get(\"height\", 224)\n    if \"shortest_edge\" in inp: \n        proc_w = proc_h = int(inp[\"shortest_edge\"])\nelse:\n    proc_w = proc_h = 224\ngrid_h = proc_h // patch\ngrid_w = proc_w // patch\n# si CLS token présent: tokens == grid_h*grid_w + 1\n\n\n# Décodeur segmentation léger\n\nclass DinoTinyDecoder(nn.Module):\n    def __init__(self, in_ch, out_ch=1):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, 256, 3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(256, 64, 3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(64, out_ch, 1)\n        )\n    def forward(self, f, out_size):\n        x = nn.functional.interpolate(f, size=out_size, mode=\"bilinear\", align_corners=False)\n        return self.conv(x)\n\n\n# Modèle complet: DINO frozen + heads\n#  - Seg head (conv decoder)\n#  - Cls head (linear sur pooled feat)\nclass DinoMultiTask(nn.Module):\n    def __init__(self, encoder, processor, seg_ch=768, freeze=True):\n        super().__init__()\n        self.encoder = encoder\n        self.processor = processor\n        if freeze:\n            for p in self.encoder.parameters():\n                p.requires_grad = False\n        self.seg_head = DinoTinyDecoder(in_ch=seg_ch, out_ch=1)\n        self.cls_head = nn.Linear(seg_ch, 2)\n\n    def forward_features(self, images):  # images: [B,3,H,W] en [0,1]\n        B,_,H,W = images.shape\n        mean = torch.tensor(self.processor.image_mean, device=images.device).view(1,3,1,1)\n        std  = torch.tensor(self.processor.image_std,  device=images.device).view(1,3,1,1)\n        x = (images - mean) / std\n\n        # Prépare entrée processor (224x224 typiquement)\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(images.device)\n\n        with torch.no_grad():\n            feats = self.encoder(**inputs).last_hidden_state  # [B, N, C] (N = grid_h*grid_w [+1])\n        B, N, C = feats.shape\n        # Retire CLS si présent\n        expected_tokens = grid_h*grid_w\n        if N == expected_tokens + 1:\n            feats_spatial = feats[:, 1:, :]  # drop CLS\n            cls_token = feats[:, 0, :]       # [B,C]\n        elif N == expected_tokens:\n            feats_spatial = feats\n            cls_token = feats.mean(dim=1)\n        else:\n            # Sécurisation: déduire s via sqrt en tenant compte éventuel CLS\n            n_wo_cls = N-1 if int(math.isclose(math.sqrt(N-1), round(math.sqrt(N-1)))) else N\n            if n_wo_cls == N-1:\n                feats_spatial = feats[:,1:,:]; cls_token = feats[:,0,:]\n            else:\n                feats_spatial = feats; cls_token = feats.mean(dim=1)\n            s = int(round(math.sqrt(feats_spatial.shape[1])))\n            # override grid dims pour cohérence locale\n            # (pas nécessaire pour la suite car on reshape dynamiquement)\n        s = int(round(math.sqrt(feats_spatial.shape[1])))\n        fmap = feats_spatial.permute(0,2,1).reshape(B, C, s, s)  # [B,C,s,s]\n        return fmap, cls_token\n\n    def forward_seg(self, images):\n        fmap, _ = self.forward_features(images)\n        B,_,H,W = images.shape\n        logits = self.seg_head(fmap, out_size=(H,W))\n        return logits\n\n    def forward_cls(self, images):\n        _, cls_token = self.forward_features(images)\n        return self.cls_head(cls_token)\n\nmodel = DinoMultiTask(dino_encoder, processor, seg_ch=768, freeze=True).to(device)\n\n\n# Datasets & Loaders\ntrain_seg_ds = ForgerySegDataset(train_auth, train_forg, MASK_DIR, img_size=IMG_SIZE)\nval_seg_ds   = ForgerySegDataset(val_auth,   val_forg,   MASK_DIR, img_size=IMG_SIZE)\n\ntrain_seg_loader = DataLoader(train_seg_ds, batch_size=BATCH_SEG, shuffle=True, num_workers=2, pin_memory=True)\nval_seg_loader   = DataLoader(val_seg_ds,   batch_size=BATCH_SEG, shuffle=False, num_workers=2, pin_memory=True)\n\n# Pour classification, on réutilise les mêmes images: label 0 (auth) / 1 (forg)\nclass ForgeryClsDataset(Dataset):\n    def __init__(self, auth_paths, forg_paths, img_size=256):\n        self.items = [(p,0) for p in auth_paths] + [(p,1) for p in forg_paths]\n        self.img_size = img_size\n    def __len__(self): return len(self.items)\n    def __getitem__(self, idx):\n        p, y = self.items[idx]\n        img = Image.open(p).convert(\"RGB\").resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR)\n        x = torch.from_numpy(np.array(img, dtype=np.float32)/255.).permute(2,0,1)\n        return x, torch.tensor(y, dtype=torch.long)\n\ntrain_cls_ds = ForgeryClsDataset(train_auth, train_forg, IMG_SIZE)\nval_cls_ds   = ForgeryClsDataset(val_auth,   val_forg, IMG_SIZE)\ntrain_cls_loader = DataLoader(train_cls_ds, batch_size=BATCH_CLS, shuffle=True,  num_workers=2, pin_memory=True)\nval_cls_loader   = DataLoader(val_cls_ds,   batch_size=BATCH_CLS, shuffle=False, num_workers=2, pin_memory=True)\n\n\n# Train segmentation head\n\ncrit_seg = nn.BCEWithLogitsLoss()\nopt_seg  = optim.AdamW(model.seg_head.parameters(), lr=LR_SEG, weight_decay=WEIGHT_DECAY)\n\nfor epoch in range(1, EPOCHS_SEG+1):\n    model.train()\n    tr_loss = 0.0\n    for imgs, masks in tqdm(train_seg_loader, desc=f\"[Seg] Epoch {epoch}/{EPOCHS_SEG}\"):\n        imgs, masks = imgs.to(device), masks.to(device)\n        logits = model.forward_seg(imgs)\n        loss = crit_seg(logits, masks)\n        opt_seg.zero_grad(); loss.backward(); opt_seg.step()\n        tr_loss += loss.item() * imgs.size(0)\n    tr_loss /= len(train_seg_loader.dataset)\n\n    # Val\n    model.eval()\n    val_loss, miou, mdice, macc = 0.0, 0.0, 0.0, 0.0\n    with torch.no_grad():\n        for imgs, masks in val_seg_loader:\n            imgs, masks = imgs.to(device), masks.to(device)\n            logits = model.forward_seg(imgs)\n            loss = crit_seg(logits, masks)\n            val_loss += loss.item() * imgs.size(0)\n            probs = torch.sigmoid(logits).cpu().numpy()\n            gts   = masks.cpu().numpy()\n            for p,g in zip(probs, gts):\n                p = p[0]; g = g[0]\n                miou  += iou_score(p, g)\n                mdice += dice_score(p, g)\n                macc  += pixel_acc(p, g)\n    n_val = len(val_seg_loader.dataset)\n    print(f\"[Seg] Epoch {epoch} | train_loss={tr_loss:.4f} | val_loss={val_loss/n_val:.4f} \"\n          f\"| IoU={miou/n_val:.3f} | Dice={mdice/n_val:.3f} | Acc={macc/n_val:.3f}\")\n\ntorch.cuda.empty_cache(); gc.collect()\n\n\n# Train classification head\n\ncrit_cls = nn.CrossEntropyLoss()\nopt_cls  = optim.AdamW(model.cls_head.parameters(), lr=LR_CLS, weight_decay=WEIGHT_DECAY)\n\nfor epoch in range(1, EPOCHS_CLS+1):\n    model.train()\n    tr_loss = 0.0\n    for imgs, labels in tqdm(train_cls_loader, desc=f\"[Cls] Epoch {epoch}/{EPOCHS_CLS}\"):\n        imgs, labels = imgs.to(device), labels.to(device)\n        logits = model.forward_cls(imgs)\n        loss = crit_cls(logits, labels)\n        opt_cls.zero_grad(); loss.backward(); opt_cls.step()\n        tr_loss += loss.item() * imgs.size(0)\n    tr_loss /= len(train_cls_loader.dataset)\n\n    # Val\n    model.eval()\n    correct, total = 0, 0\n    with torch.no_grad():\n        for imgs, labels in val_cls_loader:\n            imgs, labels = imgs.to(device), labels.to(device)\n            logits = model.forward_cls(imgs)\n            preds = logits.argmax(dim=1)\n            correct += (preds == labels).sum().item()\n            total   += labels.size(0)\n    print(f\"[Cls] Epoch {epoch} | train_loss={tr_loss:.4f} | val_acc={100.0*correct/total:.2f}%\")\n\ntorch.cuda.empty_cache(); gc.collect()\n\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n🚀 Inference & Submission Phase","metadata":{}},{"cell_type":"code","source":"\n# Inference + Submission (segmentation -> RLE, sinon 'authentic')\n\ndef predict_mask_prob(img_pil):\n    img = img_pil.convert(\"RGB\").resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR)\n    x = torch.from_numpy(np.array(img, dtype=np.float32)/255.).permute(2,0,1)[None].to(device)\n    with torch.no_grad():\n        logits = model.forward_seg(x)\n        prob = torch.sigmoid(logits)[0,0].cpu().numpy()\n    return prob\n\nTHR = 0.5\nrows = []\nif os.path.exists(TEST_DIR):\n    test_files = sorted(os.listdir(TEST_DIR))\n    print(\"Test images:\", len(test_files))\n    for fname in tqdm(test_files, desc=\"Inference\"):\n        case_id = Path(fname).stem\n        path = str(Path(TEST_DIR)/fname)\n        pil = Image.open(path).convert(\"RGB\")\n        ow, oh = pil.size\n\n        prob = predict_mask_prob(pil)  # IMG_SIZE x IMG_SIZE\n        # resize to original size\n        mask = cv2.resize(prob, (ow, oh), interpolation=cv2.INTER_NEAREST)\n        binm = (mask > THR).astype(np.uint8)\n\n        if binm.sum() == 0:\n            annot = \"authentic\"\n        else:\n            annot = rle_encode_numpy(binm)  # JSON list of [start,length,...]\n\n        rows.append({\"case_id\": case_id, \"annotation\": annot})\n\nsub = pd.DataFrame(rows, columns=[\"case_id\",\"annotation\"])\n\n# Aligne avec sample_submission\nif os.path.exists(SAMPLE_SUB):\n    ss = pd.read_csv(SAMPLE_SUB)\n    ss[\"case_id\"] = ss[\"case_id\"].astype(str)\n    if not sub.empty:\n        sub[\"case_id\"] = sub[\"case_id\"].astype(str)\n        sub = ss[[\"case_id\"]].merge(sub, on=\"case_id\", how=\"left\")\n        sub[\"annotation\"] = sub[\"annotation\"].fillna(\"authentic\")\n    else:\n        sub = ss[[\"case_id\"]].copy()\n        sub[\"annotation\"] = \"authentic\"\n\n# Sauvegarde\nOUT_PATH = \"submission.csv\"\nsub.to_csv(OUT_PATH, index=False)\nprint(\" Wrote submission:\", OUT_PATH)\nprint(sub.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:06:11.990467Z","iopub.execute_input":"2025-11-04T15:06:11.990796Z","iopub.status.idle":"2025-11-04T15:06:12.179113Z","shell.execute_reply.started":"2025-11-04T15:06:11.990756Z","shell.execute_reply":"2025-11-04T15:06:12.178422Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n🖼️ Forgery Mask Visualization\n\n","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n\n# Function: predict probability mask\n\ndef predict_mask_prob(img_pil):\n    \"\"\"Predict a pixel-wise probability mask for a given image.\"\"\"\n    img = img_pil.convert(\"RGB\").resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR)\n    x = torch.from_numpy(np.array(img, dtype=np.float32) / 255.).permute(2, 0, 1)[None].to(device)\n    with torch.no_grad():\n        logits = model.forward_seg(x)\n        prob = torch.sigmoid(logits)[0, 0].cpu().numpy()\n    return prob\n\n# Visualization on random test images\n\nN_SHOW = 5  # number of test images to visualize\nTHR = 0.5   # threshold for binary mask\n\nif os.path.exists(TEST_DIR):\n    test_files = sorted(os.listdir(TEST_DIR))\n    samples = random.sample(test_files, min(N_SHOW, len(test_files)))\n\n    plt.figure(figsize=(14, N_SHOW * 4))\n    for i, fname in enumerate(samples, 1):\n        path = str(Path(TEST_DIR) / fname)\n        pil = Image.open(path).convert(\"RGB\")\n        ow, oh = pil.size\n\n        # Predict probability mask\n        prob = predict_mask_prob(pil)\n        mask_resized = cv2.resize(prob, (ow, oh), interpolation=cv2.INTER_NEAREST)\n        binary_mask = (mask_resized > THR).astype(np.uint8)\n\n        # Visualization: original / prob mask / overlay\n        plt.subplot(N_SHOW, 3, 3*(i-1)+1)\n        plt.imshow(pil)\n        plt.title(f\"Original - {fname}\")\n        plt.axis(\"off\")\n\n        plt.subplot(N_SHOW, 3, 3*(i-1)+2)\n        plt.imshow(mask_resized, cmap=\"viridis\")\n        plt.title(\"Probability Mask\")\n        plt.axis(\"off\")\n\n        plt.subplot(N_SHOW, 3, 3*(i-1)+3)\n        plt.imshow(pil)\n        plt.imshow(binary_mask, cmap=\"Reds\", alpha=0.4)\n        plt.title(\"Overlay Mask\")\n        plt.axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T17:21:54.388812Z","iopub.execute_input":"2025-11-04T17:21:54.389136Z","iopub.status.idle":"2025-11-04T17:21:54.520240Z","shell.execute_reply.started":"2025-11-04T17:21:54.389110Z","shell.execute_reply":"2025-11-04T17:21:54.519231Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n🧩 Authentic Image Mask Visualization\n\n","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Function: predict mask on tensor batch\n\ndef predict_val_batch(model, loader, n_show=5, thr=0.5):\n    \"\"\"Display a few validation images with GT mask and predicted mask.\"\"\"\n    model.eval()\n    shown = 0\n\n    plt.figure(figsize=(12, n_show * 4))\n\n    with torch.no_grad():\n        for imgs, masks in loader:\n            imgs, masks = imgs.to(device), masks.to(device)\n            logits = model.forward_seg(imgs)\n            probs = torch.sigmoid(logits).cpu().numpy()\n            imgs_np = imgs.cpu().permute(0, 2, 3, 1).numpy()\n            gts = masks.cpu().numpy()\n\n            for j in range(len(imgs)):\n                if shown >= n_show:\n                    break\n\n                # Retrieve elements\n                img = np.clip(imgs_np[j], 0, 1)\n                gt = gts[j, 0]\n                prob = probs[j, 0]\n                pred_bin = (prob > thr).astype(np.uint8)\n\n                # Show three panels\n                plt.subplot(n_show, 3, 3 * shown + 1)\n                plt.imshow(img)\n                plt.title(\"Original\")\n                plt.axis(\"off\")\n\n                plt.subplot(n_show, 3, 3 * shown + 2)\n                plt.imshow(gt, cmap=\"gray\")\n                plt.title(\"Ground Truth\")\n                plt.axis(\"off\")\n\n                plt.subplot(n_show, 3, 3 * shown + 3)\n                plt.imshow(img)\n                plt.imshow(pred_bin, cmap=\"Reds\", alpha=0.4)\n                plt.title(\"Predicted Mask\")\n                plt.axis(\"off\")\n\n                shown += 1\n\n            if shown >= n_show:\n                break\n\n    plt.tight_layout()\n    plt.show()\n\n# Run visualization\npredict_val_batch(model, val_seg_loader, n_show=5, thr=0.5)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:06:37.844573Z","iopub.execute_input":"2025-11-04T15:06:37.845170Z","iopub.status.idle":"2025-11-04T15:06:40.656183Z","shell.execute_reply.started":"2025-11-04T15:06:37.845145Z","shell.execute_reply":"2025-11-04T15:06:40.655216Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n🔥 Forged Image Mask Visualization\n\n","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef show_forged_examples(model, loader, n_show=5, thr=0.5):\n    \"\"\"Show only forged examples from validation set (mask not empty).\"\"\"\n    model.eval()\n    shown = 0\n\n    plt.figure(figsize=(12, n_show * 4))\n\n    with torch.no_grad():\n        for imgs, masks in loader:\n            imgs, masks = imgs.to(device), masks.to(device)\n            logits = model.forward_seg(imgs)\n            probs = torch.sigmoid(logits).cpu().numpy()\n            imgs_np = imgs.cpu().permute(0, 2, 3, 1).numpy()\n            gts = masks.cpu().numpy()\n\n            for j in range(len(imgs)):\n                gt = gts[j, 0]\n                if gt.sum() == 0:  # skip authentic images\n                    continue\n\n                img = np.clip(imgs_np[j], 0, 1)\n                prob = probs[j, 0]\n                pred_bin = (prob > thr).astype(np.uint8)\n\n                # 3 panels: original / GT / predicted\n                plt.subplot(n_show, 3, 3 * shown + 1)\n                plt.imshow(img)\n                plt.title(\"Original (Forged)\")\n                plt.axis(\"off\")\n\n                plt.subplot(n_show, 3, 3 * shown + 2)\n                plt.imshow(gt, cmap=\"gray\")\n                plt.title(\"Ground Truth Mask\")\n                plt.axis(\"off\")\n\n                plt.subplot(n_show, 3, 3 * shown + 3)\n                plt.imshow(img)\n                plt.imshow(pred_bin, cmap=\"Reds\", alpha=0.4)\n                plt.title(\"Predicted Mask\")\n                plt.axis(\"off\")\n\n                shown += 1\n                if shown >= n_show:\n                    break\n\n            if shown >= n_show:\n                break\n\n    plt.tight_layout()\n    plt.show()\n\n#  Run visualization\nshow_forged_examples(model, val_seg_loader, n_show=5, thr=0.5)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:06:55.427966Z","iopub.execute_input":"2025-11-04T15:06:55.428704Z","iopub.status.idle":"2025-11-04T15:07:38.086524Z","shell.execute_reply.started":"2025-11-04T15:06:55.428659Z","shell.execute_reply":"2025-11-04T15:07:38.085394Z"}},"outputs":[],"execution_count":null}]}