{"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":14456136,"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":"<!-- ======================================\n     🧩 Unmasking the Fake — Premium Styled Summary (Final)\n     ====================================== -->\n<div style=\"font-family: Inter, Segoe UI, Roboto, Arial, sans-serif; \n            line-height:1.6; \n            color:#111; \n            border:1px solid #e5e7eb; \n            border-radius:16px; \n            padding:28px; \n            box-shadow:0 6px 20px rgba(0,0,0,0.07); \n            background:linear-gradient(180deg,#ffffff 0%, #f9fafb 100%);\">\n\n  <!-- 🌈 Title with gradient background and soft hover -->\n  <div style=\"background:linear-gradient(90deg,#7c3aed,#ec4899,#06b6d4);\n              color:white;\n              padding:14px 26px;\n              border-radius:18px;\n              box-shadow:0 3px 10px rgba(124,58,237,0.25);\n              display:inline-block;\n              transition:all 0.3s ease;\"\n       onmouseover=\"this.style.boxShadow='0 0 20px rgba(236,72,153,0.45)'\"\n       onmouseout=\"this.style.boxShadow='0 3px 10px rgba(124,58,237,0.25)'\">\n      <h2 style=\"margin:0; font-size:26px; letter-spacing:0.5px;\">\n        🧩 Unmasking the Fake with a <span style=\"white-space:nowrap;\">CNN–DINOv2</span> Hybrid\n      </h2>\n  </div>\n\n  <!-- Subtitle -->\n  <p style=\"margin:12px 0 20px; color:#334155; font-size:15px; font-style:italic;\">\n    A segmentation-driven pipeline for scientific image forgery detection — combining deep embeddings and adaptive CNN decoding.\n  </p>\n\n  <!-- Overview -->\n  <div style=\"margin:20px 0 0;\">\n    <h3 style=\"margin:8px 0; font-size:18px; border-left:4px solid #a855f7; padding-left:12px;\">🧠 Overview</h3>\n    <p style=\"margin:6px 0;\">\n      This solution implements a <strong>segmentation-based hybrid model</strong> that merges a <strong>self-supervised visual encoder</strong> \n      (from DINOv2) with a <strong>lightweight CNN decoder</strong>.  \n      Its goal is to <em>detect</em> and <em>localize</em> image manipulations at the pixel level with high precision.\n    </p>\n  </div>\n\n  <!-- Architecture -->\n  <div style=\"margin-top:16px;\">\n    <h3 style=\"margin:8px 0; font-size:18px; border-left:4px solid #ec4899; padding-left:12px;\">⚙️ Architecture</h3>\n    <ol style=\"margin:8px 0 0 20px;\">\n      <li style=\"margin:6px 0;\"><strong>Visual Encoder</strong> — extracts high-level semantic features using DINOv2 embeddings.</li>\n      <li style=\"margin:6px 0;\"><strong>CNN Decoder</strong> — converts those features into a binary segmentation mask (<code>768→256→64→1</code>).</li>\n      <li style=\"margin:6px 0;\"><strong>Resizing</strong> — all images and masks are resized to <code>256×256</code> for uniform input.</li>\n    </ol>\n  </div>\n\n  <!-- Training -->\n  <div style=\"margin-top:16px;\">\n    <h3 style=\"margin:8px 0; font-size:18px; border-left:4px solid #06b6d4; padding-left:12px;\">🏋️ Training</h3>\n    <ul style=\"margin:8px 0 0 20px;\">\n      <li style=\"margin:6px 0;\"><strong>Forged images:</strong> paired with binary <code>.npy</code> masks.</li>\n      <li style=\"margin:6px 0;\"><strong>Authentic images:</strong> use empty zero masks (no manipulation).</li>\n      <li style=\"margin:6px 0;\"><strong>Loss:</strong> <code>BCEWithLogitsLoss</code> • <strong>Optimizer:</strong> <code>AdamW</code>.</li>\n      <li style=\"margin:6px 0;\">Only the CNN head is trained — the DINOv2 encoder remains frozen.</li>\n    </ul>\n  </div>\n\n  <!-- Inference -->\n  <div style=\"margin-top:16px;\">\n    <h3 style=\"margin:8px 0; font-size:18px; border-left:4px solid #f59e0b; padding-left:12px;\">🔍 Inference & Post-Processing</h3>\n    <ul style=\"margin:8px 0 0 20px;\">\n      <li style=\"margin:6px 0;\">The CNN head outputs a probability map of suspicious regions.</li>\n      <li style=\"margin:6px 0;\">An <strong>adaptive refinement</strong> stage sharpens edges (Sobel gradients + Gaussian blur).</li>\n      <li style=\"margin:6px 0;\">Dynamic threshold: <code>μ + 0.3σ</code> enhances true/false separation.</li>\n      <li style=\"margin:6px 0;\">Classification rule: if <code>area &lt; 400</code> or <code>mean_inside &lt; 0.35</code> → labeled as “authentic”.</li>\n    </ul>\n  </div>\n\n  <!-- Evaluation -->\n  <div style=\"margin-top:16px;\">\n    <h3 style=\"margin:8px 0; font-size:18px; border-left:4px solid #22c55e; padding-left:12px;\">📊 Evaluation</h3>\n    <p style=\"margin:6px 0;\">\n      The model is evaluated on a validation subset of forged images.  \n      Each predicted mask is compared with its ground truth using the <strong>F1-score</strong>.  \n      The average score provides a reliable measure of segmentation performance.\n    </p>\n  </div>\n\n  <!-- Key Points -->\n  <div style=\"margin-top:16px;\">\n    <h3 style=\"margin:8px 0; font-size:18px; border-left:4px solid #3b82f6; padding-left:12px;\">✅ Key Highlights</h3>\n    <ul style=\"margin:8px 0 0 20px;\">\n      <li style=\"margin:6px 0;\">Hybrid design — <strong>DINOv2</strong> (semantic understanding) + <strong>CNN</strong> (spatial precision).</li>\n      <li style=\"margin:6px 0;\">Lightweight and memory-efficient — ideal for limited GPU environments.</li>\n      <li style=\"margin:6px 0;\">Edge-aware post-processing for <strong>clean and well-defined masks</strong>.</li>\n    </ul>\n  </div>\n\n ","metadata":{}},{"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\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\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 = 1\nEPOCHS_SEG = 4\nLR_SEG = 1e-5\nWEIGHT_DECAY = 2e-5\n\n\nclass ForgerySegDataset(Dataset):\n    def __init__(self, auth_paths, forg_paths, mask_dir, img_size=256):\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)\nencoder = AutoModel.from_pretrained(DINO_PATH, local_files_only=True).eval().to(device)\n\nclass DinoTinyDecoder(nn.Module):\n    def __init__(self, in_ch=768, out_ch=1):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Conv2d(in_ch,256,3,padding=1), nn.ReLU(),\n            nn.Conv2d(256,64,3,padding=1), nn.ReLU(),\n            nn.Conv2d(64,out_ch,1)\n        )\n    def forward(self, f, size):\n        return self.net(F.interpolate(f, size=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    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(): 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    def forward_seg(self,x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap,(IMG_SIZE,IMG_SIZE))\n\n#  TRAINING\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)\n\nmodel_seg = DinoSegmenter(encoder, processor).to(device)\nopt_seg = optim.AdamW(model_seg.seg_head.parameters(), lr=LR_SEG, weight_decay=WEIGHT_DECAY)\ncrit_seg = nn.BCEWithLogitsLoss()\n\nfor e in range(EPOCHS_SEG):\n    model_seg.train()\n    total_loss = 0\n    for x,m in tqdm(train_loader, desc=f\"[Segmentation] Epoch {e+1}/{EPOCHS_SEG}\"):\n        x,m = x.to(device),m.to(device)\n        loss = crit_seg(model_seg.forward_seg(x),m)\n        opt_seg.zero_grad(); loss.backward(); opt_seg.step()\n        total_loss += loss.item()\n    print(f\"  → avg_loss={total_loss/len(train_loader):.4f}\")\ntorch.save(model_seg.state_dict(),\"model_seg_final.pt\")\n\n\n# INFERENCE UTILS\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(device)\n    prob = torch.sigmoid(model_seg.forward_seg(x))[0,0].cpu().numpy()\n    return prob\n\ndef enhanced_adaptive_mask(prob, alpha_grad=0.35):\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\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\ndef pipeline_final(pil):\n    prob = segment_prob_map(pil)\n    mask, thr = finalize_mask(prob, pil.size)\n    area = int(mask.sum())\n    mean_inside = float(prob[cv2.resize(mask,(IMG_SIZE,IMG_SIZE),interpolation=cv2.INTER_NEAREST)==1].mean()) if area>0 else 0.0\n    #  condition de filtrage\n    if area < 400 or mean_inside < 0.35:\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\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}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"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\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\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 = 1\nEPOCHS_SEG = 4\nLR_SEG = 1e-5\nWEIGHT_DECAY = 2e-5\n\n\nclass ForgerySegDataset(Dataset):\n    def __init__(self, auth_paths, forg_paths, mask_dir, img_size=256):\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)\nencoder = AutoModel.from_pretrained(DINO_PATH, local_files_only=True).eval().to(device)\n\nclass DinoTinyDecoder(nn.Module):\n    def __init__(self, in_ch=768, out_ch=1):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Conv2d(in_ch,256,3,padding=1), nn.ReLU(),\n            nn.Conv2d(256,64,3,padding=1), nn.ReLU(),\n            nn.Conv2d(64,out_ch,1)\n        )\n    def forward(self, f, size):\n        return self.net(F.interpolate(f, size=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    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(): 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    def forward_seg(self,x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap,(IMG_SIZE,IMG_SIZE))\n\n#  TRAINING\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)\n\nmodel_seg = DinoSegmenter(encoder, processor).to(device)\nopt_seg = optim.AdamW(model_seg.seg_head.parameters(), lr=LR_SEG, weight_decay=WEIGHT_DECAY)\ncrit_seg = nn.BCEWithLogitsLoss()\n\nfor e in range(EPOCHS_SEG):\n    model_seg.train()\n    total_loss = 0\n    for x,m in tqdm(train_loader, desc=f\"[Segmentation] Epoch {e+1}/{EPOCHS_SEG}\"):\n        x,m = x.to(device),m.to(device)\n        loss = crit_seg(model_seg.forward_seg(x),m)\n        opt_seg.zero_grad(); loss.backward(); opt_seg.step()\n        total_loss += loss.item()\n    print(f\"  → avg_loss={total_loss/len(train_loader):.4f}\")\ntorch.save(model_seg.state_dict(),\"model_seg_final.pt\")\n\n\n# INFERENCE UTILS\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(device)\n    prob = torch.sigmoid(model_seg.forward_seg(x))[0,0].cpu().numpy()\n    return prob\n\ndef enhanced_adaptive_mask(prob, alpha_grad=0.35):\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\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\ndef pipeline_final(pil):\n    prob = segment_prob_map(pil)\n    mask, thr = finalize_mask(prob, pil.size)\n    area = int(mask.sum())\n    mean_inside = float(prob[cv2.resize(mask,(IMG_SIZE,IMG_SIZE),interpolation=cv2.INTER_NEAREST)==1].mean()) if area>0 else 0.0\n    #  condition de filtrage\n    if area < 400 or mean_inside < 0.35:\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\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}\")","metadata":{"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\n# --- RLE Encoder for Kaggle Submission ---\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()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T16:09:30.837923Z","iopub.execute_input":"2025-11-07T16:09:30.838239Z","iopub.status.idle":"2025-11-07T16:09:31.471778Z","shell.execute_reply.started":"2025-11-07T16:09:30.838209Z","shell.execute_reply":"2025-11-07T16:09:31.471087Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 Visualizing Predicted Masks with the CNN–DINOv2 Hybrid Model\n","metadata":{}},{"cell_type":"code","source":"\nimport torch, cv2, math, numpy as np, matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nIMG_SIZE = 256\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)\ndef pipeline_visual(pil):\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    # same decision rule as pipeline_final\n    AREA_THR, MEAN_THR = 800, 0.45\n    label = \"forged\" if (area >= AREA_THR and mean_inside >= MEAN_THR) else \"authentic\"\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), (256, 256))\n    gt_disp  = cv2.resize(m_gt, (256, 256))\n    pr_disp  = cv2.resize(m_pred, (256, 256))\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 — CNN–DINOv2 Hybrid\", \n             fontsize=16, fontweight=\"bold\", color=\"#b30000\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T16:09:35.929876Z","iopub.execute_input":"2025-11-07T16:09:35.9307Z","iopub.status.idle":"2025-11-07T16:09:37.563101Z","shell.execute_reply.started":"2025-11-07T16:09:35.930665Z","shell.execute_reply":"2025-11-07T16:09:37.562301Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport torch, cv2, math, numpy as np, matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nIMG_SIZE = 256\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)\ndef pipeline_visual(pil):\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    # same decision rule as pipeline_final\n    AREA_THR, MEAN_THR = 800, 0.45\n    label = \"forged\" if (area >= AREA_THR and mean_inside >= MEAN_THR) else \"authentic\"\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), (256, 256))\n    gt_disp  = cv2.resize(m_gt, (256, 256))\n    pr_disp  = cv2.resize(m_pred, (256, 256))\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 — CNN–DINOv2 Hybrid\", \n             fontsize=16, fontweight=\"bold\", color=\"#b30000\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🟢 Visualization of Authentic Images (Hybrid DINOv2-based Detector)","metadata":{}},{"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    label, m_pred, thr, area, mean = pipeline_visual(pil)  # <-- version alignée avec ta nouvelle pipeline\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), (256, 256))\n    pr_disp  = cv2.resize(m_pred, (256, 256))\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 — CNN–DINOv2 Hybrid\",\n             fontsize=16, fontweight=\"bold\", color=\"#009933\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T16:09:45.942174Z","iopub.execute_input":"2025-11-07T16:09:45.942995Z","iopub.status.idle":"2025-11-07T16:09:47.90941Z","shell.execute_reply.started":"2025-11-07T16:09:45.942961Z","shell.execute_reply":"2025-11-07T16:09:47.9085Z"}},"outputs":[],"execution_count":null}]}