{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"accelerator":"GPU","colab":{"collapsed_sections":["U4NcSAnx7wpY"],"gpuType":"L4","machine_shape":"hm","provenance":[]},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"},{"sourceId":13482618,"sourceType":"datasetVersion","datasetId":8559844}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"widgets":{"application/vnd.jupyter.widget-state+json":{"1ce6b7e521eb40d582f705679598ff9e":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","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":""}},"1eef1cd71c4f4dfd8cdb08bea9694d75":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"461eee2eac8e41ed992f1e045ec1950f":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"LabelModel","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_4907c36bac5a41c496231076a3e8031c","placeholder":"​","style":"IPY_MODEL_7fbc44070a264374801ea1fb81dbd43e","value":"Kaggle credentials successfully validated."}},"4769cfaab5904a928534bbcdb36c8ef0":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"PasswordModel","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_552026f5138e431383c1ce32b8f7dc18","placeholder":"​","style":"IPY_MODEL_50a6fe11d90840fd8905e1241c6600f9","value":""}},"4907c36bac5a41c496231076a3e8031c":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"50a6fe11d90840fd8905e1241c6600f9":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","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":""}},"531d6b9c918c46c88acf30aa3fb393d6":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","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_1eef1cd71c4f4dfd8cdb08bea9694d75","placeholder":"​","style":"IPY_MODEL_919d17df28324bf8b655bd10c2626ccf","value":"\n<b>Thank You</b></center>"}},"5368d66c59d2494f803a012337342448":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","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":""}},"552026f5138e431383c1ce32b8f7dc18":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"7241cfbefd1f4a718e2dea9b2ef41472":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","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":""}},"7fbc44070a264374801ea1fb81dbd43e":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","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":""}},"835f6009cf1a489dad644583e354529c":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"ButtonStyleModel","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":""}},"88fbece6c0dd4baa9251037733237328":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"VBoxModel","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_461eee2eac8e41ed992f1e045ec1950f"],"layout":"IPY_MODEL_b508137993f34ea7ade3be4830c723d0"}},"89ad6a52ba344146b5f432497c32fa86":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"8f521d7881ab439d96017041db11e9a3":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"LabelModel","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_89ad6a52ba344146b5f432497c32fa86","placeholder":"​","style":"IPY_MODEL_1ce6b7e521eb40d582f705679598ff9e","value":"Connecting..."}},"8fb030a625984e25955580a553f9a2df":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"919d17df28324bf8b655bd10c2626ccf":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"DescriptionStyleModel","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":""}},"a6bec01c34344f1998eb900920df5e61":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"b508137993f34ea7ade3be4830c723d0":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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%"}},"b5b8784f439e4efaa9dbbb3cc34be1f3":{"model_module":"@jupyter-widgets/base","model_module_version":"1.2.0","model_name":"LayoutModel","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}},"bbbec4eebfef4610a631bd8af2323cdb":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"TextModel","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_8fb030a625984e25955580a553f9a2df","placeholder":"​","style":"IPY_MODEL_5368d66c59d2494f803a012337342448","value":"frankmorales"}},"cceb35fb452a4f9f8bf6033d81db05a2":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"ButtonModel","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_b5b8784f439e4efaa9dbbb3cc34be1f3","style":"IPY_MODEL_835f6009cf1a489dad644583e354529c","tooltip":""}},"fb7c37dbe7724c569d7ca2218e9dd305":{"model_module":"@jupyter-widgets/controls","model_module_version":"1.5.0","model_name":"HTMLModel","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_a6bec01c34344f1998eb900920df5e61","placeholder":"​","style":"IPY_MODEL_7241cfbefd1f4a718e2dea9b2ef41472","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>"}}}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 📄 Project Documentation: Scientific Image Forgery Detection (RLUC-SIFD)\n\nThis notebook implements a semantic segmentation model using a **U-Net architecture** combined with **Error Level Analysis (ELA)** features for detecting and segmenting copy-move forgeries in scientific images. The solution is specifically tailored for the Kaggle $\\text{Recod.ai/LUC}$ SIFD competition.\n\n### 1. Configuration & Paths\n\n| Parameter | Value | Description |\n| :--- | :--- | :--- |\n| $\\text{TARGET\\_SIZE}$ | $256$ | Standard resolution for image and mask resizing before model input. |\n| $\\text{DEVICE}$ | `'cuda'` / `'cpu'` | Execution device, prioritizing GPU. |\n| $\\text{BATCH\\_SIZE}$ | $8$ | Batch size for training and inference. |\n| $\\text{EPOCHS}$ | $3$ | Number of training epochs (reduced for quick iteration). |\n| $\\text{LEARNING\\_RATE}$ | $1e-4$ | Initial learning rate for the Adam optimizer. |\n| $\\text{FIXED\\_THRESHOLD}$ | $0.45$ | Probability threshold applied to U-Net output during inference. |\n| $\\text{MIN\\_FORGERY\\_AREA}$ | $32$ | Minimum pixel area for a predicted forgery segment to be kept. |\n| $\\text{TRAIN\\_ROOT}$ | `/kaggle/input/.../train_images` | Path to the training image directories. |\n| $\\text{MASK\\_ROOT}$ | `/kaggle/input/.../train_masks` | Path to the ground-truth binary mask files ($\\text{.npy}$). |\n| $\\text{MODEL\\_SAVE\\_PATH}$ | `/tmp/model_new_scratch.pth` | Location to save the best-performing model weights. |\n\n---\n\n### 2. Utility and Feature Engineering\n\n#### $2.1.$ `compute_ela(img_path, quality=95, scale=10)`\n\nThis function generates the **Error Level Analysis (ELA)** feature map. ELA highlights areas in an image that have a different compression history (often due to forgery, copy-move, or re-saving).\n\n* **Process:**\n    1.  Loads the image and resizes it to $\\text{TARGET\\_SIZE} \\times \\text{TARGET\\_SIZE}$.\n    2.  Compresses the resized image as a JPEG with a fixed quality ($\\text{quality}=95$).\n    3.  Calculates the **absolute difference (error)** between the original resized image and the re-compressed image.\n    4.  The mean of the absolute error across the RGB channels is taken, and this $2\\text{D}$ error map is scaled by **$10$** ($\\text{scale}=10$).\n* **Output:** A $2\\text{D}$ array ($\\text{float32}$) of shape $(\\text{TARGET\\_SIZE}, \\text{TARGET\\_SIZE})$.\n\n---\n\n### 3. Model Architecture\n\n#### $3.1.$ `UNet(in_channels=4, num_classes=1)`\n\nA standard **U-Net** architecture adapted for this specific task.\n\n* **Input Channels:** $\\text{in\\_channels}=4$. This accepts the combined $\\text{RGB}$ (3 channels) and $\\text{ELA}$ (1 channel) input tensor.\n* **Encoder:** Consists of $\\text{Conv-ReLU-Dropout-Conv-ReLU}$ blocks and $\\text{MaxPool2d}$ for downsampling.\n* **Decoder:** Uses $\\text{ConvTranspose2d}$ ($\\text{upconv}$) for upsampling, followed by concatenation ($\\text{torch.cat}$) with the corresponding feature maps from the encoder ($\\text{skip connections}$).\n* **Output Layer:** A final $\\text{Conv2d}$ followed by a **$\\text{Sigmoid}$ activation** function, producing a probability map for the forgery mask.\n\n---\n\n### 4. Loss Functions\n\n#### $4.1.$ `DiceLoss(smooth=1.0)`\n\nImplements the **Dice Loss**, a common metric for highly imbalanced segmentation tasks. It measures the overlap between the predicted mask and the ground truth.\n\n$$\n\\text{Dice Loss} = 1 - \\frac{2 \\cdot |\\text{Pred} \\cap \\text{Target}| + \\text{smooth}}{|\\text{Pred}| + |\\text{Target}| + \\text{smooth}}\n$$\n\n#### $4.2.$ `HybridLoss(dice_weight=0.5)`\n\nCombines **Dice Loss** and **Binary Cross-Entropy (BCE) Loss** to provide stable training. BCE is effective for pixel-wise classification, while Dice Loss optimizes for region overlap.\n\n$$\n\\text{Hybrid Loss} = \\text{dice\\_weight} \\cdot \\text{Dice Loss} + (1 - \\text{dice\\_weight}) \\cdot \\text{BCE Loss}\n$$\n\n* $\\text{dice\\_weight}$ is set to **$0.5$**, giving equal importance to both terms.\n\n---\n\n### 5. Data Handling\n\n#### $5.1.$ `ForgeryDataset(Dataset)`\n\nA custom PyTorch Dataset class that handles loading, feature generation, and preprocessing for a single sample.\n\n* **Steps:**\n    1.  Loads the image ($\\text{RGB}$) and the mask ($\\text{npy}$).\n    2.  Computes the **ELA feature map**.\n    3.  Resizes $\\text{RGB}$ image and $\\text{ELA}$ map to $\\text{TARGET\\_SIZE}$ ($\\text{256}$).\n    4.  Resizes the binary mask to $\\text{TARGET\\_SIZE}$ using $\\text{cv2.INTER\\_NEAREST}$ to preserve pixel values.\n    5.  **Concatenates** the normalized $\\text{RGB}$ image and the $\\text{ELA}$ feature (after expanding its dimensions) to create the 4-channel input tensor.\n    6.  The final input is an $\\text{L} \\times \\text{H} \\times \\text{W}$ tensor (i.e., $4 \\times 256 \\times 256$), and the target mask is $1 \\times 256 \\times 256$.\n\n---\n\n### 6. Inference and Submission\n\n#### $6.1.$ `rle_encode(mask)`\n\nImplements the standard **Run-Length Encoding (RLE)** function required by the competition. If no forgery is detected ($\\text{mask.sum()} = 0$), it returns `'authentic'`.\n\n#### $6.2.$ `run_inference_and_segment(unet_model, test_df)`\n\nHandles prediction and post-processing on the test data:\n\n1.  **Prediction:** Processes images in batches, combining $\\text{RGB}$ and $\\text{ELA}$ features for input.\n2.  **Thresholding:** Applies the $\\text{FIXED\\_THRESHOLD}$ ($\\mathbf{0.45}$) to the model's probability output to generate an initial binary mask.\n3.  **Post-Processing:** Uses $\\text{cv2.connectedComponentsWithStats}$ to identify connected segments. Segments with an area less than $\\text{MIN\\_FORGERY\\_AREA}$ ($\\mathbf{32}$) are discarded.\n4.  **Resizing:** The cleaned binary mask is resized back to the original image dimensions using $\\text{INTER\\_NEAREST}$.\n5.  **Submission Format:** The final mask is $\\text{RLE}$ encoded.\n6.  **CSV Output:** The results are written to $\\text{submission.csv}$, ensuring the $\\text{RLE}$ string is enclosed in brackets (`[RLE_STRING]`) as often required for Kaggle submissions.","metadata":{}},{"cell_type":"markdown","source":"## 💾 EDA","metadata":{}},{"cell_type":"code","source":"!ls /kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/authentic/*.png |wc -l","metadata":{"execution":{"iopub.status.busy":"2025-10-31T18:35:31.604703Z","iopub.execute_input":"2025-10-31T18:35:31.605063Z","iopub.status.idle":"2025-10-31T18:35:34.220426Z","shell.execute_reply.started":"2025-10-31T18:35:31.605044Z","shell.execute_reply":"2025-10-31T18:35:34.219173Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls /kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images/forged/*.png |wc -l ","metadata":{"execution":{"iopub.status.busy":"2025-10-31T18:35:38.312317Z","iopub.execute_input":"2025-10-31T18:35:38.313042Z","iopub.status.idle":"2025-10-31T18:35:42.148240Z","shell.execute_reply.started":"2025-10-31T18:35:38.313017Z","shell.execute_reply":"2025-10-31T18:35:42.146673Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls /kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks/*.npy |wc -l ","metadata":{"execution":{"iopub.status.busy":"2025-10-31T18:35:48.235610Z","iopub.execute_input":"2025-10-31T18:35:48.235960Z","iopub.status.idle":"2025-10-31T18:35:51.817885Z","shell.execute_reply.started":"2025-10-31T18:35:48.235934Z","shell.execute_reply":"2025-10-31T18:35:51.815970Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## 📊 Exploratory Data Analysis (EDA)\n\nimport os\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport cv2\nfrom tqdm import tqdm\n\n# --- CONFIGURATION (from the original notebook) ---\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\nprint(\"--- Starting Basic EDA ---\")\n\n# 1. Prepare Data List (same logic as in the notebook)\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):\n            if 'forged' in root.lower():\n                case_id = os.path.splitext(f)[0]\n                img_path = os.path.join(root, f)\n                mask_path = os.path.join(MASK_ROOT, f\"{case_id}.npy\")\n                data_list.append({\n                    'case_id': case_id,\n                    'img_path': img_path,\n                    'mask_path': mask_path\n                })\nfull_df = pd.DataFrame(data_list)\n\n# Filter for images with existing masks\nif not full_df.empty:\n    full_df['mask_exists'] = full_df['mask_path'].apply(os.path.exists)\n    eda_df = full_df[full_df['mask_exists']].drop(columns=['mask_exists']).reset_index(drop=True)\nelse:\n    eda_df = pd.DataFrame(columns=['case_id', 'img_path', 'mask_path'])\n\n\n# 2. File and Case Counts\nprint(f\"\\nTotal potential images found in TRAIN_ROOT: {len(data_list)}\")\nprint(f\"Total valid image/mask pairs for training (Forged Cases): {len(eda_df)}\")\n\n# 3. Image and Mask Size Distribution\nif not eda_df.empty:\n    img_heights, img_widths = [], []\n    mask_pixels = [] # Count of non-zero pixels in the mask\n    \n    print(\"\\nAnalyzing image and mask dimensions/forgery area...\")\n    for index, row in tqdm(eda_df.iterrows(), total=len(eda_df)):\n        try:\n            # Read Image\n            img = cv2.imread(row['img_path'])\n            if img is not None and img.size > 0:\n                h, w = img.shape[:2]\n                img_heights.append(h)\n                img_widths.append(w)\n\n            # Read Mask (npy file)\n            mask = np.load(row['mask_path'])\n            # Assuming the forgery area is represented by non-zero pixels\n            mask_pixels.append(np.sum(mask > 0)) \n\n        except Exception as e:\n            # Handle files that can't be read (e.g., non-image .npy files not handled by cv2.imread)\n            pass\n\n    print(\"\\n--- Image Dimension Stats ---\")\n    print(f\"Unique Image Widths: {sorted(list(set(img_widths)))[:5]}{'...' if len(set(img_widths)) > 5 else ''}\")\n    print(f\"Unique Image Heights: {sorted(list(set(img_heights)))[:5]}{'...' if len(set(img_heights)) > 5 else ''}\")\n    print(f\"Mean Image Dimensions (H x W): {np.mean(img_heights):.0f} x {np.mean(img_widths):.0f}\")\n\n    # 4. Forgery Area Analysis\n    mask_pixels = np.array(mask_pixels)\n    forged_cases_with_area = np.sum(mask_pixels > 0)\n    total_forgery_area = np.sum(mask_pixels)\n    \n    print(\"\\n--- Forgery Area Stats ---\")\n    print(f\"Total cases with non-zero forgery area: {forged_cases_with_area} / {len(eda_df)}\")\n    print(f\"Mean Forgery Pixel Count per forged image: {np.mean(mask_pixels[mask_pixels > 0]):.0f} (pixels)\")\n    print(f\"Maximum Forgery Pixel Count: {np.max(mask_pixels)} (pixels)\")\n\n    # 5. Visualization: Forgery Area Distribution (First 100 cases for quick view)\n    plt.figure(figsize=(12, 5))\n    plt.bar(range(min(100, len(mask_pixels))), mask_pixels[:100])\n    plt.title('Forgery Pixel Count (First 100 Forged Samples)')\n    plt.xlabel('Sample Index')\n    plt.ylabel('Forged Pixel Count (Area)')\n    plt.show()\n\nelse:\n    print(\"🛑 EDA Skipped: The dataframe is empty. Check TRAIN_ROOT and MASK_ROOT paths.\")\n\nprint(\"\\n--- EDA Complete ---\")","metadata":{"execution":{"iopub.status.busy":"2025-10-31T18:35:55.155200Z","iopub.execute_input":"2025-10-31T18:35:55.155534Z","iopub.status.idle":"2025-10-31T18:38:21.675606Z","shell.execute_reply.started":"2025-10-31T18:35:55.155507Z","shell.execute_reply":"2025-10-31T18:38:21.674401Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🚀 Advanced Exploratory Data Analysis (Imbalance & Feature Check)","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# 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":{"execution":{"iopub.status.busy":"2025-10-31T18:38:42.681074Z","iopub.execute_input":"2025-10-31T18:38:42.681467Z","iopub.status.idle":"2025-10-31T18:38:51.722991Z","shell.execute_reply.started":"2025-10-31T18:38:42.681447Z","shell.execute_reply":"2025-10-31T18:38:51.722067Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🚀 File image test","metadata":{}},{"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\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":{"execution":{"iopub.status.busy":"2025-10-31T18:39:01.452916Z","iopub.execute_input":"2025-10-31T18:39:01.454108Z","iopub.status.idle":"2025-10-31T18:39:01.857184Z","shell.execute_reply.started":"2025-10-31T18:39:01.454078Z","shell.execute_reply":"2025-10-31T18:39:01.856029Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 💾 Final Corrected Training Code","metadata":{"id":"CD3ZAtsN7VJw"}},{"cell_type":"code","source":"import numpy as np\nimport cv2\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm import tqdm\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nimport time\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nimport warnings\nfrom warnings import filterwarnings\n\n# Suppress the specific UserWarning from the LR scheduler\nwarnings.filterwarnings(\"ignore\", category=UserWarning, module=\"torch.optim.lr_scheduler\")\nfilterwarnings('ignore')\n\n# --- CONFIGURATION --\nTARGET_SIZE = 256\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nBATCH_SIZE = 8\n# REDUCED EPOCHS to 3 (was 10)\nEPOCHS = 3\nLEARNING_RATE = 1e-4\n\n# --- PATHS (CORRECTED FOR KAGGLEHUB CACHE) ---\nTRAIN_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_images\"\nMASK_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/train_masks\"\nMODEL_SAVE_PATH = \"/tmp/model_new_scratch.pth\"\n\n# --- UTILITY FUNCTIONS ---\ndef compute_ela(img_path, quality=95, scale=10):\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:\n                img = cv2.cvtColor(img_data, cv2.COLOR_RGB2BGR)\n            elif img_data.ndim == 2:\n                img = cv2.cvtColor(img_data, cv2.COLOR_GRAY2BGR)\n        except Exception:\n            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)}_{time.time()}.jpg\"\n    try:\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\n    finally:\n        if os.path.exists(temp_path): os.remove(temp_path)\n    return cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE),\n                      interpolation=cv2.INTER_LINEAR).astype(np.float32)\n\nclass DiceLoss(nn.Module):\n    def __init__(self, smooth=1.0):\n        super(DiceLoss, self).__init__()\n        self.smooth = smooth\n    def forward(self, pred, target):\n        pred = pred.contiguous().view(-1)\n        target = target.contiguous().view(-1)\n        intersection = (pred * target).sum()\n        dice = (2. * intersection + self.smooth) / (pred.sum() + target.sum() + self.smooth)\n        return 1 - dice\n\n# Hybrid Loss combining Dice and BCE for stable training\nclass HybridLoss(nn.Module):\n    def __init__(self, dice_weight=0.5):\n        super(HybridLoss, self).__init__()\n        self.dice_loss = DiceLoss()\n        self.bce_loss = nn.BCELoss()\n        self.dice_weight = dice_weight\n\n    def forward(self, pred, target):\n        dice = self.dice_loss(pred, target)\n        bce = self.bce_loss(pred, target)\n        return self.dice_weight * dice + (1 - self.dice_weight) * bce\n\n# U-Net architecture\nclass UNet(nn.Module):\n    def __init__(self, in_channels=4, num_classes=1):\n        super().__init__()\n        def block(in_c, out_c):\n            return nn.Sequential(\n                nn.Conv2d(in_c, out_c, 3, 1, 1), nn.ReLU(),\n                nn.Dropout(p=0.2),\n                nn.Conv2d(out_c, out_c, 3, 1, 1), nn.ReLU()\n            )\n\n        self.enc1 = block(in_channels, 64)\n        self.enc2 = block(64, 128)\n        self.bottleneck = block(128, 256)\n        self.upconv2 = nn.ConvTranspose2d(256, 128, 2, 2)\n        self.dec2 = block(128 + 128, 128)\n        self.upconv1 = nn.ConvTranspose2d(128, 64, 2, 2)\n        self.dec1 = block(64 + 64, 64)\n        self.final_conv = nn.Conv2d(64, num_classes, 1)\n\n    def forward(self, x):\n        e1 = self.enc1(x)\n        p1 = F.max_pool2d(e1, 2)\n        e2 = self.enc2(p1)\n        p2 = F.max_pool2d(e2, 2)\n        b = self.bottleneck(p2)\n        d2 = self.upconv2(b)\n        d2 = torch.cat((d2, e2), dim=1)\n        d2 = self.dec2(d2)\n        d1 = self.upconv1(d2)\n        d1 = torch.cat((d1, e1), dim=1)\n        d1 = self.dec1(d1)\n        return torch.sigmoid(self.final_conv(d1))\n\nclass ForgeryDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row['img_path']\n        mask_path = row['mask_path']\n\n        # --- Load Image ---\n        rgb_image = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n\n        if rgb_image is None or rgb_image.size == 0:\n            try:\n                img_data = np.load(img_path)\n                if img_data.ndim == 3:\n                    rgb_image = img_data\n                elif img_data.ndim == 2:\n                    rgb_image = cv2.cvtColor(img_data, cv2.COLOR_GRAY2RGB)\n            except Exception as e:\n                raise RuntimeError(f\"Failed to load image from {img_path}: {e}\")\n\n        # --- Load Mask ---\n        try:\n            mask = np.load(mask_path)\n            if mask.ndim > 2:\n                mask = mask[:, :, 0]\n        except Exception as e:\n            raise RuntimeError(f\"Failed to load mask from {mask_path}: {e}\")\n\n        ela_feature_2d = compute_ela(img_path)\n\n        # Resize all features\n        rgb_image_resized = cv2.resize(rgb_image, (TARGET_SIZE, TARGET_SIZE))\n        ela_feature_resized = cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE))\n\n        # Use INTER_NEAREST for binary mask resizing\n        mask_resized = cv2.resize(mask.astype(np.uint8), (TARGET_SIZE, TARGET_SIZE), interpolation=cv2.INTER_NEAREST)\n\n        # Stack RGB (3) and ELA (1) for a 4-channel input\n        ela_feature_3d = np.expand_dims(ela_feature_resized, axis=-1)\n        stacked_input = np.concatenate([rgb_image_resized, ela_feature_3d], axis=-1)\n\n        # Convert to PyTorch tensors and normalize\n        image = torch.tensor(stacked_input.transpose(2, 0, 1) / 255.0, dtype=torch.float32)\n        mask = torch.tensor(mask_resized / 255.0, dtype=torch.float32).unsqueeze(0)\n\n        return image, mask\n\ndef train_model(model, train_loader, val_loader, epochs=EPOCHS, save_path=MODEL_SAVE_PATH):\n    # Use HybridLoss\n    criterion = HybridLoss(dice_weight=0.5)\n    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)\n\n    # Scheduler with patience=2 (unchanged)\n    scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\n    best_val_loss = float('inf')\n\n    model.to(DEVICE)\n    print(f\"Starting training on {DEVICE} for {epochs} epochs...\")\n\n    for epoch in range(epochs):\n        model.train()\n        train_loss_sum = 0\n\n        for inputs, targets in tqdm(train_loader, desc=f\"Epoch {epoch+1} Training\"):\n            inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)\n            optimizer.zero_grad()\n            outputs = model(inputs)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n            train_loss_sum += loss.item()\n\n        avg_train_loss = train_loss_sum / len(train_loader)\n\n        # Validation Phase\n        model.eval()\n        val_loss_sum = 0\n        with torch.no_grad():\n            for inputs, targets in val_loader:\n                inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)\n                outputs = model(inputs)\n                loss = criterion(outputs, targets)\n                val_loss_sum += loss.item()\n\n        avg_val_loss = val_loss_sum / len(val_loader)\n\n        # Scheduler Step\n        scheduler.step(avg_val_loss)\n\n        print(f\"Epoch {epoch+1} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}\")\n\n        if avg_val_loss < best_val_loss:\n            best_val_loss = avg_val_loss\n            torch.save(model.state_dict(), save_path)\n            print(f\"Model saved successfully to {save_path}. (New best Val Loss: {best_val_loss:.4f})\\n\")\n\n        current_lr = optimizer.param_groups[0]['lr']\n        print(f\"Current Learning Rate: {current_lr:.6f}\")\n\n\n# --- MAIN EXECUTION BLOCK ---\nif __name__ == '__main__':\n\n    print(\"Preparing training data paths...\\n\")\n\n    data_list = []\n\n    # Recursively walk through the TRAIN_ROOT to find all image files\n    for root, _, files in os.walk(TRAIN_ROOT):\n        for f in files:\n            valid_extensions = ('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.npy')\n\n            if f.lower().endswith(valid_extensions):\n                # Only process files in the 'forged' subdirectory, as only they have masks\n                if 'forged' in root.lower():\n                    case_id = os.path.splitext(f)[0]\n                    img_path = os.path.join(root, f)\n\n                    # Use .npy for the mask extension\n                    mask_path = os.path.join(MASK_ROOT, f\"{case_id}.npy\")\n\n                    data_list.append({\n                        'case_id': case_id,\n                        'img_path': img_path,\n                        'mask_path': mask_path\n                    })\n\n    if not data_list:\n        full_df = pd.DataFrame(columns=['case_id', 'img_path', 'mask_path'])\n    else:\n        full_df = pd.DataFrame(data_list)\n\n    if not full_df.empty:\n        # Final check: Keep only images that have a corresponding mask file\n        full_df['mask_exists'] = full_df['mask_path'].apply(os.path.exists)\n        full_df = full_df[full_df['mask_exists']].drop(columns=['mask_exists']).reset_index(drop=True)\n\n    if full_df.empty:\n        print(\"🛑 FATAL ERROR: No valid image/mask pairs found in the input paths. Cannot train. (Check file extensions/paths again)\")\n    else:\n        print(f\"✅ Found {len(full_df)} valid forged samples for training.\")\n\n        # Split data\n        train_df, val_df = train_test_split(full_df, test_size=0.1, random_state=42)\n\n        # Create DataLoaders\n        train_dataset = ForgeryDataset(train_df)\n        val_dataset = ForgeryDataset(val_df)\n        train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\n        val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\n        # Instantiate model (4 input channels: 3 RGB + 1 ELA)\n        model = UNet(in_channels=4)\n\n        # START TRAINING\n        train_model(model, train_loader, val_loader)\n\n        print(\"\\n✅ TRAINING COMPLETE. The trained model is saved and ready for inference.\")","metadata":{"id":"oeY6sOXG6-D6","outputId":"ae806cc5-1cdd-4419-a4e4-518d0109c8a6"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 💾 Final Corrected Inference Code (Robust Settings)\n","metadata":{"id":"E-rnPI8N7PVM"}},{"cell_type":"code","source":"import numpy as np\nimport cv2\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport pandas as pd\nfrom tqdm import tqdm\nimport time\nimport csv\nimport warnings\nfrom warnings import filterwarnings\n\nfilterwarnings('ignore') # Suppress warnings\n\n# --- CONFIGURATION & PATHS (ROBUST SETTINGS) ---\nTARGET_SIZE = 256\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nBATCH_SIZE = 8\n# ROBUST: Conservative threshold for the Private Leaderboard\nFIXED_THRESHOLD = 0.45\n# ROBUST: Moderate filter to remove noise while keeping small artifacts\nMIN_FORGERY_AREA = 32\n\n# CORRECTED PATHS\nTEST_IMAGE_ROOT = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/test_images\"\nSAMPLE_SUBMISSION_FILE = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection/sample_submission.csv\"\n\n# Model path should point to the successfully trained model\nmodel_path = \"/tmp/model_new_scratch.pth\"\nOUTPUT_FILENAME = \"submission.csv\"\n\n# --- UTILITY FUNCTIONS (Unchanged) ---\n\ndef compute_ela(img_path, quality=95, scale=10):\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:\n            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_{os.path.basename(img_path)}_{time.time()}.jpg\"\n    try:\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\n    finally:\n        if os.path.exists(temp_path): os.remove(temp_path)\n    return cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE),\n                      interpolation=cv2.INTER_LINEAR).astype(np.float32)\n\ndef create_test_df_robust(test_image_root, sample_submission_path):\n    master_df = pd.read_csv(sample_submission_path)\n    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')):\n                    present_files[case_id] = os.path.join(root, f)\n\n    master_df['img_path'] = master_df['case_id'].map(present_files)\n    master_df['img_path'] = master_df['img_path'].fillna('MISSING_FILE')\n    return master_df[['case_id', 'img_path']]\n\ndef rle_encode(mask):\n    if mask.sum() == 0: return \"authentic\"\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\nclass UNet(nn.Module):\n    def __init__(self, in_channels=4, num_classes=1):\n        super().__init__()\n        def block(in_c, out_c):\n            return nn.Sequential(\n                nn.Conv2d(in_c, out_c, 3, 1, 1), nn.ReLU(),\n                nn.Dropout(p=0.2),\n                nn.Conv2d(out_c, out_c, 3, 1, 1), nn.ReLU()\n            )\n\n        self.enc1 = block(in_channels, 64)\n        self.enc2 = block(64, 128)\n        self.bottleneck = block(128, 256)\n        self.upconv2 = nn.ConvTranspose2d(256, 128, 2, 2)\n        self.dec2 = block(128 + 128, 128)\n        self.upconv1 = nn.ConvTranspose2d(128, 64, 2, 2)\n        self.dec1 = block(64 + 64, 64)\n        self.final_conv = nn.Conv2d(64, num_classes, 1)\n\n    def forward(self, x):\n        e1 = self.enc1(x)\n        p1 = F.max_pool2d(e1, 2)\n        e2 = self.enc2(p1)\n        p2 = F.max_pool2d(e2, 2)\n        b = self.bottleneck(p2)\n        d2 = self.upconv2(b)\n        d2 = torch.cat((d2, e2), dim=1)\n        d2 = self.dec2(d2)\n        d1 = self.upconv1(d2)\n        d1 = torch.cat((d1, e1), dim=1)\n        d1 = self.dec1(d1)\n        return torch.sigmoid(self.final_conv(d1))\n\n# --- INFERENCE FUNCTION WITH POST-PROCESSING ---\ndef run_inference_and_segment(unet_model, test_df):\n    results = []\n    unet_model.eval()\n    images_to_process = []\n\n    for index, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Processing\"):\n        case = row['case_id']\n        img_path = row['img_path']\n\n        if img_path == 'MISSING_FILE' or img_path == 'NOT_FOUND':\n            results.append({'case_id': case, 'annotation': 'authentic'})\n            continue\n\n        try:\n            rgb_image = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n            if rgb_image is None or rgb_image.size == 0:\n                img_data = np.load(img_path)\n                if img_data.ndim == 3: rgb_image = img_data\n                elif img_data.ndim == 2: rgb_image = cv2.cvtColor(img_data, cv2.COLOR_GRAY2RGB)\n\n            if rgb_image is None or rgb_image.size == 0: raise ValueError(f\"Invalid image data for {case}\")\n\n            original_shape = rgb_image.shape[:2]\n            ela_feature_2d = compute_ela(img_path)\n\n            rgb_image_resized = cv2.resize(rgb_image, (TARGET_SIZE, TARGET_SIZE))\n            ela_feature_resized = cv2.resize(ela_feature_2d, (TARGET_SIZE, TARGET_SIZE))\n            ela_feature_3d = np.expand_dims(ela_feature_resized, axis=-1)\n            stacked_input = np.concatenate([rgb_image_resized, ela_feature_3d], axis=-1)\n            images_to_process.append((case, original_shape, stacked_input))\n\n            # Process batch\n            if len(images_to_process) == BATCH_SIZE or index == len(test_df) - 1:\n                if images_to_process:\n                    batch_inputs = torch.stack([\n                        torch.tensor(img_data.transpose(2, 0, 1) / 255.0, dtype=torch.float32)\n                        for _, _, img_data in images_to_process\n                    ]).to(DEVICE)\n\n                    with torch.no_grad():\n                        outputs = unet_model(batch_inputs).detach().cpu().numpy()\n\n                    for i, output in enumerate(outputs):\n                        case_id_out, original_shape_out, _ = images_to_process[i]\n                        output_prob = output.squeeze()\n\n                        # --- LOG PROBABILITY HERE ---\n                        max_prob = np.max(output_prob)\n                        print(f\"|--- Case {case_id_out} Max Forgery Probability: {max_prob:.4f} ---|\")\n                        # ----------------------------\n\n                        # Apply Threshold (0.45)\n                        final_mask_resized = (output_prob > FIXED_THRESHOLD).astype(np.uint8)\n\n                        # 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                                # Keep segments that meet the minimum size requirement\n                                clean_mask_resized[labels == label] = 1\n\n                        # Resize the CLEANED mask back to the original size\n                        final_mask = cv2.resize(\n                            clean_mask_resized,\n                            (original_shape_out[1], original_shape_out[0]),\n                            interpolation=cv2.INTER_NEAREST\n                        )\n\n                        rle_annotation = rle_encode(final_mask)\n                        results.append({'case_id': case_id_out, 'annotation': rle_annotation})\n\n                    images_to_process = [] # Reset batch\n        except Exception as e:\n            print(f\"Error processing case {case}: {e}. Defaulting to authentic.\")\n            results.append({'case_id': case, 'annotation': 'authentic'})\n    return pd.DataFrame(results)\n\n# --- MAIN EXECUTION BLOCK ---\nif __name__ == \"__main__\":\n\n    print(f\"--- Starting inference on {DEVICE} at {pd.Timestamp.now()} ---\")\n\n    # 1. Load Model\n    model = None\n    try:\n        model = UNet(in_channels=4).to(DEVICE)\n        model.load_state_dict(torch.load(model_path, map_location=DEVICE))\n        model.eval() # Set model to evaluation mode\n        print(f\"Model loaded successfully from {model_path}\")\n    except Exception as e:\n        print(f\"Error loading model from {model_path}. Submitting 'authentic' for all cases. Error: {e}\")\n        model = None\n\n    # 2. Prepare Data\n    test_df = create_test_df_robust(TEST_IMAGE_ROOT, SAMPLE_SUBMISSION_FILE)\n    test_df['case_id'] = test_df['case_id'].astype(str)\n\n    # 3. Run Inference\n    if model:\n        results_df = run_inference_and_segment(model, test_df)\n    else:\n        results_df = test_df[['case_id']].assign(annotation='authentic')\n\n    # 4. Finalize Submission DF\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 at {pd.Timestamp.now()}\")","metadata":{"id":"vf4Iz0976_0p","outputId":"7099ce83-3376-4454-e65e-c36e0bbcb5bf"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 💾 submission","metadata":{"id":"U4NcSAnx7wpY"}},{"cell_type":"code","source":"!cat submission.csv","metadata":{"execution":{"iopub.execute_input":"2025-10-28T08:10:25.0537Z","iopub.status.busy":"2025-10-28T08:10:25.053152Z","iopub.status.idle":"2025-10-28T08:10:25.20086Z","shell.execute_reply":"2025-10-28T08:10:25.199812Z","shell.execute_reply.started":"2025-10-28T08:10:25.053679Z"},"id":"TIckAouP3Blt","outputId":"b03ece0f-6c0e-459d-b0c7-d7ef5d78ca47","trusted":true},"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":{"execution":{"iopub.execute_input":"2025-10-28T08:10:30.993755Z","iopub.status.busy":"2025-10-28T08:10:30.992963Z","iopub.status.idle":"2025-10-28T08:10:30.999816Z","shell.execute_reply":"2025-10-28T08:10:30.999032Z","shell.execute_reply.started":"2025-10-28T08:10:30.993731Z"},"id":"RTP7ZggN3Blt","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = pd.read_csv(\"submission.csv\")\nvalidate_and_print_rle(submission_df)","metadata":{"execution":{"iopub.execute_input":"2025-10-28T08:10:35.248727Z","iopub.status.busy":"2025-10-28T08:10:35.24838Z","iopub.status.idle":"2025-10-28T08:10:35.260147Z","shell.execute_reply":"2025-10-28T08:10:35.25939Z","shell.execute_reply.started":"2025-10-28T08:10:35.248706Z"},"id":"Ly35WP3X3Blt","outputId":"c13d8b6a-fb62-4c44-d91e-ad18f90b4f72","trusted":true},"outputs":[],"execution_count":null}]}