{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14407528,"sourceType":"datasetVersion","datasetId":9153851},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"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: CNN-DINOv2 Hybrid\n\nThis notebook demonstrates a hybrid approach for image classification using both Convolutional Neural Networks (CNNs) and DINOv2, a self-supervised vision transformer model. The workflow includes:\n\n- **Data Loading & Preprocessing:** Images are loaded, resized, normalized, and split into training and validation sets.\n- **Feature Extraction:** DINOv2 is used to extract high-level features from images, leveraging its transformer-based architecture for robust representations.\n- **CNN Model Construction:** A custom CNN is built to process image data, learning spatial hierarchies and patterns.\n- **Hybrid Model Integration:** Features from DINOv2 and the CNN are combined, either by concatenation or other fusion techniques, to enhance classification performance.\n- **Training & Evaluation:** The hybrid model is trained on the dataset, with metrics such as accuracy and loss tracked. Validation is performed to assess generalization.\n- **Visualization & Analysis:** Results, including confusion matrices and sample predictions, are visualized to interpret model behavior.\n\nThis approach aims to leverage the strengths of both CNNs (local feature learning) and DINOv2 (global, context-aware representations) for improved image classification results.","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\")\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 = 518\nBATCH_SIZE = 2\n# MODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-U54/CNNDINOv2-U54/model_seg_final.pt'  # 0.310\n# MODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-U52/CNNDINOv2-U52/model_seg_final.pt'  # 0.321\nMODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-U52/CNNDINOv2-U52/model_seg_final.pt'  # 0.321\n\n# INFERENCE UTILS\nAREA_THR = 200\nMEAN_THR = 0.22\nUSE_TTA = True\nGRID_SEARCH = True\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\nclass DinoTinyDecoder(nn.Module):\n    def __init__(self, in_ch=768, out_ch=1):\n        super().__init__()\n        # Block 1: 768 -> 384\n        self.block1 = nn.Sequential(\n            nn.Conv2d(in_ch, 384, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1)\n        )\n        # Block 2: 384 -> 192\n        self.block2 = nn.Sequential(\n            nn.Conv2d(384, 192, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1)\n        )\n        # Block 3: 192 -> 96\n        self.block3 = nn.Sequential(\n            nn.Conv2d(192, 96, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True)\n        )\n        # Final Output: 96 -> 1\n        self.conv_out = nn.Conv2d(96, out_ch, kernel_size=1)\n    \n    def forward(self, f, target_size):\n        # f: [B, 768, 37, 37]\n        \n        # Step 1: Up to ~74x74\n        x = F.interpolate(self.block1(f), size=(74, 74), mode='bilinear', align_corners=False)\n        \n        # Step 2: Up to ~148x148\n        x = F.interpolate(self.block2(x), size=(148, 148), mode='bilinear', align_corners=False)\n        \n        # Step 3: Up to ~296x296\n        x = F.interpolate(self.block3(x), size=(296, 296), mode='bilinear', align_corners=False)\n        \n        # Step 4: Final jump to 518x518\n        x = self.conv_out(x)\n        x = F.interpolate(x, size=target_size, mode='bilinear', align_corners=False)\n        \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 = 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(): \n        #     feats = self.encoder(**inputs).last_hidden_state\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    def forward_seg(self,x):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap,(IMG_SIZE,IMG_SIZE))\n\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@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\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(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    \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\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    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(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\nimport itertools\nfrom sklearn.metrics import f1_score\n\ndef grid_search_area_mean(forg_paths, auth_paths, mask_dir):\n    mean_range = [round(x, 2) for x in np.arange(0.20, 0.291, 0.01)]\n    area_range = [200]\n    # 1. Use ALL images from both paths to maximize robustness\n    val_set = [(p, \"forged\") for p in forg_paths] + [(p, \"authentic\") for p in auth_paths]\n    \n    print(f\"🚀 Step 1: Caching probability maps for ALL {len(val_set)} images...\")\n    cache = []\n    for p, label in tqdm(val_set):\n        pil = Image.open(p).convert(\"RGB\")\n        w, h = pil.size\n        \n        # Get raw probability map\n        prob = segment_prob_map_with_tta(pil) if USE_TTA else segment_prob_map(pil)\n        \n        # USE OLD MASK LOGIC: mean + 0.3*std\n        mask_raw, _ = enhanced_adaptive_mask(prob) # Your function using np.mean + 0.3*np.std\n        mask_resized = cv2.resize(mask_raw, (w, h), interpolation=cv2.INTER_NEAREST)\n        \n        # Handle Ground Truth\n        if label == \"forged\":\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        else:\n            m_gt = np.zeros((h, w), np.uint8) # Authentic = blank GT\n            \n        cache.append({\"prob\": prob, \"mask\": mask_resized, \"gt\": m_gt, \"label\": label})\n\n    # 2. Sweep thresholds\n    best_f1 = -1\n    best_params = {}\n    combinations = list(itertools.product(area_range, mean_range))\n    \n    for a_thr, m_thr in combinations:\n        current_f1s = []\n        for item in cache:\n            mask = item[\"mask\"]\n            area = int(mask.sum()) # OLD AREA LOGIC\n            \n            # OLD MEAN LOGIC\n            mask_small = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n            mean_in = float(item[\"prob\"][mask_small == 1].mean()) if area > 0 else 0.0\n            \n            # Pipeline decision\n            is_forged = (area >= a_thr and mean_in >= m_thr)\n            m_pred = (mask > 0).astype(np.uint8) if is_forged else np.zeros_like(item[\"gt\"])\n            \n            # F1 Calculation (Authentic silence = 1.0, noisy prediction = 0.0)\n            f1 = f1_score(item[\"gt\"].flatten(), m_pred.flatten(), \n                          zero_division=1 if item[\"label\"] == \"authentic\" else 0)\n            current_f1s.append(f1)\n            \n        avg_f1 = np.mean(current_f1s)\n        if avg_f1 > best_f1:\n            best_f1 = avg_f1\n            best_params = {\"AREA_THR\": a_thr, \"MEAN_THR\": m_thr}\n            print(f\"⭐ New Best F1: {best_f1:.4f} -> AREA: {a_thr}, MEAN: {m_thr}\")\n\n    return best_params\n\nif GRID_SEARCH:\n    best_cfg = grid_search_area_mean(val_forg, val_auth, MASK_DIR)\n    AREA_THR = best_cfg['AREA_THR']\n    MEAN_THR = best_cfg['MEAN_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}\")\n","metadata":{"execution":{"iopub.execute_input":"2026-01-04T01:47:59.539237Z","iopub.status.busy":"2026-01-04T01:47:59.538554Z","iopub.status.idle":"2026-01-04T03:07:41.667680Z","shell.execute_reply":"2026-01-04T03:07:41.666621Z"},"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":[]},"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":"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":{"execution":{"iopub.execute_input":"2026-01-04T03:07:41.875556Z","iopub.status.busy":"2026-01-04T03:07:41.875183Z","iopub.status.idle":"2026-01-04T03:07:44.024852Z","shell.execute_reply":"2026-01-04T03:07:44.023891Z"},"papermill":{"duration":2.205312,"end_time":"2026-01-04T03:07:44.027080","exception":false,"start_time":"2026-01-04T03:07:41.821768","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"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\ndevice = 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 — CNN–DINOv2 Hybrid\", \n             fontsize=16, fontweight=\"bold\", color=\"#b30000\")\n\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-01-04T03:07:44.246586Z","iopub.status.busy":"2026-01-04T03:07:44.246190Z","iopub.status.idle":"2026-01-04T03:07:50.057691Z","shell.execute_reply":"2026-01-04T03:07:50.056644Z"},"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":[]},"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.137520","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    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), (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 — CNN–DINOv2 Hybrid\",\n             fontsize=16, fontweight=\"bold\", color=\"#009933\")\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-01-04T03:07:50.336756Z","iopub.status.busy":"2026-01-04T03:07:50.336399Z","iopub.status.idle":"2026-01-04T03:07:55.079414Z","shell.execute_reply":"2026-01-04T03:07:55.078553Z"},"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":[]},"outputs":[],"execution_count":null}]}