{"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":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"sourceType":"competition"},{"sourceId":13485946,"sourceType":"datasetVersion","datasetId":8562086}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport glob\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nimport zipfile\nimport cv2\nimport torch\nimport torchvision.transforms as T\nimport torchvision.models as models\nimport torchvision","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:43.486440Z","iopub.execute_input":"2025-10-24T09:06:43.486761Z","iopub.status.idle":"2025-10-24T09:06:43.493607Z","shell.execute_reply.started":"2025-10-24T09:06:43.486737Z","shell.execute_reply":"2025-10-24T09:06:43.492518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_test_folder(possible_names=None):\n    # common Kaggle pattern: /kaggle/input/<dataset-name>/\n    if possible_names is None:\n        possible_names = [\n            \"recodai-luc-scientific-image-forgery-detection\",\n            \"recodai_luc_scientific_image_forgery_detection\",\n            \"recodai-luc-scientific-image-forgery\",\n            \"test_images\", \"test\", \"test_images_png\"\n        ]\n    # check /kaggle/input\n    kaggle_input = Path(\"/kaggle/input\")\n    candidates = []\n    if kaggle_input.exists():\n        for d in kaggle_input.glob(\"*\"):\n            # check for a test or images subfolder\n            for name in possible_names:\n                p = d / name\n                if p.exists():\n                    candidates.append(str(p))\n            # also check likely structure: dataset/test or dataset/test_images\n            for p in (d / \"test\", d / \"test_images\", d / \"images\"):\n                if p.exists():\n                    candidates.append(str(p))\n    # fallback: current directory\n    for name in possible_names:\n        if Path(name).exists():\n            candidates.append(name)\n    # choose first candidate\n    if candidates:\n        return candidates[0]\n    # if nothing found, instruct user\n    raise FileNotFoundError(\"Could not find test image folder. Please attach the competition dataset or set 'TEST_FOLDER' manually.\")\n\n# Example\ntry:\n    TEST_FOLDER = find_test_folder()\n    print(\"Auto-detected test folder:\", TEST_FOLDER)\nexcept FileNotFoundError as e:\n    print(e)\n    # If running locally, set TEST_FOLDER manually:\n    # TEST_FOLDER = \"/path/to/test/images\"\n    TEST_FOLDER = None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:43.495094Z","iopub.execute_input":"2025-10-24T09:06:43.495378Z","iopub.status.idle":"2025-10-24T09:06:43.519637Z","shell.execute_reply.started":"2025-10-24T09:06:43.495356Z","shell.execute_reply":"2025-10-24T09:06:43.518595Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def list_image_files(test_folder):\n    exts = (\"*.png\", \"*.jpg\", \"*.jpeg\", \"*.tif\", \"*.tiff\", \"*.bmp\")\n    files = []\n    for e in exts:\n        files.extend(sorted(Path(test_folder).glob(e)))\n    return [str(p) for p in files]\n\nif TEST_FOLDER:\n    test_files = list_image_files(TEST_FOLDER)\n    print(f\"Found {len(test_files)} test images (showing up to 5):\")\n    for p in test_files[:5]:\n        print(\" \", p)\nelse:\n    test_files = []","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:43.520753Z","iopub.execute_input":"2025-10-24T09:06:43.521288Z","iopub.status.idle":"2025-10-24T09:06:43.544979Z","shell.execute_reply.started":"2025-10-24T09:06:43.521252Z","shell.execute_reply":"2025-10-24T09:06:43.543922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 4 - RLE encoding/decoding utilities (standard Kaggle format)\n# RLE encodes mask in column-major order, 1-indexed positions (common in Kaggle competitions).\ndef rle_encode(mask):\n    '''\n    mask: 2D numpy array {0,1}, 1 - mask foreground\n    returns run-length as string\n    '''\n    pixels = mask.flatten(order='F')  # Fortran order (column-major)\n    # add sentinel\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] = runs[1::2] - runs[::2]\n    rle = ' '.join(str(x) for x in runs)\n    return rle\n\ndef rle_decode(rle, shape):\n    s = rle.split()\n    starts, lengths = [np.asarray(x, dtype=int) for x in (s[0::2], s[1::2])]\n    starts -= 1\n    img = np.zeros(shape[0]*shape[1], dtype=np.uint8)\n    for lo, ln in zip(starts, lengths):\n        img[lo:lo+ln] = 1\n    return img.reshape((shape[1], shape[0]), order='F').T  # careful reshape back","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:43.546905Z","iopub.execute_input":"2025-10-24T09:06:43.547345Z","iopub.status.idle":"2025-10-24T09:06:43.567746Z","shell.execute_reply.started":"2025-10-24T09:06:43.547308Z","shell.execute_reply":"2025-10-24T09:06:43.566355Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 5 - Baseline predictor (very fast, deterministic)\n# Strategy: convert to grayscale, use Otsu threshold + morphological opening to remove noise.\ndef baseline_predict(img_path, resize_to=None):\n    \"\"\"\n    Input: image path\n    Output: binary mask (numpy uint8) same size as input image (0/1)\n    \"\"\"\n    img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n    if img is None:\n        raise ValueError(f\"Could not read {img_path}\")\n    orig_h, orig_w = img.shape[:2]\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    # optional resizing for speed\n    scale = 1.0\n    if resize_to is not None:\n        h = int((resize_to * orig_h) ** 0.5) # dummy, but we won't use this; keep as placeholder\n    # Otsu threshold\n    _, thr = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    # morphological operations to clean\n    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))\n    clean = cv2.morphologyEx(thr, cv2.MORPH_OPEN, kernel, iterations=1)\n    # optionally apply Canny edges and combine (helps find copy-move edges)\n    edges = cv2.Canny(gray, 50, 150)\n    combined = cv2.bitwise_or(clean, edges)\n    # final binary mask\n    mask = (combined > 0).astype(np.uint8)\n    # Fill small holes and remove tiny objects\n    # remove small connected components\n    num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)\n    out_mask = np.zeros_like(mask)\n    for i in range(1, num_labels):\n        area = stats[i, cv2.CC_STAT_AREA]\n        if area >= 50:  # tuneable threshold\n            out_mask[labels == i] = 1\n    return out_mask\n\n# Quick test visual (only if there are images)\nif test_files:\n    sample = test_files[0]\n    print(\"Sample:\", sample)\n    m = baseline_predict(sample)\n    print(\"Mask shape:\", m.shape, \"unique:\", np.unique(m))\n    # visualize\n    img = cv2.imread(sample)[:,:,::-1]\n    plt.figure(figsize=(10,6))\n    plt.subplot(1,2,1); plt.imshow(img); plt.axis('off'); plt.title('Image')\n    plt.subplot(1,2,2); plt.imshow(m, cmap='gray'); plt.axis('off'); plt.title('Baseline mask')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:43.568735Z","iopub.execute_input":"2025-10-24T09:06:43.569012Z","iopub.status.idle":"2025-10-24T09:06:44.600244Z","shell.execute_reply.started":"2025-10-24T09:06:43.568978Z","shell.execute_reply":"2025-10-24T09:06:44.598439Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 6 - Optional: simple deep model inference (FCN) for better baseline\n# NOTE: This uses torchvision's pretrained segmentation model (FCN/DeepLab). It may require GPU and will not be perfect for this domain.\ndef fcn_predict(img_path, device='cuda'):\n    model = models.segmentation.fcn_resnet50(pretrained=True).eval().to(device)\n    # remove after first call or move outside for speed in real notebook\n    transform = T.Compose([T.ToTensor(),\n                           T.Normalize(mean=[0.485, 0.456, 0.406],\n                                       std =[0.229, 0.224, 0.225])])\n    img = Image.open(img_path).convert(\"RGB\")\n    x = transform(img).unsqueeze(0).to(device)\n    with torch.no_grad():\n        out = model(x)['out'][0]  # 21 classes for fcn pretrained on COCO/Pascal?\n        # take max across classes and threshold\n        scores = torch.softmax(out, dim=0)\n        # simple heuristic: foreground = any class except background (class 0 often)\n        fg = scores[1:].sum(dim=0).cpu().numpy()\n        mask = (fg > 0.3).astype(np.uint8)  # threshold tunable\n    # resize to original if needed (it's same size)\n    return mask\n\n# Warning: downloading model weights may take time and GPU is beneficial.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:44.602646Z","iopub.execute_input":"2025-10-24T09:06:44.603101Z","iopub.status.idle":"2025-10-24T09:06:44.611280Z","shell.execute_reply.started":"2025-10-24T09:06:44.603042Z","shell.execute_reply":"2025-10-24T09:06:44.609951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 7 - Create predictions and produce submission files\nOUTPUT_DIR = Path(\"submission_output\")\nOUTPUT_DIR.mkdir(exist_ok=True)\n\ndef make_submission_rle(test_files, predictor_func, output_csv=\"submission.csv\", verbose=True):\n    rows = []\n    for p in tqdm(test_files, desc=\"Predicting\"):\n        fname = Path(p).name\n        mask = predictor_func(p)\n        # ensure mask is binary 0/1 and same size as image\n        if mask.dtype != np.uint8:\n            mask = mask.astype(np.uint8)\n        # encode to rle (if competition expects column-major 1-indexed)\n        rle = rle_encode(mask)\n        rows.append({\"image_id\": fname, \"EncodedPixels\": rle})\n    df = pd.DataFrame(rows)\n    df.to_csv(OUTPUT_DIR / output_csv, index=False)\n    if verbose:\n        print(\"Wrote\", OUTPUT_DIR / output_csv)\n    return df\n\ndef make_submission_pngs(test_files, predictor_func, zip_name=\"masks.zip\", out_mask_size=None):\n    mask_folder = OUTPUT_DIR / \"masks\"\n    mask_folder.mkdir(exist_ok=True)\n    for p in tqdm(test_files, desc=\"Predicting PNGs\"):\n        fname = Path(p).stem\n        mask = predictor_func(p)\n        # ensure 0/255 PNG\n        mask_img = (mask * 255).astype(np.uint8)\n        pil = Image.fromarray(mask_img)\n        # optional: resize to original dims? already same\n        pil.save(mask_folder / f\"{fname}.png\")\n    # zip them\n    zip_path = OUTPUT_DIR / zip_name\n    with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:\n        for f in sorted(mask_folder.glob(\"*.png\")):\n            zf.write(f, arcname=f.name)\n    print(\"Saved masks zip:\", zip_path)\n    return zip_path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:44.612886Z","iopub.execute_input":"2025-10-24T09:06:44.613258Z","iopub.status.idle":"2025-10-24T09:06:44.640041Z","shell.execute_reply.started":"2025-10-24T09:06:44.613224Z","shell.execute_reply":"2025-10-24T09:06:44.638948Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 8 - Run baseline to create RLE CSV and masks.zip\nif not test_files:\n    print(\"No test files detected. Set TEST_FOLDER or upload test images.\")\nelse:\n    # Baseline RLE\n    df_rle = make_submission_rle(test_files, baseline_predict, output_csv=\"submission_rle_baseline.csv\")\n    # Baseline PNGs ZIP\n    zip_path = make_submission_pngs(test_files, baseline_predict, zip_name=\"masks_baseline.zip\")\n    display(df_rle.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:44.641179Z","iopub.execute_input":"2025-10-24T09:06:44.641459Z","iopub.status.idle":"2025-10-24T09:06:45.285148Z","shell.execute_reply.started":"2025-10-24T09:06:44.641437Z","shell.execute_reply":"2025-10-24T09:06:45.283952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 9 - (Optional) Run FCN model for better masks (slow)\n# Uncomment to use FCN. On Kaggle set accelerator to GPU.\n# WARNING: This will load pretrained weights and use GPU if available.\n\"\"\"\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\ndef fcn_wrapper(p):\n    return fcn_predict(p, device=device)\n\ndf_rle_fcn = make_submission_rle(test_files, fcn_wrapper, output_csv=\"submission_rle_fcn.csv\")\nzip_path_fcn = make_submission_pngs(test_files, fcn_wrapper, zip_name=\"masks_fcn.zip\")\n\"\"\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:45.286314Z","iopub.execute_input":"2025-10-24T09:06:45.286670Z","iopub.status.idle":"2025-10-24T09:06:45.294392Z","shell.execute_reply.started":"2025-10-24T09:06:45.286635Z","shell.execute_reply":"2025-10-24T09:06:45.293153Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 10 - How to submit\nprint(\"Files generated in:\", OUTPUT_DIR.resolve())\nprint(\"- submission_rle_baseline.csv  (RLE CSV format)\")\nprint(\"- masks_baseline.zip           (ZIP of PNG masks)\")\nprint()\nprint(\"To submit on Kaggle: go to the competition -> Submit Predictions -> choose either CSV or ZIP (depending on competition rules).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-24T09:06:45.295852Z","iopub.execute_input":"2025-10-24T09:06:45.296227Z","iopub.status.idle":"2025-10-24T09:06:45.315135Z","shell.execute_reply.started":"2025-10-24T09:06:45.296202Z","shell.execute_reply":"2025-10-24T09:06:45.313947Z"}},"outputs":[],"execution_count":null}]}