{"metadata":{"kernelspec":{"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":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"colab":{"provenance":[],"machine_shape":"hm","gpuType":"L4"},"accelerator":"GPU","widgets":{"application/vnd.jupyter.widget-state+json":{"12a115fe799d429e9b1438dade0ffc6a":{"model_module":"@jupyter-widgets/controls","model_name":"VBoxModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"VBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"VBoxView","box_style":"","children":["IPY_MODEL_a3f49af60c7b409e8371402d1b11d162"],"layout":"IPY_MODEL_7daa8fccb917410c850402c6b4e0d61a"}},"5a37d2c3d677439cab2964fabfec16c7":{"model_module":"@jupyter-widgets/controls","model_name":"HTMLModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_b9030f4fb00d4046bd3db03a0c0cf43c","placeholder":"​","style":"IPY_MODEL_70176a73ba1340ed83f3f96030a946c6","value":"<center> <img\nsrc=https://www.kaggle.com/static/images/site-logo.png\nalt='Kaggle'> <br> Create an API token from <a\nhref=\"https://www.kaggle.com/settings/account\" target=\"_blank\">your Kaggle\nsettings page</a> and paste it below along with your Kaggle username. <br> </center>"}},"6cc8e32c07174f5ba9ab70b221b57987":{"model_module":"@jupyter-widgets/controls","model_name":"TextModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"TextModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"TextView","continuous_update":true,"description":"Username:","description_tooltip":null,"disabled":false,"layout":"IPY_MODEL_b417e21ab2c94a039cc1c51bfe7cc3a7","placeholder":"​","style":"IPY_MODEL_2efa56a07b6c414b9f8a263eec179a3e","value":"frankmorales"}},"0bdf6137fc444f0db1b532f0ee879a2f":{"model_module":"@jupyter-widgets/controls","model_name":"PasswordModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"PasswordModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"PasswordView","continuous_update":true,"description":"Token:","description_tooltip":null,"disabled":false,"layout":"IPY_MODEL_2a3c010615784d75a58b3597b98b0105","placeholder":"​","style":"IPY_MODEL_2de99be410664909937ce1da1ac42f8c","value":""}},"34dc31a2942041f4a85029fddd461605":{"model_module":"@jupyter-widgets/controls","model_name":"ButtonModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ButtonModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"ButtonView","button_style":"","description":"Login","disabled":false,"icon":"","layout":"IPY_MODEL_10f1fdb1c3f64540b365e07bfaa08c35","style":"IPY_MODEL_9554d978f66245b79df92d84264d582b","tooltip":""}},"8a8db783f77d4b19af3f97ffb25ad32b":{"model_module":"@jupyter-widgets/controls","model_name":"HTMLModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"HTMLView","description":"","description_tooltip":null,"layout":"IPY_MODEL_254e49928fd14eab9b0ea6353f824859","placeholder":"​","style":"IPY_MODEL_375e36719c0d41ca9f603294dd260666","value":"\n<b>Thank You</b></center>"}},"7daa8fccb917410c850402c6b4e0d61a":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":"center","align_self":null,"border":null,"bottom":null,"display":"flex","flex":null,"flex_flow":"column","grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":"50%"}},"b9030f4fb00d4046bd3db03a0c0cf43c":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"70176a73ba1340ed83f3f96030a946c6":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"b417e21ab2c94a039cc1c51bfe7cc3a7":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"2efa56a07b6c414b9f8a263eec179a3e":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"2a3c010615784d75a58b3597b98b0105":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"2de99be410664909937ce1da1ac42f8c":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"10f1fdb1c3f64540b365e07bfaa08c35":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9554d978f66245b79df92d84264d582b":{"model_module":"@jupyter-widgets/controls","model_name":"ButtonStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"ButtonStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","button_color":null,"font_weight":""}},"254e49928fd14eab9b0ea6353f824859":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"375e36719c0d41ca9f603294dd260666":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"2d4b7552d1a24887a3dd17a27cd69795":{"model_module":"@jupyter-widgets/controls","model_name":"LabelModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"LabelModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"LabelView","description":"","description_tooltip":null,"layout":"IPY_MODEL_5826e45d028841ee9e1e948f98c09804","placeholder":"​","style":"IPY_MODEL_3ecab2a06e7c4dec8a4584eb9c298331","value":"Connecting..."}},"5826e45d028841ee9e1e948f98c09804":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"3ecab2a06e7c4dec8a4584eb9c298331":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}},"a3f49af60c7b409e8371402d1b11d162":{"model_module":"@jupyter-widgets/controls","model_name":"LabelModel","model_module_version":"1.5.0","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"LabelModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"1.5.0","_view_name":"LabelView","description":"","description_tooltip":null,"layout":"IPY_MODEL_22b70ddd3a4f429387d52e49f5184378","placeholder":"​","style":"IPY_MODEL_8fcef2f320e64af7882905d3b11fad51","value":"Kaggle credentials successfully validated."}},"22b70ddd3a4f429387d52e49f5184378":{"model_module":"@jupyter-widgets/base","model_name":"LayoutModel","model_module_version":"1.2.0","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"1.2.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"overflow_x":null,"overflow_y":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"8fcef2f320e64af7882905d3b11fad51":{"model_module":"@jupyter-widgets/controls","model_name":"DescriptionStyleModel","model_module_version":"1.5.0","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"1.5.0","_model_name":"DescriptionStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"1.2.0","_view_name":"StyleView","description_width":""}}}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"This is the documentation for the final, optimized **Scientific Image Forgery Detection** solution, built on the Keras/TensorFlow framework.\n\nThe core strategy was to overcome the severe $\\mathbf{\\sim 0.00\\%}$ class imbalance (forged pixels vs. authentic pixels), which caused earlier models to fail and get \"stuck\" by predicting only the background (Authentic).\n\n***\n\n## 1. Solution Overview and Strategy\n\n| Component | Purpose | Key Result |\n| :--- | :--- | :--- |\n| **Strategy** | **Imbalance Resolution** | Used an aggressive, custom **Tversky Loss** ($\\beta=0.90$) combined with forensic data augmentation to force the model to prioritize finding the rare forged pixels (True Positives). |\n| **Architecture** | **Dual-Stream U-Net** | Uses two parallel inputs to fuse visual features (RGB) and forgery artifacts (Noise Residual) early in the network. |\n| **Training** | **Stable Convergence** | Achieved active learning metrics (val\\_loss continuously dropped) and successfully triggered $\\mathbf{EarlyStopping}$ at the optimal generalization epoch (e.g., Epoch 25), avoiding overfitting. |\n\n***\n\n## 2. Model Architecture (`build_dual_stream_unet`)\n\nThe network uses a specialized dual-stream U-Net for multimodal input processing:\n\n* **Stream 1 (RGB Input):** Learns standard visual features and contextual information from the downsampled color image.\n* **Stream 2 (Feature Input):** Learns high-frequency noise patterns extracted from the grayscale image using the **Noise Residual (NR)** calculation (Gaussian Blur subtraction). This stream specializes in forensic evidence.\n* **Fusion:** The outputs of Stream 1 and Stream 2 are concatenated and processed by the decoder paths, combining visual context with forensic evidence for final pixel-level prediction.\n\n***\n\n## 3. Imbalance Mitigation and Training Setup\n\nThe core of the success lies in aggressively handling the data imbalance in the loss function:\n\n### A. Tversky Loss Function\n\nThe model uses a customized Tversky Loss, which is an extension of the Dice Loss tailored for class imbalance.\n\n$$L_{\\text{Tversky}} = 1 - \\frac{\\text{TP} + \\epsilon}{\\text{TP} + \\alpha \\cdot \\text{FP} + \\beta \\cdot \\text{FN} + \\epsilon}$$\n\n* **$\\beta = 0.90$ (False Negative Weight):** This is the critical value. It assigns a **9x higher penalty** to the model for missing a true forgery pixel (False Negative) than for incorrectly labeling an authentic pixel as forgery (False Positive). This forces the model to stop ignoring the rare forgery class.\n* **$\\alpha = 0.10$ (False Positive Weight):** The complementary term ($\\alpha = 1 - \\beta$).\n* **Metrics:** Training monitors `val_loss` (Tversky Loss) and the standard `dice_coef` (Foreground IoU proxy).\n\n### B. Data Augmentation (`DualStreamDataGenerator`)\n\nData augmentation is applied **only to the training set** to increase the robustness of the sparse forgery signals:\n\n1.  **Spatial Augmentations:** Random horizontal and vertical flips are applied to both the image and the mask.\n2.  **Forensic Augmentation (JPEG Recompression):** To make the Noise Residual feature robust to image saving, the RGB image is randomly recompressed with a quality factor between **70 and 95** (50% probability). The Noise Residual feature is then calculated from this compressed image, simulating real-world scenarios.\n\n***\n\n## 4. Inference and Post-Processing\n\nThe inference pipeline ensures predictions are accurate and adhere to the competition format:\n\n1.  **Dual-Stream Preprocessing:** The test image is loaded, resized, and processed to generate both the **RGB input** and the **Noise Residual (NR) input** arrays.\n2.  **Prediction:** The Keras model predicts a probability mask for forgery.\n3.  **Thresholding:** A fixed threshold ($\\mathbf{0.45}$) is applied to convert the probability map into a binary mask.\n4.  **Minimum Area Filtering:** A small forgery region threshold ($\\mathbf{32}$ pixels) is applied using OpenCV's `connectedComponentsWithStats` to remove spurious noise and false positives, improving the final score quality.\n5.  **RLE Encoding:** The final binary mask is converted to the standard RLE format (`[start length] ...`), resulting in the complex output seen for forged images.\n\nThe final saved file is **`/tmp/model_new_scratch.weights.h5`** (Keras weights).","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nimport warnings\nfrom warnings import filterwarnings\n\nfilterwarnings('ignore') # Suppress warnings\n\n# --- CONFIGURATION (from the original notebook) ---\nTARGET_SIZE = 256\nTRAIN_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images\"\nMASK_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\"\n\n## COLAB\n#TRAIN_ROOT = f\"{recodai_luc_scientific_image_forgery_detection_path}/train_images\"\n#MASK_ROOT = f\"{recodai_luc_scientific_image_forgery_detection_path}/train_masks\"\n##\n\n\n# Replicate compute_ela for feature analysis\ndef compute_ela(img_path, quality=95, scale=10):\n    # ... (omitted for brevity, assume the original function is available)\n    # The original notebook's ELA function is used here.\n    img = cv2.imread(img_path)\n    if img is None or img.size == 0:\n        try:\n            img_data = np.load(img_path)\n            if img_data.ndim == 3: img = cv2.cvtColor(img_data, cv2.COLOR_RGB2BGR)\n            elif img_data.ndim == 2: img = cv2.cvtColor(img_data, cv2.COLOR_GRAY2BGR)\n        except Exception: return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    if img is None or img.size == 0:\n        return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n\n    img_resized = cv2.resize(img, (TARGET_SIZE, TARGET_SIZE))\n    temp_path = f\"/tmp/temp_ela_{os.path.basename(img_path)}.jpg\" # Simplified temp_path\n    try:\n        # Use a consistent quality setting (95)\n        cv2.imwrite(temp_path, img_resized, [cv2.IMWRITE_JPEG_QUALITY, quality])\n        compressed_img = cv2.imread(temp_path)\n        if compressed_img is None: return np.zeros((TARGET_SIZE, TARGET_SIZE), dtype=np.float32)\n        error = np.abs(img_resized.astype(np.float32) - compressed_img.astype(np.float32))\n        ela_feature_2d = np.mean(error, axis=2) * scale # Scale by 10 as in the notebook\n    finally:\n        if os.path.exists(temp_path): os.remove(temp_path)\n    return cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE), interpolation=cv2.INTER_LINEAR).astype(np.float32)\n\n# Load the filtered DataFrame (assuming the prior EDA cell's 'eda_df' is available or recreate it)\ndata_list = []\nfor root, _, files in os.walk(TRAIN_ROOT):\n    for f in files:\n        valid_extensions = ('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')\n        if f.lower().endswith(valid_extensions) and 'forged' in root.lower():\n            case_id = os.path.splitext(f)[0]\n            mask_path = os.path.join(MASK_ROOT, f\"{case_id}.npy\")\n            if os.path.exists(mask_path):\n                data_list.append({'img_path': os.path.join(root, f), 'mask_path': mask_path})\neda_df = pd.DataFrame(data_list)\n\nprint(\"--- Starting Advanced EDA (Imbalance & Feature Check) ---\")\n\nif eda_df.empty:\n    print(\"🛑 EDA Skipped: Data frame is empty.\")\nelse:\n    total_pixels = 0\n    forgery_pixels = 0\n    ela_values, rgb_means = [], []\n\n    # Process only the first 50 images to speed up ELA computation for EDA\n    for index, row in tqdm(eda_df.head(50).iterrows(), total=len(eda_df.head(50)), desc=\"Processing samples\"):\n        try:\n            # 1. Image and Mask Load\n            rgb_image = cv2.cvtColor(cv2.imread(row['img_path']), cv2.COLOR_BGR2RGB)\n            if rgb_image is None or rgb_image.size == 0: continue\n\n            mask = np.load(row['mask_path'])\n            if mask.ndim > 2: mask = mask[:, :, 0]\n\n            # 2. Imbalance Check (Use original sizes for best estimate)\n            h, w = rgb_image.shape[:2]\n            total_pixels += h * w\n            forgery_pixels += np.sum(mask > 0)\n\n            # 3. ELA Feature Check (Use 256x256 resized data)\n            ela_feature = compute_ela(row['img_path'])\n            ela_values.extend(ela_feature.flatten())\n\n            # RGB feature check (resize/normalize similar to training)\n            rgb_resized = cv2.resize(rgb_image, (TARGET_SIZE, TARGET_SIZE)) / 255.0\n            rgb_means.extend(rgb_resized.mean(axis=2).flatten())\n\n        except Exception as e:\n            # print(f\"Warning: Could not process {row['img_path']}: {e}\")\n            continue\n\n    # --- Analysis 1: Imbalance Ratio ---\n    if total_pixels > 0:\n        imbalance_ratio = (forgery_pixels / total_pixels) * 100\n        print(f\"\\n--- Imbalance Ratio (Forged Pixels) ---\")\n        print(f\"Total Pixels Sampled: {total_pixels:,}\")\n        print(f\"Forged Pixels Sampled: {forgery_pixels:,}\")\n        print(f\"Forgery Imbalance Ratio: **{imbalance_ratio:.2f}%** (Positive Class)\")\n\n    # --- Analysis 2: ELA Feature Distribution vs. RGB ---\n    if ela_values:\n        ela_values = np.array(ela_values)\n        rgb_means = np.array(rgb_means)\n\n        print(f\"\\n--- ELA Feature Distribution (Scaled by 10) ---\")\n        print(f\"ELA Feature Mean: {np.mean(ela_values):.4f}\")\n        print(f\"ELA Feature Std Dev: {np.std(ela_values):.4f}\")\n        print(f\"RGB Mean (Normalized): {np.mean(rgb_means):.4f}\")\n\n        plt.figure(figsize=(12, 5))\n        plt.hist(ela_values, bins=50, alpha=0.6, label='ELA Feature (Scaled)', color='red')\n        plt.title('Distribution of ELA Feature Values')\n        plt.xlabel('ELA Value (0 to ~2550)')\n        plt.ylabel('Frequency')\n        plt.legend()\n        plt.show()\n\n        # This histogram helps visualize if ELA is predominantly zero or clustered.\n\nprint(\"\\n--- Advanced EDA Complete ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T01:08:24.851371Z","iopub.execute_input":"2025-11-01T01:08:24.852055Z","iopub.status.idle":"2025-11-01T01:08:32.360446Z","shell.execute_reply.started":"2025-11-01T01:08:24.852033Z","shell.execute_reply":"2025-11-01T01:08:32.359724Z"},"id":"x44KuEeWeT9Z","outputId":"54d6de6f-316e-412c-d362-22bd179114c3"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport cv2\nimport os\n\n# Define the file path\nTEST_IMAGE_PATH = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images/45.png\"\n\n## COLAB\n#TRAIN_ROOT = f\"{recodai_luc_scientific_image_forgery_detection_path}/train_images\"\n#MASK_ROOT = f\"{recodai_luc_scientific_image_forgery_detection_path}/train_masks\"\n#TEST_IMAGE_PATH =  f\"{recodai_luc_scientific_image_forgery_detection_path}/test_images/45.png\"\n##\n\n## COLAB\n#TRAIN_ROOT = f\"{recodai_luc_scientific_image_forgery_detection_path}/train_images\"\n#MASK_ROOT = f\"{recodai_luc_scientific_image_forgery_detection_path}/train_masks\"\n##\n\nTEST_IMAGE_PATH =  f\"{recodai_luc_scientific_image_forgery_detection_path}/test_images/45.png\"\n\nprint(f\"Attempting to load image: {TEST_IMAGE_PATH}\")\n\nif not os.path.exists(TEST_IMAGE_PATH):\n    print(\"🛑 ERROR: The file path was not found. Please ensure the Kaggle competition data is mounted correctly.\")\nelse:\n    # Load the image using OpenCV (loads as BGR)\n    img = cv2.imread(TEST_IMAGE_PATH)\n\n    if img is None:\n        print(\"🛑 ERROR: Could not read the image file.\")\n    else:\n        # Convert the image from BGR (OpenCV default) to RGB (Matplotlib default)\n        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        # Plot the image\n        plt.figure(figsize=(10, 8))\n        plt.imshow(img_rgb)\n        plt.title(f\"Test Image 45 (Dimensions: {img.shape[0]}x{img.shape[1]})\")\n        plt.axis('off') # Hide axes for a cleaner image view\n        plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T01:08:38.066037Z","iopub.execute_input":"2025-11-01T01:08:38.066341Z","iopub.status.idle":"2025-11-01T01:08:38.51224Z","shell.execute_reply.started":"2025-11-01T01:08:38.066319Z","shell.execute_reply":"2025-11-01T01:08:38.511395Z"},"id":"U9JBlxkveT9a","outputId":"8993b120-ced9-41b6-f7a3-72dee0278f4b"},"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-01T01:21:28.462531Z","iopub.execute_input":"2025-11-01T01:21:28.462846Z","iopub.status.idle":"2025-11-01T01:21:28.469508Z","shell.execute_reply.started":"2025-11-01T01:21:28.462821Z","shell.execute_reply":"2025-11-01T01:21:28.468821Z"},"id":"_9ghuf8keT9b"},"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-01T01:21:33.094453Z","iopub.execute_input":"2025-11-01T01:21:33.094747Z","iopub.status.idle":"2025-11-01T01:21:33.105453Z","shell.execute_reply.started":"2025-11-01T01:21:33.094726Z","shell.execute_reply":"2025-11-01T01:21:33.104736Z"},"id":"5M2qEGdQeT9b","outputId":"09e957f6-7134-4fac-ee7c-75009e6414fe"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- CONFIGURATION (FINAL CODE POST-FIX) ---\n\nCOMPETITION_SLUG = \"recodai-luc-scientific-image-forgery-detection\"\n\n# --- ROBUST PATH SETUP for KaggleHub/Colab Environment ---\nKAGGLEHUB_PATH = f\"/root/.cache/kagglehub/competitions/{COMPETITION_SLUG}\"\nTRAIN_ROOT = os.path.join(KAGGLEHUB_PATH, \"train_images\", \"forged\")\nMASK_ROOT = os.path.join(KAGGLEHUB_PATH, \"train_masks\")\n\nIMAGE_SIZE = 256\nBATCH_SIZE = 16\nMAX_EPOCHS = 50\n# ---------------------\n\nimport numpy as np\nimport pandas as pd\nimport os\nimport warnings\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, backend as K\nfrom tensorflow.keras.callbacks import Callback, EarlyStopping, ReduceLROnPlateau\nimport cv2\nfrom glob import glob\nfrom tqdm import tqdm\nimport gc\nfrom sklearn.model_selection import train_test_split\nimport logging\nimport random\n\ntf.random.set_seed(42)\n\n# Suppress warnings\nwarnings.filterwarnings('ignore')\ntf.get_logger().setLevel(logging.ERROR)\n\n# --- 1. Utility Functions, Metrics, and Callbacks ---\n\ndef dice_coef(y_true, y_pred, smooth=1e-7):\n    \"\"\"Standard Dice Coefficient (used as a metric).\"\"\"\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.flatten(y_pred)\n    intersection = K.sum(y_true_f * y_pred_f)\n    return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)\n\ndef tversky_loss(y_true, y_pred, alpha=0.10, beta=0.90, smooth=1e-7): # <--- CRITICAL FIX: BETA SET TO 0.90\n    \"\"\"\n    CRITICAL FIX: Tversky Loss with beta=0.90 to aggressively prioritize False Negatives.\n    \"\"\"\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.flatten(y_pred)\n\n    TP = K.sum(y_true_f * y_pred_f)\n    FP = K.sum((1 - y_true_f) * y_pred_f)\n    FN = K.sum(y_true_f * (1 - y_pred_f))\n\n    tversky_index = (TP + smooth) / (TP + alpha * FP + beta * FN + smooth)\n\n    return 1 - tversky_index\n\nclass EpochStatReporter(Callback):\n    def __init__(self, generator):\n        super().__init__()\n        self.generator = generator\n\n    def on_epoch_end(self, epoch, logs=None):\n        skipped = self.generator.skipped_count\n        log_message = f\"Epoch {epoch + 1} finished: \"\n        for k, v in logs.items():\n            log_message += f\"{k}: {v:.4f} \"\n        log_message += f\"| TOTAL SAMPLES SKIPPED: {skipped}\"\n\n        print(\"\\n\" + \"=\"*80)\n        print(log_message)\n        print(\"=\"*80 + \"\\n\")\n\n# --- 2. Forgery Feature Extraction (Noise Residual) ---\n\ndef get_forgery_features_from_data(img_grayscale_data):\n    \"\"\"Generates a Noise Residual feature map (Stream 2 input) from augmented data.\"\"\"\n    img = img_grayscale_data.astype(np.uint8)\n\n    if img is None or img.size == 0:\n        return np.zeros((IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.float32)\n\n    blur = cv2.GaussianBlur(img, (5, 5), 0)\n    residual = img.astype(np.float32) - blur.astype(np.float32)\n\n    residual = cv2.resize(residual, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_LINEAR)\n    residual = (residual - residual.min()) / (residual.max() - residual.min() + 1e-7)\n\n    return np.stack([residual]*3, axis=-1).astype(np.float32)\n\n# --- 3. Custom Dual-Stream Data Generator (WITH AUGMENTATION) ---\n\nclass DualStreamDataGenerator(tf.keras.utils.Sequence):\n\n    def __init__(self, df, batch_size=16, shuffle=True, is_validation=False, **kwargs):\n        super().__init__(**kwargs)\n        self.df = df\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.is_validation = is_validation\n        self.on_epoch_end()\n        self.skipped_count = 0\n\n    def on_epoch_end(self):\n        self.indexes = np.arange(len(self.df))\n        if self.shuffle and not self.is_validation:\n            np.random.shuffle(self.indexes)\n        self.skipped_count = 0\n\n    def __len__(self):\n        return int(np.floor(len(self.df) / self.batch_size))\n\n    def __getitem__(self, index):\n        indexes = self.indexes[index * self.batch_size:(index + 1) * self.batch_size]\n        batch_df = self.df.iloc[indexes]\n\n        temp_X1, temp_X2, temp_Y = [], [], []\n\n        for row in batch_df.itertuples():\n            image_id = row.id\n\n            img_path_candidates = [\n                os.path.join(TRAIN_ROOT, f'{image_id}.png'),\n                os.path.join(TRAIN_ROOT, f'{image_id}.jpg'),\n                os.path.join(TRAIN_ROOT, f'{image_id}.tif'),\n                os.path.join(TRAIN_ROOT, f'{image_id}.tiff'),\n            ]\n\n            actual_img_path = next((path for path in img_path_candidates if os.path.exists(path)), None)\n\n            if actual_img_path is None:\n                self.skipped_count += 1\n                continue\n\n            mask_path = os.path.join(MASK_ROOT, f'{image_id}.npy')\n\n            # --- Load Original Data ---\n            try:\n                img_rgb_orig = cv2.cvtColor(cv2.imread(actual_img_path), cv2.COLOR_BGR2RGB)\n                mask = np.load(mask_path)\n            except Exception as e:\n                self.skipped_count += 1\n                continue\n\n            # --- AUGMENTATION (Training only) ---\n            img_rgb_final = img_rgb_orig.copy()\n\n            # 1. CRITICAL MASK FIX: Ensure mask is 2D for cv2.flip\n            mask_final = np.squeeze(mask.copy())\n            if mask_final.ndim == 3:\n                mask_final = mask_final[:, :, 0]\n            if mask_final.ndim != 2:\n                self.skipped_count += 1\n                continue\n            # -----------------------------------\n\n            if not self.is_validation:\n\n                # 2. SPATIAL AUGMENTATION (Horizontal/Vertical Flip)\n                if random.random() > 0.5: # Horizontal Flip\n                    img_rgb_final = cv2.flip(img_rgb_final, 1)\n                    mask_final = cv2.flip(mask_final, 1)\n                if random.random() > 0.5: # Vertical Flip\n                    img_rgb_final = cv2.flip(img_rgb_final, 0)\n                    mask_final = cv2.flip(mask_final, 0)\n\n                # 3. FORENSIC AUGMENTATION (Random JPEG Recompression)\n                if random.random() > 0.5:\n                    quality = random.randint(70, 95)\n                    # Recompress and decode the RGB image\n                    _, buffer = cv2.imencode('.jpg', cv2.cvtColor(img_rgb_final, cv2.COLOR_RGB2BGR), [cv2.IMWRITE_JPEG_QUALITY, quality])\n                    img_rgb_final = cv2.cvtColor(cv2.imdecode(buffer, cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB)\n\n            # --- Feature Generation & Final Preprocessing ---\n\n            try:\n                # Convert to grayscale after potential JPEG recompression\n                img_gray_final = cv2.cvtColor(img_rgb_final, cv2.COLOR_RGB2GRAY)\n\n                # X1: RGB Image (normalized and resized)\n                X1_sample = cv2.resize(img_rgb_final, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_LINEAR) / 255.0\n\n                # X2: Feature Map (Noise Residual calculated from the augmented/recompressed grayscale image)\n                X2_sample = get_forgery_features_from_data(img_gray_final)\n\n                # Y: Mask (resized, reshaped)\n                mask_resized = cv2.resize(mask_final, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_NEAREST)\n                Y_sample = mask_resized.reshape(IMAGE_SIZE, IMAGE_SIZE, 1).astype(np.float32)\n\n            except Exception as e:\n                self.skipped_count += 1\n                continue\n\n            temp_X1.append(X1_sample)\n            temp_X2.append(X2_sample)\n            temp_Y.append(Y_sample)\n\n        # --- Final Batch Construction (Handling Skips) ---\n\n        if not temp_X1:\n            placeholder_x1 = np.zeros((self.batch_size, IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.float32)\n            placeholder_x2 = np.zeros((self.batch_size, IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.float32)\n            placeholder_y = np.zeros((self.batch_size, IMAGE_SIZE, IMAGE_SIZE, 1), dtype=np.float32)\n\n            return (placeholder_x1, placeholder_x2), placeholder_y\n\n        while len(temp_X1) < self.batch_size:\n            temp_X1.append(temp_X1[-1])\n            temp_X2.append(temp_X2[-1])\n            temp_Y.append(temp_Y[-1])\n\n        return (np.array(temp_X1), np.array(temp_X2)), np.array(temp_Y)\n\n# --- 4. Dual-Stream U-Net Model (UNCHANGED) ---\ndef build_dual_stream_unet(input_shape):\n    input_rgb = layers.Input(shape=input_shape, name='rgb_input')\n    conv_rgb = models.Sequential([\n        layers.Conv2D(32, 3, activation='relu', padding='same'), layers.MaxPooling2D(),\n        layers.Conv2D(64, 3, activation='relu', padding='same')], name='rgb_stream')(input_rgb)\n    input_feat = layers.Input(shape=input_shape, name='feature_input')\n    conv_feat = models.Sequential([\n        layers.Conv2D(32, 3, activation='relu', padding='same'), layers.MaxPooling2D(),\n        layers.Conv2D(64, 3, activation='relu', padding='same')], name='feature_stream')(input_feat)\n    merged = layers.concatenate([conv_rgb, conv_feat])\n    up1 = layers.UpSampling2D(size=(2, 2))(merged)\n    conv_final = layers.Conv2D(128, 3, activation='relu', padding='same')(up1)\n    output = layers.Conv2D(1, 1, activation='sigmoid', padding='same')(conv_final)\n    model = models.Model(inputs=[input_rgb, input_feat], outputs=output)\n    return model\n\n# --- 5. Training Loop (Execution) ---\n\n# --- Setup DataFrames ---\nall_files = glob(os.path.join(TRAIN_ROOT, '*'))\ndf = pd.DataFrame([os.path.basename(f).split('.')[0] for f in all_files], columns=['id'])\n\ntrain_df, val_df = train_test_split(df, test_size=0.2, random_state=42)\n\n# --- Final Checks & Execution ---\nif len(df) == 0:\n    print(f\"FATAL: The path '{TRAIN_ROOT}' is empty. Cannot proceed.\")\n    model = None\nelse:\n    train_gen = DualStreamDataGenerator(train_df, batch_size=BATCH_SIZE, shuffle=True, is_validation=False)\n    val_gen = DualStreamDataGenerator(val_df, batch_size=BATCH_SIZE, shuffle=False, is_validation=True)\n\n    model = build_dual_stream_unet((IMAGE_SIZE, IMAGE_SIZE, 3))\n\n    model.compile(optimizer='adam', loss=tversky_loss, metrics=[dice_coef, 'accuracy'])\n\n    # Implement the stability callbacks\n    reduce_lr = ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-6,\n        verbose=1\n    )\n\n    early_stop = EarlyStopping(\n        monitor='val_loss',\n        patience=5,\n        restore_best_weights=True,\n        verbose=1\n    )\n\n    stat_reporter = EpochStatReporter(train_gen)\n    callbacks = [stat_reporter, reduce_lr, early_stop]\n\n    print(f\"Training on {len(train_df)} samples. Generator batches: {len(train_gen)}\")\n    print(f\"Validating on {len(val_df)} samples. Generator batches: {len(val_gen)}\")\n    print(f\"CRITICAL: Using Tversky Loss (beta=0.90) and Spatial/Forensic Data Augmentation for maximum performance.\")\n\n    history = model.fit(\n        train_gen,\n        epochs=MAX_EPOCHS,\n        verbose=1,\n        callbacks=callbacks,\n        validation_data=val_gen\n    )\n\n    # Save weights to the temporary directory for Part 2 inference\n    model.save_weights('/tmp/model_new_scratch.weights.h5')\n\n    # Final cleanup\n    del model; del train_gen; del val_gen; gc.collect()","metadata":{"id":"lvVHSaX8y1HF","outputId":"a4109fab-a4ed-4701-961e-70b46f0a6325"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- CONFIGURATION & PATHS ---\nCOMPETITION_SLUG = \"recodai-luc-scientific-image-forgery-detection\"\nKAGGLEHUB_PATH = f\"/root/.cache/kagglehub/competitions/{COMPETITION_SLUG}\"\nTEST_IMAGE_ROOT = os.path.join(KAGGLEHUB_PATH, \"test_images\")\nSAMPLE_SUBMISSION_FILE = os.path.join(KAGGLEHUB_PATH, \"sample_submission.csv\")\nMODEL_SAVE_PATH = \"/tmp/model_new_scratch.weights.h5\" # Must match the training save path\n\nIMAGE_SIZE = 256\nFIXED_THRESHOLD = 0.45\nMIN_FORGERY_AREA = 32\nOUTPUT_FILENAME = \"submission.csv\"\n\n# --- IMPORTS ---\nimport numpy as np\nimport pandas as pd\nimport os\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, backend as K\nimport cv2\nfrom tqdm import tqdm\nimport csv\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# --- 1. Utility Functions (from training, essential for NR and RLE) ---\n\ndef rle_encode(mask):\n    \"\"\"Encodes a binary mask using Run Length Encoding.\"\"\"\n    if mask.sum() == 0: return \"authentic\"\n    pixels = mask.T.flatten() # Transpose and flatten for RLE standard\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 get_forgery_features_from_data(img_grayscale_data):\n    \"\"\"Generates the Noise Residual (NR) feature map (Stream 2 input).\"\"\"\n    img = img_grayscale_data.astype(np.uint8)\n\n    if img is None or img.size == 0:\n        return np.zeros((IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.float32)\n\n    blur = cv2.GaussianBlur(img, (5, 5), 0)\n    residual = img.astype(np.float32) - blur.astype(np.float32)\n\n    residual = cv2.resize(residual, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_LINEAR)\n    residual = (residual - residual.min()) / (residual.max() - residual.min() + 1e-7)\n\n    return np.stack([residual]*3, axis=-1).astype(np.float32)\n\n# --- 2. Model Architecture (Must match training) ---\n\ndef build_dual_stream_unet(input_shape):\n    input_rgb = layers.Input(shape=input_shape, name='rgb_input')\n    conv_rgb = models.Sequential([\n        layers.Conv2D(32, 3, activation='relu', padding='same'), layers.MaxPooling2D(),\n        layers.Conv2D(64, 3, activation='relu', padding='same')], name='rgb_stream')(input_rgb)\n    input_feat = layers.Input(shape=input_shape, name='feature_input')\n    conv_feat = models.Sequential([\n        layers.Conv2D(32, 3, activation='relu', padding='same'), layers.MaxPooling2D(),\n        layers.Conv2D(64, 3, activation='relu', padding='same')], name='feature_stream')(input_feat)\n    merged = layers.concatenate([conv_rgb, conv_feat])\n    up1 = layers.UpSampling2D(size=(2, 2))(merged)\n    conv_final = layers.Conv2D(128, 3, activation='relu', padding='same')(up1)\n    output = layers.Conv2D(1, 1, activation='sigmoid', padding='same')(conv_final)\n    model = models.Model(inputs=[input_rgb, input_feat], outputs=output)\n    return model\n\n# --- 3. Data Preparation ---\n\ndef create_test_df_robust(test_image_root, sample_submission_path):\n    master_df = pd.read_csv(sample_submission_path)\n    # Ensure case_id in master_df is treated as string for merging robustness\n    master_df['case_id'] = master_df['case_id'].astype(str)\n\n    present_files = {}\n\n    if os.path.exists(test_image_root):\n        for f in os.listdir(test_image_root):\n            case_id = os.path.splitext(f)[0]\n            if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')):\n                present_files[case_id] = os.path.join(test_image_root, f)\n\n    master_df['img_path'] = master_df['case_id'].astype(str).map(present_files)\n    master_df = master_df.dropna(subset=['img_path']).reset_index(drop=True)\n    return master_df[['case_id', 'img_path']]\n\n# --- 4. Inference Function with Dual-Stream Preprocessing ---\n\ndef run_inference_and_segment(unet_model, test_df):\n    results = []\n\n    for index, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Processing Test Set\"):\n        case_id = str(row['case_id'])\n        img_path = row['img_path']\n\n        # --- Load and Preprocess Image ---\n        try:\n            img_bgr = cv2.imread(img_path)\n            if img_bgr is None or img_bgr.size == 0: raise ValueError(\"Invalid image data.\")\n\n            img_rgb_orig = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n            img_gray_orig = cv2.cvtColor(img_rgb_orig, cv2.COLOR_RGB2GRAY)\n\n            original_shape = img_rgb_orig.shape[:2]\n\n            # X1: RGB Stream Input (resized and normalized)\n            X1_sample = cv2.resize(img_rgb_orig, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_LINEAR) / 255.0\n\n            # X2: Feature Stream Input (Noise Residual)\n            X2_sample = get_forgery_features_from_data(img_gray_orig)\n\n            # Keras Model Input Format: List of NumPy arrays (batch dimension added)\n            # The model expects [rgb_input, feature_input]\n            input_rgb = np.expand_dims(X1_sample, axis=0)\n            input_feat = np.expand_dims(X2_sample, axis=0)\n\n            # --- Prediction ---\n            output = unet_model.predict([input_rgb, input_feat], verbose=0)\n            output_prob = output[0, :, :, 0] # Remove batch and channel dimensions\n\n            # --- Post-Processing ---\n\n            # 1. Apply Threshold (0.45)\n            final_mask_resized = (output_prob > FIXED_THRESHOLD).astype(np.uint8)\n\n            # 2. Minimum Area Filtering (32)\n            clean_mask_resized = np.zeros_like(final_mask_resized)\n\n            # Find connected components\n            num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(\n                final_mask_resized, 4, cv2.CV_32S\n            )\n\n            # Iterate through each component (label 0 is the background)\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            # 3. Resize the CLEANED mask back to the original size\n            final_mask = cv2.resize(\n                clean_mask_resized,\n                (original_shape[1], original_shape[0]),\n                interpolation=cv2.INTER_NEAREST\n            )\n\n            rle_annotation = rle_encode(final_mask)\n            results.append({'case_id': case_id, 'annotation': rle_annotation})\n\n        except Exception as e:\n            # Fallback for corrupt files\n            print(f\"Error processing case {case_id}: {e}. Defaulting to authentic.\")\n            results.append({'case_id': case_id, 'annotation': 'authentic'})\n\n    return pd.DataFrame(results)\n\n# --- MAIN EXECUTION BLOCK ---\nif __name__ == \"__main__\":\n\n    print(f\"--- Starting Keras Inference ({tf.__version__}) ---\")\n\n    # 1. Load Model (Architecture + Weights)\n    model = build_dual_stream_unet((IMAGE_SIZE, IMAGE_SIZE, 3))\n\n    try:\n        model.load_weights(MODEL_SAVE_PATH)\n        print(f\"✅ Model weights loaded successfully from {MODEL_SAVE_PATH}\")\n    except Exception as e:\n        print(f\"🛑 Error loading weights: {e}. Cannot run inference.\")\n        model = None\n\n    # 2. Prepare Data\n    test_df = create_test_df_robust(TEST_IMAGE_ROOT, SAMPLE_SUBMISSION_FILE)\n\n    if test_df.empty:\n        print(\"🛑 FATAL: No valid test samples found.\")\n\n    # 3. Run Inference\n    if model and not test_df.empty:\n        results_df = run_inference_and_segment(model, test_df)\n    else:\n        # Create a placeholder if inference fails\n        results_df = pd.DataFrame([{'case_id': str(id), 'annotation': 'authentic'} for id in test_df['case_id'].tolist()])\n\n    # 4. Finalize Submission DF\n    # CRITICAL FIX: Ensure case_id is string in both DFs before merge\n    test_df['case_id'] = test_df['case_id'].astype(str)\n    results_df['case_id'] = results_df['case_id'].astype(str)\n\n    submission_df = test_df[['case_id']].copy().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    # 5. Write CSV with 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            case_id = str(row['case_id'])\n            annotation = row['annotation']\n\n            if annotation.lower() == 'authentic':\n                 writer.writerow([case_id, annotation])\n            else:\n                 # Create the full bracketed RLE string\n                 full_rle_string = f\"[{annotation}]\"\n                 writer.writerow([case_id, full_rle_string])\n\n    print(f\"\\n✅ Created {OUTPUT_FILENAME} with {len(submission_df)} rows.\")","metadata":{"id":"iraKL4icLnvY","outputId":"3ae04f97-277f-480b-eb03-7b8a3ba5ef43"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cat submission.csv","metadata":{"id":"QZfWBlG3Ol1S","outputId":"5a4c2c56-0c87-4002-f8e7-a5905be901d6"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls -ltha /tmp/","metadata":{"id":"oNTzQb8MFS4k","outputId":"d3429b67-a996-4c17-851a-06cec65f149b"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\ndef validate_and_print_rle(submission_df):\n    \"\"\"\n    Validates RLE output structure and prints debugging info,\n    including the total count of RLE segment pairs.\n    \"\"\"\n    print(\"\\n--- RLE Output Validation Check ---\")\n\n    # Analyze the annotations\n    authentic_count = submission_df['annotation'].apply(lambda x: x.lower().strip().replace('[]', '') == 'authentic').sum()\n    rle_rows = submission_df[submission_df['annotation'].apply(lambda x: x.lower().strip().replace('[]', '') != '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    # --- NEW: Calculate Total Segment Pairs ---\n    total_rle_elements = 0\n\n    def count_rle_elements(rle_string):\n        nonlocal total_rle_elements\n        rle_string = rle_string.strip().strip('[]')\n        if not rle_string or rle_string.lower() == 'authentic':\n            return True\n        try:\n            elements = rle_string.replace(',', ' ').split()\n            num_elements = len(elements)\n            if num_elements % 2 == 0:\n                total_rle_elements += num_elements\n            return num_elements % 2 == 0\n        except:\n            return False\n\n    rle_check = rle_rows['annotation'].apply(count_rle_elements)\n\n    if rle_check.all():\n        total_pairs = total_rle_elements // 2\n        print(f\"✅ RLE Structure: All {len(rle_rows)} RLE strings are structurally valid.\")\n        print(f\"Total Segment Pairs Detected: {total_pairs} (Indicates the complexity of the forgery patterns).\")\n    else:\n        bad_rle_count = len(rle_rows) - rle_check.sum()\n        print(f\"🛑 RLE ERROR: Found {bad_rle_count} RLE strings with invalid structure.\")\n\n# --- Execution ---\ntry:\n    # Load the submission file\n    submission_df = pd.read_csv(\"submission.csv\")\n\n    # Perform validation\n    validate_and_print_rle(submission_df)\n\n    # Print the content for confirmation\n    print(\"\\n--- submission.csv Content ---\")\n    print(submission_df.to_string(index=False))\n\n    if not submission_df.empty:\n        test_case_result = submission_df.iloc[0]['annotation']\n        print(f\"\\nModel Prediction for Test Case {submission_df.iloc[0]['case_id']}: The image is classified as {test_case_result}.\")\n\nexcept FileNotFoundError:\n    print(\"Error: submission.csv not found. Please ensure the inference code was run successfully.\")","metadata":{"id":"s2OzNXTxHVks","outputId":"bf586ad2-a6b2-4c1c-8533-ac417be54cdf"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!cat submission.csv","metadata":{"id":"iOg0YE3LIieG","outputId":"3ffae2ed-61f2-437c-a5de-e1206affd304"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport cv2\nimport os\nimport numpy as np\n\n# --- CONFIGURATION & PATHS (Using the same path setup as inference) ---\nCOMPETITION_SLUG = \"recodai-luc-scientific-image-forgery-detection\"\nKAGGLEHUB_PATH = f\"/root/.cache/kagglehub/competitions/{COMPETITION_SLUG}\"\nTEST_IMAGE_ROOT = os.path.join(KAGGLEHUB_PATH, \"test_images\")\n\n# The successful inference was for case_id 45\nTARGET_CASE_ID = '45'\n\nprint(f\"Attempting to load image: {TEST_IMAGE_PATH}\")\n\n# --- Determine the actual path for case 45 ---\nTEST_IMAGE_PATH = None\ntry:\n    # Try common extensions\n    possible_extensions = ['.png', '.jpg', '.jpeg', '.tif', '.tiff']\n    for ext in possible_extensions:\n        path = os.path.join(TEST_IMAGE_ROOT, f\"{TARGET_CASE_ID}{ext}\")\n        if os.path.exists(path):\n            TEST_IMAGE_PATH = path\n            img = cv2.imread(TEST_IMAGE_PATH)\n            break\n\n    if img is None:\n        raise FileNotFoundError(f\"Image file for case {TARGET_CASE_ID} not found.\")\n\n    # OpenCV loads in BGR format; convert to RGB for Matplotlib\n    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n    # --- Plot the Image ---\n    plt.figure(figsize=(10, 8))\n    plt.imshow(img_rgb)\n\n    # CRITICAL FIX: The title reflects the actual detection result (FORGED)\n    title_text = f\"Test Image {TARGET_CASE_ID} (Dimensions: {img.shape[0]}x{img.shape[1]})\"\n    classification_text = \"Classification: FORGED (1961 Segments Detected)\"\n\n    plt.title(f\"{title_text}\\n{classification_text}\", fontsize=14)\n    plt.axis('off') # Hide axes\n    plt.show()\n\nexcept FileNotFoundError as e:\n    print(f\"🛑 ERROR: {e}\")\nexcept Exception as e:\n    print(f\"🛑 ERROR: Could not read or process the image file: {e}\")","metadata":{"id":"rSK12KpZKqCJ","outputId":"ffc124dc-1e4b-4aa3-b2e1-749e6247e125"},"outputs":[],"execution_count":null}]}