{"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":7641671,"sourceType":"datasetVersion","datasetId":4453757},{"sourceId":11202266,"sourceType":"datasetVersion","datasetId":6994269}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ====================================================\n# Recod.ai / LUC Scientific Image Forgery Detection\n# Offline-Ready Notebook (PyTorch + Albumentations)\n# ====================================================\n\nimport os\nimport cv2\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, SubsetRandomSampler\nfrom torchvision import models, transforms\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\n\n# ====================================================\n# CONFIG\n# ====================================================\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nBATCH_SIZE = 8\nEPOCHS = 12\nLR = 1e-4\nIMG_SIZE = 256\nbase_dir = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n\npaths = {\n    \"train_authentic\": os.path.join(base_dir, \"train_images\", \"authentic\"),\n    \"train_forged\": os.path.join(base_dir, \"train_images\", \"forged\"),\n    \"train_masks\": os.path.join(base_dir, \"train_masks\"),\n    \"test_images\": os.path.join(base_dir, \"test_images\"),\n    \"sample_submission\": os.path.join(base_dir, \"sample_submission.csv\")\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T08:47:22.186393Z","iopub.execute_input":"2025-11-08T08:47:22.187119Z","iopub.status.idle":"2025-11-08T08:47:57.032054Z","shell.execute_reply.started":"2025-11-08T08:47:22.187093Z","shell.execute_reply":"2025-11-08T08:47:57.031398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# TRANSFORMS\n# ====================================================\ntrain_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.RandomRotate90(p=0.5),\n    A.OneOf([\n        A.MotionBlur(p=0.2),\n        A.MedianBlur(blur_limit=3, p=0.1),\n        A.Blur(blur_limit=3, p=0.1),\n    ], p=0.3),\n    A.ColorJitter(p=0.3),\n    A.Normalize(mean=(0.485, 0.456, 0.406),\n                std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\nval_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.Normalize(mean=(0.485, 0.456, 0.406),\n                std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T08:47:57.033109Z","iopub.execute_input":"2025-11-08T08:47:57.033530Z","iopub.status.idle":"2025-11-08T08:47:57.050116Z","shell.execute_reply.started":"2025-11-08T08:47:57.033507Z","shell.execute_reply":"2025-11-08T08:47:57.049337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# DATASET\n# ====================================================\nclass ForgeryDataset(Dataset):\n    def __init__(self, authentic_dir, forged_dir, mask_dir, transform=None):\n        self.authentic_paths = sorted([\n            os.path.join(authentic_dir, f) for f in os.listdir(authentic_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))\n        ])\n        self.forged_paths = sorted([\n            os.path.join(forged_dir, f) for f in os.listdir(forged_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))\n        ])\n        self.mask_dir = mask_dir\n        self.transform = transform\n\n        # Combine and assign labels\n        self.image_paths = self.authentic_paths + self.forged_paths\n        self.labels = [0]*len(self.authentic_paths) + [1]*len(self.forged_paths)\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def __getitem__(self, idx):\n        img_path = self.image_paths[idx]\n        label = self.labels[idx]\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        # Mask exists only for forged images\n        fname = os.path.basename(img_path)\n        mask_path = os.path.join(self.mask_dir, fname)\n        if os.path.exists(mask_path):\n            mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)\n        else:\n            mask = np.zeros((img.shape[0], img.shape[1]), dtype=np.uint8)\n\n        if self.transform:\n            transformed = self.transform(image=img, mask=mask)\n            img = transformed['image']\n            mask = transformed['mask']\n\n        return img, mask.unsqueeze(0).float(), torch.tensor(label, dtype=torch.long)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T08:47:59.724397Z","iopub.execute_input":"2025-11-08T08:47:59.724686Z","iopub.status.idle":"2025-11-08T08:47:59.732743Z","shell.execute_reply.started":"2025-11-08T08:47:59.724657Z","shell.execute_reply":"2025-11-08T08:47:59.732106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# DATA LOADERS\n# ====================================================\ndataset = ForgeryDataset(paths['train_authentic'], paths['train_forged'], paths['train_masks'], transform=train_transform)\nindices = np.arange(len(dataset))\nnp.random.shuffle(indices)\ntrain_size = int(0.8 * len(indices))\ntrain_idx, val_idx = indices[:train_size], indices[train_size:]\n\ntrain_sampler = SubsetRandomSampler(train_idx)\nval_sampler = SubsetRandomSampler(val_idx)\n\ntrain_loader = DataLoader(dataset, batch_size=BATCH_SIZE, sampler=train_sampler, num_workers=2)\nval_loader = DataLoader(ForgeryDataset(paths['train_authentic'], paths['train_forged'], paths['train_masks'], transform=val_transform),\n                        batch_size=BATCH_SIZE, sampler=val_sampler, num_workers=2)\n\nprint(\"Train samples:\", len(train_idx), \"| Val samples:\", len(val_idx))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T08:47:59.933457Z","iopub.execute_input":"2025-11-08T08:47:59.933680Z","iopub.status.idle":"2025-11-08T08:48:00.013028Z","shell.execute_reply.started":"2025-11-08T08:47:59.933658Z","shell.execute_reply":"2025-11-08T08:48:00.012452Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# MODEL (ResNet50 Binary Classifier - Offline)\n# ====================================================\nfrom torchvision.models import resnet50, ResNet50_Weights\n\n# initialize ResNet50 backbone\nmodel = resnet50(weights=None)  # don't download weights\n\n# load local pretrained weights\nlocal_weights_path = \"/kaggle/input/resnet50-0676ba61-pth/resnet50-0676ba61.pth\"\nstate_dict = torch.load(local_weights_path, map_location=\"cpu\")\n\n# remove possible 'fc' mismatch when loading partial weights\nfiltered_dict = {k: v for k, v in state_dict.items() if k in model.state_dict()}\nmodel.load_state_dict(filtered_dict, strict=False)\n\n# replace the final classification head\nmodel.fc = nn.Sequential(\n    nn.Linear(model.fc.in_features, 256),\n    nn.ReLU(),\n    nn.Dropout(0.3),\n    nn.Linear(256, 2)\n)\n\nmodel = model.to(DEVICE)\n\n# ====================================================\n# LOSS & OPTIMIZER\n# ====================================================\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n\n# ====================================================\n# TRAIN LOOP\n# ====================================================\nfor epoch in range(EPOCHS):\n    model.train()\n    train_loss = 0.0\n    for imgs, masks, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS} [Train]\"):\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item() * imgs.size(0)\n    train_loss /= len(train_loader.dataset)\n\n    # Validation\n    model.eval()\n    val_loss = 0.0\n    correct, total = 0, 0\n    with torch.no_grad():\n        for imgs, masks, labels in tqdm(val_loader, desc=f\"Epoch {epoch+1}/{EPOCHS} [Val]\"):\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * imgs.size(0)\n            preds = outputs.argmax(1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n    val_acc = correct / total\n    val_loss /= len(val_loader.dataset)\n\n    print(f\"Epoch {epoch+1}: Train Loss={train_loss:.4f}, Val Loss={val_loss:.4f}, Val Acc={val_acc:.4f}\")\n\n# ====================================================\n# SAVE MODEL\n# ====================================================\ntorch.save(model.state_dict(), \"forgery_detector_resnet50.pth\")\nprint(\"✅ Model saved to forgery_detector_resnet50.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T08:48:01.458274Z","iopub.execute_input":"2025-11-08T08:48:01.458581Z","iopub.status.idle":"2025-11-08T09:17:00.546541Z","shell.execute_reply.started":"2025-11-08T08:48:01.458557Z","shell.execute_reply":"2025-11-08T09:17:00.545369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# INFERENCE & SUBMISSION\n# ====================================================\ntest_dir = paths['test_images']\ntest_files = sorted(os.listdir(test_dir))\n\nmodel.eval()\npreds = []\nfor fname in tqdm(test_files, desc=\"Predicting\"):\n    img_path = os.path.join(test_dir, fname)\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = val_transform(image=img)[\"image\"].unsqueeze(0).to(DEVICE)\n    with torch.no_grad():\n        output = model(img)\n        pred = torch.softmax(output, dim=1)[:, 1].item()\n    preds.append(pred)\n\ndf_sub = pd.read_csv(paths[\"sample_submission\"])\ndf_sub[\"predicted\"] = preds\ndf_sub.to_csv(\"submission.csv\", index=False)\nprint(\"✅ submission.csv saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T09:17:00.548721Z","iopub.execute_input":"2025-11-08T09:17:00.549038Z","iopub.status.idle":"2025-11-08T09:17:00.732721Z","shell.execute_reply.started":"2025-11-08T09:17:00.548988Z","shell.execute_reply":"2025-11-08T09:17:00.732020Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}