{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":113558,"databundleVersionId":14878066},{"sourceType":"datasetVersion","sourceId":14384696,"datasetId":9153851,"databundleVersionId":15197542},{"sourceType":"datasetVersion","sourceId":14381783,"datasetId":9184678,"databundleVersionId":15194273},{"sourceType":"modelInstanceVersion","sourceId":4534,"databundleVersionId":6346558,"modelInstanceId":3326}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":63.129007,"end_time":"2025-12-30T04:03:10.929134","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-12-30T04:02:07.800127","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"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\nfrom torchvision import transforms  # <--- NEW IMPORT\n\n# --- 1. CONFIGURATION ---\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)\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 = 714   # Divisible by 14\nBATCH_SIZE = 2   # Keep low for VRAM safety\nAREA_THR = 300   # Min pixels to be \"forged\"\nMEAN_THR = 0.25  # Confidence threshold\n\n# --- 2. MULTI-SCALE DINOv2 MODEL ---\nclass DinoMultiScaleSegmenter(nn.Module):\n    def __init__(self, encoder, processor):\n        super().__init__()\n        self.encoder, self.processor = encoder, processor\n        hidden_dim = 768 \n        \n        # Pyramid Projections (Layers 3, 6, 9, 12)\n        self.proj3  = nn.Conv2d(hidden_dim, 256, 1)\n        self.proj6  = nn.Conv2d(hidden_dim, 256, 1)\n        self.proj9  = nn.Conv2d(hidden_dim, 256, 1)\n        self.proj12 = nn.Conv2d(hidden_dim, 256, 1)\n        \n        # CNN Fusion Head\n        self.decoder = nn.Sequential(\n            nn.Conv2d(256 * 4, 256, kernel_size=3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1),\n            nn.Conv2d(256, 1, kernel_size=1)\n        )\n\n    # RENAMED to 'forward' for automatic DataParallel support\n    def forward(self, x):\n        outputs = self.encoder(x, output_hidden_states=True)\n        h = outputs.hidden_states \n        B, N, C = h[0].shape\n        s = int(math.sqrt(N-1)) \n\n        def get_map(feat):\n            return feat[:, 1:, :].permute(0, 2, 1).reshape(B, C, s, s)\n\n        m3  = self.proj3(get_map(h[3]))\n        m6  = self.proj6(get_map(h[6]))\n        m9  = self.proj9(get_map(h[9]))\n        m12 = self.proj12(get_map(h[12]))\n\n        fused = torch.cat([m3, m6, m9, m12], dim=1)\n        out = self.decoder(fused)\n        return F.interpolate(out, size=(IMG_SIZE, IMG_SIZE), mode='bilinear', align_corners=False)\n\n# --- 3. DATASET CLASS (FIXED NORMALIZATION) ---\n# Define the Transform ONCE globally\nnorm_transform = transforms.Normalize(\n    mean=[0.485, 0.456, 0.406], \n    std=[0.229, 0.224, 0.225]\n)\n\nclass ForgerySegDataset(Dataset):\n    def __init__(self, auth_paths, forg_paths, mask_dir, img_size=IMG_SIZE):\n        self.samples = [(p, os.path.join(mask_dir, Path(p).stem + \".npy\")) for p in forg_paths]\n        self.samples += [(p, None) for p in auth_paths]\n        self.img_size = img_size\n\n    def __len__(self): 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        if mask_path and os.path.exists(mask_path):\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        else:\n            mask = np.zeros((h, w), np.uint8)\n        \n        img_r = img.resize((self.img_size, self.img_size))\n        mask_r = cv2.resize(mask, (self.img_size, self.img_size), interpolation=cv2.INTER_NEAREST)\n        \n        # 1. To Tensor (0-1)\n        img_t = torch.from_numpy(np.array(img_r, np.float32)/255.).permute(2,0,1)\n        \n        # 2. APPLY NORMALIZATION (The Fix!)\n        img_t = norm_transform(img_t)\n        \n        mask_t = torch.from_numpy(mask_r[None, ...].astype(np.float32))\n        return img_t, mask_t\n\n# --- 4. INITIALIZATION ---\nprocessor = AutoImageProcessor.from_pretrained(DINO_PATH, local_files_only=True)\nencoder = AutoModel.from_pretrained(DINO_PATH, local_files_only=True).to(device)\nmodel_seg = DinoMultiScaleSegmenter(encoder, processor).to(device)\ncriterion = nn.BCEWithLogitsLoss()\n\n# Quick verification of the model setup\ntotal_params = sum(p.numel() for p in model_seg.parameters())\ntrainable_params = sum(p.numel() for p in model_seg.parameters() if p.requires_grad)\n\nprint(f\"✅ Model Initialized Successfully!\")\nprint(f\"Total Parameters: {total_params:,}\")\nprint(f\"Trainable Parameters: {trainable_params:,}\")\nprint(f\"Device: {device}\")","metadata":{"papermill":{"duration":51.374583,"end_time":"2025-12-30T04:03:03.449412","exception":false,"start_time":"2025-12-30T04:02:12.074829","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 2: Hybrid Model — DINOv2 Feature Extraction & CNN Decoder Integration","metadata":{}},{"cell_type":"code","source":"import os\nimport cv2\nimport math\nimport glob\nimport gc\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom pathlib import Path\nfrom PIL import Image\nfrom torchvision import transforms\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom transformers import AutoImageProcessor, AutoModel\n\n# --- 1. CONFIGURATION ---\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\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\"\nDINO_PATH = \"/kaggle/input/dinov2/pytorch/base/1\"\nIMG_SIZE = 714  # Must match training size\n\n# Update these paths to where your models actually are\nMODEL_PATHS = [\n    \"/kaggle/input/my-dinov2-forgery-model/model_epoch_3.pt\"\n]\n\n# --- 2. CLASS DEFINITION (REQUIRED TO LOAD MODELS) ---\nclass DinoMultiScaleSegmenter(nn.Module):\n    def __init__(self, encoder):\n        super().__init__()\n        self.encoder = encoder\n        hidden_dim = 768 \n        self.proj3  = nn.Conv2d(hidden_dim, 256, 1)\n        self.proj6  = nn.Conv2d(hidden_dim, 256, 1)\n        self.proj9  = nn.Conv2d(hidden_dim, 256, 1)\n        self.proj12 = nn.Conv2d(hidden_dim, 256, 1)\n        self.decoder = nn.Sequential(\n            nn.Conv2d(256 * 4, 256, kernel_size=3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(inplace=True),\n            nn.Dropout2d(0.1),\n            nn.Conv2d(256, 1, kernel_size=1)\n        )\n\n    def forward(self, x):\n        outputs = self.encoder(x, output_hidden_states=True)\n        h = outputs.hidden_states \n        B, N, C = h[0].shape\n        s = int(math.sqrt(N-1)) \n        def get_map(feat):\n            return feat[:, 1:, :].permute(0, 2, 1).reshape(B, C, s, s)\n        m3  = self.proj3(get_map(h[3]))\n        m6  = self.proj6(get_map(h[6]))\n        m9  = self.proj9(get_map(h[9]))\n        m12 = self.proj12(get_map(h[12]))\n        fused = torch.cat([m3, m6, m9, m12], dim=1)\n        out = self.decoder(fused)\n        return F.interpolate(out, size=(IMG_SIZE, IMG_SIZE), mode='bilinear', align_corners=False)\n\n# --- 3. HELPER FUNCTIONS ---\nnorm_transform = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n\ndef rle_encode(mask):\n    pixels = mask.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    return ' '.join(str(x) for x in runs)\n\ndef predict_ensemble(pil_img, models):\n    # Preprocess\n    img_r = pil_img.resize((IMG_SIZE, IMG_SIZE))\n    img_t = torch.from_numpy(np.array(img_r, np.float32)/255.).permute(2,0,1)\n    img_t = norm_transform(img_t).unsqueeze(0).to(DEVICE)\n    \n    total_prob = None\n    with torch.no_grad():\n        for model in models:\n            logits = model(img_t)\n            prob = torch.sigmoid(logits)\n            if total_prob is None:\n                total_prob = prob\n            else:\n                total_prob += prob\n                \n    avg_prob = total_prob / len(models)\n    return avg_prob.squeeze().cpu().numpy()\n\n# --- 4. LOAD MODELS ---\nmodels = []\nprint(f\"🔄 Loading {len(MODEL_PATHS)} models...\")\n\n# Init Base Encoder\ntry:\n    encoder_base = AutoModel.from_pretrained(DINO_PATH, local_files_only=True)\nexcept:\n    print(\"⚠️ AutoModel load failed. Check DINO_PATH.\")\n    raise\n\nfor path in MODEL_PATHS:\n    if os.path.exists(path):\n        m = DinoMultiScaleSegmenter(encoder_base)\n        state_dict = torch.load(path, map_location=DEVICE)\n        # Fix DataParallel prefix\n        new_state_dict = {k.replace(\"module.\", \"\"): v for k, v in state_dict.items()}\n        m.load_state_dict(new_state_dict)\n        m.to(DEVICE)\n        m.eval()\n        models.append(m)\n        print(f\"✅ Loaded: {path}\")\n    else:\n        print(f\"⚠️ Warning: Model not found at {path}\")\n\nif not models:\n    raise RuntimeError(\"❌ No models loaded! Check paths.\")\n\n# --- 5. ROBUST INFERENCE LOOP ---\nresults = []\nss = pd.read_csv(SAMPLE_SUB)\nss[\"case_id\"] = ss[\"case_id\"].astype(str).str.strip()\n\nprint(f\"🚀 Starting Inference on {len(ss)} images...\")\n\nfor idx, row in tqdm(ss.iterrows(), total=len(ss)):\n    case_id = row[\"case_id\"]\n    \n    # Robust file finding (matches .png, .jpg, etc.)\n    search_path = f\"{TEST_DIR}/{case_id}.*\"\n    found_files = glob.glob(search_path)\n    \n    annot = \"authentic\" # Default safe value\n    \n    if len(found_files) > 0:\n        img_path = found_files[0]\n        try:\n            pil = Image.open(img_path).convert(\"RGB\")\n            w_orig, h_orig = pil.size\n            \n            # Predict\n            prob_map = predict_ensemble(pil, models)\n            \n            # Resize\n            prob_full = cv2.resize(prob_map, (w_orig, h_orig), interpolation=cv2.INTER_LINEAR)\n            \n            # Threshold\n            mask = (prob_full > 0.25).astype(np.uint8)\n            \n            # Check Area\n            if mask.sum() >= 300:\n                rle = rle_encode(mask)\n                if len(rle) > 0:\n                    annot = rle\n            \n        except Exception as e:\n            print(f\"⚠️ Error {case_id}: {e}\")\n            # annot remains \"authentic\"\n    \n    results.append({\"case_id\": case_id, \"annotation\": annot})\n    \n    # Garbage collection to prevent OOM\n    if idx % 50 == 0:\n        gc.collect()\n\n# --- 6. SAVE ---\nsubmission = pd.DataFrame(results)\nsubmission.to_csv(OUT_PATH, index=False)\nprint(f\"✅ Saved to {OUT_PATH}. Rows: {len(submission)}\")\nprint(submission.head())","metadata":{"execution":{"iopub.status.busy":"2026-01-04T03:25:25.749462Z","iopub.execute_input":"2026-01-04T03:25:25.750039Z","iopub.status.idle":"2026-01-04T03:25:27.548083Z","shell.execute_reply.started":"2026-01-04T03:25:25.750011Z","shell.execute_reply":"2026-01-04T03:25:27.547396Z"},"papermill":{"duration":0.706627,"end_time":"2025-12-30T04:03:04.172732","exception":false,"start_time":"2025-12-30T04:03:03.466105","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🟢 Visualization of Authentic Images (Hybrid DINOv2-based Detector)","metadata":{"papermill":{"duration":0.015282,"end_time":"2025-12-30T04:03:06.517101","exception":false,"start_time":"2025-12-30T04:03:06.501819","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os, cv2, torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\n\n# --- 0. SAFETY CHECK ---\n# This ensures you ran the previous cell so 'models' and 'predict_ensemble' are defined\nif 'models' not in globals() or 'predict_ensemble' not in globals():\n    raise RuntimeError(\"⚠️ PLEASE RUN THE PREVIOUS 'CONSOLIDATED SOLUTION' CELL FIRST to load the models!\")\n\n# --- 1. SETUP PATHS ---\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\"\n\n# Grab 5 samples for validation\nval_auth = sorted([str(Path(AUTH_DIR)/f) for f in os.listdir(AUTH_DIR)])[:5]\nval_forg = sorted([str(Path(FORG_DIR)/f) for f in os.listdir(FORG_DIR)])[:5]\nprint(f\"✅ Loaded {len(val_forg)} forged and {len(val_auth)} authentic samples.\")\n\n# --- 2. DEFINE WRAPPER (Connects Visualizer to Ensemble) ---\ndef ensemble_visual_wrapper(pil_img):\n    # --- FIX: Pass 'models' explicitly to the prediction function ---\n    prob_map = predict_ensemble(pil_img, models)\n    \n    # Threshold & Clean\n    mask = (prob_map > 0.30).astype(np.uint8) \n    kernel = np.ones((5,5), np.uint8)\n    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)\n    \n    # Calc Stats\n    area = int(mask.sum())\n    mean_score = float(prob_map[mask==1].mean()) if area > 0 else 0.0\n    \n    # Label Logic\n    if area < 300:\n        label = \"authentic\"\n    else:\n        label = \"forged\"\n        \n    return label, mask, {\"area\": area, \"mean\": mean_score}\n\n# --- 3. DEFINE VISUALIZATION FUNCTION ---\ndef run_visual_diagnostics(sample_paths, is_forged=True):\n    n = len(sample_paths)\n    if n == 0: return\n\n    cols = 3 if is_forged else 2\n    fig, axes = plt.subplots(n, cols, figsize=(cols * 5, n * 4))\n    if n == 1: axes = np.expand_dims(axes, axis=0)\n    \n    for i, p in enumerate(sample_paths):\n        pil = Image.open(p).convert(\"RGB\")\n        \n        # Call Wrapper\n        label, mask, dbg = ensemble_visual_wrapper(pil)\n        \n        # Display\n        img_disp = np.array(pil.resize((IMG_SIZE, IMG_SIZE)))\n        \n        # Col 1: Original\n        axes[i, 0].imshow(img_disp)\n        axes[i, 0].set_title(f\"Original {'(Forged)' if is_forged else '(Auth)'}\", fontsize=10)\n        axes[i, 0].axis(\"off\")\n        \n        if is_forged:\n            # Col 2: GT\n            mask_path = Path(MASK_DIR)/f\"{Path(p).stem}.npy\"\n            if os.path.exists(mask_path):\n                m_gt = np.load(mask_path)\n                if m_gt.ndim == 3: m_gt = np.max(m_gt, axis=0)\n                gt_disp = cv2.resize((m_gt > 0).astype(np.uint8), (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n                axes[i, 1].imshow(gt_disp, cmap=\"gray\")\n                axes[i, 1].set_title(\"Ground Truth\", fontsize=10)\n            else:\n                axes[i, 1].text(0.5, 0.5, \"No Mask Found\", ha='center')\n                \n            axes[i, 1].axis(\"off\")\n            pred_col = 2\n        else:\n            pred_col = 1\n            \n        # Col 3: Prediction\n        axes[i, pred_col].imshow(img_disp)\n        if mask is not None:\n            mask_disp = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n            axes[i, pred_col].imshow(mask_disp, cmap=\"Reds\", alpha=0.4)\n            \n        # Color Logic\n        is_correct = (label == \"authentic\" and not is_forged) or (label == \"forged\" and is_forged)\n        color = \"green\" if is_correct else \"red\"\n        \n        axes[i, pred_col].set_title(f\"Pred: {label.upper()}\\nArea: {dbg['area']}\", color=color, weight=\"bold\")\n        axes[i, pred_col].axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()\n\n# --- 4. RUN IT ---\nprint(\"\\n🔍 Checking Forged Samples...\")\nrun_visual_diagnostics(val_forg[:3], is_forged=True)\n\nprint(\"\\n🟢 Checking Authentic Samples...\")\nrun_visual_diagnostics(val_auth[:3], is_forged=False)","metadata":{"papermill":{"duration":1.397446,"end_time":"2025-12-30T04:03:07.965401","exception":false,"start_time":"2025-12-30T04:03:06.567955","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\ndef validate_submission(submission_path, sample_path):\n    print(\"🕵️ BEGINNING SUBMISSION CHECK...\\n\")\n    \n    # 1. Load Files\n    try:\n        sub = pd.read_csv(submission_path)\n        sample = pd.read_csv(sample_path)\n        print(\"✅ Files loaded successfully.\")\n    except Exception as e:\n        print(f\"❌ ERROR: Could not load files. {e}\")\n        return\n\n    # 2. Check Column Names\n    expected_cols = [\"case_id\", \"annotation\"]\n    if list(sub.columns) != expected_cols:\n        print(f\"❌ ERROR: Incorrect column names. Found {list(sub.columns)}, expected {expected_cols}\")\n        return\n    print(\"✅ Column names are correct.\")\n\n    # 3. Check Row Count\n    if len(sub) != len(sample):\n        print(f\"❌ ERROR: Row count mismatch. Prediction has {len(sub)}, Sample has {len(sample)}\")\n        return\n    print(f\"✅ Row count matches ({len(sub)} rows).\")\n\n    # 4. Check IDs Match Exactly\n    # Force string comparison to avoid \"123\" vs 123 mismatches\n    sub_ids = set(sub[\"case_id\"].astype(str).str.strip())\n    sample_ids = set(sample[\"case_id\"].astype(str).str.strip())\n    \n    if sub_ids != sample_ids:\n        diff = sample_ids - sub_ids\n        print(f\"❌ ERROR: You are missing IDs from the sample submission: {list(diff)[:5]}...\")\n        return\n    print(\"✅ All Case IDs are present and matching.\")\n\n    # 5. Check Annotation Format\n    # Rules: Must be 'authentic' OR a string of numbers (RLE)\n    invalid_rows = []\n    for idx, row in sub.iterrows():\n        annot = row[\"annotation\"]\n        \n        # Check for NaNs\n        if pd.isna(annot):\n            invalid_rows.append(f\"Row {idx}: Found NaN/Empty\")\n            continue\n            \n        annot = str(annot).strip()\n        \n        # Check for empty string (Not allowed, must be 'authentic')\n        if annot == \"\":\n            invalid_rows.append(f\"Row {idx}: Found empty string (Should be 'authentic')\")\n            continue\n            \n        # If it's not 'authentic', it must be RLE (numbers)\n        if annot != \"authentic\":\n            # Simple check: Try to split by space and ensure mostly numbers\n            parts = annot.split()\n            if not parts[0].isdigit():\n                invalid_rows.append(f\"Row {idx}: Invalid RLE format (starts with '{parts[0]}')\")\n\n    if invalid_rows:\n        print(f\"❌ ERROR: Found {len(invalid_rows)} invalid annotations:\")\n        for err in invalid_rows[:5]: print(err)\n        print(\"...\")\n        return\n    \n    print(\"✅ Annotation format looks correct (Mixed 'authentic' and RLE).\")\n    print(\"\\n🎉 PASSED! Your file is ready to submit.\")\n\n# --- RUN THE VALIDATOR ---\nvalidate_submission(\"submission.csv\", SAMPLE_SUB)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T02:55:38.402074Z","iopub.execute_input":"2026-01-04T02:55:38.402407Z","iopub.status.idle":"2026-01-04T02:55:38.420017Z","shell.execute_reply.started":"2026-01-04T02:55:38.402379Z","shell.execute_reply":"2026-01-04T02:55:38.419122Z"}},"outputs":[],"execution_count":null}]}