{"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"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Cell 1: Imports & Config\nimport os, glob, random\nfrom pathlib import Path\nimport numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom typing import List, Tuple\n\n# ==== CONFIG: adjust to your dataset layout ====\nCOMP_DIR   = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nTRAIN_DIR  = f\"{COMP_DIR}/train_images\"\nMASK_DIR   = f\"{COMP_DIR}/train_masks\"\nFORGED_DIR = f\"{TRAIN_DIR}/forged\"\n\n# Visual / augmentation controls\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED)\n\n# Copy–move placement controls (for Step 2)\nPLACE_TRIES         = 80      # attempts per copy\nROT_MIN, ROT_MAX    = -20, 20 # random rotation per copy (deg)\nALLOWED_OVERLAP_PX  = 0       # forbid overlapping original forged area\n\n# Optional: save previews\nSAVE_PREVIEW = True\nPREVIEW_DIR  = \"/kaggle/working/aug_copy_move_preview\"\nos.makedirs(PREVIEW_DIR, exist_ok=True)\n\n# Utility\ndef pil_to_np_rgb(pil_img: Image.Image) -> np.ndarray:\n    return np.array(pil_img.convert(\"RGB\"))\n\ndef load_rgb(path: str) -> Image.Image:\n    return Image.open(path).convert(\"RGB\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:12:25.194153Z","iopub.execute_input":"2025-10-29T19:12:25.194843Z","iopub.status.idle":"2025-10-29T19:12:25.469837Z","shell.execute_reply.started":"2025-10-29T19:12:25.194808Z","shell.execute_reply":"2025-10-29T19:12:25.469060Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell: Setup Cellpose (GPU if available)\nimport sys, subprocess, importlib\n\ndef _pip_install(pkg):\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", pkg])\n\ntry:\n    import torch\n    _ = torch.cuda.is_available()\nexcept Exception:\n    import torch  # ensure torch is imported\n\n# Try import; if missing, install cellpose + deps\ntry:\n    from cellpose import models as _cp_models\nexcept Exception:\n    _pip_install(\"cellpose>=3.0.10\")\n    _pip_install(\"opencv-python-headless>=4.10.0.84\")\n    _pip_install(\"scikit-image>=0.22.0\")\n    from cellpose import models as _cp_models\n\nprint(\"Torch:\", torch.__version__, \"| CUDA:\", torch.cuda.is_available())\nprint(\"Cellpose imported from:\", _cp_models.__file__)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:12:25.471014Z","iopub.execute_input":"2025-10-29T19:12:25.471335Z","iopub.status.idle":"2025-10-29T19:14:17.807356Z","shell.execute_reply.started":"2025-10-29T19:12:25.471309Z","shell.execute_reply":"2025-10-29T19:14:17.806666Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell: Cellpose model loader + segmentation\nimport numpy as np\nimport cv2\n\ndef get_cellpose_model(model_type=\"cyto2\", gpu=None):\n    \"\"\"\n    Works with Cellpose v2 (Cellpose) and v3 (CellposeModel).\n    \"\"\"\n    if gpu is None:\n        gpu = bool(torch.cuda.is_available())\n    # v3: CellposeModel; v2: Cellpose\n    if hasattr(_cp_models, \"CellposeModel\"):\n        # v3+\n        return _cp_models.CellposeModel(model_type=model_type, gpu=gpu)\n    else:\n        # v2.x fallback\n        return _cp_models.Cellpose(gpu=gpu, model_type=model_type)\n\ndef cellpose_segment(img_rgb, model=None, diameter=None, flow_threshold=0.4, cellprob_threshold=0.0, anisotropy=None):\n    \"\"\"\n    Returns:\n      masks_lab: HxW int labels (0=background, 1..K)\n      union_bin: HxW uint8 {0,1}\n    \"\"\"\n    if model is None:\n        model = get_cellpose_model(\"cyto2\")\n    # API differences: v3 .eval returns (masks, flows, styles)\n    # v2 returns (masks, flows, styles, diams)\n    out = model.eval(\n        img_rgb,\n        diameter=diameter,\n        channels=[0, 0],  # treat as grayscale content\n        flow_threshold=flow_threshold,\n        cellprob_threshold=cellprob_threshold,\n        anisotropy=anisotropy,\n        normalize=True,\n        augment=False\n    )\n    if isinstance(out, (list, tuple)) and len(out) >= 3:\n        masks_lab = out[0]  # HxW labels\n    else:\n        # unexpected shape\n        masks_lab = None\n\n    if masks_lab is None:\n        union = np.zeros(img_rgb.shape[:2], np.uint8)\n        return union, union\n\n    union = (masks_lab > 0).astype(np.uint8)\n    return masks_lab.astype(np.int32), union\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:17.808071Z","iopub.execute_input":"2025-10-29T19:14:17.808422Z","iopub.status.idle":"2025-10-29T19:14:17.815496Z","shell.execute_reply.started":"2025-10-29T19:14:17.808406Z","shell.execute_reply":"2025-10-29T19:14:17.814694Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell: Forbidden mask (dilate cell regions)\nfrom skimage.morphology import binary_dilation, disk\n\nFORBID_DILATE_PX = 8   # margin around detected cells (tune)\nFORBID_MAX_OVERLAP_PX = 0  # reject any candidate overlapping > this many pixels with forbidden mask\n\ndef build_forbidden_mask(img_rgb, model=None, dilate_px=FORBID_DILATE_PX):\n    masks_lab, union = cellpose_segment(img_rgb, model=model)\n    if union.sum() == 0:\n        return np.zeros(union.shape, np.uint8)\n    # dilate to add safety margin around cells\n    forb = binary_dilation(union.astype(bool), disk(max(1, int(dilate_px)))).astype(np.uint8)\n    return forb\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:17.816822Z","iopub.execute_input":"2025-10-29T19:14:17.817055Z","iopub.status.idle":"2025-10-29T19:14:17.831542Z","shell.execute_reply.started":"2025-10-29T19:14:17.817039Z","shell.execute_reply":"2025-10-29T19:14:17.831056Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 2: Mask I/O & Visualization Helpers\n\ndef load_mask_instances(mask_path: str) -> List[np.ndarray]:\n    \"\"\"\n    Load competition mask npy into a list of binary instance masks (H, W) uint8 {0,1}.\n    Supports npy saved as list/tuple, dict, 2D union, or 3D stack.\n    \"\"\"\n    m = np.load(mask_path, allow_pickle=True)\n    if isinstance(m, (list, tuple)):\n        arrs = []\n        for item in m:\n            a = np.asarray(item)\n            if a.ndim == 2:\n                arrs.append((a > 0).astype(np.uint8))\n            elif a.ndim == 3:\n                # collapse channels if any\n                arrs.append(((a > 0).sum(axis=0) > 0).astype(np.uint8))\n        return arrs\n    if isinstance(m, dict):\n        key = \"masks\" if \"masks\" in m else list(m.keys())[0]\n        m = np.asarray(m[key])\n    m = np.asarray(m)\n    if m.ndim == 2:\n        return [ (m > 0).astype(np.uint8) ]\n    elif m.ndim == 3:\n        # assume (N,H,W)\n        stack = m if m.shape[0] < 64 else np.transpose(m, (2,0,1))\n        return [ (stack[i] > 0).astype(np.uint8) for i in range(stack.shape[0]) ]\n    else:\n        raise ValueError(f\"Unsupported mask shape in {mask_path}\")\n\ndef overlay_instances(img: np.ndarray, insts: List[np.ndarray], alpha: float = 0.35) -> np.ndarray:\n    \"\"\"Color each instance differently and alpha-blend.\"\"\"\n    h, w = img.shape[:2]\n    vis = img.copy()\n    if vis.ndim == 2:\n        vis = cv2.cvtColor(vis, cv2.COLOR_GRAY2BGR)\n\n    # random but deterministic colors\n    rng = np.random.RandomState(1234)\n    colors = [tuple(map(int, rng.randint(64, 255, size=3))) for _ in insts]\n\n    for m, col in zip(insts, colors):\n        if m.shape != (h, w):\n            m = cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST)\n        if m.sum() == 0:\n            continue\n        overlay = vis.copy()\n        overlay[m > 0] = (0.65 * vis[m > 0] + 0.35 * np.array(col)).astype(np.uint8)\n        vis = overlay\n    return vis\n\ndef draw_instance_outlines_with_labels(img: np.ndarray, insts: List[np.ndarray]) -> np.ndarray:\n    \"\"\"Draw green contours and put instance index near centroid.\"\"\"\n    h, w = img.shape[:2]\n    vis = img.copy()\n    if vis.ndim == 2:\n        vis = cv2.cvtColor(vis, cv2.COLOR_GRAY2BGR)\n\n    for idx, m in enumerate(insts, start=1):\n        if m.shape != (h, w):\n            m = cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST)\n        if m.sum() == 0:\n            continue\n        cnts, _ = cv2.findContours(m.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        cv2.drawContours(vis, cnts, -1, (0, 255, 0), 2, lineType=cv2.LINE_AA)\n        # centroid\n        ys, xs = np.where(m > 0)\n        if len(xs) > 0:\n            cx, cy = int(xs.mean()), int(ys.mean())\n            cv2.putText(vis, f\"{idx}\", (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (20, 20, 255), 2, cv2.LINE_AA)\n    return vis\n\ndef union_from_insts(insts: List[np.ndarray], shape_hw: Tuple[int,int]) -> np.ndarray:\n    H, W = shape_hw\n    u = np.zeros((H, W), np.uint8)\n    for m in insts:\n        if m.shape != (H, W):\n            m = cv2.resize(m, (W, H), interpolation=cv2.INTER_NEAREST)\n        u |= (m > 0).astype(np.uint8)\n    return u\n\ndef bbox_from_mask(mask: np.ndarray) -> Tuple[int,int,int,int]:\n    ys, xs = np.where(mask > 0)\n    if len(xs) == 0:\n        return 0,0,0,0\n    x0, x1 = xs.min(), xs.max() + 1\n    y0, y1 = ys.min(), ys.max() + 1\n    return x0, y0, x1, y1\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:17.832306Z","iopub.execute_input":"2025-10-29T19:14:17.832552Z","iopub.status.idle":"2025-10-29T19:14:17.851681Z","shell.execute_reply.started":"2025-10-29T19:14:17.832532Z","shell.execute_reply":"2025-10-29T19:14:17.851172Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 3: Rotation & Placement helpers\n\ndef rotate_patch_and_mask(patch_rgb: np.ndarray, patch_mask: np.ndarray, angle_deg: float):\n    \"\"\"Rotate patch and mask around center; returns expanded canvas (no cropping).\"\"\"\n    H, W = patch_rgb.shape[:2]\n    center = (W / 2.0, H / 2.0)\n    M = cv2.getRotationMatrix2D(center, angle_deg, 1.0)\n    cos, sin = abs(M[0,0]), abs(M[0,1])\n    newW = int(W * cos + H * sin)\n    newH = int(H * cos + W * sin)\n    M[0,2] += (newW / 2) - center[0]\n    M[1,2] += (newH / 2) - center[1]\n\n    rot_rgb  = cv2.warpAffine(patch_rgb,  M, (newW, newH), flags=cv2.INTER_LINEAR,  borderMode=cv2.BORDER_REFLECT_101)\n    rot_mask = cv2.warpAffine(patch_mask, M, (newW, newH), flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_CONSTANT, borderValue=0)\n    rot_mask = (rot_mask > 0).astype(np.uint8)\n    return rot_rgb, rot_mask\n\nimport random\n\nFEATHER_PX_RANGE = (6, 18)\nFEATHER_GAMMA    = 1.0\n\ndef feather_alpha(mask01: np.ndarray, radius_px: int, gamma: float = 1.0) -> np.ndarray:\n    m = (mask01 > 0).astype(np.uint8)\n    if m.sum() == 0:\n        return np.zeros_like(mask01, dtype=np.float32)\n    dist = cv2.distanceTransform(m, distanceType=cv2.DIST_L2, maskSize=3)\n    a = np.clip(dist / max(1, float(radius_px)), 0.0, 1.0)\n    if gamma != 1.0:\n        a = np.power(a, gamma)\n    return a.astype(np.float32)\n\ndef try_place_once(img_rgb: np.ndarray,\n                   union_mask: np.ndarray,\n                   src_patch_rgb: np.ndarray,\n                   src_patch_mask: np.ndarray,\n                   angle: float,\n                   max_overlap_px: int,\n                   place_tries: int = 80,\n                   forbid_mask: np.ndarray = None,\n                   forbid_max_overlap_px: int = FORBID_MAX_OVERLAP_PX) -> tuple[bool, np.ndarray, np.ndarray]:\n    \"\"\"\n    Feathered copy-move with forbidden-area check.\n    - Rejects placements overlapping 'forbid_mask' > forbid_max_overlap_px.\n    \"\"\"\n    H, W = img_rgb.shape[:2]\n    rot_rgb, rot_mask = rotate_patch_and_mask(src_patch_rgb, src_patch_mask, angle)\n    h, w = rot_rgb.shape[:2]\n    if h >= H or w >= W:\n        return False, img_rgb, np.zeros((H, W), np.uint8)\n\n    forb = None\n    if forbid_mask is not None and forbid_mask.shape == union_mask.shape:\n        forb = forbid_mask\n\n    for _ in range(place_tries):\n        x = random.randint(0, W - w)\n        y = random.randint(0, H - h)\n\n        target_win = union_mask[y:y+h, x:x+w]\n        overlap_union = int((target_win & (rot_mask > 0)).sum())\n        if overlap_union > max_overlap_px:\n            continue\n\n        if forb is not None:\n            forb_win = forb[y:y+h, x:x+w]\n            overlap_forbid = int((forb_win & (rot_mask > 0)).sum())\n            if overlap_forbid > forbid_max_overlap_px:\n                continue\n\n        # feather alpha\n        feather_px = random.randint(FEATHER_PX_RANGE[0], FEATHER_PX_RANGE[1])\n        alpha_local = feather_alpha(rot_mask, radius_px=feather_px, gamma=FEATHER_GAMMA)\n\n        new_img = img_rgb.copy().astype(np.float32)\n        tgt = new_img[y:y+h, x:x+w, :]\n        src = rot_rgb.astype(np.float32)\n        m = (rot_mask > 0)\n        if m.any():\n            a3 = np.repeat(alpha_local[:, :, None], 3, axis=2)\n            tgt[m] = (a3[m] * src[m] + (1.0 - a3[m]) * tgt[m])\n        new_img = np.clip(new_img, 0, 255).astype(np.uint8)\n\n        placed = np.zeros((H, W), np.uint8)\n        placed[y:y+h, x:x+w][m] = 1\n\n        return True, new_img, placed\n\n    return False, img_rgb, np.zeros((H, W), np.uint8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:17.852428Z","iopub.execute_input":"2025-10-29T19:14:17.852608Z","iopub.status.idle":"2025-10-29T19:14:17.872799Z","shell.execute_reply.started":"2025-10-29T19:14:17.852593Z","shell.execute_reply":"2025-10-29T19:14:17.872270Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 4: Step 1 — Pick 3 forged images, visualize & labelize their instance masks\n\nforged_paths = sorted(glob.glob(str(Path(FORGED_DIR) / \"*\")))\nprint(\"Forged images found:\", len(forged_paths))\nassert len(forged_paths) > 0, \"No forged images found.\"\n\n# pick three (deterministic with SEED)\nrng = np.random.RandomState(SEED)\nsel_paths = list(rng.choice(forged_paths, size=min(3, len(forged_paths)), replace=False))\nsel_paths = [Path(p) for p in sel_paths]\nsel_paths\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:17.873679Z","iopub.execute_input":"2025-10-29T19:14:17.873878Z","iopub.status.idle":"2025-10-29T19:14:17.922067Z","shell.execute_reply.started":"2025-10-29T19:14:17.873863Z","shell.execute_reply":"2025-10-29T19:14:17.921521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 5: Visualize the 3 selections with labels\nfor p in sel_paths:\n    case_id = p.stem\n    mask_path = Path(MASK_DIR) / f\"{case_id}.npy\"\n    if not mask_path.exists():\n        print(f\"⚠️ Missing mask for {case_id}, skipping.\")\n        continue\n\n    img = pil_to_np_rgb(load_rgb(str(p)))\n    H, W = img.shape[:2]\n    insts = load_mask_instances(str(mask_path))\n    # ensure mask sizes match image\n    insts = [cv2.resize(m, (W, H), interpolation=cv2.INTER_NEAREST).astype(np.uint8) if m.shape != (H, W) else (m>0).astype(np.uint8) for m in insts]\n\n    overlay = overlay_instances(img, insts)\n    labeled = draw_instance_outlines_with_labels(img, insts)\n\n    union = union_from_insts(insts, (H, W))\n    print(f\"{case_id} | HxW={H}x{W} | instances={len(insts)} | union px={int(union.sum())}\")\n\n    fig = plt.figure(figsize=(14, 8))\n    ax1 = plt.subplot(1,3,1); ax1.imshow(img);     ax1.set_title(\"Original\"); ax1.axis(\"off\")\n    ax2 = plt.subplot(1,3,2); ax2.imshow(overlay); ax2.set_title(\"Colored overlay\"); ax2.axis(\"off\")\n    ax3 = plt.subplot(1,3,3); ax3.imshow(labeled); ax3.set_title(\"Outlines + labels\"); ax3.axis(\"off\")\n    plt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:17.922680Z","iopub.execute_input":"2025-10-29T19:14:17.922876Z","iopub.status.idle":"2025-10-29T19:14:23.344075Z","shell.execute_reply.started":"2025-10-29T19:14:17.922861Z","shell.execute_reply":"2025-10-29T19:14:23.343309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell: Generate N examples, each from a random forged image, blocking real cells\ncellpose_model = get_cellpose_model(\"cyto2\")  # or \"nuclei\" depending on your data\n\nassert len(forged_paths) > 0, \"No forged images found.\"\nprint(f\"Forged pool: {len(forged_paths)} images\")\n\nN_EXAMPLES = 5\nCOPIES_PER_EXAMPLE = 5\n\nexamples_done = 0\nattempts = 0\nMAX_GLOBAL_ATTEMPTS = N_EXAMPLES * 10\n\nwhile examples_done < N_EXAMPLES and attempts < MAX_GLOBAL_ATTEMPTS:\n    attempts += 1\n    src_path = Path(random.choice(forged_paths))\n    case_id  = src_path.stem\n    mask_path = Path(MASK_DIR) / f\"{case_id}.npy\"\n    if not mask_path.exists():\n        continue\n\n    img = pil_to_np_rgb(load_rgb(str(src_path)))\n    H, W = img.shape[:2]\n    insts = load_mask_instances(str(mask_path))\n    if len(insts) == 0:\n        continue\n    insts = [cv2.resize(m, (W, H), interpolation=cv2.INTER_NEAREST).astype(np.uint8)\n             if m.shape != (H, W) else (m > 0).astype(np.uint8) for m in insts]\n    areas = [int(m.sum()) for m in insts]\n    if not any(a > 0 for a in areas):\n        continue\n\n    # source instance (prefer larger)\n    k = min(5, len(insts)); top_idxs = np.argsort(areas)[::-1][:k]\n    src_idx = int(random.choice(top_idxs))\n    src_mask = insts[src_idx]\n    x0, y0, x1, y1 = bbox_from_mask(src_mask)\n    if x1-x0 <= 1 or y1-y0 <= 1:\n        continue\n    src_patch_mask = src_mask[y0:y1, x0:x1]\n    src_patch_rgb  = img[y0:y1, x0:x1, :]\n\n    union_orig = union_from_insts(insts, (H, W))\n\n    # ---- Build forbidden mask from Cellpose (real cells) ----\n    forbid_mask = build_forbidden_mask(img, model=cellpose_model, dilate_px=FORBID_DILATE_PX)\n\n    work_img   = img.copy()\n    work_union = union_orig.copy()\n    placed_list = []\n\n    made = 0; tries = 0\n    while made < COPIES_PER_EXAMPLE and tries < PLACE_TRIES * 4:\n        tries += 1\n        angle = random.uniform(ROT_MIN, ROT_MAX)\n        ok, new_img, placed_mask = try_place_once(\n            work_img, work_union,\n            src_patch_rgb, src_patch_mask,\n            angle, ALLOWED_OVERLAP_PX,\n            place_tries=PLACE_TRIES,\n            forbid_mask=forbid_mask,\n            forbid_max_overlap_px=FORBID_MAX_OVERLAP_PX\n        )\n        if not ok:\n            continue\n        work_img   = new_img\n        work_union |= placed_mask\n        placed_list.append(placed_mask)\n        made += 1\n\n        # Optional: also forbid stacking on top of our *new* copies\n        forbid_mask |= placed_mask\n\n    if len(placed_list) == 0:\n        continue\n\n    # ---- (Your visuals & optional saving here — unchanged) ----\n    new_union = np.zeros((H, W), np.uint8)\n    for m in placed_list: new_union |= m\n\n    # Show the forbidden mask overlay (optional)\n    forb_rgb = img.copy()\n    forb_rgb[forbid_mask > 0] = (0.6*forb_rgb[forbid_mask > 0] + 0.4*np.array([64, 128, 255])).astype(np.uint8)\n\n    print(f\"[{examples_done+1}/{N_EXAMPLES}] {case_id} | clones={len(placed_list)} | new_union px={int(new_union.sum())}\")\n\n    # ---- visuals ----\n    # original forged outline (green)\n    orig_outline = img.copy()\n    cnts, _ = cv2.findContours(union_orig.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    cv2.drawContours(orig_outline, cnts, -1, (0, 255, 0), 2, lineType=cv2.LINE_AA)\n\n    # new placements union\n    new_union = np.zeros((H, W), np.uint8)\n    for m in placed_list: new_union |= m\n\n    overlay = img.copy()\n    overlay[new_union > 0] = (0.65 * overlay[new_union > 0] + 0.35 * np.array([255, 0, 0])).astype(np.uint8)\n\n    result_outline = overlay.copy()\n    if new_union.sum() > 0:\n        cnts2, _ = cv2.findContours(new_union.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        cv2.drawContours(result_outline, cnts2, -1, (0, 128, 255), 2, lineType=cv2.LINE_AA)\n\n    print(f\"[{examples_done+1}/{N_EXAMPLES}] {case_id} | src inst #{src_idx+1} \"\n          f\"| patch {src_patch_rgb.shape[1]}x{src_patch_rgb.shape[0]} \"\n          f\"| clones={len(placed_list)} | new_union px={int(new_union.sum())}\")\n\n    fig = plt.figure(figsize=(14, 10))\n    ax1 = plt.subplot(2,2,1); ax1.imshow(img);            ax1.set_title(\"Original\"); ax1.axis(\"off\")\n    ax2 = plt.subplot(2,2,2); ax2.imshow(orig_outline);    ax2.set_title(\"Original forged outline (green)\"); ax2.axis(\"off\")\n    ax3 = plt.subplot(2,2,3); ax3.imshow(overlay);         ax3.set_title(\"Overlay of new placements (red)\"); ax3.axis(\"off\")\n    ax4 = plt.subplot(2,2,4); ax4.imshow(result_outline);  ax4.set_title(\"New placements outline (blue)\"); ax4.axis(\"off\")\n    plt.tight_layout(); plt.show()\n\n    # ---- Clean final result (no labels/outlines) ----\n    clean_result = work_img.copy()\n    clean_mask   = (union_orig | new_union).astype(np.uint8)\n\n    fig = plt.figure(figsize=(12, 6))\n    ax1 = plt.subplot(1,2,1); ax1.imshow(clean_result); ax1.set_title(\"Final forged image (no labels)\"); ax1.axis(\"off\")\n    ax2 = plt.subplot(1,2,2); ax2.imshow(clean_mask, cmap=\"gray\"); ax2.set_title(\"Final combined mask\"); ax2.axis(\"off\")\n    plt.tight_layout(); plt.show()\n\n    # ---- Optional: save ----\n    if SAVE_PREVIEW:\n        base = f\"{case_id}_ex{examples_done+1}\"\n        cv2.imwrite(str(Path(PREVIEW_DIR) / f\"{base}_result.png\"),\n                    cv2.cvtColor(clean_result, cv2.COLOR_RGB2BGR))\n        # Save instance list as object array: original insts + new placed masks\n        np.save(Path(PREVIEW_DIR) / f\"{base}_mask.npy\",\n                np.array(insts + [m for m in placed_list], dtype=object),\n                allow_pickle=True)\n        print(f\"💾 Saved: {Path(PREVIEW_DIR) / (base + '_result.png')}\")\n        print(f\"💾 Saved: {Path(PREVIEW_DIR) / (base + '_mask.npy')}\")\n\n    examples_done += 1\n\nif examples_done < N_EXAMPLES:\n    print(f\"⚠️ Only produced {examples_done}/{N_EXAMPLES} examples; not enough valid regions to place.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:14:23.344720Z","iopub.execute_input":"2025-10-29T19:14:23.344980Z","iopub.status.idle":"2025-10-29T19:18:05.590153Z","shell.execute_reply.started":"2025-10-29T19:14:23.344952Z","shell.execute_reply":"2025-10-29T19:18:05.589488Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==== Bulk synthetic generator → authentic / forged / masks ====\n# - For each image:\n#     * authentic: write N variants with light aug (no copy-move, no mask)\n#     * forged:    create 1–5 copy-moves, safe to real cells (Cellpose), feather edges,\n#                  then apply the same geo aug to image+mask(s) and save paired <id>.png/.npy\n# - Limits: MAX_AUTH_IMAGES, MAX_FORGED_IMAGES\n# - Filenames: <id>.png in OUT_AUTH/OUT_FORGED, same <id>.npy in OUT_MASK (for forged only)\n\nimport os, glob, random, uuid\nfrom pathlib import Path\nimport numpy as np\nimport cv2\nfrom tqdm import tqdm\n\n# -------------------\n# OUTPUT DIRS\n# -------------------\nOUT_ROOT      = Path(\"/kaggle/working/synth_dataset\")\nOUT_AUTH_DIR  = OUT_ROOT / \"authentic\"\nOUT_FORGED_DIR= OUT_ROOT / \"forged\"\nOUT_MASK_DIR  = OUT_ROOT / \"masks_npy\"\nOUT_AUTH_DIR.mkdir(parents=True, exist_ok=True)\nOUT_FORGED_DIR.mkdir(parents=True, exist_ok=True)\nOUT_MASK_DIR.mkdir(parents=True, exist_ok=True)\n\n# -------------------\n# LIMITS\n# -------------------\nMAX_AUTH_IMAGES   = 20   # process at most this many authentic source images\nMAX_FORGED_IMAGES = 20   # process at most this many forged source images\n\n# -------------------\n# PER-IMAGE REPEATS & COPY-MOVES\n# -------------------\nN_REPEATS_PER_IMAGE  = 5        # variants per source image\nCOPIES_PER_VARIANT_R = (1, 5)   # min/max #copies per variant (inclusive)\n\n# -------------------\n# PLACEMENT / CELLPOSE SAFETY\n# -------------------\nALLOWED_OVERLAP_PX      = 0         # with original forged union\nPLACE_TRIES             = 80\nFORBID_DILATE_PX        = 10        # dilate cellpose mask (margin)\nFORBID_MAX_OVERLAP_PX   = 0         # overlap with forbidden (cellpose) mask\n\n# -------------------\n# SOURCE INSTANCE FILTER\n# -------------------\nMIN_SRC_AREA = 64\n\n# -------------------\n# AUGMENTATION (geo + photometric)\n# -------------------\nAUG_PROB_FLIP_H  = 0.5\nAUG_PROB_FLIP_V  = 0.15\nAUG_PROB_ROT90   = 0.25\n\nAUG_BRIGHT_ALPHA_R = (0.9, 1.1)   # contrast gain\nAUG_BRIGHT_BETA_R  = (-12, 12)    # brightness shift\nAUG_ENABLE_HUE     = True\nAUG_HUE_SHIFT_R    = (-10, 10)    # degrees (OpenCV hue is 0..180)\n\ndef apply_brightness_contrast(img_rgb, alpha, beta):\n    out = img_rgb.astype(np.float32)\n    out = out * float(alpha) + float(beta)\n    return np.clip(out, 0, 255).astype(np.uint8)\n\ndef apply_hue(img_rgb, delta_deg):\n    if delta_deg == 0:\n        return img_rgb\n    hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV).astype(np.float32)\n    hsv[..., 0] = (hsv[..., 0] + (delta_deg / 2)) % 180.0\n    hsv = np.clip(hsv, 0, 255).astype(np.uint8)\n    return cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)\n\ndef aug_geo_image_and_masks(img_rgb, masks_list):\n    \"\"\"Apply same random geo aug to image and list of binary masks.\"\"\"\n    out_img = img_rgb.copy()\n    out_masks = [m.copy() for m in masks_list]\n    # H flip\n    if random.random() < AUG_PROB_FLIP_H:\n        out_img   = cv2.flip(out_img, 1)\n        out_masks = [cv2.flip(m, 1) for m in out_masks]\n    # V flip\n    if random.random() < AUG_PROB_FLIP_V:\n        out_img   = cv2.flip(out_img, 0)\n        out_masks = [cv2.flip(m, 0) for m in out_masks]\n    # rot 90*k\n    if random.random() < AUG_PROB_ROT90:\n        k = random.choice([1,2,3])\n        out_img   = np.rot90(out_img, k).copy()\n        out_masks = [np.rot90(m, k).copy() for m in out_masks]\n    return out_img, out_masks\n\ndef aug_photo_image(img_rgb):\n    \"\"\"Photometric aug on image only.\"\"\"\n    alpha = random.uniform(*AUG_BRIGHT_ALPHA_R)\n    beta  = random.uniform(*AUG_BRIGHT_BETA_R)\n    out   = apply_brightness_contrast(img_rgb, alpha, beta)\n    if AUG_ENABLE_HUE and random.random() < 0.5:\n        delta = random.uniform(*AUG_HUE_SHIFT_R)\n        out   = apply_hue(out, delta)\n    return out\n\n# -------------------\n# COLLECT INPUTS\n# -------------------\nauth_paths = sorted(glob.glob(str(Path(TRAIN_DIR) / \"authentic\" / \"*\")))\nforg_paths = sorted(glob.glob(str(Path(TRAIN_DIR) / \"forged\"    / \"*\")))\n\nif MAX_AUTH_IMAGES > 0 and len(auth_paths) > MAX_AUTH_IMAGES:\n    random.shuffle(auth_paths)\n    auth_paths = auth_paths[:MAX_AUTH_IMAGES]\n\nif MAX_FORGED_IMAGES > 0 and len(forg_paths) > MAX_FORGED_IMAGES:\n    random.shuffle(forg_paths)\n    forg_paths = forg_paths[:MAX_FORGED_IMAGES]\n\nprint(f\"Source authentic: {len(auth_paths)} | forged: {len(forg_paths)}\")\nassert 'cellpose_model' in globals(), \"cellpose_model not found. Run the Cellpose setup cell.\"\n\n# -------------------\n# HELPERS (instance construction when no GT)\n# -------------------\ndef instances_from_cellpose(img_rgb, min_area=MIN_SRC_AREA):\n    lab, uni = cellpose_segment(img_rgb, model=cellpose_model)\n    insts = []\n    if uni.sum() == 0: \n        return insts\n    K = int(lab.max())\n    for k in range(1, K+1):\n        mk = (lab == k).astype(np.uint8)\n        if mk.sum() >= min_area:\n            insts.append(mk)\n    return insts\n\n# -------------------\n# AUTHENTIC: write N variants (no masks)\n# -------------------\nauth_saved = 0\nfor p in tqdm(auth_paths, desc=\"Authentic variants\", ncols=100):\n    p = Path(p)\n    img = pil_to_np_rgb(load_rgb(str(p)))\n\n    for rep in range(1, N_REPEATS_PER_IMAGE+1):\n        out_img, _ = aug_geo_image_and_masks(img, [])  # geo (no masks to sync)\n        out_img    = aug_photo_image(out_img)          # photo\n\n        # ID → easy mapping scheme (class_origName_repNN_uuid8.png)\n        uid = uuid.uuid4().hex[:8]\n        out_id = f\"auth_{p.stem}_rep{rep:02d}_{uid}\"\n        cv2.imwrite(str(OUT_AUTH_DIR / f\"{out_id}.png\"),\n                    cv2.cvtColor(out_img, cv2.COLOR_RGB2BGR))\n        auth_saved += 1\n\n# -------------------\n# FORGED: create copy-moves then save paired <id>.png + <id>.npy\n# -------------------\nforg_saved = 0\nfor p in tqdm(forg_paths, desc=\"Forged variants\", ncols=100):\n    p = Path(p)\n    case_id = p.stem\n\n    img = pil_to_np_rgb(load_rgb(str(p)))\n    H, W = img.shape[:2]\n\n    # Load GT instances if present; otherwise fall back to Cellpose\n    mask_path = Path(MASK_DIR) / f\"{case_id}.npy\"\n    insts = []\n    if mask_path.exists():\n        try:\n            raw = load_mask_instances(str(mask_path))\n            for m in raw:\n                if m.shape != (H, W):\n                    m = cv2.resize(m, (W, H), interpolation=cv2.INTER_NEAREST)\n                m = (m > 0).astype(np.uint8)\n                if m.sum() >= MIN_SRC_AREA:\n                    insts.append(m)\n        except Exception:\n            insts = []\n\n    if len(insts) == 0:\n        insts = instances_from_cellpose(img, MIN_SRC_AREA)\n        if len(insts) == 0:\n            continue  # nothing to copy\n\n    union_orig  = union_from_insts(insts, (H, W))\n    forbid_mask = build_forbidden_mask(img, model=cellpose_model, dilate_px=FORBID_DILATE_PX)\n\n    # Prefer larger instances as source\n    areas   = [int(m.sum()) for m in insts]\n    k       = min(5, len(insts))\n    top_ixs = np.argsort(areas)[::-1][:k]\n    src_idx = int(random.choice(top_ixs))\n    src_m   = insts[src_idx]\n\n    x0, y0, x1, y1 = bbox_from_mask(src_m)\n    if (x1-x0) < 2 or (y1-y0) < 2:\n        continue\n    src_patch_mask = src_m[y0:y1, x0:x1]\n    src_patch_rgb  = img[y0:y1, x0:x1, :]\n\n    for rep in range(1, N_REPEATS_PER_IMAGE+1):\n        copies_needed = random.randint(*COPIES_PER_VARIANT_R)\n\n        work_img   = img.copy()\n        work_union = union_orig.copy()\n        placed     = []\n\n        made = 0\n        tries_total = 0\n        max_total_tries = copies_needed * (PLACE_TRIES * 3)\n\n        while made < copies_needed and tries_total < max_total_tries:\n            tries_total += 1\n            angle = random.uniform(-20.0, 20.0)  # reuse your ROT_MIN/ROT_MAX if you prefer\n            ok, new_img, placed_mask = try_place_once(\n                work_img, work_union,\n                src_patch_rgb, src_patch_mask,\n                angle,\n                ALLOWED_OVERLAP_PX,\n                place_tries=PLACE_TRIES,\n                forbid_mask=forbid_mask,\n                forbid_max_overlap_px=FORBID_MAX_OVERLAP_PX\n            )\n            if not ok:\n                continue\n            work_img   = new_img\n            work_union |= placed_mask\n            placed.append(placed_mask)\n            made += 1\n\n            # prevent stacking new copies onto each other\n            forbid_mask |= placed_mask\n\n        if len(placed) == 0:\n            continue\n\n        # Final instances: originals + new placed ones\n        final_insts = insts + placed\n\n        # Geo aug (sync image & masks), then photometric (image only)\n        aug_img, aug_masks = aug_geo_image_and_masks(work_img, final_insts)\n        aug_img = aug_photo_image(aug_img)\n        aug_masks = [(m > 0).astype(np.uint8) for m in aug_masks]\n\n        # ID → paired filenames\n        uid = uuid.uuid4().hex[:8]\n        out_id = f\"forg_{case_id}_rep{rep:02d}_{uid}\"\n        out_img_path = OUT_FORGED_DIR / f\"{out_id}.png\"\n        out_npy_path = OUT_MASK_DIR   / f\"{out_id}.npy\"\n\n        cv2.imwrite(str(out_img_path), cv2.cvtColor(aug_img, cv2.COLOR_RGB2BGR))\n        np.save(out_npy_path, np.array(aug_masks, dtype=object), allow_pickle=True)\n        forg_saved += 1\n\nprint(f\"✅ Done.\\n  Authentic written: {auth_saved} → {OUT_AUTH_DIR}\\n  Forged written:    {forg_saved} → {OUT_FORGED_DIR}\\n  Mask NPYs:         {forg_saved} → {OUT_MASK_DIR}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T19:18:05.592155Z","iopub.execute_input":"2025-10-29T19:18:05.592623Z"}},"outputs":[],"execution_count":null}]}