{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"},{"sourceId":626388,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":471548,"modelId":487451}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\n# # This Python 3 environment comes with many helpful analytics libraries installed\n# # It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# # For example, here's several helpful packages to load\n\n# import numpy as np # linear algebra\n# import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# # Input data files are available in the read-only \"../input/\" directory\n# # For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\n# import os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# # You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# # You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:15:02.842061Z","iopub.execute_input":"2025-11-01T15:15:02.842309Z","iopub.status.idle":"2025-11-01T15:15:02.857429Z","shell.execute_reply.started":"2025-11-01T15:15:02.842286Z","shell.execute_reply":"2025-11-01T15:15:02.856604Z"},"_kg_hide-output":true,"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport json\nfrom pathlib import Path\nclass Config:\n    \"\"\"Advanced configuration with optimized parameters\"\"\"\n    \n    # Paths\n    BASE_PATH = Path('/kaggle/input/recodai-luc-scientific-image-forgery-detection')\n    TRAIN_IMAGES_DIR = BASE_PATH / 'train_images'\n    TRAIN_MASKS_DIR = BASE_PATH / 'train_masks'\n    TEST_IMAGES_DIR = BASE_PATH / 'test_images'\n    SAMPLE_SUB_PATH = BASE_PATH / 'sample_submission.csv'\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:26.500673Z","iopub.execute_input":"2025-11-01T15:21:26.500901Z","iopub.status.idle":"2025-11-01T15:21:26.510070Z","shell.execute_reply.started":"2025-11-01T15:21:26.500878Z","shell.execute_reply":"2025-11-01T15:21:26.509128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def discover_and_map_data():\n    \"\"\"Discover all images and correctly map their corresponding .npy masks.\"\"\"\n    import numpy as np\n    from pathlib import Path\n\n    config = Config()\n\n    # Discover all training images\n    train_images = []\n    authentic_dir = config.TRAIN_IMAGES_DIR / 'authentic'\n    forged_dir = config.TRAIN_IMAGES_DIR / 'forged'\n\n    # Get authentic images\n    authentic_images = []\n    if authentic_dir.exists():\n        authentic_images = list(authentic_dir.glob('*.[jpJP][npNP][gG]*'))\n        train_images.extend(authentic_images)\n        print(f\"Found {len(authentic_images)} authentic images\")\n\n    # Get forged images\n    forged_images = []\n    if forged_dir.exists():\n        forged_images = list(forged_dir.glob('*.[jpJP][npNP][gG]*'))\n        train_images.extend(forged_images)\n        print(f\"Found {len(forged_images)} forged images\")\n\n    # Create mask mapping\n    mask_mapping = {}\n    forged_with_masks = set()\n\n    if config.TRAIN_MASKS_DIR.exists():\n        # Masks are stored as .npy files\n        for mask_file in config.TRAIN_MASKS_DIR.glob('*.npy'):\n            img_id = mask_file.stem  # e.g. '10015' from '10015.npy'\n\n            # Check if this mask corresponds to a forged image\n            matching_imgs = [\n                forged_img for forged_img in forged_images\n                if forged_img.stem == img_id\n            ]\n\n            if matching_imgs:\n                forged_img = matching_imgs[0]\n                actual_img_id = forged_img.stem\n                if actual_img_id not in mask_mapping:\n                    mask_mapping[actual_img_id] = []\n                mask_mapping[actual_img_id].append(mask_file)\n                forged_with_masks.add(forged_img)\n\n    print(f\"Created mask mapping for {len(mask_mapping)} images\")\n    print(f\"Forged images with masks: {len(forged_with_masks)}\")\n\n    # Get test images\n    test_images = []\n    if config.TEST_IMAGES_DIR.exists():\n        test_images = list(config.TEST_IMAGES_DIR.glob('*.[jpJP][npNP][gG]*'))\n        print(f\"Found {len(test_images)} test images\")\n\n    return train_images, mask_mapping, test_images, forged_with_masks\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:26.511367Z","iopub.execute_input":"2025-11-01T15:21:26.511646Z","iopub.status.idle":"2025-11-01T15:21:26.525714Z","shell.execute_reply.started":"2025-11-01T15:21:26.511630Z","shell.execute_reply":"2025-11-01T15:21:26.524781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\nfrom PIL import Image\nimport cv2\nimport random\n\n# Assuming Config and discover_and_map_data() are defined\ntrain_images, mask_mapping, test_images, forged_with_masks = discover_and_map_data()\n\nprint(f\"Total train images: {len(train_images)}\")\nprint(f\"Forged with masks: {len(forged_with_masks)}\")\nprint(f\"Authentic: {len(train_images) - len(forged_with_masks)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:33.301478Z","iopub.execute_input":"2025-11-01T15:21:33.301757Z","iopub.status.idle":"2025-11-01T15:21:37.950370Z","shell.execute_reply.started":"2025-11-01T15:21:33.301732Z","shell.execute_reply":"2025-11-01T15:21:37.949525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Summarize the image distribution\ndata_summary = {\n    \"Authentic\": len([p for p in train_images if 'authentic' in str(p)]),\n    \"Forged\": len([p for p in train_images if 'forged' in str(p)]),\n    \"Forged_with_masks\": len(mask_mapping),\n    \"Test Images\": len(test_images)\n}\npd.DataFrame(data_summary, index=[\"Count\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:38.959060Z","iopub.execute_input":"2025-11-01T15:21:38.959647Z","iopub.status.idle":"2025-11-01T15:21:38.990921Z","shell.execute_reply.started":"2025-11-01T15:21:38.959622Z","shell.execute_reply":"2025-11-01T15:21:38.990251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"size = []\nleng=0\nfor image_path in random.sample(train_images, min(300, len(train_images))):\n    img = Image.open(image_path)\n    size.append(img.size)\ndf_sizes = pd.DataFrame(size, columns=['width', 'height'])\nplt.figure(figsize=(7,5))\nsns.scatterplot(x='width', y='height', data=df_sizes)\nplt.title('Image Resolution Distribution')\nplt.xlabel('Width')\nplt.ylabel('Height')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:42.172994Z","iopub.execute_input":"2025-11-01T15:21:42.173276Z","iopub.status.idle":"2025-11-01T15:21:44.459453Z","shell.execute_reply.started":"2025-11-01T15:21:42.173256Z","shell.execute_reply":"2025-11-01T15:21:44.458662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(df_sizes.describe())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:47.659661Z","iopub.execute_input":"2025-11-01T15:21:47.660328Z","iopub.status.idle":"2025-11-01T15:21:47.671824Z","shell.execute_reply.started":"2025-11-01T15:21:47.660302Z","shell.execute_reply":"2025-11-01T15:21:47.671078Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mask_coverage = []\n\nfor img_id, mask_files in list(mask_mapping.items())[:300]:  # sample subset for speed\n    total_mask = np.zeros(np.load(mask_files[0]).shape, dtype=np.uint8)\n    for mf in mask_files:\n        mask = np.load(mf)\n        total_mask = np.logical_or(total_mask, mask)\n    forged_area_ratio = np.sum(total_mask) / total_mask.size\n    mask_coverage.append(forged_area_ratio)\n\nplt.figure(figsize=(7,5))\nsns.histplot(mask_coverage, bins=30, kde=True)\nplt.title(\"Distribution of Forged Area Ratio per Image\")\nplt.xlabel(\"Forged area ratio\")\nplt.ylabel(\"Frequency\")\nplt.show()\n\nprint(f\"Average forged area ratio: {np.mean(mask_coverage):.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:50.390999Z","iopub.execute_input":"2025-11-01T15:21:50.391267Z","iopub.status.idle":"2025-11-01T15:21:54.726527Z","shell.execute_reply.started":"2025-11-01T15:21:50.391246Z","shell.execute_reply":"2025-11-01T15:21:54.725799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_image_and_mask(image_path, mask_paths=None):\n    \"\"\"\n    Display image and overlay one or more associated .npy masks.\n    Handles variable shapes and multiple masks per image.\n    \"\"\"\n    import cv2\n    import numpy as np\n    from PIL import Image\n    import matplotlib.pyplot as plt\n\n    img = np.array(Image.open(image_path).convert(\"RGB\"))\n    h, w = img.shape[:2]\n    combined_mask = np.zeros((h, w), dtype=np.uint8)\n\n    if mask_paths:\n        for mask_path in mask_paths:\n            mask = np.load(mask_path)\n\n            # 🧩 Handle different possible mask shapes\n            # e.g. (2,256,256), (256,256,2), (256,256)\n            if mask.ndim == 3:\n                if mask.shape[0] <= 3:\n                    mask = np.any(mask, axis=0).astype(np.uint8)\n                elif mask.shape[-1] <= 3:\n                    mask = np.any(mask, axis=-1).astype(np.uint8)\n                else:\n                    # If multiple masks are stacked, combine all\n                    mask = np.any(mask, axis=0).astype(np.uint8)\n            elif mask.ndim == 2:\n                mask = mask.astype(np.uint8)\n            else:\n                mask = mask.squeeze().astype(np.uint8)\n\n            # Resize mask to match image dimensions\n            mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)\n\n            # Combine with previously loaded masks\n            combined_mask = np.logical_or(combined_mask, mask)\n\n    #  Display results\n    plt.figure(figsize=(10, 4))\n    plt.subplot(1, 2, 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(\"Original Image\")\n\n    plt.subplot(1, 2, 2)\n    plt.imshow(img)\n    if mask_paths:\n        plt.imshow(combined_mask, alpha=0.5, cmap='jet')\n        plt.title(f\"Forgery Mask Overlay ({len(mask_paths)} mask file(s))\")\n    else:\n        plt.title(\"No Mask (Authentic)\")\n    plt.axis('off')\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pick random forged case\nsample_id = random.choice(list(mask_mapping.keys()))\nimage_path = next((f for f in forged_with_masks if sample_id in str(f)), None)\n\nif image_path:\n    show_image_and_mask(image_path, mask_mapping[sample_id])\nelse:\n    print(\"No image match found for that mask ID.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\nimport cv2\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:21:55.888742Z","iopub.execute_input":"2025-11-01T15:21:55.889003Z","iopub.status.idle":"2025-11-01T15:22:00.792425Z","shell.execute_reply.started":"2025-11-01T15:21:55.888986Z","shell.execute_reply":"2025-11-01T15:22:00.791760Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ForgeryDataset(Dataset):\n    def __init__(self, image_paths, mask_mapping, image_size=(512, 512), augment=False):\n        self.image_paths = image_paths\n        self.mask_mapping = mask_mapping\n        self.image_size = image_size\n        self.augment = augment\n\n        # Albumentations handles resize + normalization\n        self.transform = A.Compose([\n            A.Resize(*image_size),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.3),\n            A.RandomRotate90(p=0.3),\n            A.RandomBrightnessContrast(p=0.3),\n            A.GaussNoise(p=0.2),\n            A.MotionBlur(p=0.2),\n            A.Normalize(mean=(0.485, 0.456, 0.406),\n                        std=(0.229, 0.224, 0.225)),\n            ToTensorV2()\n        ])\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def load_mask(self, img_id, h, w):\n        \"\"\"Merge multiple .npy masks for an image ID; return unresized mask\"\"\"\n        if img_id not in self.mask_mapping:\n            return np.zeros((h, w), dtype=np.uint8)\n\n        # Load image-sized mask template (same size as image)\n        combined_mask = np.zeros((h, w), dtype=np.uint8)\n        for mf in self.mask_mapping[img_id]:\n            mask = np.load(mf)\n\n            # Merge channels if present\n            if mask.ndim == 3:\n                mask = np.any(mask, axis=0).astype(np.uint8)\n            else:\n                mask = mask.squeeze().astype(np.uint8)\n\n            # If mask not same size as image, resize later using Albumentations\n            if mask.shape != (h, w):\n                mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)\n            combined_mask = np.logical_or(combined_mask, mask)\n\n        return combined_mask.astype(np.float32)\n\n    def __getitem__(self, idx):\n        img_path = self.image_paths[idx]\n        img_id = Path(img_path).stem\n        img = np.array(Image.open(img_path).convert(\"RGB\"))\n        h, w = img.shape[:2]\n\n        mask = self.load_mask(img_id, h, w)\n\n        # Albumentations will resize both image and mask together\n        augmented = self.transform(image=img, mask=mask)\n        img = augmented[\"image\"]\n        mask = augmented[\"mask\"].unsqueeze(0)  # (1, H, W)\n\n        return img, mask\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:22.418326Z","iopub.execute_input":"2025-11-01T15:22:22.419101Z","iopub.status.idle":"2025-11-01T15:22:22.428701Z","shell.execute_reply.started":"2025-11-01T15:22:22.419069Z","shell.execute_reply":"2025-11-01T15:22:22.427834Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nforged_img_paths = [p for p in train_images if 'forged' in str(p)]\ntrain_paths, val_paths = train_test_split(forged_img_paths, test_size=0.2, random_state=42)\n\ntrain_dataset = ForgeryDataset(train_paths, mask_mapping, augment=True)\nval_dataset = ForgeryDataset(val_paths, mask_mapping, augment=False)\n\ntrain_loader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=2, shuffle=False, num_workers=2)\n\nprint(f\"Train: {len(train_dataset)}, Val: {len(val_dataset)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:22.569883Z","iopub.execute_input":"2025-11-01T15:22:22.570556Z","iopub.status.idle":"2025-11-01T15:22:22.585924Z","shell.execute_reply.started":"2025-11-01T15:22:22.570535Z","shell.execute_reply":"2025-11-01T15:22:22.585219Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, random, cv2, torch, numpy as np, pandas as pd\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\nfrom pathlib import Path\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:23.697903Z","iopub.execute_input":"2025-11-01T15:22:23.698798Z","iopub.status.idle":"2025-11-01T15:22:23.703440Z","shell.execute_reply.started":"2025-11-01T15:22:23.698770Z","shell.execute_reply":"2025-11-01T15:22:23.702515Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DoubleConv(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Conv2d(in_channels, out_channels, 3, padding=1, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_channels, out_channels, 3, padding=1, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        return self.net(x)\n\n\nclass UNet(nn.Module):\n    def __init__(self, n_channels=3, n_classes=1):\n        super().__init__()\n        self.inc = DoubleConv(n_channels, 64)\n        self.down1 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(64, 128))\n        self.down2 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(128, 256))\n        self.down3 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(256, 512))\n        self.down4 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(512, 512))\n\n        self.up1 = nn.ConvTranspose2d(512, 512, 2, stride=2)\n        self.conv1 = DoubleConv(1024, 256)\n        self.up2 = nn.ConvTranspose2d(256, 256, 2, stride=2)\n        self.conv2 = DoubleConv(512, 128)\n        self.up3 = nn.ConvTranspose2d(128, 128, 2, stride=2)\n        self.conv3 = DoubleConv(256, 64)\n        self.up4 = nn.ConvTranspose2d(64, 64, 2, stride=2)\n        self.conv4 = DoubleConv(128, 64)\n\n        self.outc = nn.Conv2d(64, n_classes, 1)\n\n    def forward(self, x):\n        x1 = self.inc(x)\n        x2 = self.down1(x1)\n        x3 = self.down2(x2)\n        x4 = self.down3(x3)\n        x5 = self.down4(x4)\n        x = self.up1(x5)\n        x = self.conv1(torch.cat([x, x4], dim=1))\n        x = self.up2(x)\n        x = self.conv2(torch.cat([x, x3], dim=1))\n        x = self.up3(x)\n        x = self.conv3(torch.cat([x, x2], dim=1))\n        x = self.up4(x)\n        x = self.conv4(torch.cat([x, x1], dim=1))\n        return self.outc(x)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:23.904434Z","iopub.execute_input":"2025-11-01T15:22:23.905226Z","iopub.status.idle":"2025-11-01T15:22:23.916378Z","shell.execute_reply.started":"2025-11-01T15:22:23.905192Z","shell.execute_reply":"2025-11-01T15:22:23.915699Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def dice_loss(pred, target, smooth=1.):\n    pred = torch.sigmoid(pred)\n    num = 2 * (pred * target).sum() + smooth\n    den = pred.sum() + target.sum() + smooth\n    return 1 - num / den\n\ndef combined_loss(pred, target):\n    bce = F.binary_cross_entropy_with_logits(pred, target)\n    dice = dice_loss(pred, target)\n    return 0.5*bce + 0.5*dice\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:25.611664Z","iopub.execute_input":"2025-11-01T15:22:25.611945Z","iopub.status.idle":"2025-11-01T15:22:25.617105Z","shell.execute_reply.started":"2025-11-01T15:22:25.611926Z","shell.execute_reply":"2025-11-01T15:22:25.616245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate(model, loader, device):\n    model.eval()\n    dice_scores = []\n    with torch.no_grad():\n        for imgs, masks in loader:\n            imgs, masks = imgs.to(device), masks.to(device)\n            preds = torch.sigmoid(model(imgs))\n            preds = (preds > 0.5).float()\n            dice = (2 * (preds * masks).sum()) / ((preds + masks).sum() + 1e-7)\n            dice_scores.append(dice.item())\n    return np.mean(dice_scores)\n\n\ndef train_model(model, train_loader, val_loader, epochs=20, lr=1e-4, save_path=\"unet_basic.pt\"):\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    model.to(device)\n    opt = torch.optim.Adam(model.parameters(), lr=lr)\n    best_dice = 0\n\n    for epoch in range(epochs):\n        model.train()\n        total_loss = 0\n        pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{epochs}\")\n        for imgs, masks in pbar:\n            imgs, masks = imgs.to(device), masks.to(device)\n            opt.zero_grad()\n            preds = model(imgs)\n            loss = combined_loss(preds, masks)\n            loss.backward()\n            opt.step()\n            total_loss += loss.item()\n            pbar.set_postfix(loss=loss.item())\n\n        val_dice = validate(model, val_loader, device)\n        print(f\"Epoch {epoch+1} | Train Loss: {total_loss/len(train_loader):.4f} | Val Dice: {val_dice:.4f}\")\n        if val_dice > best_dice:\n            best_dice = val_dice\n            torch.save(model, save_path)\n            print(\"Saved best model\")\n    print(f\"🏁 Training complete. Best Dice: {best_dice:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:26.506090Z","iopub.execute_input":"2025-11-01T15:22:26.506670Z","iopub.status.idle":"2025-11-01T15:22:26.514713Z","shell.execute_reply.started":"2025-11-01T15:22:26.506647Z","shell.execute_reply":"2025-11-01T15:22:26.513769Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Assuming discover_and_map_data() returns train_images, mask_mapping, test_images, forged_with_masks\ntrain_paths = [p for p in train_images if 'forged' in str(p)]\nfrom sklearn.model_selection import train_test_split\ntrn, val = train_test_split(train_paths, test_size=0.2, random_state=42)\n\ntrain_ds = ForgeryDataset(trn, mask_mapping, augment=True)\nval_ds   = ForgeryDataset(val, mask_mapping, augment=False)\ntrain_loader = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_ds, batch_size=2, shuffle=False, num_workers=2)\n\nmodel = UNet(n_channels=3, n_classes=1)\ntrain_model(model, train_loader, val_loader, epochs=20, lr=1e-4)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T15:22:57.443659Z","iopub.execute_input":"2025-11-01T15:22:57.443971Z","iopub.status.idle":"2025-11-01T17:49:31.137313Z","shell.execute_reply.started":"2025-11-01T15:22:57.443951Z","shell.execute_reply":"2025-11-01T17:49:31.136323Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}