{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"},{"sourceId":13661046,"sourceType":"datasetVersion","datasetId":8670261}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"KAGGLEHUB_PATH=\"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T21:49:24.728850Z","iopub.execute_input":"2025-11-08T21:49:24.729225Z","iopub.status.idle":"2025-11-08T21:49:24.735465Z","shell.execute_reply.started":"2025-11-08T21:49:24.729200Z","shell.execute_reply":"2025-11-08T21:49:24.734802Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\nimport csv\nimport warnings\nimport gc\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.utils.data as data\nimport cv2\nfrom tqdm.auto import tqdm\nimport sys\nimport logging\n\n# --- FINAL SUBMISSION CONFIGURATION ---\nIMAGE_SIZE = 256\nMODEL_INPUT_CHANNELS = 4 # Match your successful 4-channel input (RGB + ELA)\nOUTPUT_FILENAME = \"submission.csv\"\n\n# CRITICAL: Path to the newly optimized model weights\nFINAL_MODEL_PATH = \"/kaggle/input/rluc-sfic-st/submission_B060_Final.pth\" \n\n# Inference Parameters\nFIXED_THRESHOLD = 0.50      # Use 0.50 for the final decision threshold\nMIN_FORGERY_AREA = 64\nTversky_BETA = 0.60         # Beta used for the final loading/validation check\nalpha = 0.40\n\n# Kaggle Paths\nTEST_IMAGE_ROOT = os.path.join(KAGGLEHUB_PATH, \"test_images\")\nSAMPLE_SUBMISSION_FILE = os.path.join(KAGGLEHUB_PATH, \"sample_submission.csv\")\n\n# --- CORE FUNCTIONS (Required for loading the model and inference) ---\n\nclass TverskyLoss(nn.Module):\n    # This loss definition is only used to satisfy PyTorch's requirement for loading weights trained with this beta.\n    def __init__(self, alpha=alpha, beta=Tversky_BETA, smooth=1e-7):\n        super(TverskyLoss, self).__init__(); self.alpha = alpha; self.beta = beta; self.smooth = smooth\n    def forward(self, inputs, targets):\n        inputs = inputs.view(-1); targets = targets.view(-1)\n        TP = (inputs * targets).sum(); FP = ((1 - targets) * inputs).sum()\n        FN = (targets * (1 - inputs)).sum()\n        tversky_index = (TP + self.smooth) / (TP + self.alpha * FP + self.beta * FN + self.smooth)\n        return 1 - tversky_index\n\n# Helper function to build the correct Convolution Block structure\ndef ConvBlock(in_channels, out_channels):\n    return nn.Sequential(\n        nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),\n        nn.ReLU(inplace=True), \n        nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),\n        nn.ReLU(inplace=True)\n    )\n\nclass UNet(nn.Module):\n    # Reconstructed U-Net architecture matching the layer names and channels from the 0.303 checkpoint.\n    def __init__(self, in_channels, out_channels):\n        super(UNet, self).__init__()\n        \n        # Encoder\n        self.enc1 = ConvBlock(in_channels, 64)\n        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)\n        self.enc2 = ConvBlock(64, 128)\n        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)\n        \n        # Bottleneck\n        self.bottleneck = ConvBlock(128, 256)\n        \n        # Decoder\n        self.upconv2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)\n        self.dec2 = ConvBlock(128 + 128, 128) \n        \n        self.upconv1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)\n        self.dec1 = ConvBlock(64 + 64, 64) \n        \n        # Final Output\n        self.final_conv = nn.Conv2d(64, out_channels, kernel_size=1) \n        \n    def forward(self, x):\n        e1 = self.enc1(x)\n        p1 = self.pool1(e1)\n        e2 = self.enc2(p1)\n        p2 = self.pool2(e2)\n        b = self.bottleneck(p2)\n        d2 = self.upconv2(b)\n        d2 = F.interpolate(d2, size=e2.shape[2:], mode='nearest') \n        d2 = self.dec2(torch.cat((d2, e2), dim=1))\n        d1 = self.upconv1(d2)\n        d1 = F.interpolate(d1, size=e1.shape[2:], mode='nearest')\n        d1 = self.dec1(torch.cat((d1, e1), dim=1))\n        return torch.sigmoid(self.final_conv(d1))\n\n\ndef get_ela_feature_data(img_path):\n    \"\"\"Generates the single-channel ELA feature input (Must match training preprocessing).\"\"\"\n    try:\n        img = cv2.imread(img_path)\n        img_resized = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE))\n        ela_feature = np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=np.float32) \n        return ela_feature\n    except Exception:\n        return np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=np.float32)\n\ndef rle_encode(mask):\n    \"\"\"Encodes a binary mask into a space-separated RLE string.\"\"\"\n    if mask.sum() == 0: return \"authentic\"\n    pixels = mask.T.flatten(); pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    # NOTE: Returns space-separated RLE string: \"N N N N...\"\n    return ' '.join(str(x) for x in runs)\n\ndef create_test_df_robust(test_image_root, sample_submission_path):\n    master_df = pd.read_csv(sample_submission_path); master_df['case_id'] = master_df['case_id'].astype(str)\n    present_files = {}\n    if os.path.exists(test_image_root):\n        for root, _, files in os.walk(test_image_root):\n            for f in files:\n                case_id = os.path.splitext(f)[0]\n                if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')) and case_id.isdigit():\n                    present_files[case_id] = os.path.join(root, f)\n    master_df['img_path'] = master_df['case_id'].map(present_files).fillna('MISSING_FILE')\n    return master_df[master_df['img_path'] != 'MISSING_FILE'][['case_id', 'img_path']].reset_index(drop=True)\n\ndef run_submission_inference(unet_model, test_df, fixed_threshold, min_forgery_area):\n    results = []\n    unet_model.to('cpu').eval() # Run on CPU for stability in inference\n    \n    with torch.no_grad():\n        for index, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Generating Submission\"):\n            case_id = str(row['case_id']); img_path = row['img_path']\n            gc.collect()\n\n            img_bgr = cv2.imread(img_path)\n            if img_bgr is None: continue\n            \n            img_rgb_orig = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB);\n            \n            # Prepare 4-Channel Input (RGB + ELA)\n            rgb_resized = cv2.resize(img_rgb_orig, (IMAGE_SIZE, IMAGE_SIZE)) / 255.0\n            ela_feature = get_ela_feature_data(img_path)\n            \n            input_4ch = np.dstack([rgb_resized, np.expand_dims(ela_feature, axis=-1)])\n            \n            # Convert to PyTorch format (C, H, W) and add batch dim (1, C, H, W)\n            input_tensor = torch.from_numpy(input_4ch).permute(2, 0, 1).float().unsqueeze(0).to('cpu')\n\n            # Prediction\n            output_prob = unet_model(input_tensor).squeeze().numpy()\n            \n            # Post-Processing\n            final_mask_resized = (output_prob > fixed_threshold).astype(np.uint8)\n            clean_mask_resized = np.zeros_like(final_mask_resized)\n\n            num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(final_mask_resized, 4, cv2.CV_32S)\n\n            for label in range(1, num_labels):\n                area = stats[label, cv2.CC_STAT_AREA]\n                if area >= min_forgery_area:\n                    clean_mask_resized[labels == label] = 1\n            \n            original_shape = img_rgb_orig.shape[:2]\n            final_mask = cv2.resize(clean_mask_resized, (original_shape[1], original_shape[0]), interpolation=cv2.INTER_NEAREST)\n            rle_annotation = rle_encode(final_mask); \n            results.append({'case_id': case_id, 'annotation': rle_annotation})\n\n    return pd.DataFrame(results)\n\n# --- 3. FINAL EXECUTION BLOCK FOR INFERENCE ---\nif __name__ == \"__main__\":\n\n    print(\"\\n--- Starting FINAL SUBMISSION INFERENCE ---\")\n\n    # 1. Load Model\n    model = UNet(in_channels=MODEL_INPUT_CHANNELS, out_channels=1)\n\n    try:\n        if not os.path.exists(FINAL_MODEL_PATH):\n             raise FileNotFoundError(f\"Final model weights not found at: {FINAL_MODEL_PATH}.\")\n        \n        # Suppress UserWarning on load_weights\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\", UserWarning)\n            # CRITICAL FIX: Load the state dict and map to CPU\n            model.load_state_dict(torch.load(FINAL_MODEL_PATH, map_location=torch.device('cpu')), strict=False)\n        \n        print(f\"✅ Loaded Final CTF Model weights: {FINAL_MODEL_PATH}\")\n    except Exception as e:\n        print(f\"🛑 FATAL Error loading final weights: {e}. Aborting submission.\")\n        sys.exit(1)\n\n    # 2. Generate Submission File for Test Data\n    print(\"\\n--- Generating Kaggle Submission File ---\")\n    test_df = create_test_df_robust(TEST_IMAGE_ROOT, SAMPLE_SUBMISSION_FILE)\n\n    if test_df.empty:\n        submission_df = pd.DataFrame(columns=['case_id', 'annotation'])\n    else:\n        print(f\"Processing {len(test_df)} test case(s)...\")\n        results_df = run_submission_inference(model, test_df, FIXED_THRESHOLD, MIN_FORGERY_AREA)\n        submission_df = pd.read_csv(SAMPLE_SUBMISSION_FILE)[['case_id']].astype(str)\n        submission_df = submission_df.merge(results_df, on='case_id', how='left')\n        submission_df['annotation'] = submission_df['annotation'].fillna('authentic')\n        submission_df = submission_df[['case_id', 'annotation']].sort_values('case_id').reset_index(drop=True)\n\n    # 3. Write Final CSV (Guaranteed Correct RLE Formatting)\n    with open(OUTPUT_FILENAME, \"w\", newline='') as f:\n        writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)\n        writer.writerow(['case_id', 'annotation'])\n\n        for _, row in submission_df.iterrows():\n            annotation = row['annotation']\n\n            if annotation.lower() == 'authentic':\n                writer.writerow([row['case_id'], annotation])\n            else:\n                # CRITICAL FIX: Generate the exact comma-separated string required inside the brackets.\n                \n                # 1. Split the space-separated numbers (e.g., \"442080 34 442384 40\")\n                rle_list = annotation.split(' ')\n                \n                # 2. Join the list using \", \" (e.g., \"442080, 34, 442384, 40\")\n                comma_separated_rle = \", \".join(rle_list)\n                \n                # 3. Wrap in brackets.\n                full_rle_string = f\"[{comma_separated_rle}]\"\n                \n                writer.writerow([row['case_id'], full_rle_string])\n\n    print(f\"\\n✅ FINAL SUBMISSION CREATED: {OUTPUT_FILENAME} with {len(submission_df)} total rows. Please submit this file.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-08T21:50:29.088364Z","iopub.execute_input":"2025-11-08T21:50:29.089059Z","iopub.status.idle":"2025-11-08T21:50:29.665085Z","shell.execute_reply.started":"2025-11-08T21:50:29.089029Z","shell.execute_reply":"2025-11-08T21:50:29.664529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate_and_print_rle(submission_df):\n    \"\"\"\n    Validates RLE output structure and prints debugging info.\n    Checks for: 1. Authentic/RLE count. 2. Even number of RLE elements.\n    \"\"\"\n    print(\"\\n--- RLE Output Validation Check ---\")\n\n    # Analyze the annotations\n    authentic_count = submission_df['annotation'].apply(lambda x: x == 'authentic').sum()\n    rle_rows = submission_df[submission_df['annotation'] != 'authentic']\n\n    print(f\"Total Submissions: {len(submission_df)}\")\n    print(f\"Authentic (No Forgery) Count: {authentic_count}\")\n    print(f\"RLE Annotated (Forged) Count: {len(rle_rows)}\")\n\n    # CRITICAL CHECK: RLE strings must always have an even number of elements (start, length, start, length...)\n    rle_check = rle_rows['annotation'].apply(lambda x: len(x.split(' ')) % 2 == 0)\n\n    if rle_check.all():\n        print(f\"✅ RLE Structure: All {len(rle_rows)} RLE strings contain an even number of elements.\")\n    else:\n        # Prints a warning if any RLE string has an odd number of elements (a common error)\n        bad_rle_count = len(rle_rows) - rle_check.sum()\n        print(f\"🛑 RLE ERROR: Found {bad_rle_count} RLE strings with an odd number of elements (Invalid pairing).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T21:50:39.723016Z","iopub.execute_input":"2025-11-08T21:50:39.723772Z","iopub.status.idle":"2025-11-08T21:50:39.728694Z","shell.execute_reply.started":"2025-11-08T21:50:39.723744Z","shell.execute_reply":"2025-11-08T21:50:39.728143Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = pd.read_csv(\"submission.csv\")\nvalidate_and_print_rle(submission_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T21:50:45.390308Z","iopub.execute_input":"2025-11-08T21:50:45.391093Z","iopub.status.idle":"2025-11-08T21:50:45.399866Z","shell.execute_reply.started":"2025-11-08T21:50:45.391055Z","shell.execute_reply":"2025-11-08T21:50:45.399112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cat submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T21:51:11.653676Z","iopub.execute_input":"2025-11-08T21:51:11.654413Z","iopub.status.idle":"2025-11-08T21:51:11.783470Z","shell.execute_reply.started":"2025-11-08T21:51:11.654387Z","shell.execute_reply":"2025-11-08T21:51:11.782564Z"}},"outputs":[],"execution_count":null}]}