{"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.12.12"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"},{"sourceId":14407528,"sourceType":"datasetVersion","datasetId":9153851},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":120.316674,"end_time":"2026-01-14T22:19:05.509222","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-14T22:17:05.192548","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Scientific Image Forgery Detection - Master Notebook\n\nThis notebook merges the strongest components from various models to achieve high-accuracy scientific image forgery detection. It utilizes a frozen DINOv2 backbone combined with a trainable CNN decoder and Error Level Analysis (ELA).\n\n**Core Methodology:**\n1. **DINOv2 + Decoder**: High-level semantic features extracted via DINOv2.\n2. **ELA Integration**: Compression artifact analysis to highlight manipulated regions.\n3. **Multi-Scale TTA**: Inference across multiple resolutions with 4-way Test-Time Augmentation.\n4. **Adaptive Post-Processing**: Sobel gradient enhancement and area/confidence filtering.","metadata":{"papermill":{"duration":0.004378,"end_time":"2026-01-14T22:17:08.223774","exception":false,"start_time":"2026-01-14T22:17:08.219396","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os, cv2, json, math, random, torch, io\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\n# --- Environment Strategy & Paths ---\nIS_KAGGLE = os.path.exists('/kaggle/input')\n\nif IS_KAGGLE:\n    BASE_DIR  = '/kaggle/input/recodai-luc-scientific-image-forgery-detection'\n    DINO_PATH = '/kaggle/input/dinov2/pytorch/base/1'\n    MODEL_LOC = '/kaggle/input/cnndinov2-pbd/CNNDINOv2-U52/CNNDINOv2-U52/model_seg_final.pt'\nelse:\n    BASE_DIR  = r'D:\\AI_Testing\\Recod.aiLUC - Scientific Image Forgery Detection\\Lv2\\recodai-luc-scientific-image-forgery-detection'\n    DINO_PATH = r'D:\\AI_Testing\\Recod.aiLUC - Scientific Image Forgery Detection\\Lv2\\dinov2-pytorch-base-v1'\n    MODEL_LOC = r'D:\\AI_Testing\\Recod.aiLUC - Scientific Image Forgery Detection\\Lv2\\archive\\CNNDINOv2-U52\\CNNDINOv2-U52\\model_seg_final.pt'\n\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'\n\n# --- Global Config ---\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    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_everything(42)\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nIMG_SIZE = 518\nIMG_SIZE_H = 518 \nBATCH_SIZE = 2\n\nAREA_THR = 360\nMEAN_THR = 0.20\nUSE_TTA = True\nUSE_ELA = True\nELA_WEIGHT = 0.36\nELA_QUALITY = 90\n\nprint(f'Device: {device}')\nprint(f'ELA Enabled: {USE_ELA}')","metadata":{"execution":{"iopub.status.busy":"2026-01-15T19:50:29.901813Z","iopub.execute_input":"2026-01-15T19:50:29.902369Z","iopub.status.idle":"2026-01-15T19:50:29.918809Z","shell.execute_reply.started":"2026-01-15T19:50:29.902330Z","shell.execute_reply":"2026-01-15T19:50:29.917635Z"},"papermill":{"duration":35.276233,"end_time":"2026-01-14T22:17:43.503556","exception":false,"start_time":"2026-01-14T22:17:08.227323","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate_paths():\n    for p, name in zip([BASE_DIR, DINO_PATH, MODEL_LOC], ['BASE_DIR', 'DINO_PATH', 'MODEL_LOC']):\n        if not os.path.exists(p):\n            print(f'❌ {name} not found: {p}')\n        else:\n            print(f'✅ {name} validated')\n\nvalidate_paths()","metadata":{"execution":{"iopub.status.busy":"2026-01-15T19:50:29.920871Z","iopub.execute_input":"2026-01-15T19:50:29.921329Z","iopub.status.idle":"2026-01-15T19:50:29.944997Z","shell.execute_reply.started":"2026-01-15T19:50:29.921287Z","shell.execute_reply":"2026-01-15T19:50:29.943921Z"},"papermill":{"duration":0.023778,"end_time":"2026-01-14T22:17:43.530865","exception":false,"start_time":"2026-01-14T22:17:43.507087","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Extraction: ELA & Forensic Analysis","metadata":{"papermill":{"duration":0.003983,"end_time":"2026-01-14T22:17:43.538517","exception":false,"start_time":"2026-01-14T22:17:43.534534","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def compute_ela(pil_image, quality=90):\n    buffer = io.BytesIO()\n    pil_image.save(buffer, 'JPEG', quality=quality)\n    buffer.seek(0)\n    recompressed = Image.open(buffer).convert('RGB')\n    original = np.array(pil_image, dtype=np.float32)\n    compressed = np.array(recompressed, dtype=np.float32)\n    ela = np.abs(original - compressed)\n    scale = 255.0 / (ela.max() + 1e-6)\n    ela = np.clip(ela * scale, 0, 255).astype(np.uint8)\n    return Image.fromarray(ela)\n\ndef compute_ela_heatmap(pil_image, quality=90):\n    ela_img = compute_ela(pil_image, quality)\n    heatmap = np.array(ela_img.convert('L'), dtype=np.float32) / 255.0\n    return heatmap\n\ndef detect_hidden_jpeg(pil_image):\n    arr = np.array(pil_image.convert('L'), dtype=np.float32)\n    h, w = arr.shape\n    if h < 16 or w < 16: return False, 0.0\n    grad_x = np.abs(arr[:, 1:] - arr[:, :-1])\n    grad_y = np.abs(arr[1:, :] - arr[:-1, :])\n    def check_periodicity(grads, axis=0):\n        sums = grads.sum(axis=axis)\n        if len(sums) < 16: return 0.0\n        peaks = [sums[i::8].mean() for i in range(8)]\n        return np.max(peaks) / (np.mean(peaks) + 1e-6)\n    score_x = check_periodicity(grad_x, axis=0)\n    score_y = check_periodicity(grad_y, axis=1)\n    final_score = (score_x + score_y) / 2.0\n    return final_score > 1.2, final_score","metadata":{"execution":{"iopub.status.busy":"2026-01-15T19:50:29.946384Z","iopub.execute_input":"2026-01-15T19:50:29.947020Z","iopub.status.idle":"2026-01-15T19:50:29.963140Z","shell.execute_reply.started":"2026-01-15T19:50:29.946989Z","shell.execute_reply":"2026-01-15T19:50:29.962238Z"},"papermill":{"duration":0.017591,"end_time":"2026-01-14T22:17:43.559736","exception":false,"start_time":"2026-01-14T22:17:43.542145","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Architecture (DINOv2 + CNN Decoder)","metadata":{"papermill":{"duration":0.00359,"end_time":"2026-01-14T22:17:43.566945","exception":false,"start_time":"2026-01-14T22:17:43.563355","status":"completed"},"tags":[]}},{"cell_type":"code","source":"processor = 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        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        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        self.block3 = nn.Sequential(\n            nn.Conv2d(192, 96, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True)\n        )\n        self.conv_out = nn.Conv2d(96, out_ch, kernel_size=1)\n\n    def forward(self, f, target_size):\n        # Interpolation to reconstruct high-res mask\n        x = F.interpolate(self.block1(f), size=(74, 74), mode='bilinear', align_corners=False)\n        x = F.interpolate(self.block2(x), size=(148, 148), mode='bilinear', align_corners=False)\n        x = F.interpolate(self.block3(x), size=(296, 296), mode='bilinear', align_corners=False)\n        x = self.conv_out(x)\n        x = F.interpolate(x, size=target_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 = DinoTinyDecoder(768, 1)\n\n    def forward_features(self, x):\n        # Dynamic scaling for different image sizes\n        imgs = (x*255).clamp(0,255).byte().permute(0,2,3,1).cpu().numpy()\n        inputs = self.processor(images=list(imgs), return_tensors=\"pt\").to(x.device)\n        feats = self.encoder(**inputs).last_hidden_state\n        B, N, C = feats.shape\n        fmap = feats[:, 1:, :].permute(0, 2, 1)\n        s = int(math.sqrt(N - 1))\n        fmap = fmap.reshape(B, C, s, s)\n        return fmap\n\n    def forward_seg(self, x, target_size):\n        fmap = self.forward_features(x)\n        return self.seg_head(fmap, target_size)\n\nmodel_seg = DinoSegmenter(encoder, processor).to(device)\n\nif os.path.exists(MODEL_LOC):\n    model_seg.load_state_dict(torch.load(MODEL_LOC, map_location=device))\n    print(f'✅ Model loaded from: {MODEL_LOC}')\n    model_seg.eval()\nelse:\n    print('⚠️ Pretrained model not found, proceeding in inference mode with uninitialized head.')","metadata":{"execution":{"iopub.status.busy":"2026-01-15T19:50:29.964388Z","iopub.execute_input":"2026-01-15T19:50:29.964735Z","iopub.status.idle":"2026-01-15T19:50:30.584534Z","shell.execute_reply.started":"2026-01-15T19:50:29.964680Z","shell.execute_reply":"2026-01-15T19:50:30.583382Z"},"papermill":{"duration":5.325692,"end_time":"2026-01-14T22:17:48.896337","exception":false,"start_time":"2026-01-14T22:17:43.570645","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Advanced Inference Logic (TTA & Multi-Scale)","metadata":{"papermill":{"duration":0.003839,"end_time":"2026-01-14T22:17:48.904054","exception":false,"start_time":"2026-01-14T22:17:48.900215","status":"completed"},"tags":[]}},{"cell_type":"code","source":"@torch.no_grad()\ndef segment_prob_map(pil, size=IMG_SIZE):\n    # Base single prediction\n    x = torch.from_numpy(np.array(pil.resize((size, size)), np.float32)/255.).permute(2,0,1)[None].to(device)\n    prob = torch.sigmoid(model_seg.forward_seg(x, (IMG_SIZE, IMG_SIZE)))[0,0].cpu().numpy()\n    return prob\n\n@torch.no_grad()\ndef segment_prob_map_with_tta(pil, size=IMG_SIZE):\n    # 4-Way TTA: Original, H-Flip, V-Flip, Dual-Flip\n    x = torch.from_numpy(np.array(pil.resize((size, size)), np.float32)/255.).permute(2,0,1)[None].to(device)\n    preds = []\n    \n    # 1. Original\n    preds.append(torch.sigmoid(model_seg.forward_seg(x, (IMG_SIZE, IMG_SIZE))))\n    \n    # 2. H-Flip\n    pred_h = torch.sigmoid(model_seg.forward_seg(torch.flip(x, dims=[3]), (IMG_SIZE, IMG_SIZE)))\n    preds.append(torch.flip(pred_h, dims=[3]))\n    \n    # 3. V-Flip\n    pred_v = torch.sigmoid(model_seg.forward_seg(torch.flip(x, dims=[2]), (IMG_SIZE, IMG_SIZE)))\n    preds.append(torch.flip(pred_v, dims=[2]))\n    \n    # 4. H+V Flip\n    pred_hv = torch.sigmoid(model_seg.forward_seg(torch.flip(x, dims=[2, 3]), (IMG_SIZE, IMG_SIZE)))\n    preds.append(torch.flip(pred_hv, dims=[2, 3]))\n    \n    prob = torch.stack(preds).mean(0)[0, 0].cpu().numpy()\n    return prob\n\ndef segment_multi_scale(pil):\n    # Average predictions from base and high-res\n    if USE_TTA:\n        p1 = segment_prob_map_with_tta(pil, size=IMG_SIZE)\n        p2 = segment_prob_map_with_tta(pil, size=IMG_SIZE_H)\n    else:\n        p1 = segment_prob_map(pil, size=IMG_SIZE)\n        p2 = segment_prob_map(pil, size=IMG_SIZE_H)\n    \n    return (p1 + p2) / 2.0","metadata":{"papermill":{"duration":0.017197,"end_time":"2026-01-14T22:17:48.925026","exception":false,"start_time":"2026-01-14T22:17:48.907829","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T19:50:30.586960Z","iopub.execute_input":"2026-01-15T19:50:30.587410Z","iopub.status.idle":"2026-01-15T19:50:30.598717Z","shell.execute_reply.started":"2026-01-15T19:50:30.587381Z","shell.execute_reply":"2026-01-15T19:50:30.597503Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Post-Processing: Adaptive Masking & Refinement","metadata":{"papermill":{"duration":0.00358,"end_time":"2026-01-14T22:17:48.932314","exception":false,"start_time":"2026-01-14T22:17:48.928734","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def enhanced_adaptive_mask(prob, alpha_grad=0.45):\n    # Gradient-based boost to sharpen edges\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    # Stats-based adaptive threshold\n    thr = np.mean(enhanced) + 0.3 * np.std(enhanced)\n    mask = (enhanced > thr).astype(np.uint8)\n    \n    # Clean up small noise\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","metadata":{"papermill":{"duration":0.01396,"end_time":"2026-01-14T22:17:48.949804","exception":false,"start_time":"2026-01-14T22:17:48.935844","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T19:50:30.599949Z","iopub.execute_input":"2026-01-15T19:50:30.600358Z","iopub.status.idle":"2026-01-15T19:50:30.618603Z","shell.execute_reply.started":"2026-01-15T19:50:30.600322Z","shell.execute_reply":"2026-01-15T19:50:30.617561Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Final Inference Pipeline (RGB + ELA + Multi-Scale)","metadata":{"papermill":{"duration":0.003694,"end_time":"2026-01-14T22:17:48.957177","exception":false,"start_time":"2026-01-14T22:17:48.953483","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def pipeline_master(pil, use_ela=USE_ELA, ela_weight=ELA_WEIGHT):\n    # 1. Multi-scale RGB Segmenter Prediction\n    prob_rgb = segment_multi_scale(pil)\n    \n    prob_combined = prob_rgb.copy()\n    ela_info = {}\n\n    if use_ela:\n        # 2. ELA forensics image\n        ela_img = compute_ela(pil, quality=ELA_QUALITY)\n        \n        # 3. Model prediction on ELA image\n        if USE_TTA:\n            prob_ela = segment_prob_map_with_tta(ela_img, size=IMG_SIZE)\n        else:\n            prob_ela = segment_prob_map(ela_img, size=IMG_SIZE)\n            \n        # 4. Direct ELA heatmap boost\n        ela_heatmap = compute_ela_heatmap(pil, quality=ELA_QUALITY)\n        ela_heatmap = cv2.resize(ela_heatmap, (IMG_SIZE, IMG_SIZE))\n        \n        # 5. Weighted Blend (RGB + Model-on-ELA + Raw-ELA-Heatmap)\n        prob_combined = (\n            (1 - ela_weight) * prob_rgb + \n            ela_weight * 0.7 * prob_ela + \n            ela_weight * 0.3 * ela_heatmap\n        )\n        \n        ela_info = {\n            'ela_max': float(ela_heatmap.max()),\n            'ela_mean': float(ela_heatmap.mean()),\n        }\n    \n    # 6. Final Masking & Classification\n    mask, thr = finalize_mask(prob_combined, pil.size)\n    area = int(mask.sum())\n    \n    # Confidence estimation inside detected region\n    mask_small = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n    mean_inside = float(prob_combined[mask_small==1].mean()) if area > 0 else 0.0\n    \n    # Area and Mean Threshold Filter\n    if area < AREA_THR or mean_inside < MEAN_THR:\n        return 'authentic', None, {'area': area, 'mean_inside': mean_inside}\n    \n    return 'forged', mask, {'area': area, 'mean_inside': mean_inside, 'thr': thr, **ela_info}","metadata":{"papermill":{"duration":0.015742,"end_time":"2026-01-14T22:17:48.976544","exception":false,"start_time":"2026-01-14T22:17:48.960802","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T19:50:30.619946Z","iopub.execute_input":"2026-01-15T19:50:30.620352Z","iopub.status.idle":"2026-01-15T19:50:30.645738Z","shell.execute_reply.started":"2026-01-15T19:50:30.620316Z","shell.execute_reply":"2026-01-15T19:50:30.644680Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submission Generation","metadata":{"papermill":{"duration":0.003793,"end_time":"2026-01-14T22:17:48.984081","exception":false,"start_time":"2026-01-14T22:17:48.980288","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def 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\nSAMPLE_SUB = f\"{BASE_DIR}/sample_submission.csv\"\nOUT_PATH = \"submission.csv\"\n\nif os.path.exists(TEST_DIR):\n    test_files = sorted(os.listdir(TEST_DIR))\n    rows = []\n    for f in tqdm(test_files, desc=\"Final Inference\"):\n        pil = Image.open(os.path.join(TEST_DIR, f)).convert(\"RGB\")\n        label, mask, info = pipeline_master(pil)\n        \n        # Extract numeric case_id from filename (e.g., '45.png' -> '45')\n        case_id = Path(f).stem\n        \n        if label == \"authentic\":\n            annotation = \"authentic\"\n        else:\n            annotation = rle_encode(mask)\n            \n        rows.append({\"case_id\": case_id, \"annotation\": annotation})\n\n    df = pd.DataFrame(rows)\n    df.to_csv(OUT_PATH, index=False)\n    print(f\"✅ Submission saved to {OUT_PATH}\")\nelse:\n    print(\"⚠️ TEST_DIR not found, skipping submission generation.\")","metadata":{"papermill":{"duration":9.266719,"end_time":"2026-01-14T22:17:58.255378","exception":false,"start_time":"2026-01-14T22:17:48.988659","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T19:50:30.646842Z","iopub.execute_input":"2026-01-15T19:50:30.647220Z","iopub.status.idle":"2026-01-15T19:50:43.514938Z","shell.execute_reply.started":"2026-01-15T19:50:30.647181Z","shell.execute_reply":"2026-01-15T19:50:43.513931Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Qualitative Comparison: Authentic vs Forged","metadata":{"papermill":{"duration":0.003913,"end_time":"2026-01-14T22:17:58.263357","exception":false,"start_time":"2026-01-14T22:17:58.259444","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def visualize_comparison_batch(image_ids, save_dir='comparisons'):\n    if not os.path.exists(save_dir): os.makedirs(save_dir)\n    \n    for img_id in image_ids:\n        auth_path = os.path.join(AUTH_DIR, img_id)\n        forg_path = os.path.join(FORG_DIR, img_id)\n        \n        if not (os.path.exists(auth_path) and os.path.exists(forg_path)):\n            print(f'⚠️ Skipping {img_id}: Not found in both directories.')\n            continue\n            \n        print(f'\\n🔍 Visualizing Sample: {img_id}')\n        fig, axes = plt.subplots(2, 3, figsize=(18, 10))\n        \n        for i, (path, label_type) in enumerate([(auth_path, 'Authentic'), (forg_path, 'Forged')]):\n            pil = Image.open(path).convert('RGB')\n            label, mask, info = pipeline_master(pil)\n            ela_heatmap = compute_ela_heatmap(pil, quality=ELA_QUALITY)\n            \n            # Column 1: Original\n            axes[i, 0].imshow(pil)\n            axes[i, 0].set_title(f'{label_type} (Actual)')\n            axes[i, 0].axis('off')\n            \n            # Column 2: ELA Heatmap\n            axes[i, 1].imshow(ela_heatmap, cmap='hot')\n            axes[i, 1].set_title(f'ELA Heatmap (Max: {ela_heatmap.max():.2f})')\n            axes[i, 1].axis('off')\n            \n            # Column 3: Predicted Mask/Logic\n            if mask is not None:\n                axes[i, 2].imshow(pil)\n                axes[i, 2].imshow(mask, cmap='jet', alpha=0.5)\n                axes[i, 2].set_title(f'Prediction: {label} (Area: {info[\"area\"]})')\n            else:\n                axes[i, 2].imshow(pil)\n                overlay = np.zeros((*pil.size[::-1], 3), dtype=np.uint8)\n                axes[i, 2].imshow(overlay, alpha=0.3)\n                axes[i, 2].set_title(f'Prediction: {label}')\n            axes[i, 2].axis('off')\n        \n        plt.tight_layout()\n        plt.savefig(os.path.join(save_dir, f'comparison_{img_id}'))\n        plt.show()\n\n# List of images showcased in previous notebooks\nshowcase_ids = ['10.png']\n\nvisualize_comparison_batch(showcase_ids)","metadata":{"papermill":{"duration":64.083399,"end_time":"2026-01-14T22:19:02.350718","exception":false,"start_time":"2026-01-14T22:17:58.267319","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2026-01-15T19:50:43.517167Z","iopub.execute_input":"2026-01-15T19:50:43.517455Z","iopub.status.idle":"2026-01-15T19:51:11.179742Z","shell.execute_reply.started":"2026-01-15T19:50:43.517429Z","shell.execute_reply":"2026-01-15T19:51:11.178646Z"}},"outputs":[],"execution_count":null}]}