{"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":"gpu","dataSources":[{"sourceId":8089,"databundleVersionId":44321,"sourceType":"competition"},{"sourceId":29047,"sourceType":"datasetVersion","datasetId":22655},{"sourceId":1132317,"sourceType":"datasetVersion","datasetId":635428},{"sourceId":1267593,"sourceType":"datasetVersion","datasetId":723383},{"sourceId":5375321,"sourceType":"datasetVersion","datasetId":3118528},{"sourceId":643855,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":485534,"modelId":500985}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ---------- RUN THIS CELL FIRST (after imports & config) ----------\nimport zipfile, imageio\n\n# 1) Unzip Data Science Bowl stage1_train.zip into /kaggle/working/dsb_train (if not already)\ndsb_zip_path = \"/kaggle/input/data-science-bowl-2018/stage1_train.zip\"\ndsb_working = \"/kaggle/working/dsb_train\"\n\nif os.path.exists(dsb_zip_path) and not os.path.exists(dsb_working):\n    print(\"Unzipping DSB stage1_train.zip ->\", dsb_working)\n    with zipfile.ZipFile(dsb_zip_path, 'r') as z:\n        z.extractall(dsb_working)\nelse:\n    if os.path.exists(dsb_working):\n        print(\"DSB already unzipped at\", dsb_working)\n    else:\n        print(\"Warning: DSB zip not found at\", dsb_zip_path, \" — check /kaggle/input contents\")\n\n# 2) Set dsb_folder and camvid_folder explicitly (override previous autodetection)\n#    DSB: point to unzipped stage1_train folder (if available), else fall back to input\nif os.path.exists(dsb_working):\n    # note: some zips extract a top-level 'stage1_train' folder, others extract into dsb_working/<id> directly\n    if os.path.exists(os.path.join(dsb_working, \"stage1_train\")):\n        dsb_folder = os.path.join(dsb_working, \"stage1_train\")\n    else:\n        # many versions extract directly into dsb_working/<id>/<images,masks>\n        # if we see folders with images subfolder, use dsb_working\n        dsb_folder = dsb_working\nelse:\n    # fallback: try input folder (may still work if Kaggle auto-unzipped)\n    dsb_folder = \"/kaggle/input/data-science-bowl-2018\"\nprint(\"Using DSB folder:\", dsb_folder)\n\n# 3) CamVid: point to the nested CamVid folder\npossible_camvid = \"/kaggle/input/camvid/CamVid\"\nif os.path.exists(possible_camvid):\n    camvid_folder = possible_camvid\nelse:\n    camvid_folder = \"/kaggle/input/camvid\"\nprint(\"Using CamVid folder:\", camvid_folder)\n\n# 4) Rebuild DSB lists (calls your existing build_dsb_lists function)\ndsb_imgs, dsb_samples = ([], [])\nif os.path.exists(dsb_folder):\n    dsb_imgs, dsb_samples = build_dsb_lists(dsb_folder)\nprint(\"Found DSB samples:\", len(dsb_imgs))\n\n# 5) Recompute CamVid image/mask lists using your find_camvid_image_mask_pairs\n(cam_train_imgs, cam_train_masks), (cam_val_imgs, cam_val_masks), (cam_test_imgs, cam_test_masks) = ([],[]), ([],[]), ([],[])\nif os.path.exists(camvid_folder):\n    (cam_train_imgs, cam_train_masks), (cam_val_imgs, cam_val_masks), (cam_test_imgs, cam_test_masks) = find_camvid_image_mask_pairs(camvid_folder)\nprint(\"CamVid train images:\", len(cam_train_imgs), \"train masks:\", len(cam_train_masks))\nprint(\"CamVid val images:\", len(cam_val_imgs), \"val masks:\", len(cam_val_masks))\nprint(\"CamVid test images:\", len(cam_test_imgs), \"test masks:\", len(cam_test_masks))\n\n# 6) If CamVid color map is empty, try reading class_dict from the explicit folder\nif (not camvid_class_map or len(camvid_class_map)==0) and os.path.exists(camvid_folder):\n    camvid_class_map = read_camvid_class_dict(camvid_folder)\n    print(\"CamVid class map entries (after re-read):\", len(camvid_class_map) if camvid_class_map else 0)\n\n# quick sanity checks\nif len(dsb_imgs) == 0:\n    print(\">>> No DSB samples found. Check that the unzip produced folders like '<id>/images/*.png' inside\", dsb_folder)\nif len(cam_train_imgs) == 0 or len(cam_train_masks) == 0:\n    print(\">>> CamVid train/mask lists empty. Check train / train_labels names inside\", camvid_folder)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === Kaggle-ready notebook: dataset prep + training helpers adapted to your file layout ===\nimport os, zipfile, shutil, sys, random, csv\nfrom glob import glob\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, backend as K\nfrom tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping\nimport matplotlib.pyplot as plt\n\n# -----------------------------\n# Config\n# -----------------------------\nSEED = 42\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n\nINPUT_BASE = \"/kaggle/input\"   # Kaggle input root\nDSB_NAME = \"data-science-bowl-2018\"   # if your dataset folder name differs, adapt below\nCAMVID_NAME = \"camvid/CamVid\"                # if different, adapt\n\nIMG_SIZE = 256\nBATCH_SIZE = 8\nEPOCHS = 40\nAUTOTUNE = tf.data.AUTOTUNE\n\n\n\n\n# -----------------------------\n# Helper: locate dataset folders (auto-unzip if necessary)\n# -----------------------------\ndef ensure_unzipped(prefix, candidate_zip_names, expected_folder_name=None):\n    \"\"\"\n    prefix: base input folder (e.g. /kaggle/input)\n    candidate_zip_names: list of zip filenames (or parts) that may exist under prefix\n    expected_folder_name: optional subfolder expected after unzip\n    returns full path to folder if found/unzipped, else None\n    \"\"\"\n    # look for existing folder first\n    if expected_folder_name:\n        cand_folder = os.path.join(prefix, expected_folder_name)\n        if os.path.exists(cand_folder):\n            return cand_folder\n    # search for any folder under /kaggle/input that matches known names\n    for d in os.listdir(prefix):\n        lower = d.lower()\n        if any(k in lower for k in candidate_zip_names):\n            path = os.path.join(prefix, d)\n            # if it's a zip file path (rare), unzip it\n            if path.endswith(\".zip\"):\n                # unzip to folder with same name (without .zip)\n                out_folder = path[:-4]\n                if not os.path.exists(out_folder):\n                    with zipfile.ZipFile(path, 'r') as z:\n                        z.extractall(out_folder)\n                return out_folder\n            # if it's a folder, return it\n            if os.path.isdir(path):\n                # look for expected subfolder inside, else return path\n                if expected_folder_name and os.path.exists(os.path.join(path, expected_folder_name)):\n                    return os.path.join(path, expected_folder_name)\n                return path\n    # fallback: try to find any folder that contains stage1_train or CamVid structure\n    for d in os.listdir(prefix):\n        path = os.path.join(prefix, d)\n        if os.path.isdir(path):\n            if \"stage1_train\" in os.listdir(path) or \"stage1_train.zip\" in os.listdir(path):\n                return path\n            if \"CamVid\" in os.listdir(path) or \"train\" in os.listdir(path) and \"train_labels\" in os.listdir(path):\n                return path\n    return None\n\n# locate Data Science Bowl folder (we expect folder containing stage1_train)\ndsb_folder = ensure_unzipped(INPUT_BASE, [\"data-science-bowl\", \"stage1_train\", \"stage1_train.zip\"], expected_folder_name=\"stage1_train\")\nif dsb_folder is None:\n    # sometimes dataset folder is named exactly 'data-science-bowl-2018'\n    dsb_folder = os.path.join(INPUT_BASE, \"data-science-bowl-2018\")\nprint(\"DSB folder:\", dsb_folder)\n# locate CamVid folder\ncamvid_folder = ensure_unzipped(INPUT_BASE, [\"camvid\", \"CamVid\", \"camvid.zip\"], expected_folder_name=None)\nprint(\"CamVid folder:\", camvid_folder)\n\n# -----------------------------\n# Utility functions for images & masks\n# -----------------------------\ndef read_image(path, size=IMG_SIZE):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_image(img, channels=3, expand_animations=False)\n    img = tf.image.resize(img, [size, size])\n    img = tf.cast(img, tf.float32) / 255.0\n    return img\n\ndef read_mask_binary(path, size=IMG_SIZE):\n    m = tf.io.read_file(path)\n    m = tf.image.decode_image(m, channels=1, expand_animations=False)\n    m = tf.image.resize(m, [size, size], method=\"nearest\")\n    m = tf.cast(m > 127, tf.float32)\n    return m\n\n# combine instance masks for DSB sample folder\ndef combine_instance_masks_for_sample(sample_dir, size=IMG_SIZE):\n    mask_files = glob(os.path.join(sample_dir, \"masks\", \"*.png\"))\n    if len(mask_files) == 0:\n        return tf.zeros([size, size, 1], tf.float32)\n    comb = None\n    for mpath in mask_files:\n        m = tf.io.read_file(mpath)\n        m = tf.image.decode_image(m, channels=1, expand_animations=False)\n        m = tf.image.resize(m, [size, size], method=\"nearest\")\n        m = tf.cast(m > 127, tf.float32)\n        if comb is None:\n            comb = m\n        else:\n            comb = tf.maximum(comb, m)\n    return comb\n\n# -----------------------------\n# Build list of DSB samples (stage1_train)\n# -----------------------------\ndef build_dsb_lists(dsb_root):\n    # dsb_root expected to contain stage1_train folder OR be stage1_train folder itself\n    if os.path.basename(dsb_root).lower().startswith(\"stage1_train\"):\n        stage1 = dsb_root\n    else:\n        maybe = os.path.join(dsb_root, \"stage1_train\")\n        if os.path.exists(maybe):\n            stage1 = maybe\n        else:\n            stage1 = dsb_root\n    sample_dirs = [d for d in glob(os.path.join(stage1, \"*\")) if os.path.isdir(d)]\n    image_paths = []\n    sample_dirs_filtered = []\n    for s in sample_dirs:\n        imgs = glob(os.path.join(s, \"images\", \"*.png\"))\n        if len(imgs) == 1:\n            image_paths.append(imgs[0])\n            sample_dirs_filtered.append(s)\n    return image_paths, sample_dirs_filtered\n\n# build DSB lists\ndsb_imgs, dsb_samples = ([], [])\nif dsb_folder:\n    dsb_imgs, dsb_samples = build_dsb_lists(dsb_folder)\nprint(\"Found DSB samples:\", len(dsb_imgs))\n\n\n\n\n# -----------------------------\n# CamVid: read file lists & class dict\n# -----------------------------\ndef read_camvid_class_dict(camvid_root):\n    # look for class_dict.csv in root or any child folder\n    candidates = glob(os.path.join(camvid_root, \"**\", \"class_dict.csv\"), recursive=True)\n    if not candidates:\n        return None\n    csvp = candidates[0]\n    mapping = {}\n    # try to detect format: possible formats:\n    # name,r,g,b  OR  name,index OR index,name OR name,class_index,r,g,b\n    with open(csvp, newline='') as f:\n        reader = csv.reader(f)\n        rows = list(reader)\n    # parse heuristically\n    for r in rows:\n        if len(r) >= 4:\n            # assume format name,r,g,b or name,index,r,g,b ; try to parse last 3 as ints\n            try:\n                # last 3 entries are rgb\n                r_, g_, b_ = int(r[-3]), int(r[-2]), int(r[-1])\n                name = r[0]\n                # assign next available id if none\n                mapping[(r_, g_, b_)] = mapping.get((r_, g_, b_), len(mapping))\n            except:\n                continue\n        elif len(r) == 2:\n            # maybe name,index or index,name\n            try:\n                a0 = int(r[0]); a1 = int(r[1])\n                # both numbers? skip\n                continue\n            except:\n                # store name->index not helpful here\n                continue\n    return mapping\n\ncamvid_class_map = None\nif camvid_folder:\n    camvid_class_map = read_camvid_class_dict(camvid_folder)\n    print(\"CamVid color map found entries:\", len(camvid_class_map) if camvid_class_map else 0)\n\n# find images/masks folders for camvid\ndef find_camvid_image_mask_pairs(camvid_root):\n    # common layout: train, train_labels, val, val_labels, test, test_labels\n    imgs = []\n    masks = []\n    train_imgs = sorted(glob(os.path.join(camvid_root, \"train\", \"*.png\")))\n    train_masks = sorted(glob(os.path.join(camvid_root, \"train_labels\", \"*.png\")))\n    val_imgs = sorted(glob(os.path.join(camvid_root, \"val\", \"*.png\")))\n    val_masks = sorted(glob(os.path.join(camvid_root, \"val_labels\", \"*.png\")))\n    test_imgs = sorted(glob(os.path.join(camvid_root, \"test\", \"*.png\")))\n    test_masks = sorted(glob(os.path.join(camvid_root, \"test_labels\", \"*.png\")))\n    # if folder names differ in capitalization\n    if len(train_imgs) == 0:\n        train_imgs = sorted(glob(os.path.join(camvid_root, \"Train\", \"*.png\")))\n    if len(train_masks) == 0:\n        train_masks = sorted(glob(os.path.join(camvid_root, \"Train_labels\", \"*.png\")))    \n    return (train_imgs, train_masks), (val_imgs, val_masks), (test_imgs, test_masks)\n\n(cam_train_imgs, cam_train_masks), (cam_val_imgs, cam_val_masks), (cam_test_imgs, cam_test_masks) = ([],[]), ([],[]), ([],[])\nif camvid_folder:\n    (cam_train_imgs, cam_train_masks), (cam_val_imgs, cam_val_masks), (cam_test_imgs, cam_test_masks) = find_camvid_image_mask_pairs(camvid_folder)\n    print(\"CamVid train images:\", len(cam_train_imgs), \"train masks:\", len(cam_train_masks))\n    print(\"CamVid val images:\", len(cam_val_imgs), \"val masks:\", len(cam_val_masks))\n\n# -----------------------------\n# Mask decoding helpers for CamVid: color -> class_id or integer masks\n# -----------------------------\ndef decode_color_mask_to_id(mask_rgb_uint8, color_map):\n    # mask_rgb_uint8: HxWx3 uint8 tensor\n    # color_map: dict {(r,g,b): id}\n    mask = tf.cast(mask_rgb_uint8, tf.int32)\n    h = tf.shape(mask)[0]; w = tf.shape(mask)[1]\n    out = tf.zeros((h,w), dtype=tf.int32)\n    for (r,g,b), cid in color_map.items():\n        match = tf.reduce_all(tf.equal(mask, tf.constant([r,g,b], dtype=tf.int32)), axis=-1)\n        out = tf.where(match, cid, out)\n    return out\n\ndef camvid_map_fn(img_path, mask_path, class_map=None, size=IMG_SIZE):\n    img = read_image(img_path, size)\n    mraw = tf.io.read_file(mask_path)\n    mask = tf.image.decode_image(mraw, channels=3, expand_animations=False)\n    # If mask is single-channel stored as png (some variants), try channels=1 fallback\n    if tf.shape(mask)[-1] == 3 and class_map:\n        # color-coded mask -> convert to ids\n        mask = tf.image.resize(mask, [size, size], method=\"nearest\")\n        mask_ids = decode_color_mask_to_id(tf.cast(mask, tf.uint8), class_map)\n        mask_onehot = tf.one_hot(tf.cast(mask_ids, tf.int32), depth=(max(class_map.values())+1))\n        return img, tf.cast(mask_onehot, tf.float32)\n    else:\n        # try decode single-channel / integer labels\n        mask = tf.image.decode_image(mraw, channels=1, expand_animations=False)\n        mask = tf.image.resize(mask, [size, size], method=\"nearest\")\n        # if mask values are already class indices (0..C-1)\n        mask_int = tf.cast(tf.squeeze(mask, axis=-1), tf.int32)\n        # infer number of classes from max in mask + 1 (note: dynamic)\n        maxv = tf.reduce_max(mask_int)\n        depth = tf.cast(maxv + 1, tf.int32)\n        mask_onehot = tf.one_hot(mask_int, depth=depth)\n        # If depth is 1 (weird), fallback to single channel float\n        if depth.numpy() <= 1:\n            return img, tf.cast(tf.expand_dims(tf.cast(mask_int > 0, tf.float32), -1), tf.float32)\n        return img, tf.cast(mask_onehot, tf.float32)\n\n# -----------------------------\n# Build tf.data datasets\n# -----------------------------\ndef build_dsb_tfds_from_lists(img_list, sample_list, batch_size=BATCH_SIZE, augment=False):\n    # uses py_function to combine instance masks per sample dir\n    ds = tf.data.Dataset.from_tensor_slices((img_list, sample_list))\n    def _wrap(img_p, samp_p):\n        img = read_image(img_p.numpy().decode('utf-8'), IMG_SIZE)\n        mask = combine_instance_masks_for_sample(samp_p.numpy().decode('utf-8'), IMG_SIZE)\n        img = tf.ensure_shape(img, (IMG_SIZE, IMG_SIZE, 3))\n        mask = tf.ensure_shape(mask, (IMG_SIZE, IMG_SIZE, 1))\n        return img, mask\n    def map_fn(a,b):\n        img, mask = tf.py_function(_wrap, [a,b], [tf.float32, tf.float32])\n        img.set_shape((IMG_SIZE, IMG_SIZE, 3))\n        mask.set_shape((IMG_SIZE, IMG_SIZE, 1))\n        return img, mask\n    ds = ds.map(map_fn, num_parallel_calls=AUTOTUNE)\n    if augment:\n        ds = ds.map(augment_batch, num_parallel_calls=AUTOTUNE)\n    ds = ds.shuffle(512).batch(batch_size).prefetch(AUTOTUNE)\n    return ds\n\ndef build_camvid_tfds(img_paths, mask_paths, class_map, batch_size=BATCH_SIZE, augment=False):\n    ds = tf.data.Dataset.from_tensor_slices((img_paths, mask_paths))\n    def _map(a,b):\n        img, mask = camvid_map_fn(a, b, class_map, IMG_SIZE)\n        return img, mask\n    ds = ds.map(lambda a,b: tf.py_function(lambda x,y: _map(x,y), [a,b], [tf.float32, tf.float32]),\n                num_parallel_calls=AUTOTUNE)\n    # set static shapes (mask channels dynamic depending on class_map)\n    def _set_shapes(img, mask):\n        img.set_shape((IMG_SIZE, IMG_SIZE, 3))\n        # mask shape: (IMG_SIZE,IMG_SIZE,C) OR (IMG_SIZE,IMG_SIZE,1) if fallback binary\n        return img, mask\n    ds = ds.map(_set_shapes, num_parallel_calls=AUTOTUNE)\n    if augment:\n        ds = ds.map(augment_batch, num_parallel_calls=AUTOTUNE)\n    ds = ds.shuffle(1024).batch(batch_size).prefetch(AUTOTUNE)\n    return ds\n\n# small augment\ndef augment_batch(img, mask):\n    if tf.random.uniform([]) > 0.5:\n        img = tf.image.flip_left_right(img)\n        mask = tf.image.flip_left_right(mask)\n    img = tf.image.random_brightness(img, 0.08)\n    return img, mask\n\n\n\n\n\n# ---------- RUN THIS CELL FIRST (after imports & config) ----------\nimport zipfile, imageio\n\n# 1) Unzip Data Science Bowl stage1_train.zip into /kaggle/working/dsb_train (if not already)\ndsb_zip_path = \"/kaggle/input/data-science-bowl-2018/stage1_train.zip\"\ndsb_working = \"/kaggle/working/dsb_train\"\n\nif os.path.exists(dsb_zip_path) and not os.path.exists(dsb_working):\n    print(\"Unzipping DSB stage1_train.zip ->\", dsb_working)\n    with zipfile.ZipFile(dsb_zip_path, 'r') as z:\n        z.extractall(dsb_working)\nelse:\n    if os.path.exists(dsb_working):\n        print(\"DSB already unzipped at\", dsb_working)\n    else:\n        print(\"Warning: DSB zip not found at\", dsb_zip_path, \" — check /kaggle/input contents\")\n\n# 2) Set dsb_folder and camvid_folder explicitly (override previous autodetection)\n#    DSB: point to unzipped stage1_train folder (if available), else fall back to input\nif os.path.exists(dsb_working):\n    # note: some zips extract a top-level 'stage1_train' folder, others extract into dsb_working/<id> directly\n    if os.path.exists(os.path.join(dsb_working, \"stage1_train\")):\n        dsb_folder = os.path.join(dsb_working, \"stage1_train\")\n    else:\n        # many versions extract directly into dsb_working/<id>/<images,masks>\n        # if we see folders with images subfolder, use dsb_working\n        dsb_folder = dsb_working\nelse:\n    # fallback: try input folder (may still work if Kaggle auto-unzipped)\n    dsb_folder = \"/kaggle/input/data-science-bowl-2018\"\nprint(\"Using DSB folder:\", dsb_folder)\n\n# 3) CamVid: point to the nested CamVid folder\npossible_camvid = \"/kaggle/input/camvid/CamVid\"\nif os.path.exists(possible_camvid):\n    camvid_folder = possible_camvid\nelse:\n    camvid_folder = \"/kaggle/input/camvid\"\nprint(\"Using CamVid folder:\", camvid_folder)\n\n# 4) Rebuild DSB lists (calls your existing build_dsb_lists function)\ndsb_imgs, dsb_samples = ([], [])\nif os.path.exists(dsb_folder):\n    dsb_imgs, dsb_samples = build_dsb_lists(dsb_folder)\nprint(\"Found DSB samples:\", len(dsb_imgs))\n\n# 5) Recompute CamVid image/mask lists using your find_camvid_image_mask_pairs\n(cam_train_imgs, cam_train_masks), (cam_val_imgs, cam_val_masks), (cam_test_imgs, cam_test_masks) = ([],[]), ([],[]), ([],[])\nif os.path.exists(camvid_folder):\n    (cam_train_imgs, cam_train_masks), (cam_val_imgs, cam_val_masks), (cam_test_imgs, cam_test_masks) = find_camvid_image_mask_pairs(camvid_folder)\nprint(\"CamVid train images:\", len(cam_train_imgs), \"train masks:\", len(cam_train_masks))\nprint(\"CamVid val images:\", len(cam_val_imgs), \"val masks:\", len(cam_val_masks))\nprint(\"CamVid test images:\", len(cam_test_imgs), \"test masks:\", len(cam_test_masks))\n\n# 6) If CamVid color map is empty, try reading class_dict from the explicit folder\nif (not camvid_class_map or len(camvid_class_map)==0) and os.path.exists(camvid_folder):\n    camvid_class_map = read_camvid_class_dict(camvid_folder)\n    print(\"CamVid class map entries (after re-read):\", len(camvid_class_map) if camvid_class_map else 0)\n\n# quick sanity checks\nif len(dsb_imgs) == 0:\n    print(\">>> No DSB samples found. Check that the unzip produced folders like '<id>/images/*.png' inside\", dsb_folder)\nif len(cam_train_imgs) == 0 or len(cam_train_masks) == 0:\n    print(\">>> CamVid train/mask lists empty. Check train / train_labels names inside\", camvid_folder)\n\n# -----------------------------\n# Setup DSB train/val split\n# -----------------------------\nif len(dsb_imgs) > 0:\n    idxs = list(range(len(dsb_imgs)))\n    random.shuffle(idxs)\n    s = int(0.8 * len(idxs))\n    train_idx = idxs[:s]\n    val_idx = idxs[s:]\n    train_imgs = [dsb_imgs[i] for i in train_idx]\n    train_samples = [dsb_samples[i] for i in train_idx]\n    val_imgs = [dsb_imgs[i] for i in val_idx]\n    val_samples = [dsb_samples[i] for i in val_idx]\n    train_dsb_ds = build_dsb_tfds_from_lists(train_imgs, train_samples, batch_size=BATCH_SIZE, augment=True)\n    val_dsb_ds = build_dsb_tfds_from_lists(val_imgs, val_samples, batch_size=BATCH_SIZE, augment=False)\nelse:\n    train_dsb_ds = None\n    val_dsb_ds = None\n\n# -----------------------------\n# Setup CamVid tfds\n# -----------------------------\nif camvid_folder and len(cam_train_imgs) > 0:\n    # if we parsed a color map from class_dict.csv use that; else, try to build mapping from masks (rare)\n    class_map = camvid_class_map\n    # if no class_map parsed, try to build automatically by scanning mask unique colors on a small sample (costly)\n    if (not class_map) and len(cam_train_masks) > 0:\n        # attempt auto-detection on first 50 masks\n        sample_masks = cam_train_masks[:min(50, len(cam_train_masks))]\n        colset = {}\n        for p in sample_masks:\n            import imageio\n            im = imageio.imread(p)\n            if im.ndim == 3:\n                h,w,_ = im.shape\n                # sample some pixels\n                for rr in range(0, h, max(1, h//16)):\n                    for cc in range(0, w, max(1, w//16)):\n                        col = tuple(map(int, im[rr,cc,:3]))\n                        colset[col] = colset.get(col, 0) + 1\n        # create mapping\n        class_map = {c:i for i, c in enumerate(sorted(colset.keys()))}\n        print(\"Auto-detected CamVid colors:\", len(class_map))\n    # build datasets\n    cam_train_ds = build_camvid_tfds(cam_train_imgs, cam_train_masks, class_map, batch_size=BATCH_SIZE, augment=True)\n    cam_val_ds   = build_camvid_tfds(cam_val_imgs, cam_val_masks, class_map, batch_size=BATCH_SIZE, augment=False)\nelse:\n    cam_train_ds = None\n    cam_val_ds = None\n\n# -----------------------------\n# Model building: UNet & UNet++\n# -----------------------------\ndef conv_block(x, filters):\n    x = layers.Conv2D(filters, 3, padding='same', activation='relu')(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Conv2D(filters, 3, padding='same', activation='relu')(x)\n    x = layers.BatchNormalization()(x)\n    return x\n\ndef build_unet(input_shape=(IMG_SIZE,IMG_SIZE,3), num_classes=1):\n    inputs = layers.Input(shape=input_shape)\n    c1 = conv_block(inputs, 64); p1 = layers.MaxPooling2D()(c1)\n    c2 = conv_block(p1, 128); p2 = layers.MaxPooling2D()(c2)\n    c3 = conv_block(p2, 256); p3 = layers.MaxPooling2D()(c3)\n    c4 = conv_block(p3, 512); p4 = layers.MaxPooling2D()(c4)\n    c5 = conv_block(p4, 1024)\n    u6 = layers.Conv2DTranspose(512, 2, strides=2, padding='same')(c5); u6 = layers.concatenate([u6, c4]); c6 = conv_block(u6, 512)\n    u7 = layers.Conv2DTranspose(256, 2, strides=2, padding='same')(c6); u7 = layers.concatenate([u7, c3]); c7 = conv_block(u7, 256)\n    u8 = layers.Conv2DTranspose(128, 2, strides=2, padding='same')(c7); u8 = layers.concatenate([u8, c2]); c8 = conv_block(u8, 128)\n    u9 = layers.Conv2DTranspose(64, 2, strides=2, padding='same')(c8); u9 = layers.concatenate([u9, c1]); c9 = conv_block(u9, 64)\n    if num_classes == 1:\n        outputs = layers.Conv2D(1, 1, activation='sigmoid')(c9)\n    else:\n        outputs = layers.Conv2D(num_classes, 1, activation='softmax')(c9)\n    return models.Model(inputs, outputs, name=\"UNet\")\n\ndef build_unetpp(input_shape=(IMG_SIZE,IMG_SIZE,3), num_classes=1):\n    inputs = layers.Input(shape=input_shape)\n    x00 = conv_block(inputs, 64); p0 = layers.MaxPooling2D()(x00)\n    x10 = conv_block(p0, 128); p1 = layers.MaxPooling2D()(x10)\n    x20 = conv_block(p1, 256); p2 = layers.MaxPooling2D()(x20)\n    x30 = conv_block(p2, 512); p3 = layers.MaxPooling2D()(x30)\n    x40 = conv_block(p3, 1024)\n    x01 = layers.Conv2DTranspose(64,2,strides=2,padding='same')(x10); x01 = layers.concatenate([x01,x00]); x01 = conv_block(x01,64)\n    x11 = layers.Conv2DTranspose(128,2,strides=2,padding='same')(x20); x11 = layers.concatenate([x11,x10]); x11 = conv_block(x11,128)\n    x21 = layers.Conv2DTranspose(256,2,strides=2,padding='same')(x30); x21 = layers.concatenate([x21,x20]); x21 = conv_block(x21,256)\n    x02 = layers.Conv2DTranspose(64,2,strides=2,padding='same')(x11); x02 = layers.concatenate([x02,x00,x01]); x02 = conv_block(x02,64)\n    x03 = layers.Conv2DTranspose(64,2,strides=2,padding='same')(x21); x03 = layers.concatenate([x03,x00,x01,x02]); x03 = conv_block(x03,64)\n    if num_classes == 1:\n        outputs = layers.Conv2D(1,1,activation='sigmoid')(x03)\n    else:\n        outputs = layers.Conv2D(num_classes,1,activation='softmax')(x03)\n    return models.Model(inputs, outputs, name=\"UNet++\")\n\n# -----------------------------\n# Losses/metrics\n# -----------------------------\ndef dice_coef(y_true, y_pred, smooth=1e-6):\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.flatten(y_pred)\n    inter = K.sum(y_true_f * y_pred_f)\n    return (2. * inter + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)\n\ndef dice_loss(y_true, y_pred):\n    return 1 - dice_coef(y_true, y_pred)\n\ndef bce_dice_loss(y_true, y_pred):\n    return tf.keras.losses.BinaryCrossentropy()(y_true, y_pred) + dice_loss(y_true, y_pred)\n\ndef mean_iou_metric(num_classes):\n    def mean_iou(y_true, y_pred):\n        y_pred_labels = tf.argmax(y_pred, axis=-1)\n        y_true_labels = tf.argmax(y_true, axis=-1)\n        m = tf.metrics.MeanIoU(num_classes=num_classes)\n        m.update_state(y_true_labels, tf.cast(y_pred_labels, tf.int32))\n        return m.result()\n    return mean_iou\n\n# -----------------------------\n# Training helper\n# -----------------------------\ndef compile_and_train(model, train_ds, val_ds, loss, metrics, model_name, epochs=EPOCHS):\n    model.compile(optimizer=tf.keras.optimizers.Adam(1e-4), loss=loss, metrics=metrics)\n    ckpt = ModelCheckpoint(f\"/kaggle/working/{model_name}.h5\", save_best_only=True, monitor=\"val_loss\")\n    rl = ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=5)\n    es = EarlyStopping(monitor=\"val_loss\", patience=10, restore_best_weights=True)\n    history = model.fit(train_ds, validation_data=val_ds, epochs=epochs, callbacks=[ckpt, rl, es])\n    return history\n\n# -----------------------------\n# Train (conditionally, only if datasets exist)\n# -----------------------------\n# 1) DSB binary train\nif train_dsb_ds is not None and val_dsb_ds is not None:\n    print(\"Training UNet on DSB (binary)...\")\n    unet_dsb = build_unet(input_shape=(IMG_SIZE,IMG_SIZE,3), num_classes=1)\n    history_unet_dsb = compile_and_train(unet_dsb, train_dsb_ds, val_dsb_ds, loss=bce_dice_loss,\n                                         metrics=[dice_coef, tf.keras.metrics.BinaryAccuracy()],\n                                         model_name=\"unet_dsb_binary\", epochs=EPOCHS)\n    print(\"Training UNet++ on DSB (binary)...\")\n    unetpp_dsb = build_unetpp(input_shape=(IMG_SIZE,IMG_SIZE,3), num_classes=1)\n    history_unetpp_dsb = compile_and_train(unetpp_dsb, train_dsb_ds, val_dsb_ds, loss=bce_dice_loss,\n                                           metrics=[dice_coef, tf.keras.metrics.BinaryAccuracy()],\n                                           model_name=\"unetpp_dsb_binary\", epochs=EPOCHS)\nelse:\n    print(\"DSB datasets not found or empty; skipping DSB training.\")\n\n# 2) CamVid multi-class train\nif cam_train_ds is not None and cam_val_ds is not None:\n    # infer number of classes from one batch\n    sample_img, sample_mask = next(iter(cam_train_ds.take(1)))\n    _, h, w, c = sample_mask.shape\n    NUM_CLASSES = int(c)\n    print(\"Detected CamVid classes:\", NUM_CLASSES)\n    print(\"Training UNet on CamVid (multi-class)...\")\n    unet_cam = build_unet(input_shape=(IMG_SIZE,IMG_SIZE,3), num_classes=NUM_CLASSES)\n    metrics_cam = [tf.keras.metrics.CategoricalAccuracy(), mean_iou_metric(NUM_CLASSES)]\n    history_unet_cam = compile_and_train(unet_cam, cam_train_ds, cam_val_ds,\n                                         loss=tf.keras.losses.CategoricalCrossentropy(),\n                                         metrics=metrics_cam,\n                                         model_name=\"unet_camvid_multiclass\", epochs=EPOCHS)\n    print(\"Training UNet++ on CamVid (multi-class)...\")\n    unetpp_cam = build_unetpp(input_shape=(IMG_SIZE,IMG_SIZE,3), num_classes=NUM_CLASSES)\n    history_unetpp_cam = compile_and_train(unetpp_cam, cam_train_ds, cam_val_ds,\n                                           loss=tf.keras.losses.CategoricalCrossentropy(),\n                                           metrics=metrics_cam,\n                                           model_name=\"unetpp_camvid_multiclass\", epochs=EPOCHS)\nelse:\n    print(\"CamVid datasets not found or empty; skipping CamVid training.\")\n\n# -----------------------------\n# Visualization helper\n# -----------------------------\ndef visualize_prediction(model, ds, multiclass=False, n=3):\n    it = iter(ds.unbatch().batch(1))\n    for i in range(n):\n        try:\n            img, mask = next(it)\n        except StopIteration:\n            break\n        pred = model.predict(img)\n        plt.figure(figsize=(12,4))\n        plt.subplot(1,3,1); plt.imshow(img[0]); plt.title(\"Image\"); plt.axis(\"off\")\n        if multiclass:\n            gt = np.argmax(mask[0], axis=-1); pr = np.argmax(pred[0], axis=-1)\n            plt.subplot(1,3,2); plt.imshow(gt); plt.title(\"GT\"); plt.axis(\"off\")\n            plt.subplot(1,3,3); plt.imshow(pr); plt.title(\"Pred\"); plt.axis(\"off\")\n        else:\n            plt.subplot(1,3,2); plt.imshow(mask[0,:,:,0], cmap='gray'); plt.title(\"GT\"); plt.axis(\"off\")\n            plt.subplot(1,3,3); plt.imshow((pred[0,:,:,0] > 0.5).astype(float), cmap='gray'); plt.title(\"Pred\"); plt.axis(\"off\")\n        plt.show()\n\n# Example: after training you can call:\n# visualize_prediction(unet_dsb, val_dsb_ds, multiclass=False)\n# visualize_prediction(unet_cam, cam_val_ds, multiclass=True)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-14T17:27:18.106487Z","iopub.execute_input":"2025-11-14T17:27:18.106887Z","iopub.status.idle":"2025-11-14T17:29:23.928Z","shell.execute_reply.started":"2025-11-14T17:27:18.106854Z","shell.execute_reply":"2025-11-14T17:29:23.92659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, zipfile, cv2, io\nimport numpy as np\nimport tensorflow as tf\nfrom sklearn.model_selection import train_test_split\nfrom tensorflow.keras import layers, models, callbacks\n\n# Configuration\nIMG_HW = 256\nBATCH = 16\nSEED = 7\nEPOCHS = 30\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n\n# ============================================================================\n# Data Preparation - Read directly from ZIP\n# ============================================================================\n\ndef build_file_lists_from_zip(zip_path):\n    \"\"\"Build lists of samples directly from zip file\"\"\"\n    samples = []\n    \n    with zipfile.ZipFile(zip_path, 'r') as Z:\n        # Get all files in zip\n        all_files = Z.namelist()\n        \n        # Extract unique sample IDs (folder names)\n        sample_ids = set()\n        for f in all_files:\n            if '/' in f:\n                sample_id = f.split('/')[0]\n                if sample_id:  # Not empty\n                    sample_ids.add(sample_id)\n        \n        # For each sample, verify it has images and masks\n        for sample_id in sorted(sample_ids):\n            img_files = [f for f in all_files if f.startswith(f\"{sample_id}/images/\") and f.endswith('.png')]\n            mask_files = [f for f in all_files if f.startswith(f\"{sample_id}/masks/\") and f.endswith('.png')]\n            \n            if img_files and mask_files:\n                samples.append({\n                    'id': sample_id,\n                    'image': img_files[0],\n                    'masks': mask_files,\n                    'zip_path': zip_path\n                })\n    \n    print(f\"Found {len(samples)} valid samples in zip\")\n    return samples\n\ndef load_from_zip(sample_dict):\n    \"\"\"Load image and masks directly from zip file\"\"\"\n    zip_path = sample_dict['zip_path']\n    \n    with zipfile.ZipFile(zip_path, 'r') as Z:\n        # Load image\n        img_data = Z.read(sample_dict['image'])\n        img_array = np.frombuffer(img_data, np.uint8)\n        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (IMG_HW, IMG_HW))\n        img = img.astype(np.float32) / 255.0\n        \n        # Load and merge masks\n        masks = []\n        for mask_file in sample_dict['masks']:\n            mask_data = Z.read(mask_file)\n            mask_array = np.frombuffer(mask_data, np.uint8)\n            mask = cv2.imdecode(mask_array, cv2.IMREAD_GRAYSCALE)\n            mask = cv2.resize(mask, (IMG_HW, IMG_HW))\n            masks.append(mask > 0)\n        \n        # Merge masks\n        merged_mask = np.any(np.stack(masks, axis=0), axis=0).astype(np.float32)\n        merged_mask = merged_mask[..., np.newaxis]\n    \n    return img, merged_mask\n\ndef tf_load_wrapper(sample_json):\n    \"\"\"TensorFlow wrapper for loading from zip\"\"\"\n    def _load(json_str):\n        import json\n        sample_dict = json.loads(json_str.numpy().decode('utf-8'))\n        img, mask = load_from_zip(sample_dict)\n        return img.astype(np.float32), mask.astype(np.float32)\n    \n    img, mask = tf.py_function(_load, [sample_json], [tf.float32, tf.float32])\n    img.set_shape([IMG_HW, IMG_HW, 3])\n    mask.set_shape([IMG_HW, IMG_HW, 1])\n    \n    return img, mask\n\n# ============================================================================\n# Data Augmentation\n# ============================================================================\n\ndef augment(img, mask):\n    \"\"\"Apply random augmentations to image and mask\"\"\"\n    # Random horizontal flip\n    if tf.random.uniform(()) > 0.5:\n        img = tf.image.flip_left_right(img)\n        mask = tf.image.flip_left_right(mask)\n    \n    # Random vertical flip\n    if tf.random.uniform(()) > 0.5:\n        img = tf.image.flip_up_down(img)\n        mask = tf.image.flip_up_down(mask)\n    \n    # Random rotation (90 degree increments)\n    k = tf.random.uniform((), 0, 4, dtype=tf.int32)\n    img = tf.image.rot90(img, k)\n    mask = tf.image.rot90(mask, k)\n    \n    return img, mask\n\n# ============================================================================\n# U-Net Model\n# ============================================================================\n\ndef conv_block(x, filters):\n    \"\"\"Standard convolutional block with 2 conv layers\"\"\"\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation(\"relu\")(x)\n    \n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation(\"relu\")(x)\n    \n    return x\n\ndef build_unet(input_shape=(IMG_HW, IMG_HW, 3)):\n    \"\"\"Build U-Net architecture with proper bottleneck\"\"\"\n    inputs = layers.Input(input_shape)\n    \n    # Encoder (downsampling path)\n    c1 = conv_block(inputs, 32)\n    p1 = layers.MaxPooling2D()(c1)\n    \n    c2 = conv_block(p1, 64)\n    p2 = layers.MaxPooling2D()(c2)\n    \n    c3 = conv_block(p2, 128)\n    p3 = layers.MaxPooling2D()(c3)\n    \n    c4 = conv_block(p3, 256)\n    p4 = layers.MaxPooling2D()(c4)\n    \n    # Bottleneck\n    bottleneck = conv_block(p4, 512)\n    \n    # Decoder (upsampling path)\n    u1 = layers.UpSampling2D()(bottleneck)\n    u1 = layers.Concatenate()([u1, c4])\n    u1 = conv_block(u1, 256)\n    \n    u2 = layers.UpSampling2D()(u1)\n    u2 = layers.Concatenate()([u2, c3])\n    u2 = conv_block(u2, 128)\n    \n    u3 = layers.UpSampling2D()(u2)\n    u3 = layers.Concatenate()([u3, c2])\n    u3 = conv_block(u3, 64)\n    \n    u4 = layers.UpSampling2D()(u3)\n    u4 = layers.Concatenate()([u4, c1])\n    u4 = conv_block(u4, 32)\n    \n    # Output\n    outputs = layers.Conv2D(1, 1, activation=\"sigmoid\")(u4)\n    \n    return models.Model(inputs, outputs, name=\"U-Net\")\n\n# ============================================================================\n# Loss and Metrics\n# ============================================================================\n\ndef dice_coefficient(y_true, y_pred, smooth=1e-6):\n    \"\"\"Dice coefficient metric\"\"\"\n    y_true_f = tf.reshape(y_true, [-1])\n    y_pred_f = tf.reshape(y_pred, [-1])\n    \n    intersection = tf.reduce_sum(y_true_f * y_pred_f)\n    union = tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f)\n    \n    return (2. * intersection + smooth) / (union + smooth)\n\ndef dice_loss(y_true, y_pred):\n    \"\"\"Dice loss (1 - dice_coefficient)\"\"\"\n    return 1 - dice_coefficient(y_true, y_pred)\n\ndef combined_loss(y_true, y_pred):\n    \"\"\"Combined BCE and Dice loss\"\"\"\n    bce = tf.keras.losses.binary_crossentropy(y_true, y_pred)\n    return bce + dice_loss(y_true, y_pred)\n\n# ============================================================================\n# Main Training Pipeline\n# ============================================================================\n\ndef main():\n    import json\n    \n    # Paths\n    zip_root = \"/kaggle/input/data-science-bowl-2018\"\n    train_zip = os.path.join(zip_root, \"stage1_train.zip\")\n    \n    # Load sample list from zip (no extraction needed!)\n    print(\"Reading sample list from zip (no extraction)...\")\n    samples = build_file_lists_from_zip(train_zip)\n    \n    # Train/validation split\n    train_samples, val_samples = train_test_split(\n        samples,\n        test_size=0.2,\n        random_state=SEED\n    )\n    \n    print(f\"Training samples: {len(train_samples)}\")\n    print(f\"Validation samples: {len(val_samples)}\")\n    \n    # Convert samples to JSON strings for tf.data\n    train_jsons = [json.dumps(s) for s in train_samples]\n    val_jsons = [json.dumps(s) for s in val_samples]\n    \n    # Create datasets\n    train_ds = tf.data.Dataset.from_tensor_slices(train_jsons)\n    val_ds = tf.data.Dataset.from_tensor_slices(val_jsons)\n    \n    # Prepare training dataset with augmentation\n    print(\"\\nPreparing datasets...\")\n    train_ds = (train_ds\n                .shuffle(1000, seed=SEED)\n                .map(tf_load_wrapper, num_parallel_calls=tf.data.AUTOTUNE)\n                .map(augment, num_parallel_calls=tf.data.AUTOTUNE)\n                .batch(BATCH)\n                .prefetch(tf.data.AUTOTUNE))\n    \n    # Prepare validation dataset (no augmentation)\n    val_ds = (val_ds\n              .map(tf_load_wrapper, num_parallel_calls=tf.data.AUTOTUNE)\n              .batch(BATCH)\n              .prefetch(tf.data.AUTOTUNE))\n    \n    # Build model\n    print(\"\\nBuilding model...\")\n    model = build_unet()\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n        loss=combined_loss,\n        metrics=['binary_accuracy', dice_coefficient]\n    )\n    model.summary()\n    \n    # Callbacks\n    checkpoint_cb = callbacks.ModelCheckpoint(\n        'best_unet.keras',\n        save_best_only=True,\n        monitor='val_dice_coefficient',\n        mode='max',\n        verbose=1\n    )\n    \n    early_stop_cb = callbacks.EarlyStopping(\n        monitor='val_dice_coefficient',\n        patience=10,\n        mode='max',\n        restore_best_weights=True,\n        verbose=1\n    )\n    \n    reduce_lr_cb = callbacks.ReduceLROnPlateau(\n        monitor='val_dice_coefficient',\n        factor=0.5,\n        patience=5,\n        mode='max',\n        min_lr=1e-7,\n        verbose=1\n    )\n    \n    # Train\n    print(\"\\nStarting training...\")\n    history = model.fit(\n        train_ds,\n        validation_data=val_ds,\n        epochs=EPOCHS,\n        callbacks=[checkpoint_cb, early_stop_cb, reduce_lr_cb],\n        verbose=1\n    )\n    \n    print(\"\\nTraining complete!\")\n    return model, history\n\nif __name__ == \"__main__\":\n    model, history = main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T13:16:52.90306Z","iopub.execute_input":"2025-12-09T13:16:52.903348Z","iopub.status.idle":"2025-12-09T14:32:48.669575Z","shell.execute_reply.started":"2025-12-09T13:16:52.903328Z","shell.execute_reply":"2025-12-09T14:32:48.668896Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, zipfile, cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import Rectangle\nimport json\n\n# Configuration\nIMG_HW = 256\n\n# ============================================================================\n# Load Model\n# ============================================================================\n\ndef load_model():\n    \"\"\"Load the trained model\"\"\"\n    model = tf.keras.models.load_model(\n        'best_unet.keras',\n        custom_objects={\n            'combined_loss': lambda y_t, y_p: tf.keras.losses.binary_crossentropy(y_t, y_p) + (1 - dice_coefficient(y_t, y_p)),\n            'dice_coefficient': dice_coefficient\n        }\n    )\n    print(\"✓ Model loaded successfully\")\n    return model\n\ndef dice_coefficient(y_true, y_pred, smooth=1e-6):\n    \"\"\"Dice coefficient metric\"\"\"\n    y_true_f = tf.reshape(y_true, [-1])\n    y_pred_f = tf.reshape(y_pred, [-1])\n    intersection = tf.reduce_sum(y_true_f * y_pred_f)\n    union = tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f)\n    return (2. * intersection + smooth) / (union + smooth)\n\n# ============================================================================\n# Data Loading Functions\n# ============================================================================\n\ndef load_from_zip(sample_dict):\n    \"\"\"Load image and masks directly from zip file\"\"\"\n    zip_path = sample_dict['zip_path']\n    \n    with zipfile.ZipFile(zip_path, 'r') as Z:\n        # Load image\n        img_data = Z.read(sample_dict['image'])\n        img_array = np.frombuffer(img_data, np.uint8)\n        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (IMG_HW, IMG_HW))\n        img_normalized = img.astype(np.float32) / 255.0\n        \n        # Load and merge masks\n        masks = []\n        for mask_file in sample_dict['masks']:\n            mask_data = Z.read(mask_file)\n            mask_array = np.frombuffer(mask_data, np.uint8)\n            mask = cv2.imdecode(mask_array, cv2.IMREAD_GRAYSCALE)\n            mask = cv2.resize(mask, (IMG_HW, IMG_HW))\n            masks.append(mask > 0)\n        \n        # Merge masks\n        merged_mask = np.any(np.stack(masks, axis=0), axis=0).astype(np.float32)\n        merged_mask = merged_mask[..., np.newaxis]\n    \n    return img, img_normalized, merged_mask\n\ndef build_file_lists_from_zip(zip_path):\n    \"\"\"Build lists of samples directly from zip file\"\"\"\n    samples = []\n    \n    with zipfile.ZipFile(zip_path, 'r') as Z:\n        all_files = Z.namelist()\n        \n        sample_ids = set()\n        for f in all_files:\n            if '/' in f:\n                sample_id = f.split('/')[0]\n                if sample_id:\n                    sample_ids.add(sample_id)\n        \n        for sample_id in sorted(sample_ids):\n            img_files = [f for f in all_files if f.startswith(f\"{sample_id}/images/\") and f.endswith('.png')]\n            mask_files = [f for f in all_files if f.startswith(f\"{sample_id}/masks/\") and f.endswith('.png')]\n            \n            if img_files and mask_files:\n                samples.append({\n                    'id': sample_id,\n                    'image': img_files[0],\n                    'masks': mask_files,\n                    'zip_path': zip_path\n                })\n    \n    return samples\n\n# ============================================================================\n# Evaluation Metrics\n# ============================================================================\n\ndef calculate_iou(y_true, y_pred, threshold=0.5):\n    \"\"\"Calculate Intersection over Union\"\"\"\n    y_pred_binary = (y_pred > threshold).astype(np.float32)\n    y_true_binary = (y_true > 0.5).astype(np.float32)\n    \n    intersection = np.sum(y_true_binary * y_pred_binary)\n    union = np.sum(y_true_binary) + np.sum(y_pred_binary) - intersection\n    \n    if union == 0:\n        return 1.0 if intersection == 0 else 0.0\n    \n    return intersection / union\n\ndef calculate_dice(y_true, y_pred, threshold=0.5):\n    \"\"\"Calculate Dice coefficient\"\"\"\n    y_pred_binary = (y_pred > threshold).astype(np.float32)\n    y_true_binary = (y_true > 0.5).astype(np.float32)\n    \n    intersection = np.sum(y_true_binary * y_pred_binary)\n    total = np.sum(y_true_binary) + np.sum(y_pred_binary)\n    \n    if total == 0:\n        return 1.0 if intersection == 0 else 0.0\n    \n    return (2.0 * intersection) / total\n\ndef calculate_pixel_accuracy(y_true, y_pred, threshold=0.5):\n    \"\"\"Calculate pixel-wise accuracy\"\"\"\n    y_pred_binary = (y_pred > threshold).astype(np.float32)\n    y_true_binary = (y_true > 0.5).astype(np.float32)\n    \n    correct = np.sum(y_true_binary == y_pred_binary)\n    total = y_true_binary.size\n    \n    return correct / total\n\n# ============================================================================\n# Visualization Functions\n# ============================================================================\n\ndef visualize_predictions(images, masks_true, masks_pred, metrics, n_samples=8):\n    \"\"\"Visualize predictions with metrics\"\"\"\n    n_cols = 4\n    n_rows = n_samples\n    \n    fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4*n_rows))\n    \n    for i in range(n_samples):\n        # Original image\n        axes[i, 0].imshow(images[i])\n        axes[i, 0].set_title(f'Original Image\\nSample {i+1}')\n        axes[i, 0].axis('off')\n        \n        # Ground truth mask\n        axes[i, 1].imshow(images[i])\n        axes[i, 1].imshow(masks_true[i, :, :, 0], alpha=0.5, cmap='Reds')\n        axes[i, 1].set_title('Ground Truth Mask')\n        axes[i, 1].axis('off')\n        \n        # Predicted mask\n        axes[i, 2].imshow(images[i])\n        axes[i, 2].imshow(masks_pred[i, :, :, 0], alpha=0.5, cmap='Greens')\n        axes[i, 2].set_title('Predicted Mask')\n        axes[i, 2].axis('off')\n        \n        # Overlay comparison\n        axes[i, 3].imshow(images[i])\n        # True positive: yellow, False positive: red, False negative: blue\n        pred_binary = (masks_pred[i, :, :, 0] > 0.5).astype(float)\n        true_binary = (masks_true[i, :, :, 0] > 0.5).astype(float)\n        \n        # Create RGB overlay\n        overlay = np.zeros((*pred_binary.shape, 3))\n        overlay[:, :, 0] = (pred_binary > true_binary).astype(float)  # Red: False positive\n        overlay[:, :, 2] = (true_binary > pred_binary).astype(float)  # Blue: False negative\n        overlay[:, :, 1] = (pred_binary * true_binary)  # Green: True positive\n        \n        axes[i, 3].imshow(overlay, alpha=0.6)\n        axes[i, 3].set_title(\n            f'Overlay\\n'\n            f'Dice: {metrics[i][\"dice\"]:.3f} | '\n            f'IoU: {metrics[i][\"iou\"]:.3f}\\n'\n            f'Accuracy: {metrics[i][\"pixel_acc\"]:.3f}'\n        )\n        axes[i, 3].axis('off')\n        \n        # Add legend on first row\n        if i == 0:\n            from matplotlib.patches import Patch\n            legend_elements = [\n                Patch(facecolor='green', alpha=0.6, label='True Positive'),\n                Patch(facecolor='red', alpha=0.6, label='False Positive'),\n                Patch(facecolor='blue', alpha=0.6, label='False Negative')\n            ]\n            axes[i, 3].legend(handles=legend_elements, loc='upper right', \n                            bbox_to_anchor=(1.0, 1.0), fontsize=8)\n    \n    plt.tight_layout()\n    plt.savefig('prediction_samples.png', dpi=150, bbox_inches='tight')\n    print(\"✓ Visualization saved to 'prediction_samples.png'\")\n    plt.show()\n\ndef plot_metrics_distribution(all_metrics):\n    \"\"\"Plot distribution of metrics across all samples\"\"\"\n    dice_scores = [m['dice'] for m in all_metrics]\n    iou_scores = [m['iou'] for m in all_metrics]\n    pixel_accs = [m['pixel_acc'] for m in all_metrics]\n    \n    fig, axes = plt.subplots(1, 3, figsize=(15, 4))\n    \n    # Dice scores\n    axes[0].hist(dice_scores, bins=30, color='skyblue', edgecolor='black', alpha=0.7)\n    axes[0].axvline(np.mean(dice_scores), color='red', linestyle='--', \n                   label=f'Mean: {np.mean(dice_scores):.3f}')\n    axes[0].set_xlabel('Dice Coefficient')\n    axes[0].set_ylabel('Frequency')\n    axes[0].set_title('Dice Coefficient Distribution')\n    axes[0].legend()\n    axes[0].grid(alpha=0.3)\n    \n    # IoU scores\n    axes[1].hist(iou_scores, bins=30, color='lightcoral', edgecolor='black', alpha=0.7)\n    axes[1].axvline(np.mean(iou_scores), color='red', linestyle='--', \n                   label=f'Mean: {np.mean(iou_scores):.3f}')\n    axes[1].set_xlabel('IoU Score')\n    axes[1].set_ylabel('Frequency')\n    axes[1].set_title('IoU Score Distribution')\n    axes[1].legend()\n    axes[1].grid(alpha=0.3)\n    \n    # Pixel accuracy\n    axes[2].hist(pixel_accs, bins=30, color='lightgreen', edgecolor='black', alpha=0.7)\n    axes[2].axvline(np.mean(pixel_accs), color='red', linestyle='--', \n                   label=f'Mean: {np.mean(pixel_accs):.3f}')\n    axes[2].set_xlabel('Pixel Accuracy')\n    axes[2].set_ylabel('Frequency')\n    axes[2].set_title('Pixel Accuracy Distribution')\n    axes[2].legend()\n    axes[2].grid(alpha=0.3)\n    \n    plt.tight_layout()\n    plt.savefig('metrics_distribution.png', dpi=150, bbox_inches='tight')\n    print(\"✓ Metrics distribution saved to 'metrics_distribution.png'\")\n    plt.show()\n\n# ============================================================================\n# Main Testing Function\n# ============================================================================\n\ndef test_model(model, test_samples, n_visualize=8):\n    \"\"\"Test model on validation/test samples\"\"\"\n    print(f\"\\nTesting on {len(test_samples)} samples...\")\n    \n    all_metrics = []\n    images_vis = []\n    masks_true_vis = []\n    masks_pred_vis = []\n    \n    # Process all samples\n    for i, sample in enumerate(test_samples):\n        if i % 50 == 0:\n            print(f\"Processing {i}/{len(test_samples)}...\")\n        \n        # Load image and mask\n        img_orig, img_norm, mask_true = load_from_zip(sample)\n        \n        # Predict\n        mask_pred = model.predict(img_norm[np.newaxis, ...], verbose=0)[0]\n        \n        # Calculate metrics\n        metrics = {\n            'dice': calculate_dice(mask_true, mask_pred),\n            'iou': calculate_iou(mask_true, mask_pred),\n            'pixel_acc': calculate_pixel_accuracy(mask_true, mask_pred)\n        }\n        all_metrics.append(metrics)\n        \n        # Store for visualization\n        if i < n_visualize:\n            images_vis.append(img_orig)\n            masks_true_vis.append(mask_true)\n            masks_pred_vis.append(mask_pred)\n    \n    # Print summary statistics\n    print(\"\\n\" + \"=\"*60)\n    print(\"TEST RESULTS SUMMARY\")\n    print(\"=\"*60)\n    print(f\"Number of samples: {len(test_samples)}\")\n    print(f\"\\nDice Coefficient:\")\n    print(f\"  Mean:   {np.mean([m['dice'] for m in all_metrics]):.4f}\")\n    print(f\"  Median: {np.median([m['dice'] for m in all_metrics]):.4f}\")\n    print(f\"  Std:    {np.std([m['dice'] for m in all_metrics]):.4f}\")\n    print(f\"  Min:    {np.min([m['dice'] for m in all_metrics]):.4f}\")\n    print(f\"  Max:    {np.max([m['dice'] for m in all_metrics]):.4f}\")\n    \n    print(f\"\\nIoU Score:\")\n    print(f\"  Mean:   {np.mean([m['iou'] for m in all_metrics]):.4f}\")\n    print(f\"  Median: {np.median([m['iou'] for m in all_metrics]):.4f}\")\n    print(f\"  Std:    {np.std([m['iou'] for m in all_metrics]):.4f}\")\n    \n    print(f\"\\nPixel Accuracy:\")\n    print(f\"  Mean:   {np.mean([m['pixel_acc'] for m in all_metrics]):.4f}\")\n    print(f\"  Median: {np.median([m['pixel_acc'] for m in all_metrics]):.4f}\")\n    print(f\"  Std:    {np.std([m['pixel_acc'] for m in all_metrics]):.4f}\")\n    print(\"=\"*60)\n    \n    # Visualize predictions\n    print(f\"\\nGenerating visualizations for {n_visualize} samples...\")\n    visualize_predictions(\n        np.array(images_vis),\n        np.array(masks_true_vis),\n        np.array(masks_pred_vis),\n        all_metrics[:n_visualize],\n        n_samples=n_visualize\n    )\n    \n    # Plot metrics distribution\n    print(\"\\nPlotting metrics distribution...\")\n    plot_metrics_distribution(all_metrics)\n    \n    return all_metrics\n\n# ============================================================================\n# Main Execution\n# ============================================================================\n\ndef main():\n    print(\"=\"*60)\n    print(\"U-NET MODEL TESTING\")\n    print(\"=\"*60)\n    \n    # Load model\n    model = load_model()\n    \n    # Load validation samples (same split as training)\n    from sklearn.model_selection import train_test_split\n    \n    zip_root = \"/kaggle/input/data-science-bowl-2018\"\n    train_zip = os.path.join(zip_root, \"stage1_train.zip\")\n    \n    print(\"\\nLoading samples from zip...\")\n    all_samples = build_file_lists_from_zip(train_zip)\n    \n    # Use same split as training\n    _, test_samples = train_test_split(\n        all_samples,\n        test_size=0.2,\n        random_state=7  # Same seed as training\n    )\n    \n    print(f\"Found {len(test_samples)} validation samples\")\n    \n    # Test the model\n    metrics = test_model(model, test_samples, n_visualize=8)\n    \n    print(\"\\n✓ Testing complete!\")\n    print(\"Check the generated images:\")\n    print(\"  - prediction_samples.png\")\n    print(\"  - metrics_distribution.png\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T14:32:56.793435Z","iopub.execute_input":"2025-12-09T14:32:56.793943Z","iopub.status.idle":"2025-12-09T14:33:54.593744Z","shell.execute_reply.started":"2025-12-09T14:32:56.793918Z","shell.execute_reply":"2025-12-09T14:33:54.593016Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\nimport os\n\nzip_root = \"/kaggle/input/data-science-bowl-2018\"\ntrain_zip = os.path.join(zip_root, \"stage1_train.zip\")\n\n# Peek inside the zip without extracting\nwith zipfile.ZipFile(train_zip, 'r') as z:\n    files = z.namelist()[:20]  # First 20 files\n    print(\"First 20 items in stage1_train.zip:\")\n    for f in files:\n        print(f\"  {f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T00:58:55.370382Z","iopub.execute_input":"2025-11-08T00:58:55.371112Z","iopub.status.idle":"2025-11-08T00:58:55.531057Z","shell.execute_reply.started":"2025-11-08T00:58:55.371073Z","shell.execute_reply":"2025-11-08T00:58:55.530124Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, cv2\nimport numpy as np\nimport tensorflow as tf\nfrom glob import glob\nfrom tensorflow.keras import layers, models, callbacks\nfrom scipy.spatial.distance import cdist\n\n# Configuration\nIMG_HEIGHT = 512  # Increased from 256 for better detail\nIMG_WIDTH = 512   # Increased from 256 for better detail\nBATCH = 8         # Reduced due to larger images\nSEED = 7\nEPOCHS = 50\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n\n# Enable mixed precision\nfrom tensorflow.keras import mixed_precision\npolicy = mixed_precision.Policy('mixed_float16')\nmixed_precision.set_global_policy(policy)\n\n# Configure GPU\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    for gpu in gpus:\n        tf.config.experimental.set_memory_growth(gpu, True)\n\n# ============================================================================\n# Cityscapes Class Configuration\n# ============================================================================\n\n# Cityscapes standard 19 classes\nCITYSCAPES_CLASSES = {\n    'road': {'id': 0, 'color': (128, 64, 128)},\n    'sidewalk': {'id': 1, 'color': (244, 35, 232)},\n    'building': {'id': 2, 'color': (70, 70, 70)},\n    'wall': {'id': 3, 'color': (102, 102, 156)},\n    'fence': {'id': 4, 'color': (190, 153, 153)},\n    'pole': {'id': 5, 'color': (153, 153, 153)},\n    'traffic_light': {'id': 6, 'color': (250, 170, 30)},\n    'traffic_sign': {'id': 7, 'color': (220, 220, 0)},\n    'vegetation': {'id': 8, 'color': (107, 142, 35)},\n    'terrain': {'id': 9, 'color': (152, 251, 152)},\n    'sky': {'id': 10, 'color': (70, 130, 180)},\n    'person': {'id': 11, 'color': (220, 20, 60)},\n    'rider': {'id': 12, 'color': (255, 0, 0)},\n    'car': {'id': 13, 'color': (0, 0, 142)},\n    'truck': {'id': 14, 'color': (0, 0, 70)},\n    'bus': {'id': 15, 'color': (0, 60, 100)},\n    'train': {'id': 16, 'color': (0, 80, 100)},\n    'motorcycle': {'id': 17, 'color': (0, 0, 230)},\n    'bicycle': {'id': 18, 'color': (119, 11, 32)},\n}\n\nCLASS_NAMES = [info['name'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)]\n\nCLASS_COLORS = np.array([info['color'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)])\n\nNUM_CLASSES = len(CITYSCAPES_CLASSES) + 1  # +1 for void/unlabeled\n\nprint(f\"Configured {NUM_CLASSES} classes (19 + void)\")\n\n# ============================================================================\n# Improved Color Mapping (Nearest Neighbor)\n# ============================================================================\n\ndef rgb_to_class_nearest(rgb_mask, class_colors):\n    \"\"\"\n    Map RGB mask to class IDs using nearest neighbor matching.\n    This handles interpolated/compressed masks.\n    \"\"\"\n    h, w = rgb_mask.shape[:2]\n    \n    # Check if mask is actually grayscale ID-encoded (R==G==B)\n    # Sample a subset for efficiency\n    sample_size = min(1000, h * w)\n    sample_indices = np.random.choice(h * w, sample_size, replace=False)\n    sample_pixels = rgb_mask.reshape(-1, 3)[sample_indices]\n    is_grayscale = np.mean(sample_pixels[:,0] == sample_pixels[:,1]) > 0.95 and \\\n                   np.mean(sample_pixels[:,1] == sample_pixels[:,2]) > 0.95\n    \n    if is_grayscale:\n        # It's ID-encoded, map IDs to our classes\n        id_mask = rgb_mask[:,:,0]\n        \n        # Standard Cityscapes trainId mapping (0-18 for valid classes, 255 for ignore)\n        trainid_to_class = {\n            0: 0,   # road\n            1: 1,   # sidewalk\n            2: 2,   # building\n            3: 3,   # wall\n            4: 4,   # fence\n            5: 5,   # pole\n            6: 6,   # traffic light\n            7: 7,   # traffic sign\n            8: 8,   # vegetation\n            9: 9,   # terrain\n            10: 10, # sky\n            11: 11, # person\n            12: 12, # rider\n            13: 13, # car\n            14: 14, # truck\n            15: 15, # bus\n            16: 16, # train\n            17: 17, # motorcycle\n            18: 18, # bicycle\n        }\n        \n        class_mask = np.full((h, w), NUM_CLASSES - 1, dtype=np.int32)  # Default to void\n        for train_id, class_id in trainid_to_class.items():\n            class_mask[id_mask == train_id] = class_id\n        \n        return class_mask\n    \n    # Otherwise, use color-based matching\n    pixels = rgb_mask.reshape(-1, 3).astype(np.float32)\n    \n    # Calculate distances to all class colors\n    distances = cdist(pixels, class_colors.astype(np.float32), metric='euclidean')\n    \n    # Find nearest class for each pixel\n    nearest_classes = np.argmin(distances, axis=1)\n    \n    # Assign void class to very dark pixels (likely void/unlabeled)\n    black_pixels = np.all(pixels < 10, axis=1)\n    nearest_classes[black_pixels] = NUM_CLASSES - 1\n    \n    # Reshape back\n    class_mask = nearest_classes.reshape(h, w).astype(np.int32)\n    \n    return class_mask\n\n# ============================================================================\n# Data Preparation\n# ============================================================================\n\ndef build_file_lists(data_root):\n    \"\"\"Build file lists for Cityscapes dataset\"\"\"\n    train_imgs = sorted(glob(os.path.join(data_root, \"train\", \"*.jpg\")))\n    val_imgs = sorted(glob(os.path.join(data_root, \"val\", \"*.jpg\")))\n    \n    print(f\"\\nDataset Statistics:\")\n    print(f\"  Train: {len(train_imgs)} images\")\n    print(f\"  Val:   {len(val_imgs)} images\")\n    \n    return {\n        'train': train_imgs,\n        'val': val_imgs\n    }\n\ndef split_image_mask(combined_img):\n    \"\"\"Split the combined image into input image and mask\"\"\"\n    h, w = combined_img.shape[:2]\n    mid = w // 2\n    image = combined_img[:, :mid, :]\n    mask = combined_img[:, mid:, :]\n    return image, mask\n\ndef load_and_preprocess(img_path, class_colors):\n    \"\"\"Load and preprocess combined image\"\"\"\n    # Read combined image\n    combined = cv2.imread(img_path)\n    combined = cv2.cvtColor(combined, cv2.COLOR_BGR2RGB)\n    \n    # Split into image and mask\n    image, mask_rgb = split_image_mask(combined)\n    \n    # Resize (no interpolation on mask to preserve colors as much as possible)\n    image = cv2.resize(image, (IMG_WIDTH, IMG_HEIGHT))\n    mask_rgb = cv2.resize(mask_rgb, (IMG_WIDTH, IMG_HEIGHT), interpolation=cv2.INTER_NEAREST)\n    \n    # Normalize image\n    image = image.astype(np.float32) / 255.0\n    \n    # Convert mask using nearest neighbor matching\n    mask_class = rgb_to_class_nearest(mask_rgb, class_colors)\n    \n    # AGGRESSIVE: Force all values to valid range\n    mask_class = np.clip(mask_class, 0, NUM_CLASSES - 1).astype(np.int32)\n    \n    # Debug: Check for invalid values\n    if np.any(mask_class < 0) or np.any(mask_class >= NUM_CLASSES):\n        print(f\"WARNING: Invalid mask values in {img_path}\")\n        print(f\"  Min: {mask_class.min()}, Max: {mask_class.max()}\")\n        mask_class = np.clip(mask_class, 0, NUM_CLASSES - 1).astype(np.int32)\n    \n    return image, mask_class\n\ndef create_tf_dataset(img_paths, class_colors, augment=False):\n    \"\"\"Create TensorFlow dataset\"\"\"\n    \n    def load_fn(img_path):\n        def _load(ip):\n            ip_str = ip.numpy().decode('utf-8')\n            img, mask = load_and_preprocess(ip_str, class_colors)\n            \n            # CRITICAL: Aggressively clip in numpy before returning\n            mask = np.clip(mask, 0, NUM_CLASSES - 1).astype(np.int32)\n            \n            return img.astype(np.float32), mask\n        \n        img, mask = tf.py_function(_load, [img_path], [tf.float32, tf.int32])\n        img.set_shape([IMG_HEIGHT, IMG_WIDTH, 3])\n        mask.set_shape([IMG_HEIGHT, IMG_WIDTH])\n        \n        # Additional TensorFlow-level clip as safety\n        mask = tf.clip_by_value(mask, 0, NUM_CLASSES - 1)\n        mask = tf.cast(mask, tf.int32)\n        \n        return img, mask\n    \n    def augment_fn(img, mask):\n        # Random horizontal flip\n        if tf.random.uniform(()) > 0.5:\n            img = tf.image.flip_left_right(img)\n            mask = tf.image.flip_left_right(mask[..., tf.newaxis])[..., 0]\n        \n        # Random brightness and contrast\n        img = tf.image.random_brightness(img, 0.3)\n        img = tf.image.random_contrast(img, 0.7, 1.3)\n        img = tf.image.random_saturation(img, 0.7, 1.3)\n        img = tf.image.random_hue(img, 0.1)\n        \n        # Random gamma adjustment (improves lighting variations)\n        gamma = tf.random.uniform([], 0.8, 1.2)\n        img = tf.clip_by_value(img, 0.0, 1.0) ** gamma\n        \n        img = tf.clip_by_value(img, 0.0, 1.0)\n        \n        # Ensure mask stays in range after augmentation\n        mask = tf.clip_by_value(mask, 0, NUM_CLASSES - 1)\n        mask = tf.cast(mask, tf.int32)\n        \n        return img, mask\n    \n    dataset = tf.data.Dataset.from_tensor_slices(img_paths)\n    dataset = dataset.map(load_fn, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    if augment:\n        dataset = dataset.map(augment_fn, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    return dataset\n\n# ============================================================================\n# Model Architecture\n# ============================================================================\n\ndef conv_block(x, filters, dropout_rate=0.1):\n    \"\"\"Convolutional block with batch norm and dropout\"\"\"\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation(\"relu\")(x)\n    x = layers.Dropout(dropout_rate)(x)\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation(\"relu\")(x)\n    return x\n\ndef build_unet(num_classes, input_height=IMG_HEIGHT, input_width=IMG_WIDTH):\n    \"\"\"Build U-Net model\"\"\"\n    inputs = layers.Input((input_height, input_width, 3))\n    \n    # Encoder\n    c1 = conv_block(inputs, 64, 0.1)\n    p1 = layers.MaxPooling2D()(c1)\n    \n    c2 = conv_block(p1, 128, 0.1)\n    p2 = layers.MaxPooling2D()(c2)\n    \n    c3 = conv_block(p2, 256, 0.2)\n    p3 = layers.MaxPooling2D()(c3)\n    \n    c4 = conv_block(p3, 512, 0.2)\n    p4 = layers.MaxPooling2D()(c4)\n    \n    # Bottleneck\n    bottleneck = conv_block(p4, 1024, 0.3)\n    \n    # Decoder\n    u1 = layers.UpSampling2D()(bottleneck)\n    u1 = layers.Concatenate()([u1, c4])\n    u1 = conv_block(u1, 512, 0.2)\n    \n    u2 = layers.UpSampling2D()(u1)\n    u2 = layers.Concatenate()([u2, c3])\n    u2 = conv_block(u2, 256, 0.2)\n    \n    u3 = layers.UpSampling2D()(u2)\n    u3 = layers.Concatenate()([u3, c2])\n    u3 = conv_block(u3, 128, 0.1)\n    \n    u4 = layers.UpSampling2D()(u3)\n    u4 = layers.Concatenate()([u4, c1])\n    u4 = conv_block(u4, 64, 0.1)\n    \n    # Output\n    outputs = layers.Conv2D(num_classes, 1, activation=\"softmax\", dtype='float32')(u4)\n    \n    return models.Model(inputs, outputs, name=\"U-Net-Cityscapes\")\n\n# ============================================================================\n# Custom Metrics\n# ============================================================================\n\nclass MeanIoU(tf.keras.metrics.Metric):\n    \"\"\"Custom Mean IoU metric\"\"\"\n    def __init__(self, num_classes, name='mean_iou', **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.num_classes = num_classes\n        self.total_iou = self.add_weight(name='total_iou', initializer='zeros')\n        self.count = self.add_weight(name='count', initializer='zeros')\n    \n    def update_state(self, y_true, y_pred, sample_weight=None):\n        y_pred = tf.argmax(y_pred, axis=-1)\n        y_true = tf.cast(y_true, tf.int32)\n        \n        iou_sum = 0.0\n        valid_classes = 0.0\n        \n        for class_id in range(self.num_classes):\n            y_true_c = tf.cast(tf.equal(y_true, class_id), tf.float32)\n            y_pred_c = tf.cast(tf.equal(y_pred, class_id), tf.float32)\n            \n            intersection = tf.reduce_sum(y_true_c * y_pred_c)\n            union = tf.reduce_sum(y_true_c) + tf.reduce_sum(y_pred_c) - intersection\n            \n            # Calculate IoU\n            iou = tf.cond(union > 0, lambda: intersection / union, lambda: 0.0)\n            \n            # Only count classes present in ground truth\n            class_exists = tf.cast(tf.reduce_sum(y_true_c) > 0, tf.float32)\n            iou_sum += iou * class_exists\n            valid_classes += class_exists\n        \n        mean_iou = tf.cond(valid_classes > 0, \n                          lambda: iou_sum / valid_classes, \n                          lambda: 0.0)\n        \n        self.total_iou.assign_add(mean_iou)\n        self.count.assign_add(1.0)\n    \n    def result(self):\n        return tf.cond(self.count > 0,\n                      lambda: self.total_iou / self.count,\n                      lambda: 0.0)\n    \n    def reset_state(self):\n        self.total_iou.assign(0.0)\n        self.count.assign(0.0)\n\n# ============================================================================\n# Main Training Loop\n# ============================================================================\n\ndef main():\n    print(\"=\"*70)\n    print(\"CITYSCAPES TRAINING - SEMANTIC SEGMENTATION (FIXED)\")\n    print(\"=\"*70)\n    \n    print(f\"\\nClass configuration ({NUM_CLASSES} classes):\")\n    for i, name in enumerate(CLASS_NAMES):\n        print(f\"  {i}: {name}\")\n    print(f\"  {NUM_CLASSES-1}: void/unlabeled\")\n    \n    # Data paths\n    data_root = \"/kaggle/input/cityscapes-image-pairs/cityscapes_data\"\n    \n    # Build file lists\n    data = build_file_lists(data_root)\n    \n    # Create datasets\n    print(\"\\nCreating datasets with nearest-neighbor color mapping...\")\n    train_ds = create_tf_dataset(data['train'], CLASS_COLORS, augment=True)\n    train_ds = train_ds.shuffle(500).batch(BATCH).prefetch(tf.data.AUTOTUNE)\n    \n    val_ds = create_tf_dataset(data['val'], CLASS_COLORS, augment=False)\n    val_ds = val_ds.batch(BATCH).prefetch(tf.data.AUTOTUNE)\n    \n    # Build model\n    print(\"\\nBuilding model...\")\n    model = build_unet(NUM_CLASSES)\n    \n    # NOTE: Skipping class weights due to dataset issues\n    # Will rely on augmentation and architecture to handle imbalance\n    \n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n        loss=tf.keras.losses.SparseCategoricalCrossentropy(),\n        metrics=['accuracy', MeanIoU(NUM_CLASSES)]\n    )\n    \n    print(\"\\nModel Summary:\")\n    model.summary()\n    \n    # Callbacks\n    checkpoint_cb = callbacks.ModelCheckpoint(\n        'best_unet_cityscapes_fixed.keras',\n        save_best_only=True,\n        monitor='val_mean_iou',\n        mode='max',\n        verbose=1\n    )\n    \n    early_stop_cb = callbacks.EarlyStopping(\n        monitor='val_mean_iou',\n        patience=15,\n        mode='max',\n        restore_best_weights=True,\n        verbose=1\n    )\n    \n    reduce_lr_cb = callbacks.ReduceLROnPlateau(\n        monitor='val_mean_iou',\n        factor=0.5,\n        patience=7,\n        mode='max',\n        min_lr=1e-7,\n        verbose=1\n    )\n    \n    # Train\n    print(\"\\nStarting training...\")\n    print(f\"Image size: {IMG_HEIGHT}x{IMG_WIDTH}\")\n    print(f\"Batch size: {BATCH}\")\n    print(f\"Epochs: {EPOCHS}\")\n    print(\"-\"*70)\n    \n    history = model.fit(\n        train_ds,\n        validation_data=val_ds,\n        epochs=EPOCHS,\n        callbacks=[checkpoint_cb, early_stop_cb, reduce_lr_cb],\n        verbose=1\n    )\n    \n    print(\"\\n\" + \"=\"*70)\n    print(\"✓ Training complete!\")\n    print(\"=\"*70)\n    print(\"Model saved to: best_unet_cityscapes_fixed.keras\")\n    \n    # Print final metrics\n    final_train_iou = history.history['mean_iou'][-1]\n    final_val_iou = history.history['val_mean_iou'][-1]\n    best_val_iou = max(history.history['val_mean_iou'])\n    \n    print(f\"\\nFinal Metrics:\")\n    print(f\"  Train mIoU: {final_train_iou:.4f}\")\n    print(f\"  Val mIoU:   {final_val_iou:.4f}\")\n    print(f\"  Best Val mIoU: {best_val_iou:.4f}\")\n    \n    return model, history\n\nif __name__ == \"__main__\":\n    model, history = main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-15T14:38:11.322043Z","iopub.execute_input":"2025-11-15T14:38:11.322627Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, cv2\nimport numpy as np\nimport tensorflow as tf\nfrom glob import glob\nfrom tensorflow.keras import layers, models, callbacks\nfrom tensorflow.keras.applications import ResNet50\nfrom scipy.spatial.distance import cdist\n\n# Suppress TensorFlow warnings\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\nos.environ['CUDA_VISIBLE_DEVICES'] = '0'  # Use only GPU 0\n\n# Configuration - PROPER TRAINING (Not fast mode anymore)\nIMG_HEIGHT = 384  # Increased from 256\nIMG_WIDTH = 384   # Increased from 256\nBATCH = 8         # Adjusted for 384x384\nSEED = 7\nEPOCHS = 40       # Increased from 25\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n\n# ============================================================================\n# SINGLE GPU Configuration\n# ============================================================================\n\nprint(\"🔍 Configuring hardware...\")\ngpus = tf.config.list_physical_devices('GPU')\n\n# Configure memory growth\nif gpus:\n    try:\n        tf.config.experimental.set_memory_growth(gpus[0], True)\n        print(f\"✅ Using single GPU: {gpus[0]}\")\n    except RuntimeError as e:\n        print(f\"⚠️  GPU configuration: {e}\")\n\n# Use default strategy (single GPU)\nstrategy = tf.distribute.get_strategy()\nprint(f\"   Batch size: {BATCH}\")\nprint(f\"   Epochs: {EPOCHS}\")\n\n# Mixed precision for speed\nfrom tensorflow.keras import mixed_precision\npolicy = mixed_precision.Policy('mixed_float16')\nmixed_precision.set_global_policy(policy)\nprint(f\"   Mixed precision: {policy.name}\")\n\n# ============================================================================\n# Cityscapes Configuration\n# ============================================================================\n\nCITYSCAPES_CLASSES = {\n    'road': {'id': 0, 'color': (128, 64, 128)},\n    'sidewalk': {'id': 1, 'color': (244, 35, 232)},\n    'building': {'id': 2, 'color': (70, 70, 70)},\n    'wall': {'id': 3, 'color': (102, 102, 156)},\n    'fence': {'id': 4, 'color': (190, 153, 153)},\n    'pole': {'id': 5, 'color': (153, 153, 153)},\n    'traffic_light': {'id': 6, 'color': (250, 170, 30)},\n    'traffic_sign': {'id': 7, 'color': (220, 220, 0)},\n    'vegetation': {'id': 8, 'color': (107, 142, 35)},\n    'terrain': {'id': 9, 'color': (152, 251, 152)},\n    'sky': {'id': 10, 'color': (70, 130, 180)},\n    'person': {'id': 11, 'color': (220, 20, 60)},\n    'rider': {'id': 12, 'color': (255, 0, 0)},\n    'car': {'id': 13, 'color': (0, 0, 142)},\n    'truck': {'id': 14, 'color': (0, 0, 70)},\n    'bus': {'id': 15, 'color': (0, 60, 100)},\n    'train': {'id': 16, 'color': (0, 80, 100)},\n    'motorcycle': {'id': 17, 'color': (0, 0, 230)},\n    'bicycle': {'id': 18, 'color': (119, 11, 32)},\n}\n\nCLASS_NAMES = [info['name'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)]\n\nCLASS_COLORS = np.array([info['color'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)], dtype=np.float32)\n\nNUM_CLASSES = len(CITYSCAPES_CLASSES) + 1\n\n# ============================================================================\n# OPTIMIZED Data Processing\n# ============================================================================\n\ndef rgb_to_class_fast(rgb_mask, class_colors):\n    \"\"\"Faster RGB to class conversion with caching\"\"\"\n    h, w = rgb_mask.shape[:2]\n    \n    # Quick grayscale check (sample-based)\n    sample = rgb_mask[::10, ::10]  # Sample every 10th pixel\n    if np.allclose(sample[:,:,0], sample[:,:,1]) and np.allclose(sample[:,:,1], sample[:,:,2]):\n        # Grayscale ID encoding\n        id_mask = rgb_mask[:,:,0]\n        class_mask = np.clip(id_mask, 0, NUM_CLASSES - 1).astype(np.int32)\n        class_mask[id_mask > 18] = NUM_CLASSES - 1\n        return class_mask\n    \n    # Color-based matching (optimized)\n    pixels = rgb_mask.reshape(-1, 3).astype(np.float32)\n    \n    # Use sklearn's faster pairwise distances if available\n    try:\n        from sklearn.metrics.pairwise import euclidean_distances\n        distances = euclidean_distances(pixels, class_colors)\n    except:\n        distances = cdist(pixels, class_colors, metric='euclidean')\n    \n    nearest_classes = np.argmin(distances, axis=1)\n    \n    # Mark black pixels as void\n    black_mask = np.all(pixels < 10, axis=1)\n    nearest_classes[black_mask] = NUM_CLASSES - 1\n    \n    return nearest_classes.reshape(h, w).astype(np.int32)\n\ndef build_file_lists(data_root):\n    \"\"\"Build file lists\"\"\"\n    train_imgs = sorted(glob(os.path.join(data_root, \"train\", \"*.jpg\")))\n    val_imgs = sorted(glob(os.path.join(data_root, \"val\", \"*.jpg\")))\n    \n    print(f\"\\n📂 Dataset:\")\n    print(f\"   Train: {len(train_imgs)} images\")\n    print(f\"   Val: {len(val_imgs)} images\")\n    \n    return {'train': train_imgs, 'val': val_imgs}\n\ndef load_and_preprocess(img_path, class_colors):\n    \"\"\"Optimized loading and preprocessing\"\"\"\n    combined = cv2.imread(img_path)\n    if combined is None:\n        raise ValueError(f\"Failed to load {img_path}\")\n    \n    combined = cv2.cvtColor(combined, cv2.COLOR_BGR2RGB)\n    \n    # Split\n    h, w = combined.shape[:2]\n    mid = w // 2\n    image = combined[:, :mid, :]\n    mask_rgb = combined[:, mid:, :]\n    \n    # Resize with optimized interpolation\n    image = cv2.resize(image, (IMG_WIDTH, IMG_HEIGHT), interpolation=cv2.INTER_LINEAR)\n    mask_rgb = cv2.resize(mask_rgb, (IMG_WIDTH, IMG_HEIGHT), interpolation=cv2.INTER_NEAREST)\n    \n    # Normalize\n    image = image.astype(np.float32) / 255.0\n    \n    # Convert mask\n    mask_class = rgb_to_class_fast(mask_rgb, class_colors)\n    mask_class = np.clip(mask_class, 0, NUM_CLASSES - 1).astype(np.int32)\n    \n    return image, mask_class\n\ndef create_tf_dataset(img_paths, class_colors, augment=False):\n    \"\"\"Optimized TensorFlow dataset pipeline\"\"\"\n    \n    def load_fn(img_path):\n        def _load(ip):\n            ip_str = ip.numpy().decode('utf-8')\n            img, mask = load_and_preprocess(ip_str, class_colors)\n            return img.astype(np.float32), mask.astype(np.int32)\n        \n        img, mask = tf.py_function(_load, [img_path], [tf.float32, tf.int32])\n        img.set_shape([IMG_HEIGHT, IMG_WIDTH, 3])\n        mask.set_shape([IMG_HEIGHT, IMG_WIDTH])\n        \n        # Safety clipping\n        mask = tf.clip_by_value(mask, 0, NUM_CLASSES - 1)\n        mask = tf.cast(mask, tf.int32)\n        \n        return img, mask\n    \n    def augment_fn(img, mask):\n        \"\"\"Lightweight augmentation\"\"\"\n        # Horizontal flip only (fastest)\n        if tf.random.uniform(()) > 0.5:\n            img = tf.image.flip_left_right(img)\n            mask = tf.image.flip_left_right(mask[..., tf.newaxis])[..., 0]\n        \n        # Light color jitter\n        img = tf.image.random_brightness(img, 0.2)\n        img = tf.image.random_contrast(img, 0.8, 1.2)\n        img = tf.clip_by_value(img, 0.0, 1.0)\n        \n        return img, mask\n    \n    # Optimized pipeline\n    dataset = tf.data.Dataset.from_tensor_slices(img_paths)\n    \n    # CRITICAL: More parallel workers for loading\n    dataset = dataset.map(load_fn, \n                         num_parallel_calls=tf.data.AUTOTUNE,\n                         deterministic=False)  # Non-deterministic = faster\n    \n    if augment:\n        dataset = dataset.map(augment_fn, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    return dataset\n\n# ============================================================================\n# ResNet-UNet Model\n# ============================================================================\n\ndef build_resnet_unet(num_classes, input_height=IMG_HEIGHT, input_width=IMG_WIDTH):\n    \"\"\"Build U-Net with ResNet50 backbone - PROPER VERSION\"\"\"\n    \n    inputs = layers.Input((input_height, input_width, 3))\n    \n    # ResNet50 Backbone (Pretrained)\n    resnet = ResNet50(\n        include_top=False,\n        weights='imagenet',\n        input_tensor=inputs\n    )\n    \n    # Skip connections\n    skip1 = resnet.get_layer('conv1_relu').output\n    skip2 = resnet.get_layer('conv2_block3_out').output\n    skip3 = resnet.get_layer('conv3_block4_out').output\n    skip4 = resnet.get_layer('conv4_block6_out').output\n    bottleneck = resnet.get_layer('conv5_block3_out').output\n    \n    # CRITICAL: Fine-tune last 2 blocks (not freeze all!)\n    for layer in resnet.layers:\n        if 'conv5' in layer.name or 'conv4' in layer.name:\n            layer.trainable = True  # Train these\n        else:\n            layer.trainable = False  # Freeze early layers\n    \n    trainable = sum([layer.trainable for layer in resnet.layers])\n    print(f\"\\n🔧 ResNet50: {trainable}/{len(resnet.layers)} layers trainable (fine-tuning)\")\n    \n    # Proper decoder (double conv blocks)\n    def decoder_block(x, skip, filters, dropout=0.2):\n        x = layers.UpSampling2D(size=(2, 2))(x)\n        x = layers.Concatenate()([x, skip])\n        x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n        x = layers.BatchNormalization()(x)\n        x = layers.Activation(\"relu\")(x)\n        x = layers.Dropout(dropout)(x)\n        x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n        x = layers.BatchNormalization()(x)\n        x = layers.Activation(\"relu\")(x)\n        return x\n    \n    d1 = decoder_block(bottleneck, skip4, 512, 0.2)\n    d2 = decoder_block(d1, skip3, 256, 0.2)\n    d3 = decoder_block(d2, skip2, 128, 0.1)\n    d4 = decoder_block(d3, skip1, 64, 0.1)\n    d5 = layers.UpSampling2D(size=(2, 2))(d4)\n    \n    outputs = layers.Conv2D(num_classes, 1, activation=\"softmax\", dtype='float32')(d5)\n    \n    return models.Model(inputs, outputs, name=\"ResNet50-UNet\")\n\n# ============================================================================\n# Metrics\n# ============================================================================\n\nclass MeanIoU(tf.keras.metrics.Metric):\n    def __init__(self, num_classes, name='mean_iou', **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.num_classes = num_classes\n        self.total_iou = self.add_weight(name='total_iou', initializer='zeros', dtype=tf.float32)\n        self.count = self.add_weight(name='count', initializer='zeros', dtype=tf.float32)\n    \n    def update_state(self, y_true, y_pred, sample_weight=None):\n        y_pred = tf.argmax(y_pred, axis=-1)\n        y_true = tf.cast(y_true, tf.int32)\n        \n        iou_sum = 0.0\n        valid_classes = 0.0\n        \n        for class_id in range(self.num_classes):\n            y_true_c = tf.cast(tf.equal(y_true, class_id), tf.float32)\n            y_pred_c = tf.cast(tf.equal(y_pred, class_id), tf.float32)\n            \n            intersection = tf.reduce_sum(y_true_c * y_pred_c)\n            union = tf.reduce_sum(y_true_c) + tf.reduce_sum(y_pred_c) - intersection\n            \n            iou = tf.cond(union > 0, lambda: intersection / union, lambda: 0.0)\n            \n            class_exists = tf.cast(tf.reduce_sum(y_true_c) > 0, tf.float32)\n            iou_sum += iou * class_exists\n            valid_classes += class_exists\n        \n        mean_iou = tf.cond(valid_classes > 0, \n                          lambda: iou_sum / valid_classes, \n                          lambda: 0.0)\n        \n        self.total_iou.assign_add(mean_iou)\n        self.count.assign_add(1.0)\n    \n    def result(self):\n        return tf.cond(self.count > 0,\n                      lambda: self.total_iou / self.count,\n                      lambda: 0.0)\n    \n    def reset_state(self):\n        self.total_iou.assign(0.0)\n        self.count.assign(0.0)\n\n# ============================================================================\n# Main Training\n# ============================================================================\n\ndef main():\n    print(\"=\"*70)\n    print(\"CITYSCAPES - ResNet50-UNet (OPTIMIZED)\")\n    print(\"=\"*70)\n    \n    data_root = \"/kaggle/input/cityscapes-image-pairs/cityscapes_data\"\n    \n    # Load data\n    data = build_file_lists(data_root)\n    \n    # Create datasets with OPTIMIZATIONS\n    print(\"\\n📦 Creating optimized datasets...\")\n    train_ds = create_tf_dataset(data['train'], CLASS_COLORS, augment=True)\n    train_ds = train_ds.shuffle(500)  # Smaller shuffle buffer for speed\n    train_ds = train_ds.batch(BATCH)\n    train_ds = train_ds.prefetch(tf.data.AUTOTUNE)\n    \n    val_ds = create_tf_dataset(data['val'], CLASS_COLORS, augment=False)\n    val_ds = val_ds.batch(BATCH)\n    val_ds = val_ds.prefetch(tf.data.AUTOTUNE)\n    \n    # Build model (no strategy scope needed for single GPU)\n    print(\"\\n🏗️ Building model...\")\n    model = build_resnet_unet(NUM_CLASSES)\n    \n    # Lower learning rate for fine-tuning pretrained weights\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=5e-5, clipnorm=1.0),\n        loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),\n        metrics=['accuracy', MeanIoU(NUM_CLASSES)]\n    )\n    \n    # Callbacks - PROPER TRAINING\n    checkpoint_cb = callbacks.ModelCheckpoint(\n        'best_resnet_unet_cityscapes.keras',\n        save_best_only=True,\n        monitor='val_mean_iou',\n        mode='max',\n        verbose=1\n    )\n    \n    early_stop_cb = callbacks.EarlyStopping(\n        monitor='val_mean_iou',\n        patience=12,  # More patience\n        mode='max',\n        restore_best_weights=True,\n        verbose=1\n    )\n    \n    reduce_lr_cb = callbacks.ReduceLROnPlateau(\n        monitor='val_mean_iou',\n        factor=0.5,\n        patience=6,  # More patience\n        mode='max',\n        min_lr=1e-7,\n        verbose=1\n    )\n    \n    # Train\n    print(\"\\n🚀 PROPER TRAINING MODE\")\n    print(f\"   Resolution: {IMG_HEIGHT}x{IMG_WIDTH} (good balance)\")\n    print(f\"   Batch size: {BATCH}\")\n    print(f\"   Epochs: {EPOCHS}\")\n    print(f\"   Fine-tuning: conv4 + conv5 blocks\")\n    print(f\"   Expected: 40-50% mIoU\")\n    print(\"-\"*70)\n    \n    history = model.fit(\n        train_ds,\n        validation_data=val_ds,\n        epochs=EPOCHS,\n        callbacks=[checkpoint_cb, early_stop_cb, reduce_lr_cb],\n        verbose=1\n    )\n    \n    print(\"\\n\" + \"=\"*70)\n    print(\"✓ TRAINING COMPLETE!\")\n    print(\"=\"*70)\n    \n    best_val_iou = max(history.history['val_mean_iou'])\n    print(f\"\\n📊 Best Val mIoU: {best_val_iou:.4f}\")\n    print(f\"   Target: 40-50% (proper training)\")\n    print(f\"   Your result: {best_val_iou*100:.1f}%\")\n    \n    if best_val_iou > 0.40:\n        print(\"   ✅ EXCELLENT! Above 40%!\")\n    elif best_val_iou > 0.35:\n        print(\"   ✓ GOOD! Getting there!\")\n    else:\n        print(\"   ⚠️  Try training longer or higher resolution\")\n    \n    return model, history\n\nif __name__ == \"__main__\":\n    model, history = main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T23:46:25.868713Z","iopub.execute_input":"2025-11-22T23:46:25.869311Z","iopub.status.idle":"2025-11-23T01:47:26.382784Z","shell.execute_reply.started":"2025-11-22T23:46:25.869283Z","shell.execute_reply":"2025-11-23T01:47:26.381989Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import Patch\nfrom glob import glob\nfrom scipy.spatial.distance import cdist\n\n# Configuration (MUST match training)\nIMG_HEIGHT = 512\nIMG_WIDTH = 512  # Half of the combined image width\n\n# ============================================================================\n# Cityscapes Class Configuration (MUST match training)\n# ============================================================================\n\n# Cityscapes Class Configuration (MUST match training)\n# ============================================================================\n\nCITYSCAPES_CLASSES = {\n    'road': {'id': 0, 'color': (128, 64, 128)},\n    'sidewalk': {'id': 1, 'color': (244, 35, 232)},\n    'building': {'id': 2, 'color': (70, 70, 70)},\n    'wall': {'id': 3, 'color': (102, 102, 156)},\n    'fence': {'id': 4, 'color': (190, 153, 153)},\n    'pole': {'id': 5, 'color': (153, 153, 153)},\n    'traffic_light': {'id': 6, 'color': (250, 170, 30)},\n    'traffic_sign': {'id': 7, 'color': (220, 220, 0)},\n    'vegetation': {'id': 8, 'color': (107, 142, 35)},\n    'terrain': {'id': 9, 'color': (152, 251, 152)},\n    'sky': {'id': 10, 'color': (70, 130, 180)},\n    'person': {'id': 11, 'color': (220, 20, 60)},\n    'rider': {'id': 12, 'color': (255, 0, 0)},\n    'car': {'id': 13, 'color': (0, 0, 142)},\n    'truck': {'id': 14, 'color': (0, 0, 70)},\n    'bus': {'id': 15, 'color': (0, 60, 100)},\n    'train': {'id': 16, 'color': (0, 80, 100)},\n    'motorcycle': {'id': 17, 'color': (0, 0, 230)},\n    'bicycle': {'id': 18, 'color': (119, 11, 32)},\n}\n\nCLASS_NAMES = [info['name'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)] + ['void']\n\nCLASS_COLORS_ARRAY = np.array([info['color'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)])\n\nVISUALIZATION_COLORS = list(CLASS_COLORS_ARRAY) + [(0, 0, 0)]  # Add black for void\n\nNUM_CLASSES = len(CLASS_NAMES)\n\n# ============================================================================\n# Custom Metric (for loading model)\n# ============================================================================\n\nclass MeanIoU(tf.keras.metrics.Metric):\n    \"\"\"Custom Mean IoU metric\"\"\"\n    def __init__(self, num_classes=20, name='mean_iou', **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.num_classes = num_classes\n        self.total_iou = self.add_weight(name='total_iou', initializer='zeros')\n        self.count = self.add_weight(name='count', initializer='zeros')\n    \n    def update_state(self, y_true, y_pred, sample_weight=None):\n        y_pred = tf.argmax(y_pred, axis=-1)\n        y_true = tf.cast(y_true, tf.int32)\n        \n        iou_sum = 0.0\n        valid_classes = 0.0\n        \n        for class_id in range(self.num_classes):\n            y_true_c = tf.cast(tf.equal(y_true, class_id), tf.float32)\n            y_pred_c = tf.cast(tf.equal(y_pred, class_id), tf.float32)\n            \n            intersection = tf.reduce_sum(y_true_c * y_pred_c)\n            union = tf.reduce_sum(y_true_c) + tf.reduce_sum(y_pred_c) - intersection\n            \n            iou = tf.cond(union > 0, lambda: intersection / union, lambda: 0.0)\n            \n            class_exists = tf.cast(tf.reduce_sum(y_true_c) > 0, tf.float32)\n            iou_sum += iou * class_exists\n            valid_classes += class_exists\n        \n        mean_iou = tf.cond(valid_classes > 0, \n                          lambda: iou_sum / valid_classes, \n                          lambda: 0.0)\n        \n        self.total_iou.assign_add(mean_iou)\n        self.count.assign_add(1.0)\n    \n    def result(self):\n        return tf.cond(self.count > 0,\n                      lambda: self.total_iou / self.count,\n                      lambda: 0.0)\n    \n    def reset_state(self):\n        self.total_iou.assign(0.0)\n        self.count.assign(0.0)\n\n# ============================================================================\n# Model Loading\n# ============================================================================\n\ndef load_model(model_path='best_unet_cityscapes_fixed.keras'):\n    \"\"\"Load trained model\"\"\"\n    model = tf.keras.models.load_model(\n        model_path,\n        custom_objects={'MeanIoU': MeanIoU}\n    )\n    print(f\"✓ Model loaded successfully from {model_path}\")\n    print(f\"  Input shape: {model.input_shape}\")\n    print(f\"  Output shape: {model.output_shape}\")\n    return model\n\n# ============================================================================\n# Data Loading & Processing\n# ============================================================================\n\ndef split_image_mask(combined_img):\n    \"\"\"Split combined image into input and mask\"\"\"\n    h, w = combined_img.shape[:2]\n    mid = w // 2\n    image = combined_img[:, :mid, :]\n    mask = combined_img[:, mid:, :]\n    return image, mask\n\ndef rgb_to_class_mask(rgb_mask, class_colors):\n    \"\"\"\n    Convert RGB mask to class indices using nearest neighbor.\n    Handles interpolated/compressed masks.\n    \"\"\"\n    h, w = rgb_mask.shape[:2]\n    \n    # Reshape for distance calculation\n    pixels = rgb_mask.reshape(-1, 3).astype(np.float32)\n    \n    # Calculate distances to all class colors\n    distances = cdist(pixels, class_colors.astype(np.float32), metric='euclidean')\n    \n    # Find nearest class\n    nearest_classes = np.argmin(distances, axis=1)\n    \n    # Assign void class to very dark pixels\n    black_pixels = np.all(pixels < 10, axis=1)\n    nearest_classes[black_pixels] = NUM_CLASSES - 1\n    \n    # Reshape back\n    class_mask = nearest_classes.reshape(h, w).astype(np.int32)\n    \n    return class_mask\n\ndef class_mask_to_rgb(class_mask):\n    \"\"\"Convert class mask to RGB for visualization\"\"\"\n    h, w = class_mask.shape\n    rgb_mask = np.zeros((h, w, 3), dtype=np.uint8)\n    \n    for class_id, color in enumerate(VISUALIZATION_COLORS):\n        mask = (class_mask == class_id)\n        rgb_mask[mask] = color\n    \n    return rgb_mask\n\ndef load_test_data(data_root):\n    \"\"\"Load validation set images\"\"\"\n    val_imgs = sorted(glob(os.path.join(data_root, \"val\", \"*.jpg\")))\n    print(f\"\\nValidation samples found: {len(val_imgs)}\")\n    return val_imgs\n\ndef load_and_predict(model, img_path):\n    \"\"\"Load image, predict, and return all components\"\"\"\n    # Load combined image\n    combined = cv2.imread(img_path)\n    combined = cv2.cvtColor(combined, cv2.COLOR_BGR2RGB)\n    \n    # Split into image and ground truth mask\n    image, mask_rgb = split_image_mask(combined)\n    \n    # Resize for model input\n    img_resized = cv2.resize(image, (IMG_WIDTH, IMG_HEIGHT))\n    mask_resized = cv2.resize(mask_rgb, (IMG_WIDTH, IMG_HEIGHT), interpolation=cv2.INTER_NEAREST)\n    \n    # Convert ground truth using nearest neighbor (use the array, not dict)\n    true_mask = rgb_to_class_mask(mask_resized, CLASS_COLORS_ARRAY)\n    \n    # Predict\n    img_norm = img_resized.astype(np.float32) / 255.0\n    pred_probs = model.predict(img_norm[np.newaxis, ...], verbose=0)[0]\n    pred_mask = np.argmax(pred_probs, axis=-1)\n    \n    return img_resized, true_mask, pred_mask\n\n# ============================================================================\n# Metrics Calculation\n# ============================================================================\n\ndef calculate_metrics(y_true, y_pred, num_classes=NUM_CLASSES):\n    \"\"\"Calculate per-class and overall metrics\"\"\"\n    metrics = {\n        'per_class_iou': [],\n        'per_class_dice': [],\n        'per_class_pixel_count': []\n    }\n    \n    for class_id in range(num_classes):\n        true_mask = (y_true == class_id).astype(np.float32)\n        pred_mask = (y_pred == class_id).astype(np.float32)\n        \n        intersection = np.sum(true_mask * pred_mask)\n        union = np.sum(true_mask) + np.sum(pred_mask) - intersection\n        \n        iou = intersection / union if union > 0 else 0.0\n        metrics['per_class_iou'].append(iou)\n        \n        dice = (2 * intersection) / (np.sum(true_mask) + np.sum(pred_mask)) if (np.sum(true_mask) + np.sum(pred_mask)) > 0 else 0.0\n        metrics['per_class_dice'].append(dice)\n        \n        metrics['per_class_pixel_count'].append(np.sum(true_mask))\n    \n    # Calculate mean IoU only for classes present in ground truth\n    present_classes = [iou for iou, count in zip(metrics['per_class_iou'], metrics['per_class_pixel_count']) if count > 0]\n    metrics['mean_iou'] = np.mean(present_classes) if present_classes else 0.0\n    metrics['pixel_accuracy'] = np.mean(y_true == y_pred)\n    metrics['mean_dice'] = np.mean([d for d, c in zip(metrics['per_class_dice'], metrics['per_class_pixel_count']) if c > 0])\n    \n    return metrics\n\n# ============================================================================\n# Visualization\n# ============================================================================\n\ndef visualize_predictions(images, masks_true, masks_pred, all_metrics, n_samples=6):\n    \"\"\"Visualize predictions with ground truth\"\"\"\n    n_rows = min(n_samples, len(images))\n    fig, axes = plt.subplots(n_rows, 4, figsize=(20, 5*n_rows))\n    if n_rows == 1:\n        axes = axes.reshape(1, -1)\n    \n    for i in range(n_rows):\n        # Original image\n        axes[i, 0].imshow(images[i])\n        axes[i, 0].set_title(f'Original Image\\nSample {i+1}', fontsize=11, fontweight='bold')\n        axes[i, 0].axis('off')\n        \n        # Ground truth\n        gt_rgb = class_mask_to_rgb(masks_true[i])\n        axes[i, 1].imshow(gt_rgb)\n        axes[i, 1].set_title('Ground Truth', fontsize=11, fontweight='bold')\n        axes[i, 1].axis('off')\n        \n        # Prediction\n        pred_rgb = class_mask_to_rgb(masks_pred[i])\n        axes[i, 2].imshow(pred_rgb)\n        m = all_metrics[i]\n        axes[i, 2].set_title(f'Prediction\\nmIoU: {m[\"mean_iou\"]:.3f}', fontsize=11, fontweight='bold')\n        axes[i, 2].axis('off')\n        \n        # Overlay\n        axes[i, 3].imshow(images[i])\n        axes[i, 3].imshow(pred_rgb, alpha=0.5)\n        axes[i, 3].set_title(f'Overlay\\nAcc: {m[\"pixel_accuracy\"]:.3f}', fontsize=11, fontweight='bold')\n        axes[i, 3].axis('off')\n    \n    # Legend\n    legend_elements = [\n        Patch(facecolor=np.array(color)/255.0, label=name)\n        for color, name in zip(VISUALIZATION_COLORS, CLASS_NAMES)\n    ]\n    fig.legend(handles=legend_elements, loc='center right', \n              bbox_to_anchor=(0.99, 0.5), fontsize=9, ncol=1)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(right=0.88)\n    plt.savefig('cityscapes_predictions.png', dpi=150, bbox_inches='tight')\n    print(\"✓ Saved: cityscapes_predictions.png\")\n    plt.show()\n\ndef plot_per_class_metrics(all_metrics):\n    \"\"\"Plot per-class IoU and Dice scores\"\"\"\n    # Calculate average metrics across all samples\n    avg_iou = np.mean([m['per_class_iou'] for m in all_metrics], axis=0)\n    avg_dice = np.mean([m['per_class_dice'] for m in all_metrics], axis=0)\n    avg_counts = np.mean([m['per_class_pixel_count'] for m in all_metrics], axis=0)\n    \n    fig, axes = plt.subplots(1, 2, figsize=(18, 6))\n    x = np.arange(NUM_CLASSES)\n    \n    # IoU plot\n    colors = ['green' if c > 0 else 'lightgray' for c in avg_counts]\n    axes[0].bar(x, avg_iou, color=colors, alpha=0.7, edgecolor='black')\n    axes[0].axhline(np.mean([iou for iou, c in zip(avg_iou, avg_counts) if c > 0]), \n                   color='red', linestyle='--', linewidth=2,\n                   label=f'Mean (present classes): {np.mean([iou for iou, c in zip(avg_iou, avg_counts) if c > 0]):.3f}')\n    axes[0].set_xlabel('Class', fontsize=12, fontweight='bold')\n    axes[0].set_ylabel('IoU Score', fontsize=12, fontweight='bold')\n    axes[0].set_title('Per-Class IoU - Cityscapes', fontsize=14, fontweight='bold')\n    axes[0].set_xticks(x)\n    axes[0].set_xticklabels(CLASS_NAMES, rotation=45, ha='right', fontsize=9)\n    axes[0].legend(fontsize=10)\n    axes[0].grid(alpha=0.3, axis='y')\n    axes[0].set_ylim([0, 1])\n    \n    # Dice plot\n    axes[1].bar(x, avg_dice, color=colors, alpha=0.7, edgecolor='black')\n    axes[1].axhline(np.mean([dice for dice, c in zip(avg_dice, avg_counts) if c > 0]),\n                   color='red', linestyle='--', linewidth=2,\n                   label=f'Mean (present classes): {np.mean([dice for dice, c in zip(avg_dice, avg_counts) if c > 0]):.3f}')\n    axes[1].set_xlabel('Class', fontsize=12, fontweight='bold')\n    axes[1].set_ylabel('Dice Score', fontsize=12, fontweight='bold')\n    axes[1].set_title('Per-Class Dice - Cityscapes', fontsize=14, fontweight='bold')\n    axes[1].set_xticks(x)\n    axes[1].set_xticklabels(CLASS_NAMES, rotation=45, ha='right', fontsize=9)\n    axes[1].legend(fontsize=10)\n    axes[1].grid(alpha=0.3, axis='y')\n    axes[1].set_ylim([0, 1])\n    \n    plt.tight_layout()\n    plt.savefig('cityscapes_per_class_metrics.png', dpi=150, bbox_inches='tight')\n    print(\"✓ Saved: cityscapes_per_class_metrics.png\")\n    plt.show()\n\n# ============================================================================\n# Main Testing Function\n# ============================================================================\n\ndef test_model(model, val_imgs, n_visualize=6, max_samples=None):\n    \"\"\"Test model on validation set\"\"\"\n    if max_samples:\n        val_imgs = val_imgs[:max_samples]\n    \n    print(f\"\\nTesting on {len(val_imgs)} samples...\")\n    \n    all_metrics = []\n    images_vis, masks_true_vis, masks_pred_vis = [], [], []\n    \n    for i, img_path in enumerate(val_imgs):\n        if (i + 1) % 20 == 0 or i == 0:\n            print(f\"Processing {i+1}/{len(val_imgs)}...\")\n        \n        img, true_mask, pred_mask = load_and_predict(model, img_path)\n        \n        metrics = calculate_metrics(true_mask, pred_mask)\n        all_metrics.append(metrics)\n        \n        if i < n_visualize:\n            images_vis.append(img)\n            masks_true_vis.append(true_mask)\n            masks_pred_vis.append(pred_mask)\n    \n    # Print summary\n    print(\"\\n\" + \"=\"*70)\n    print(\"CITYSCAPES TEST RESULTS\")\n    print(\"=\"*70)\n    \n    mean_ious = [m['mean_iou'] for m in all_metrics]\n    pixel_accs = [m['pixel_accuracy'] for m in all_metrics]\n    \n    print(f\"\\nOverall Metrics:\")\n    print(f\"  Mean IoU:        {np.mean(mean_ious):.4f} (±{np.std(mean_ious):.4f})\")\n    print(f\"  Median IoU:      {np.median(mean_ious):.4f}\")\n    print(f\"  Pixel Accuracy:  {np.mean(pixel_accs):.4f}\")\n    \n    print(f\"\\nPer-Class IoU (averaged across all samples):\")\n    avg_iou = np.mean([m['per_class_iou'] for m in all_metrics], axis=0)\n    avg_counts = np.mean([m['per_class_pixel_count'] for m in all_metrics], axis=0)\n    \n    for name, iou, count in zip(CLASS_NAMES, avg_iou, avg_counts):\n        presence = \"✓\" if count > 0 else \"✗\"\n        print(f\"  {presence} {name:18s}: {iou:.4f}\")\n    \n    print(\"=\"*70)\n    \n    # Visualizations\n    visualize_predictions(images_vis, masks_true_vis, masks_pred_vis, \n                         all_metrics[:n_visualize], n_visualize)\n    plot_per_class_metrics(all_metrics)\n    \n    return all_metrics\n\n# ============================================================================\n# Main Entry Point\n# ============================================================================\n\ndef main():\n    print(\"=\"*70)\n    print(\"CITYSCAPES SEMANTIC SEGMENTATION - MODEL TESTING (FIXED)\")\n    print(\"=\"*70)\n    \n    # Paths\n    data_root = \"/kaggle/input/cityscapes-image-pairs/cityscapes_data\"\n    model_path = \"best_unet_cityscapes_fixed.keras\"\n    \n    # Load model\n    model = load_model(model_path)\n    \n    # Load validation data\n    val_imgs = load_test_data(data_root)\n    \n    if len(val_imgs) == 0:\n        print(\"\\n❌ No validation images found!\")\n        print(f\"Please check the path: {data_root}/val/\")\n        return\n    \n    # Ask user for sample size\n    print(f\"\\nHow many samples to test?\")\n    print(f\"  Available: {len(val_imgs)} images\")\n    n_test = input(f\"Enter number (or press Enter for all): \").strip()\n    \n    if n_test and n_test.isdigit():\n        n_test = int(n_test)\n    else:\n        n_test = None\n    \n    # Test model\n    metrics = test_model(model, val_imgs, n_visualize=6, max_samples=n_test)\n    \n    print(\"\\n✓ Testing complete!\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-15T03:29:52.477229Z","iopub.execute_input":"2025-11-15T03:29:52.47786Z","iopub.status.idle":"2025-11-15T03:30:39.451494Z","shell.execute_reply.started":"2025-11-15T03:29:52.477836Z","shell.execute_reply":"2025-11-15T03:30:39.450757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Delete old model\n#import os\n#os.remove('/kaggle/working/per_class_metrics.png')\n\n# Run the updated training script\n# It will now use class weights and train for 60 epochs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-08T13:33:43.415962Z","iopub.execute_input":"2025-11-08T13:33:43.416686Z","iopub.status.idle":"2025-11-08T13:33:43.420447Z","shell.execute_reply.started":"2025-11-08T13:33:43.41666Z","shell.execute_reply":"2025-11-08T13:33:43.419644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom glob import glob\nimport os\n\n# Load a sample image\ndata_root = \"/kaggle/input/cityscapes-image-pairs/cityscapes_data\"\nsample_imgs = glob(os.path.join(data_root, \"train\", \"*.jpg\"))[:3]\n\nprint(\"Analyzing Cityscapes dataset format...\\n\")\nprint(\"=\"*70)\n\nfor idx, img_path in enumerate(sample_imgs):\n    print(f\"\\nSample {idx+1}: {os.path.basename(img_path)}\")\n    print(\"-\"*70)\n    \n    # Load combined image\n    combined = cv2.imread(img_path)\n    combined_rgb = cv2.cvtColor(combined, cv2.COLOR_BGR2RGB)\n    \n    h, w = combined.shape[:2]\n    mid = w // 2\n    \n    # Split\n    image = combined_rgb[:, :mid, :]\n    mask = combined_rgb[:, mid:, :]\n    \n    print(f\"Combined image shape: {combined.shape}\")\n    print(f\"Image shape: {image.shape}\")\n    print(f\"Mask shape: {mask.shape}\")\n    \n    # Analyze mask\n    unique_colors = np.unique(mask.reshape(-1, 3), axis=0)\n    print(f\"\\nNumber of unique colors in mask: {len(unique_colors)}\")\n    print(f\"First 10 unique colors (RGB):\")\n    for i, color in enumerate(unique_colors[:10]):\n        pixel_count = np.sum(np.all(mask == color, axis=-1))\n        percentage = (pixel_count / (mask.shape[0] * mask.shape[1])) * 100\n        print(f\"  {i}: RGB{tuple(color)} - {pixel_count} pixels ({percentage:.2f}%)\")\n    \n    # Check if it's grayscale mask encoded as RGB\n    is_grayscale = np.all(mask[:,:,0] == mask[:,:,1]) and np.all(mask[:,:,1] == mask[:,:,2])\n    print(f\"\\nIs mask grayscale (encoded as RGB)? {is_grayscale}\")\n    \n    if is_grayscale:\n        mask_gray = mask[:,:,0]\n        unique_ids = np.unique(mask_gray)\n        print(f\"Unique mask IDs: {unique_ids}\")\n        print(f\"These are likely class IDs or trainIDs\")\n    \n    # Visualize\n    fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n    \n    axes[0].imshow(image)\n    axes[0].set_title('Input Image')\n    axes[0].axis('off')\n    \n    axes[1].imshow(mask)\n    axes[1].set_title('Mask (as RGB)')\n    axes[1].axis('off')\n    \n    if is_grayscale:\n        axes[2].imshow(mask_gray, cmap='tab20')\n        axes[2].set_title('Mask IDs (colorized)')\n    else:\n        axes[2].imshow(mask)\n        axes[2].set_title('Mask (RGB)')\n    axes[2].axis('off')\n    \n    plt.tight_layout()\n    plt.savefig(f'debug_sample_{idx+1}.png', dpi=150, bbox_inches='tight')\n    print(f\"\\n✓ Saved: debug_sample_{idx+1}.png\")\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"DIAGNOSIS COMPLETE\")\nprint(\"=\"*70)\nprint(\"\\nCheck the saved images to understand the mask format.\")\nprint(\"If masks are grayscale with IDs 0-19, we need to update the training code!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T22:11:40.559002Z","iopub.execute_input":"2025-11-14T22:11:40.559281Z","iopub.status.idle":"2025-11-14T22:11:44.07529Z","shell.execute_reply.started":"2025-11-14T22:11:40.55926Z","shell.execute_reply":"2025-11-14T22:11:44.074298Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import Patch\nfrom glob import glob\nfrom scipy.spatial.distance import cdist\n\n# Configuration (MUST match training)\nIMG_HEIGHT = 256\nIMG_WIDTH = 256  # Half of the combined image width\n\n# ============================================================================\n# Cityscapes Class Configuration (MUST match training)\n# ============================================================================\n\n# Cityscapes Class Configuration (MUST match training)\n# ============================================================================\n\nCITYSCAPES_CLASSES = {\n    'road': {'id': 0, 'color': (128, 64, 128)},\n    'sidewalk': {'id': 1, 'color': (244, 35, 232)},\n    'building': {'id': 2, 'color': (70, 70, 70)},\n    'wall': {'id': 3, 'color': (102, 102, 156)},\n    'fence': {'id': 4, 'color': (190, 153, 153)},\n    'pole': {'id': 5, 'color': (153, 153, 153)},\n    'traffic_light': {'id': 6, 'color': (250, 170, 30)},\n    'traffic_sign': {'id': 7, 'color': (220, 220, 0)},\n    'vegetation': {'id': 8, 'color': (107, 142, 35)},\n    'terrain': {'id': 9, 'color': (152, 251, 152)},\n    'sky': {'id': 10, 'color': (70, 130, 180)},\n    'person': {'id': 11, 'color': (220, 20, 60)},\n    'rider': {'id': 12, 'color': (255, 0, 0)},\n    'car': {'id': 13, 'color': (0, 0, 142)},\n    'truck': {'id': 14, 'color': (0, 0, 70)},\n    'bus': {'id': 15, 'color': (0, 60, 100)},\n    'train': {'id': 16, 'color': (0, 80, 100)},\n    'motorcycle': {'id': 17, 'color': (0, 0, 230)},\n    'bicycle': {'id': 18, 'color': (119, 11, 32)},\n}\n\nCLASS_NAMES = [info['name'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)] + ['void']\n\nCLASS_COLORS_ARRAY = np.array([info['color'] for info in sorted(\n    [{'name': k, **v} for k, v in CITYSCAPES_CLASSES.items()],\n    key=lambda x: x['id']\n)])\n\nVISUALIZATION_COLORS = list(CLASS_COLORS_ARRAY) + [(0, 0, 0)]  # Add black for void\n\nNUM_CLASSES = len(CLASS_NAMES)\n\n# ============================================================================\n# Custom Metric (for loading model)\n# ============================================================================\n\nclass MeanIoU(tf.keras.metrics.Metric):\n    \"\"\"Custom Mean IoU metric\"\"\"\n    def __init__(self, num_classes=20, name='mean_iou', **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.num_classes = num_classes\n        self.total_iou = self.add_weight(name='total_iou', initializer='zeros')\n        self.count = self.add_weight(name='count', initializer='zeros')\n    \n    def update_state(self, y_true, y_pred, sample_weight=None):\n        y_pred = tf.argmax(y_pred, axis=-1)\n        y_true = tf.cast(y_true, tf.int32)\n        \n        iou_sum = 0.0\n        valid_classes = 0.0\n        \n        for class_id in range(self.num_classes):\n            y_true_c = tf.cast(tf.equal(y_true, class_id), tf.float32)\n            y_pred_c = tf.cast(tf.equal(y_pred, class_id), tf.float32)\n            \n            intersection = tf.reduce_sum(y_true_c * y_pred_c)\n            union = tf.reduce_sum(y_true_c) + tf.reduce_sum(y_pred_c) - intersection\n            \n            iou = tf.cond(union > 0, lambda: intersection / union, lambda: 0.0)\n            \n            class_exists = tf.cast(tf.reduce_sum(y_true_c) > 0, tf.float32)\n            iou_sum += iou * class_exists\n            valid_classes += class_exists\n        \n        mean_iou = tf.cond(valid_classes > 0, \n                          lambda: iou_sum / valid_classes, \n                          lambda: 0.0)\n        \n        self.total_iou.assign_add(mean_iou)\n        self.count.assign_add(1.0)\n    \n    def result(self):\n        return tf.cond(self.count > 0,\n                      lambda: self.total_iou / self.count,\n                      lambda: 0.0)\n    \n    def reset_state(self):\n        self.total_iou.assign(0.0)\n        self.count.assign(0.0)\n\n# ============================================================================\n# Model Loading\n# ============================================================================\n\ndef load_model(model_path='best_unet_cityscapes_fixed.keras'):\n    \"\"\"Load trained model\"\"\"\n    model = tf.keras.models.load_model(\n        model_path,\n        custom_objects={'MeanIoU': MeanIoU}\n    )\n    print(f\"✓ Model loaded successfully from {model_path}\")\n    print(f\"  Input shape: {model.input_shape}\")\n    print(f\"  Output shape: {model.output_shape}\")\n    return model\n\n# ============================================================================\n# Data Loading & Processing\n# ============================================================================\n\ndef split_image_mask(combined_img):\n    \"\"\"Split combined image into input and mask\"\"\"\n    h, w = combined_img.shape[:2]\n    mid = w // 2\n    image = combined_img[:, :mid, :]\n    mask = combined_img[:, mid:, :]\n    return image, mask\n\ndef rgb_to_class_mask(rgb_mask, class_colors):\n    \"\"\"\n    Convert RGB mask to class indices using nearest neighbor.\n    Handles interpolated/compressed masks.\n    \"\"\"\n    h, w = rgb_mask.shape[:2]\n    \n    # Reshape for distance calculation\n    pixels = rgb_mask.reshape(-1, 3).astype(np.float32)\n    \n    # Calculate distances to all class colors\n    distances = cdist(pixels, class_colors.astype(np.float32), metric='euclidean')\n    \n    # Find nearest class\n    nearest_classes = np.argmin(distances, axis=1)\n    \n    # Assign void class to very dark pixels\n    black_pixels = np.all(pixels < 10, axis=1)\n    nearest_classes[black_pixels] = NUM_CLASSES - 1\n    \n    # Reshape back\n    class_mask = nearest_classes.reshape(h, w).astype(np.int32)\n    \n    return class_mask\n\ndef class_mask_to_rgb(class_mask):\n    \"\"\"Convert class mask to RGB for visualization\"\"\"\n    h, w = class_mask.shape\n    rgb_mask = np.zeros((h, w, 3), dtype=np.uint8)\n    \n    for class_id, color in enumerate(VISUALIZATION_COLORS):\n        mask = (class_mask == class_id)\n        rgb_mask[mask] = color\n    \n    return rgb_mask\n\ndef load_test_data(data_root):\n    \"\"\"Load validation set images\"\"\"\n    val_imgs = sorted(glob(os.path.join(data_root, \"val\", \"*.jpg\")))\n    print(f\"\\nValidation samples found: {len(val_imgs)}\")\n    return val_imgs\n\ndef load_and_predict(model, img_path):\n    \"\"\"Load image, predict, and return all components\"\"\"\n    # Load combined image\n    combined = cv2.imread(img_path)\n    combined = cv2.cvtColor(combined, cv2.COLOR_BGR2RGB)\n    \n    # Split into image and ground truth mask\n    image, mask_rgb = split_image_mask(combined)\n    \n    # Resize for model input\n    img_resized = cv2.resize(image, (IMG_WIDTH, IMG_HEIGHT))\n    mask_resized = cv2.resize(mask_rgb, (IMG_WIDTH, IMG_HEIGHT), interpolation=cv2.INTER_NEAREST)\n    \n    # Convert ground truth using nearest neighbor (use the array, not dict)\n    true_mask = rgb_to_class_mask(mask_resized, CLASS_COLORS_ARRAY)\n    \n    # Predict\n    img_norm = img_resized.astype(np.float32) / 255.0\n    pred_probs = model.predict(img_norm[np.newaxis, ...], verbose=0)[0]\n    pred_mask = np.argmax(pred_probs, axis=-1)\n    \n    return img_resized, true_mask, pred_mask\n\n# ============================================================================\n# Metrics Calculation\n# ============================================================================\n\ndef calculate_metrics(y_true, y_pred, num_classes=NUM_CLASSES):\n    \"\"\"Calculate per-class and overall metrics\"\"\"\n    metrics = {\n        'per_class_iou': [],\n        'per_class_dice': [],\n        'per_class_pixel_count': []\n    }\n    \n    for class_id in range(num_classes):\n        true_mask = (y_true == class_id).astype(np.float32)\n        pred_mask = (y_pred == class_id).astype(np.float32)\n        \n        intersection = np.sum(true_mask * pred_mask)\n        union = np.sum(true_mask) + np.sum(pred_mask) - intersection\n        \n        iou = intersection / union if union > 0 else 0.0\n        metrics['per_class_iou'].append(iou)\n        \n        dice = (2 * intersection) / (np.sum(true_mask) + np.sum(pred_mask)) if (np.sum(true_mask) + np.sum(pred_mask)) > 0 else 0.0\n        metrics['per_class_dice'].append(dice)\n        \n        metrics['per_class_pixel_count'].append(np.sum(true_mask))\n    \n    # Calculate mean IoU only for classes present in ground truth\n    present_classes = [iou for iou, count in zip(metrics['per_class_iou'], metrics['per_class_pixel_count']) if count > 0]\n    metrics['mean_iou'] = np.mean(present_classes) if present_classes else 0.0\n    metrics['pixel_accuracy'] = np.mean(y_true == y_pred)\n    metrics['mean_dice'] = np.mean([d for d, c in zip(metrics['per_class_dice'], metrics['per_class_pixel_count']) if c > 0])\n    \n    return metrics\n\n# ============================================================================\n# Visualization\n# ============================================================================\n\ndef visualize_predictions(images, masks_true, masks_pred, all_metrics, n_samples=6):\n    \"\"\"Visualize predictions with ground truth\"\"\"\n    n_rows = min(n_samples, len(images))\n    fig, axes = plt.subplots(n_rows, 4, figsize=(20, 5*n_rows))\n    if n_rows == 1:\n        axes = axes.reshape(1, -1)\n    \n    for i in range(n_rows):\n        # Original image\n        axes[i, 0].imshow(images[i])\n        axes[i, 0].set_title(f'Original Image\\nSample {i+1}', fontsize=11, fontweight='bold')\n        axes[i, 0].axis('off')\n        \n        # Ground truth\n        gt_rgb = class_mask_to_rgb(masks_true[i])\n        axes[i, 1].imshow(gt_rgb)\n        axes[i, 1].set_title('Ground Truth', fontsize=11, fontweight='bold')\n        axes[i, 1].axis('off')\n        \n        # Prediction\n        pred_rgb = class_mask_to_rgb(masks_pred[i])\n        axes[i, 2].imshow(pred_rgb)\n        m = all_metrics[i]\n        axes[i, 2].set_title(f'Prediction\\nmIoU: {m[\"mean_iou\"]:.3f}', fontsize=11, fontweight='bold')\n        axes[i, 2].axis('off')\n        \n        # Overlay\n        axes[i, 3].imshow(images[i])\n        axes[i, 3].imshow(pred_rgb, alpha=0.5)\n        axes[i, 3].set_title(f'Overlay\\nAcc: {m[\"pixel_accuracy\"]:.3f}', fontsize=11, fontweight='bold')\n        axes[i, 3].axis('off')\n    \n    # Legend\n    legend_elements = [\n        Patch(facecolor=np.array(color)/255.0, label=name)\n        for color, name in zip(VISUALIZATION_COLORS, CLASS_NAMES)\n    ]\n    fig.legend(handles=legend_elements, loc='center right', \n              bbox_to_anchor=(0.99, 0.5), fontsize=9, ncol=1)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(right=0.88)\n    plt.savefig('cityscapes_predictions.png', dpi=150, bbox_inches='tight')\n    print(\"✓ Saved: cityscapes_predictions.png\")\n    plt.show()\n\ndef plot_per_class_metrics(all_metrics):\n    \"\"\"Plot per-class IoU and Dice scores\"\"\"\n    # Calculate average metrics across all samples\n    avg_iou = np.mean([m['per_class_iou'] for m in all_metrics], axis=0)\n    avg_dice = np.mean([m['per_class_dice'] for m in all_metrics], axis=0)\n    avg_counts = np.mean([m['per_class_pixel_count'] for m in all_metrics], axis=0)\n    \n    fig, axes = plt.subplots(1, 2, figsize=(18, 6))\n    x = np.arange(NUM_CLASSES)\n    \n    # IoU plot\n    colors = ['green' if c > 0 else 'lightgray' for c in avg_counts]\n    axes[0].bar(x, avg_iou, color=colors, alpha=0.7, edgecolor='black')\n    axes[0].axhline(np.mean([iou for iou, c in zip(avg_iou, avg_counts) if c > 0]), \n                   color='red', linestyle='--', linewidth=2,\n                   label=f'Mean (present classes): {np.mean([iou for iou, c in zip(avg_iou, avg_counts) if c > 0]):.3f}')\n    axes[0].set_xlabel('Class', fontsize=12, fontweight='bold')\n    axes[0].set_ylabel('IoU Score', fontsize=12, fontweight='bold')\n    axes[0].set_title('Per-Class IoU - Cityscapes', fontsize=14, fontweight='bold')\n    axes[0].set_xticks(x)\n    axes[0].set_xticklabels(CLASS_NAMES, rotation=45, ha='right', fontsize=9)\n    axes[0].legend(fontsize=10)\n    axes[0].grid(alpha=0.3, axis='y')\n    axes[0].set_ylim([0, 1])\n    \n    # Dice plot\n    axes[1].bar(x, avg_dice, color=colors, alpha=0.7, edgecolor='black')\n    axes[1].axhline(np.mean([dice for dice, c in zip(avg_dice, avg_counts) if c > 0]),\n                   color='red', linestyle='--', linewidth=2,\n                   label=f'Mean (present classes): {np.mean([dice for dice, c in zip(avg_dice, avg_counts) if c > 0]):.3f}')\n    axes[1].set_xlabel('Class', fontsize=12, fontweight='bold')\n    axes[1].set_ylabel('Dice Score', fontsize=12, fontweight='bold')\n    axes[1].set_title('Per-Class Dice - Cityscapes', fontsize=14, fontweight='bold')\n    axes[1].set_xticks(x)\n    axes[1].set_xticklabels(CLASS_NAMES, rotation=45, ha='right', fontsize=9)\n    axes[1].legend(fontsize=10)\n    axes[1].grid(alpha=0.3, axis='y')\n    axes[1].set_ylim([0, 1])\n    \n    plt.tight_layout()\n    plt.savefig('cityscapes_per_class_metrics.png', dpi=150, bbox_inches='tight')\n    print(\"✓ Saved: cityscapes_per_class_metrics.png\")\n    plt.show()\n\n# ============================================================================\n# Main Testing Function\n# ============================================================================\n\ndef test_model(model, val_imgs, n_visualize=6, max_samples=None):\n    \"\"\"Test model on validation set\"\"\"\n    if max_samples:\n        val_imgs = val_imgs[:max_samples]\n    \n    print(f\"\\nTesting on {len(val_imgs)} samples...\")\n    \n    all_metrics = []\n    images_vis, masks_true_vis, masks_pred_vis = [], [], []\n    \n    for i, img_path in enumerate(val_imgs):\n        if (i + 1) % 20 == 0 or i == 0:\n            print(f\"Processing {i+1}/{len(val_imgs)}...\")\n        \n        img, true_mask, pred_mask = load_and_predict(model, img_path)\n        \n        metrics = calculate_metrics(true_mask, pred_mask)\n        all_metrics.append(metrics)\n        \n        if i < n_visualize:\n            images_vis.append(img)\n            masks_true_vis.append(true_mask)\n            masks_pred_vis.append(pred_mask)\n    \n    # Print summary\n    print(\"\\n\" + \"=\"*70)\n    print(\"CITYSCAPES TEST RESULTS\")\n    print(\"=\"*70)\n    \n    mean_ious = [m['mean_iou'] for m in all_metrics]\n    pixel_accs = [m['pixel_accuracy'] for m in all_metrics]\n    \n    print(f\"\\nOverall Metrics:\")\n    print(f\"  Mean IoU:        {np.mean(mean_ious):.4f} (±{np.std(mean_ious):.4f})\")\n    print(f\"  Median IoU:      {np.median(mean_ious):.4f}\")\n    print(f\"  Pixel Accuracy:  {np.mean(pixel_accs):.4f}\")\n    \n    print(f\"\\nPer-Class IoU (averaged across all samples):\")\n    avg_iou = np.mean([m['per_class_iou'] for m in all_metrics], axis=0)\n    avg_counts = np.mean([m['per_class_pixel_count'] for m in all_metrics], axis=0)\n    \n    for name, iou, count in zip(CLASS_NAMES, avg_iou, avg_counts):\n        presence = \"✓\" if count > 0 else \"✗\"\n        print(f\"  {presence} {name:18s}: {iou:.4f}\")\n    \n    print(\"=\"*70)\n    \n    # Visualizations\n    visualize_predictions(images_vis, masks_true_vis, masks_pred_vis, \n                         all_metrics[:n_visualize], n_visualize)\n    plot_per_class_metrics(all_metrics)\n    \n    return all_metrics\n\n# ============================================================================\n# Main Entry Point\n# ============================================================================\n\ndef main():\n    print(\"=\"*70)\n    print(\"CITYSCAPES SEMANTIC SEGMENTATION - MODEL TESTING\")\n    print(\"=\"*70)\n    \n    # Paths\n    data_root = \"/kaggle/input/cityscapes-image-pairs/cityscapes_data\"\n    model_path = \"best_resnet_unet_cityscapes.keras\"  # Fixed filename\n    \n    # Check if model exists\n    if not os.path.exists(model_path):\n        print(f\"\\n❌ Model file not found: {model_path}\")\n        print(\"\\nLooking for available .keras files...\")\n        keras_files = glob(\"*.keras\")\n        if keras_files:\n            print(f\"Found: {keras_files}\")\n            model_path = keras_files[0]\n            print(f\"Using: {model_path}\")\n        else:\n            print(\"No .keras files found in current directory!\")\n            return\n    \n    # Load model\n    model = load_model(model_path)\n    \n    # Load validation data\n    val_imgs = load_test_data(data_root)\n    \n    if len(val_imgs) == 0:\n        print(\"\\n❌ No validation images found!\")\n        print(f\"Please check the path: {data_root}/val/\")\n        return\n    \n    # Ask user for sample size\n    print(f\"\\nHow many samples to test?\")\n    print(f\"  Available: {len(val_imgs)} images\")\n    n_test = input(f\"Enter number (or press Enter for all): \").strip()\n    \n    if n_test and n_test.isdigit():\n        n_test = int(n_test)\n    else:\n        n_test = None\n    \n    # Test model\n    metrics = test_model(model, val_imgs, n_visualize=6, max_samples=n_test)\n    \n    print(\"\\n✓ Testing complete!\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, h5py, cv2\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom glob import glob\nfrom tensorflow.keras import layers, models, callbacks\nfrom sklearn.model_selection import train_test_split\n\n# Suppress warnings and configure GPU FIRST (before any TF operations)\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\n\n# GPU Configuration - MUST BE BEFORE ANY TF OPERATIONS\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        print(f\"✅ Configured {len(gpus)} GPU(s) with memory growth\")\n    except RuntimeError as e:\n        # Memory growth must be set before GPUs have been initialized\n        print(f\"⚠️  GPU already initialized: {e}\")\n\n# Configuration for HIGH ACCURACY + SPEED\nIMG_SIZE = 128  # Resize to 128x128 (much faster!)\nORIGINAL_SIZE = 240  # Dataset original size\nCHANNELS = 4    # Dataset has 4 channels\nNUM_CLASSES = 4 # 0=Background, 1=Necrotic/Core, 2=Edema, 3=Enhancing\nBATCH = 32      # Increased for smaller images\nEPOCHS = 50\nSEED = 42\nMAX_SLICES_IN_MEMORY = 5000  # Load only 5K slices max (memory-efficient)\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n\n# Mixed precision for speed\nfrom tensorflow.keras import mixed_precision\npolicy = mixed_precision.Policy('mixed_float16')\nmixed_precision.set_global_policy(policy)\n\nprint(\"=\"*70)\nprint(\"BraTS2020 BRAIN TUMOR SEGMENTATION - U-NET\")\nprint(\"Dataset: Pre-processed H5 format\")\nprint(\"Target: 90-95% Dice Score\")\nprint(\"=\"*70)\n\n# ============================================================================\n# Dataset Explanation & Loading\n# ============================================================================\n\n\"\"\"\n📚 BRATS2020 DATASET STRUCTURE:\n\nThis pre-processed version contains:\n- volume_X_slice_Y.h5: Each file contains ONE 2D slice with:\n  * 'image': (128, 128, 3) - 3 MRI modalities combined\n  * 'mask': (128, 128) - segmentation mask with 4 classes\n\nClasses (from BraTS Challenge):\n  0: Background (healthy brain tissue)\n  1: Necrotic and Non-Enhancing Tumor Core (NCR/NET)\n  2: Peritumoral Edema (ED) - swelling around tumor\n  3: GD-Enhancing Tumor (ET) - active tumor\n\nWhy 3 channels instead of 4?\n  - This dataset combined/reduced the original 4 MRI modalities\n  - Still contains essential tumor information\n  - Faster to train, similar accuracy\n\nCSV Files:\n  - meta_data.csv: Slice-level information\n  - name_mapping.csv: Patient ID mappings\n  - survival_info.csv: Clinical survival data (not needed for segmentation)\n\"\"\"\n\ndef load_h5_slice(filepath):\n    \"\"\"Load a single H5 file containing one slice\"\"\"\n    with h5py.File(filepath, 'r') as f:\n        image = f['image'][:]  # (240, 240, 4)\n        mask = f['mask'][:]    # (240, 240, 3)\n    \n    # Resize to 128x128 for speed (240x240 is too slow!)\n    image = cv2.resize(image, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_LINEAR)\n    \n    # Handle mask - convert to single channel if needed\n    if len(mask.shape) == 3:\n        # Mask has 3 channels - take argmax to get class labels\n        mask = np.argmax(mask, axis=-1).astype(np.uint8)\n    \n    # Resize mask with nearest neighbor (preserve labels)\n    mask = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n    \n    # Normalize image to [0, 1]\n    image = image.astype(np.float32)\n    if image.max() > 0:\n        image = image / image.max()\n    \n    mask = mask.astype(np.int32)\n    \n    return image, mask\n\ndef load_brats_dataset(data_root, max_slices=MAX_SLICES_IN_MEMORY):\n    \"\"\"\n    Load BraTS2020 pre-processed dataset (MEMORY-EFFICIENT VERSION)\n    \n    Strategy:\n    - Don't load all 57K slices (too much RAM!)\n    - Sample a subset for efficient training\n    - Use generator for larger datasets\n    \n    Args:\n        data_root: Path to data folder with H5 files\n        max_slices: Maximum slices to load (default: 5000)\n    \n    Returns:\n        X: Images (N, 128, 128, 3)\n        y: Masks (N, 128, 128)\n    \"\"\"\n    print(\"\\n📂 Loading BraTS2020 dataset (memory-efficient)...\")\n    \n    # Find all H5 files\n    h5_files = sorted(glob(os.path.join(data_root, \"volume_*_slice_*.h5\")))\n    \n    total_files = len(h5_files)\n    print(f\"   Found {total_files} slice files\")\n    \n    # Sample uniformly across dataset (don't just take first N)\n    if max_slices and max_slices < total_files:\n        step = total_files // max_slices\n        h5_files = h5_files[::step][:max_slices]\n        print(f\"   📊 Using {len(h5_files)} slices (sampled uniformly)\")\n        print(f\"   Memory usage: ~{len(h5_files) * 128 * 128 * 4 * 4 / 1e9:.2f} GB\")\n    else:\n        print(f\"   ⚠️  Loading ALL {total_files} slices (may use 20+ GB RAM!)\")\n    \n    images = []\n    masks = []\n    \n    print(\"   Loading slices...\")\n    for idx, filepath in enumerate(h5_files):\n        if (idx + 1) % 500 == 0:\n            print(f\"   Loaded {idx+1}/{len(h5_files)} slices...\")\n        \n        try:\n            img, mask = load_h5_slice(filepath)\n            \n            # Only keep slices with tumor (skip background-only slices)\n            if np.sum(mask > 0) > 50:  # At least 50 tumor pixels\n                images.append(img)\n                masks.append(mask)\n        \n        except Exception as e:\n            if idx < 10:  # Only print first few errors\n                print(f\"   ⚠️  Error loading {filepath}: {e}\")\n            continue\n    \n    X = np.array(images, dtype=np.float32)\n    y = np.array(masks, dtype=np.int32)\n    \n    print(f\"\\n✅ Dataset loaded successfully!\")\n    print(f\"   Total slices: {len(X)} (after filtering)\")\n    print(f\"   Image shape: {X.shape}\")\n    print(f\"   Mask shape: {y.shape}\")\n    print(f\"   Memory usage: ~{X.nbytes / 1e9:.2f} GB\")\n    \n    # Class distribution\n    print(f\"\\n📊 Class Distribution:\")\n    total_pixels = y.size\n    for class_id in range(NUM_CLASSES):\n        count = np.sum(y == class_id)\n        pct = 100 * count / total_pixels\n        class_names = ['Background', 'Necrotic Core', 'Edema', 'Enhancing']\n        print(f\"   Class {class_id} ({class_names[class_id]:15s}): {pct:5.2f}%\")\n    \n    return X, y\n\n# ============================================================================\n# Data Augmentation\n# ============================================================================\n\ndef augment_batch(images, masks):\n    \"\"\"\n    Apply strong augmentation for medical images\n    \n    Medical data augmentation principles:\n    - Geometric transforms OK (brain is symmetric)\n    - Intensity changes OK (MRI varies by scanner)\n    - NO color changes (not RGB images)\n    \"\"\"\n    \n    # Masks should be 2D: (batch, height, width)\n    # No need to check dimensions - they're already correct from loading\n    \n    # Random horizontal flip (brain is symmetric)\n    if tf.random.uniform(()) > 0.5:\n        images = tf.image.flip_left_right(images)\n        masks = tf.expand_dims(masks, -1)  # (B, H, W, 1)\n        masks = tf.image.flip_left_right(masks)\n        masks = tf.squeeze(masks, -1)  # Back to (B, H, W)\n    \n    # Random vertical flip\n    if tf.random.uniform(()) > 0.5:\n        images = tf.image.flip_up_down(images)\n        masks = tf.expand_dims(masks, -1)\n        masks = tf.image.flip_up_down(masks)\n        masks = tf.squeeze(masks, -1)\n    \n    # Random rotation (90, 180, 270 degrees)\n    k = tf.random.uniform((), minval=0, maxval=4, dtype=tf.int32)\n    images = tf.image.rot90(images, k=k)\n    masks = tf.expand_dims(masks, -1)\n    masks = tf.image.rot90(masks, k=k)\n    masks = tf.squeeze(masks, -1)\n    \n    # Random brightness/contrast (MRI intensity variations)\n    if tf.random.uniform(()) > 0.5:\n        images = tf.image.random_brightness(images, 0.2)\n        images = tf.image.random_contrast(images, 0.8, 1.2)\n        images = tf.clip_by_value(images, 0.0, 1.0)\n    \n    # Ensure correct types\n    images = tf.cast(images, tf.float32)\n    masks = tf.cast(masks, tf.int32)\n    \n    return images, masks\n\n# ============================================================================\n# U-Net Model Architecture\n# ============================================================================\n\ndef conv_block(x, filters, dropout=0.1):\n    \"\"\"\n    Standard U-Net convolutional block\n    \n    Why this works for medical images:\n    - Double conv preserves spatial information\n    - BatchNorm stabilizes training\n    - Dropout prevents overfitting (medical data is limited)\n    \"\"\"\n    x = layers.Conv2D(filters, 3, padding='same', kernel_initializer='he_normal')(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation('relu')(x)\n    x = layers.Dropout(dropout)(x)\n    \n    x = layers.Conv2D(filters, 3, padding='same', kernel_initializer='he_normal')(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation('relu')(x)\n    \n    return x\n\ndef build_unet(input_shape=(IMG_SIZE, IMG_SIZE, CHANNELS), num_classes=NUM_CLASSES):\n    \"\"\"\n    Classic U-Net Architecture (Ronneberger et al., 2015)\n    \n    Perfect for medical imaging because:\n    1. Symmetric encoder-decoder preserves spatial details\n    2. Skip connections recover fine-grained features\n    3. Works with small datasets (medical data is limited)\n    4. No pretrained backbone needed (medical images are different from ImageNet)\n    \n    Architecture:\n    - Encoder: 4 downsampling blocks (64 → 128 → 256 → 512 filters)\n    - Bottleneck: 1024 filters\n    - Decoder: 4 upsampling blocks with skip connections\n    - Output: Softmax over 4 classes\n    \"\"\"\n    \n    inputs = layers.Input(input_shape, name='input')\n    \n    # ========================================================================\n    # ENCODER (Contracting Path)\n    # ========================================================================\n    \n    # Block 1: 128x128 → 64x64\n    c1 = conv_block(inputs, 64, dropout=0.1)\n    p1 = layers.MaxPooling2D((2, 2))(c1)\n    \n    # Block 2: 64x64 → 32x32\n    c2 = conv_block(p1, 128, dropout=0.1)\n    p2 = layers.MaxPooling2D((2, 2))(c2)\n    \n    # Block 3: 32x32 → 16x16\n    c3 = conv_block(p2, 256, dropout=0.2)\n    p3 = layers.MaxPooling2D((2, 2))(c3)\n    \n    # Block 4: 16x16 → 8x8\n    c4 = conv_block(p3, 512, dropout=0.2)\n    p4 = layers.MaxPooling2D((2, 2))(c4)\n    \n    # ========================================================================\n    # BOTTLENECK: 8x8 (deepest features)\n    # ========================================================================\n    \n    c5 = conv_block(p4, 1024, dropout=0.3)\n    \n    # ========================================================================\n    # DECODER (Expanding Path with Skip Connections)\n    # ========================================================================\n    \n    # Block 6: 8x8 → 16x16\n    u6 = layers.Conv2DTranspose(512, (2, 2), strides=(2, 2), padding='same')(c5)\n    u6 = layers.concatenate([u6, c4], name='skip_4')  # Skip connection\n    c6 = conv_block(u6, 512, dropout=0.2)\n    \n    # Block 7: 16x16 → 32x32\n    u7 = layers.Conv2DTranspose(256, (2, 2), strides=(2, 2), padding='same')(c6)\n    u7 = layers.concatenate([u7, c3], name='skip_3')  # Skip connection\n    c7 = conv_block(u7, 256, dropout=0.2)\n    \n    # Block 8: 32x32 → 64x64\n    u8 = layers.Conv2DTranspose(128, (2, 2), strides=(2, 2), padding='same')(c7)\n    u8 = layers.concatenate([u8, c2], name='skip_2')  # Skip connection\n    c8 = conv_block(u8, 128, dropout=0.1)\n    \n    # Block 9: 64x64 → 128x128\n    u9 = layers.Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same')(c8)\n    u9 = layers.concatenate([u9, c1], name='skip_1')  # Skip connection\n    c9 = conv_block(u9, 64, dropout=0.1)\n    \n    # ========================================================================\n    # OUTPUT: 128x128x4 (one channel per class)\n    # ========================================================================\n    \n    outputs = layers.Conv2D(num_classes, (1, 1), activation='softmax', \n                           dtype='float32', name='output')(c9)\n    \n    model = models.Model(inputs=[inputs], outputs=[outputs], name='UNet-BraTS2020')\n    \n    return model\n\n# ============================================================================\n# Loss Functions & Metrics\n# ============================================================================\n\ndef dice_coefficient(y_true, y_pred, smooth=1e-6):\n    \"\"\"\n    Dice Similarity Coefficient (DSC) - THE metric for medical segmentation\n    \n    Formula: DSC = 2*|A ∩ B| / (|A| + |B|)\n    \n    Why Dice for medical imaging:\n    - Handles class imbalance (tumor is tiny vs background)\n    - Range [0, 1]: 0=no overlap, 1=perfect match\n    - Directly measures segmentation quality\n    - Standard metric in medical imaging competitions\n    \"\"\"\n    \n    # Convert predictions to one-hot\n    y_true_f = tf.cast(tf.reshape(y_true, [-1]), tf.int32)\n    y_pred_f = tf.reshape(y_pred, [-1, NUM_CLASSES])\n    y_true_oh = tf.one_hot(y_true_f, NUM_CLASSES)\n    \n    # Calculate intersection and union per class\n    intersection = tf.reduce_sum(y_true_oh * y_pred_f, axis=0)\n    union = tf.reduce_sum(y_true_oh, axis=0) + tf.reduce_sum(y_pred_f, axis=0)\n    \n    # Dice per class\n    dice = (2. * intersection + smooth) / (union + smooth)\n    \n    # Average across classes\n    return tf.reduce_mean(dice)\n\ndef dice_loss(y_true, y_pred):\n    \"\"\"Dice loss = 1 - Dice coefficient\"\"\"\n    return 1 - dice_coefficient(y_true, y_pred)\n\ndef combined_loss(y_true, y_pred):\n    \"\"\"\n    Combined Loss: Dice + Cross-Entropy\n    \n    Why combine both:\n    - Dice: Handles class imbalance, optimizes the metric we care about\n    - Cross-Entropy: Provides strong gradients, stable training\n    - 50/50 split works well in practice\n    \"\"\"\n    ce = tf.keras.losses.sparse_categorical_crossentropy(y_true, y_pred)\n    ce = tf.reduce_mean(ce)\n    \n    return 0.5 * dice_loss(y_true, y_pred) + 0.5 * ce\n\n# ============================================================================\n# Training Callbacks\n# ============================================================================\n\nclass MetricsLogger(callbacks.Callback):\n    \"\"\"Custom callback to log detailed metrics\"\"\"\n    def on_epoch_end(self, epoch, logs=None):\n        if logs:\n            dice = logs.get('val_dice_coefficient', 0)\n            acc = logs.get('val_accuracy', 0)\n            loss = logs.get('val_loss', 0)\n            \n            print(f\"\\n📊 Epoch {epoch+1} Summary:\")\n            print(f\"   Val Dice: {dice:.4f} ({dice*100:.2f}%)\")\n            print(f\"   Val Acc:  {acc:.4f} ({acc*100:.2f}%)\")\n            print(f\"   Val Loss: {loss:.4f}\")\n\n# ============================================================================\n# Main Training Function\n# ============================================================================\n\ndef main():\n    # Data path\n    data_root = \"/kaggle/input/brats2020-training-data/BraTS2020_training_data/content/data\"\n    \n    print(\"\\n🔍 Checking dataset...\")\n    sample_files = glob(os.path.join(data_root, \"volume_*_slice_*.h5\"))\n    print(f\"   Found {len(sample_files)} H5 files total\")\n    \n    if len(sample_files) == 0:\n        print(\"❌ No H5 files found! Check the data path.\")\n        return\n    \n    print(f\"\\n💡 Memory Management:\")\n    print(f\"   Total slices available: {len(sample_files)}\")\n    print(f\"   Will load: {MAX_SLICES_IN_MEMORY} slices (sampled uniformly)\")\n    print(f\"   This ensures training works on any GPU!\")\n    print(f\"   (You can increase MAX_SLICES_IN_MEMORY if you have >20GB RAM)\")\n    \n    # Load dataset (memory-efficient - loads 5K slices by default)\n    X, y = load_brats_dataset(data_root, max_slices=MAX_SLICES_IN_MEMORY)\n    \n    # Train/validation split (85/15)\n    X_train, X_val, y_train, y_val = train_test_split(\n        X, y, test_size=0.15, random_state=SEED, shuffle=True\n    )\n    \n    print(f\"\\n📊 Dataset Split:\")\n    print(f\"   Training:   {len(X_train)} slices\")\n    print(f\"   Validation: {len(X_val)} slices\")\n    \n    # Create TF datasets with augmentation\n    print(\"\\n📦 Creating data pipelines...\")\n    train_ds = tf.data.Dataset.from_tensor_slices((X_train, y_train))\n    train_ds = train_ds.shuffle(2000).batch(BATCH).map(\n        augment_batch, num_parallel_calls=tf.data.AUTOTUNE\n    ).prefetch(tf.data.AUTOTUNE)\n    \n    val_ds = tf.data.Dataset.from_tensor_slices((X_val, y_val))\n    val_ds = val_ds.batch(BATCH).prefetch(tf.data.AUTOTUNE)\n    \n    # Build model\n    print(\"\\n🏗️  Building U-Net model...\")\n    model = build_unet()\n    \n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n        loss=combined_loss,\n        metrics=['accuracy', dice_coefficient]\n    )\n    \n    print(\"\\n📋 Model Architecture:\")\n    total_params = model.count_params()\n    print(f\"   Total parameters: {total_params:,}\")\n    print(f\"   Trainable: {total_params:,} (all)\")\n    \n    # Callbacks\n    checkpoint_cb = callbacks.ModelCheckpoint(\n        'best_unet_brats2020.keras',\n        save_best_only=True,\n        monitor='val_dice_coefficient',\n        mode='max',\n        verbose=1\n    )\n    \n    early_stop_cb = callbacks.EarlyStopping(\n        monitor='val_dice_coefficient',\n        patience=15,\n        mode='max',\n        restore_best_weights=True,\n        verbose=1\n    )\n    \n    reduce_lr_cb = callbacks.ReduceLROnPlateau(\n        monitor='val_dice_coefficient',\n        factor=0.5,\n        patience=7,\n        mode='max',\n        min_lr=1e-7,\n        verbose=1\n    )\n    \n    metrics_cb = MetricsLogger()\n    \n    # Train\n    print(\"\\n🚀 Starting training...\")\n    print(f\"   Batch size: {BATCH}\")\n    print(f\"   Epochs: {EPOCHS}\")\n    print(f\"   Augmentation: Flips, rotations, brightness, zoom\")\n    print(f\"   Loss: Combined Dice + Cross-Entropy\")\n    print(f\"   Metric: Dice Coefficient (target: 90-95%)\")\n    print(\"=\"*70)\n    \n    history = model.fit(\n        train_ds,\n        validation_data=val_ds,\n        epochs=EPOCHS,\n        callbacks=[checkpoint_cb, early_stop_cb, reduce_lr_cb, metrics_cb],\n        verbose=1\n    )\n    \n    # Final results\n    print(\"\\n\" + \"=\"*70)\n    print(\"✅ TRAINING COMPLETE!\")\n    print(\"=\"*70)\n    \n    best_dice = max(history.history['val_dice_coefficient'])\n    best_acc = max(history.history['val_accuracy'])\n    \n    print(f\"\\n🏆 Final Results:\")\n    print(f\"   Best Validation Dice:     {best_dice:.4f} ({best_dice*100:.2f}%)\")\n    print(f\"   Best Validation Accuracy: {best_acc:.4f} ({best_acc*100:.2f}%)\")\n    \n    if best_dice >= 0.95:\n        print(\"\\n   🎉 OUTSTANDING! Achieved 95%+ Dice!\")\n        print(\"   This is publication-quality performance!\")\n    elif best_dice >= 0.90:\n        print(\"\\n   ✅ EXCELLENT! Above 90% Dice!\")\n        print(\"   This is very strong performance for medical segmentation!\")\n    elif best_dice >= 0.85:\n        print(\"\\n   ✓ GOOD! Above 85% Dice!\")\n        print(\"   Consider training longer for 90%+\")\n    else:\n        print(\"\\n   ⚠️  Below 85% - try training longer\")\n    \n    print(f\"\\n💾 Model saved to: best_unet_brats2020.keras\")\n    \n    return model, history\n\nif __name__ == \"__main__\":\n    model, history = main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T15:46:47.432406Z","iopub.execute_input":"2025-12-09T15:46:47.433178Z","iopub.status.idle":"2025-12-09T16:01:02.63754Z","shell.execute_reply.started":"2025-12-09T15:46:47.433149Z","shell.execute_reply":"2025-12-09T16:01:02.636777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport h5py\nimport cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom glob import glob\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\n# Suppress warnings and configure GPU\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\n\n# GPU Configuration\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        print(f\"✅ Configured {len(gpus)} GPU(s) with memory growth\")\n    except RuntimeError as e:\n        print(f\"⚠️  GPU already initialized: {e}\")\n\n# Configuration (MUST match training)\nIMG_SIZE = 128\nCHANNELS = 4\nNUM_CLASSES = 4\nCLASS_NAMES = ['Background', 'Necrotic Core', 'Edema', 'Enhancing']\n\ndef load_h5_slice(filepath):\n    \"\"\"Load a single H5 file containing one slice (same as training)\"\"\"\n    with h5py.File(filepath, 'r') as f:\n        image = f['image'][:]  # (240, 240, 4)\n        mask = f['mask'][:]    # (240, 240, 3)\n    \n    # Resize to 128x128 for speed\n    image = cv2.resize(image, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_LINEAR)\n    \n    # Handle mask - convert to single channel if needed\n    if len(mask.shape) == 3:\n        mask = np.argmax(mask, axis=-1).astype(np.uint8)\n    \n    # Resize mask with nearest neighbor (preserve labels)\n    mask = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n    \n    # Normalize image to [0, 1]\n    image = image.astype(np.float32)\n    if image.max() > 0:\n        image = image / image.max()\n    \n    mask = mask.astype(np.int32)\n    \n    return image, mask\n\ndef load_test_dataset(data_root, max_samples=None):\n    \"\"\"\n    Load test dataset for evaluation\n    \"\"\"\n    print(\"\\n📂 Loading test dataset...\")\n    \n    # Find all H5 files\n    h5_files = sorted(glob(os.path.join(data_root, \"volume_*_slice_*.h5\")))\n    \n    if max_samples and max_samples < len(h5_files):\n        h5_files = h5_files[:max_samples]\n    \n    print(f\"   Found {len(h5_files)} test slices\")\n    \n    images = []\n    masks = []\n    file_paths = []\n    \n    for idx, filepath in enumerate(h5_files):\n        if (idx + 1) % 500 == 0:\n            print(f\"   Loaded {idx+1}/{len(h5_files)} slices...\")\n        \n        try:\n            img, mask = load_h5_slice(filepath)\n            images.append(img)\n            masks.append(mask)\n            file_paths.append(filepath)\n        except Exception as e:\n            print(f\"   ⚠️  Error loading {filepath}: {e}\")\n            continue\n    \n    X_test = np.array(images, dtype=np.float32)\n    y_test = np.array(masks, dtype=np.int32)\n    \n    print(f\"✅ Test dataset loaded successfully!\")\n    print(f\"   Test slices: {len(X_test)}\")\n    print(f\"   Image shape: {X_test.shape}\")\n    print(f\"   Mask shape: {y_test.shape}\")\n    \n    return X_test, y_test, file_paths\n\ndef dice_coefficient(y_true, y_pred, smooth=1e-6):\n    \"\"\"\n    Dice Similarity Coefficient (same as training)\n    \"\"\"\n    # Convert predictions to one-hot\n    y_true_f = tf.cast(tf.reshape(y_true, [-1]), tf.int32)\n    y_pred_f = tf.reshape(y_pred, [-1, NUM_CLASSES])\n    y_true_oh = tf.one_hot(y_true_f, NUM_CLASSES)\n    \n    # Calculate intersection and union per class\n    intersection = tf.reduce_sum(y_true_oh * y_pred_f, axis=0)\n    union = tf.reduce_sum(y_true_oh, axis=0) + tf.reduce_sum(y_pred_f, axis=0)\n    \n    # Dice per class\n    dice = (2. * intersection + smooth) / (union + smooth)\n    \n    # Average across classes\n    return tf.reduce_mean(dice)\n\ndef calculate_metrics(y_true, y_pred):\n    \"\"\"\n    Calculate comprehensive segmentation metrics\n    \"\"\"\n    # Flatten arrays for metrics calculation\n    y_true_flat = y_true.flatten()\n    y_pred_flat = y_pred.flatten()\n    \n    # Dice coefficient per class\n    dice_scores = []\n    for class_id in range(NUM_CLASSES):\n        true_class = (y_true_flat == class_id)\n        pred_class = (y_pred_flat == class_id)\n        \n        if np.sum(true_class) + np.sum(pred_class) == 0:\n            dice_scores.append(1.0)  # Perfect score if no pixels of this class\n        else:\n            intersection = np.sum(true_class & pred_class)\n            dice = (2. * intersection) / (np.sum(true_class) + np.sum(pred_class))\n            dice_scores.append(dice)\n    \n    # Overall accuracy\n    accuracy = np.mean(y_true_flat == y_pred_flat)\n    \n    # IoU (Jaccard Index) per class\n    iou_scores = []\n    for class_id in range(NUM_CLASSES):\n        true_class = (y_true_flat == class_id)\n        pred_class = (y_pred_flat == class_id)\n        \n        if np.sum(true_class | pred_class) == 0:\n            iou_scores.append(1.0)\n        else:\n            intersection = np.sum(true_class & pred_class)\n            union = np.sum(true_class | pred_class)\n            iou = intersection / union\n            iou_scores.append(iou)\n    \n    return {\n        'dice_scores': dice_scores,\n        'mean_dice': np.mean(dice_scores),\n        'accuracy': accuracy,\n        'iou_scores': iou_scores,\n        'mean_iou': np.mean(iou_scores)\n    }\n\ndef visualize_predictions(images, true_masks, pred_masks, num_samples=5, save_dir='predictions'):\n    \"\"\"\n    Visualize predictions vs ground truth\n    \"\"\"\n    if not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n    \n    # Select random samples\n    indices = np.random.choice(len(images), min(num_samples, len(images)), replace=False)\n    \n    # Create colormap for segmentation\n    colors = np.array([\n        [0, 0, 0],        # Background - Black\n        [255, 0, 0],      # Necrotic Core - Red\n        [0, 255, 0],      # Edema - Green  \n        [0, 0, 255]       # Enhancing - Blue\n    ], dtype=np.uint8)\n    \n    for i, idx in enumerate(indices):\n        fig, axes = plt.subplots(1, 4, figsize=(20, 5))\n        \n        # Original image (use first channel for display)\n        img_display = images[idx][:, :, 0]  # Use first modality\n        axes[0].imshow(img_display, cmap='gray')\n        axes[0].set_title('Input Image')\n        axes[0].axis('off')\n        \n        # Ground truth\n        true_rgb = colors[true_masks[idx]]\n        axes[1].imshow(true_rgb)\n        axes[1].set_title('Ground Truth')\n        axes[1].axis('off')\n        \n        # Prediction\n        pred_rgb = colors[pred_masks[idx]]\n        axes[2].imshow(pred_rgb)\n        axes[2].set_title('Prediction')\n        axes[2].axis('off')\n        \n        # Difference\n        diff = np.zeros_like(true_rgb)\n        diff_mask = (true_masks[idx] != pred_masks[idx])\n        diff[diff_mask] = [255, 255, 0]  # Yellow for differences\n        \n        axes[3].imshow(diff)\n        axes[3].set_title('Differences (Yellow)')\n        axes[3].axis('off')\n        \n        plt.tight_layout()\n        plt.savefig(f'{save_dir}/prediction_{i+1}.png', dpi=150, bbox_inches='tight')\n        plt.close()\n    \n    print(f\"✅ Saved {len(indices)} prediction visualizations to {save_dir}/\")\n\ndef plot_confusion_matrix(y_true, y_pred, save_path='confusion_matrix.png'):\n    \"\"\"\n    Plot normalized confusion matrix\n    \"\"\"\n    y_true_flat = y_true.flatten()\n    y_pred_flat = y_pred.flatten()\n    \n    # Calculate confusion matrix\n    cm = confusion_matrix(y_true_flat, y_pred_flat, labels=range(NUM_CLASSES))\n    \n    # Normalize by true labels\n    cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n    \n    plt.figure(figsize=(10, 8))\n    sns.heatmap(cm_normalized, annot=True, fmt='.2f', cmap='Blues',\n                xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES)\n    plt.title('Normalized Confusion Matrix')\n    plt.xlabel('Predicted Label')\n    plt.ylabel('True Label')\n    plt.tight_layout()\n    plt.savefig(save_path, dpi=150, bbox_inches='tight')\n    plt.close()\n    \n    print(f\"✅ Confusion matrix saved to {save_path}\")\n\ndef test_model(model_path, test_data_root, max_test_samples=1000):\n    \"\"\"\n    Main testing function\n    \"\"\"\n    print(\"=\"*70)\n    print(\"🧪 BRAIN TUMOR SEGMENTATION - MODEL TESTING\")\n    print(\"=\"*70)\n    \n    # Load the trained model\n    print(f\"\\n📥 Loading model from: {model_path}\")\n    try:\n        model = tf.keras.models.load_model(model_path, compile=False)\n        print(\"✅ Model loaded successfully!\")\n    except Exception as e:\n        print(f\"❌ Error loading model: {e}\")\n        return\n    \n    # Recompile with dice metric\n    model.compile(\n        optimizer='adam',\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy', dice_coefficient]\n    )\n    \n    # Load test data\n    X_test, y_test, file_paths = load_test_dataset(test_data_root, max_test_samples)\n    \n    if len(X_test) == 0:\n        print(\"❌ No test data loaded!\")\n        return\n    \n    # Make predictions\n    print(\"\\n🔮 Making predictions...\")\n    y_pred_probs = model.predict(X_test, verbose=1)\n    y_pred = np.argmax(y_pred_probs, axis=-1)\n    \n    print(f\"✅ Predictions completed!\")\n    print(f\"   Prediction shape: {y_pred.shape}\")\n    \n    # Calculate metrics\n    print(\"\\n📊 Calculating metrics...\")\n    metrics = calculate_metrics(y_test, y_pred)\n    \n    # Print detailed results\n    print(\"\\n\" + \"=\"*70)\n    print(\"🏆 TEST RESULTS\")\n    print(\"=\"*70)\n    \n    print(f\"\\n📈 Overall Metrics:\")\n    print(f\"   Mean Dice Score:    {metrics['mean_dice']:.4f} ({metrics['mean_dice']*100:.2f}%)\")\n    print(f\"   Overall Accuracy:   {metrics['accuracy']:.4f} ({metrics['accuracy']*100:.2f}%)\")\n    print(f\"   Mean IoU:           {metrics['mean_iou']:.4f} ({metrics['mean_iou']*100:.2f}%)\")\n    \n    print(f\"\\n🎯 Per-Class Dice Scores:\")\n    for i, (dice, iou) in enumerate(zip(metrics['dice_scores'], metrics['iou_scores'])):\n        print(f\"   {CLASS_NAMES[i]:15s}: Dice = {dice:.4f} ({dice*100:.2f}%) | IoU = {iou:.4f} ({iou*100:.2f}%)\")\n    \n    # Performance evaluation\n    print(f\"\\n📋 Performance Evaluation:\")\n    if metrics['mean_dice'] >= 0.95:\n        print(\"   🎉 OUTSTANDING! 95%+ Dice - Publication quality!\")\n    elif metrics['mean_dice'] >= 0.90:\n        print(\"   ✅ EXCELLENT! 90%+ Dice - Strong performance!\")\n    elif metrics['mean_dice'] >= 0.85:\n        print(\"   ✓ GOOD! 85%+ Dice - Solid results!\")\n    elif metrics['mean_dice'] >= 0.75:\n        print(\"   📈 FAIR! 75%+ Dice - Room for improvement\")\n    else:\n        print(\"   ⚠️  NEEDS IMPROVEMENT - Consider retraining\")\n    \n    # Visualizations\n    print(f\"\\n🎨 Generating visualizations...\")\n    visualize_predictions(X_test, y_test, y_pred, num_samples=8, save_dir='test_predictions')\n    plot_confusion_matrix(y_test, y_pred, 'test_confusion_matrix.png')\n    \n    # Save detailed results\n    results_file = 'test_results.txt'\n    with open(results_file, 'w') as f:\n        f.write(\"BraTS2020 Segmentation Test Results\\n\")\n        f.write(\"=\"*50 + \"\\n\\n\")\n        f.write(f\"Test Samples: {len(X_test)}\\n\")\n        f.write(f\"Mean Dice: {metrics['mean_dice']:.4f}\\n\")\n        f.write(f\"Accuracy: {metrics['accuracy']:.4f}\\n\")\n        f.write(f\"Mean IoU: {metrics['mean_iou']:.4f}\\n\\n\")\n        \n        f.write(\"Per-Class Results:\\n\")\n        for i, (dice, iou) in enumerate(zip(metrics['dice_scores'], metrics['iou_scores'])):\n            f.write(f\"{CLASS_NAMES[i]}: Dice={dice:.4f}, IoU={iou:.4f}\\n\")\n    \n    print(f\"✅ Detailed results saved to {results_file}\")\n    \n    # Return results for further analysis\n    return {\n        'model': model,\n        'X_test': X_test,\n        'y_test': y_test,\n        'y_pred': y_pred,\n        'y_pred_probs': y_pred_probs,\n        'metrics': metrics,\n        'file_paths': file_paths\n    }\n\ndef test_single_image(model_path, image_path):\n    \"\"\"\n    Test on a single image for quick verification\n    \"\"\"\n    print(f\"\\n🔍 Testing single image: {image_path}\")\n    \n    # Load model\n    model = tf.keras.models.load_model(model_path, compile=False)\n    \n    # Load and preprocess image\n    image, true_mask = load_h5_slice(image_path)\n    \n    # Add batch dimension\n    image_batch = np.expand_dims(image, axis=0)\n    \n    # Predict\n    pred_probs = model.predict(image_batch, verbose=0)\n    pred_mask = np.argmax(pred_probs[0], axis=-1)\n    \n    # Calculate dice for this slice\n    dice = calculate_metrics(true_mask, pred_mask)['mean_dice']\n    \n    print(f\"   Single image Dice: {dice:.4f} ({dice*100:.2f}%)\")\n    \n    # Visualize\n    plt.figure(figsize=(15, 5))\n    \n    # Original image\n    plt.subplot(1, 3, 1)\n    plt.imshow(image[:, :, 0], cmap='gray')\n    plt.title('Input Image')\n    plt.axis('off')\n    \n    # Ground truth\n    plt.subplot(1, 3, 2)\n    colors = np.array([[0,0,0], [255,0,0], [0,255,0], [0,0,255]], dtype=np.uint8)\n    plt.imshow(colors[true_mask])\n    plt.title('Ground Truth')\n    plt.axis('off')\n    \n    # Prediction\n    plt.subplot(1, 3, 3)\n    plt.imshow(colors[pred_mask])\n    plt.title(f'Prediction (Dice: {dice:.3f})')\n    plt.axis('off')\n    \n    plt.tight_layout()\n    plt.savefig('single_prediction.png', dpi=150, bbox_inches='tight')\n    plt.show()\n    \n    return pred_mask, dice\n\n# Main execution\nif __name__ == \"__main__\":\n    # Configuration\n    MODEL_PATH = \"best_unet_brats2020.keras\"  # Update this path if needed\n    TEST_DATA_ROOT = \"/kaggle/input/brats2020-training-data/BraTS2020_training_data/content/data\"\n    \n    # Option 1: Full test suite\n    print(\"🚀 Starting comprehensive model evaluation...\")\n    results = test_model(MODEL_PATH, TEST_DATA_ROOT, max_test_samples=1000)\n    \n    # Option 2: Quick single image test (uncomment to use)\n    # sample_files = glob(os.path.join(TEST_DATA_ROOT, \"volume_*_slice_*.h5\"))\n    # if sample_files:\n    #     test_single_image(MODEL_PATH, sample_files[0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T16:12:17.830452Z","iopub.execute_input":"2025-12-09T16:12:17.831175Z","iopub.status.idle":"2025-12-09T16:13:06.097869Z","shell.execute_reply.started":"2025-12-09T16:12:17.831152Z","shell.execute_reply":"2025-12-09T16:13:06.097138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport os\nfrom matplotlib.colors import ListedColormap\n\ndef create_enhanced_visualizations(X_test, y_test, y_pred, num_samples=6, save_dir='detailed_predictions'):\n    \"\"\"\n    Create enhanced visualizations with overlays and detailed comparisons\n    \"\"\"\n    if not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n    \n    # Select random samples (ensure we get some with tumors)\n    tumor_indices = []\n    non_tumor_indices = []\n    \n    for i in range(len(X_test)):\n        if np.sum(y_test[i] > 0) > 50:  # Has tumor\n            tumor_indices.append(i)\n        else:\n            non_tumor_indices.append(i)\n    \n    # Mix of tumor and non-tumor cases\n    if len(tumor_indices) >= num_samples//2:\n        selected_indices = np.random.choice(tumor_indices, num_samples//2, replace=False)\n        if len(non_tumor_indices) >= num_samples//2:\n            selected_indices = np.concatenate([\n                selected_indices,\n                np.random.choice(non_tumor_indices, num_samples//2, replace=False)\n            ])\n    else:\n        selected_indices = np.random.choice(len(X_test), num_samples, replace=False)\n    \n    # Create custom colormap with transparency\n    colors = [\n        [0, 0, 0, 0.0],        # Background - Transparent\n        [1, 0, 0, 0.7],        # Necrotic Core - Red\n        [0, 1, 0, 0.7],        # Edema - Green  \n        [0, 0, 1, 0.7]         # Enhancing - Blue\n    ]\n    cmap = ListedColormap(colors)\n    \n    class_names = ['Background', 'Necrotic Core', 'Edema', 'Enhancing']\n    \n    for i, idx in enumerate(selected_indices):\n        fig, axes = plt.subplots(2, 4, figsize=(20, 10))\n        fig.suptitle(f'Sample {i+1} - Detailed Segmentation Analysis', fontsize=16, fontweight='bold')\n        \n        image = X_test[idx]\n        true_mask = y_test[idx]\n        pred_mask = y_pred[idx]\n        \n        # Calculate dice for this specific slice\n        slice_dice = calculate_slice_dice(true_mask, pred_mask)\n        \n        # Row 1: Basic Views\n        # Original image (T1 modality - usually best for visualization)\n        axes[0, 0].imshow(image[:, :, 0], cmap='gray')\n        axes[0, 0].set_title('T1 Modality\\n(Input Image)', fontweight='bold')\n        axes[0, 0].axis('off')\n        \n        # Ground Truth\n        im1 = axes[0, 1].imshow(true_mask, cmap=cmap, vmin=0, vmax=3)\n        axes[0, 1].set_title('Ground Truth Mask', fontweight='bold')\n        axes[0, 1].axis('off')\n        \n        # Prediction\n        im2 = axes[0, 2].imshow(pred_mask, cmap=cmap, vmin=0, vmax=3)\n        axes[0, 2].set_title('Predicted Mask', fontweight='bold')\n        axes[0, 2].axis('off')\n        \n        # Differences\n        diff_mask = np.zeros_like(true_mask, dtype=bool)\n        diff_mask = (true_mask != pred_mask) & (true_mask != 0)  # Only show tumor differences\n        \n        axes[0, 3].imshow(image[:, :, 0], cmap='gray')\n        axes[0, 3].imshow(diff_mask, cmap='Reds', alpha=0.6)\n        axes[0, 3].set_title(f'Differences (Red)\\nSlice Dice: {slice_dice:.3f}', fontweight='bold')\n        axes[0, 3].axis('off')\n        \n        # Row 2: Advanced Views\n        # Overlay Ground Truth\n        axes[1, 0].imshow(image[:, :, 0], cmap='gray')\n        axes[1, 0].imshow(true_mask, cmap=cmap, alpha=0.6)\n        axes[1, 0].set_title('Ground Truth Overlay', fontweight='bold')\n        axes[1, 0].axis('off')\n        \n        # Overlay Prediction\n        axes[1, 1].imshow(image[:, :, 0], cmap='gray')\n        axes[1, 1].imshow(pred_mask, cmap=cmap, alpha=0.6)\n        axes[1, 1].set_title('Prediction Overlay', fontweight='bold')\n        axes[1, 1].axis('off')\n        \n        # Side-by-side comparison\n        axes[1, 2].imshow(image[:, :, 0], cmap='gray')\n        axes[1, 2].imshow(true_mask, cmap=cmap, alpha=0.4, label='True')\n        axes[1, 2].imshow(pred_mask, cmap=cmap, alpha=0.4, label='Pred')\n        axes[1, 2].set_title('True vs Pred Overlay', fontweight='bold')\n        axes[1, 2].axis('off')\n        \n        # Class distribution\n        true_counts = [np.sum(true_mask == j) for j in range(4)]\n        pred_counts = [np.sum(pred_mask == j) for j in range(4)]\n        \n        x = np.arange(4)\n        width = 0.35\n        axes[1, 3].bar(x - width/2, true_counts, width, label='True', alpha=0.7)\n        axes[1, 3].bar(x + width/2, pred_counts, width, label='Pred', alpha=0.7)\n        axes[1, 3].set_title('Pixel Counts per Class')\n        axes[1, 3].set_xticks(x)\n        axes[1, 3].set_xticklabels(['BG', 'Necrotic', 'Edema', 'Enhancing'], rotation=45)\n        axes[1, 3].legend()\n        axes[1, 3].grid(True, alpha=0.3)\n        \n        plt.tight_layout()\n        plt.savefig(f'{save_dir}/detailed_prediction_{i+1}.png', dpi=150, bbox_inches='tight')\n        plt.close()\n    \n    print(f\"✅ Saved {len(selected_indices)} detailed visualizations to {save_dir}/\")\n    \n    # Create legend\n    create_legend(save_dir)\n\ndef calculate_slice_dice(true_mask, pred_mask):\n    \"\"\"Calculate dice coefficient for a single slice\"\"\"\n    dice_scores = []\n    for class_id in range(1, 4):  # Skip background\n        true_class = (true_mask == class_id)\n        pred_class = (pred_mask == class_id)\n        \n        if np.sum(true_class) + np.sum(pred_class) == 0:\n            dice_scores.append(1.0)\n        else:\n            intersection = np.sum(true_class & pred_class)\n            dice = (2. * intersection) / (np.sum(true_class) + np.sum(pred_class))\n            dice_scores.append(dice)\n    \n    return np.mean(dice_scores)\n\ndef create_legend(save_dir):\n    \"\"\"Create a legend for the segmentation colors\"\"\"\n    fig, ax = plt.subplots(figsize=(8, 1))\n    colors = ['black', 'red', 'green', 'blue']\n    labels = ['Background', 'Necrotic Core', 'Edema', 'Enhancing Tumor']\n    \n    for i, (color, label) in enumerate(zip(colors, labels)):\n        ax.barh(0, 1, left=i, color=color, edgecolor='black', label=label)\n    \n    ax.set_xlim(0, 4)\n    ax.set_ylim(0, 1)\n    ax.set_xticks(np.arange(4) + 0.5)\n    ax.set_xticklabels(labels, rotation=45)\n    ax.set_yticks([])\n    ax.set_title('Segmentation Classes Legend')\n    ax.grid(False)\n    \n    plt.tight_layout()\n    plt.savefig(f'{save_dir}/legend.png', dpi=150, bbox_inches='tight')\n    plt.close()\n\ndef analyze_per_class_performance(X_test, y_test, y_pred):\n    \"\"\"Detailed analysis of each class performance\"\"\"\n    print(\"\\n\" + \"=\"*70)\n    print(\"🔍 PER-CLASS PERFORMANCE ANALYSIS\")\n    print(\"=\"*70)\n    \n    for class_id in range(1, 4):  # Skip background\n        class_name = ['Necrotic Core', 'Edema', 'Enhancing'][class_id-1]\n        \n        # Find slices that contain this class\n        slices_with_class = []\n        for i in range(len(X_test)):\n            if np.sum(y_test[i] == class_id) > 10:  # At least 10 pixels\n                slices_with_class.append(i)\n        \n        if not slices_with_class:\n            continue\n            \n        # Calculate average dice for this class across relevant slices\n        class_dice_scores = []\n        for idx in slices_with_class[:100]:  # Sample first 100\n            true_class = (y_test[idx] == class_id)\n            pred_class = (y_pred[idx] == class_id)\n            \n            if np.sum(true_class) + np.sum(pred_class) > 0:\n                intersection = np.sum(true_class & pred_class)\n                dice = (2. * intersection) / (np.sum(true_class) + np.sum(pred_class))\n                class_dice_scores.append(dice)\n        \n        if class_dice_scores:\n            avg_dice = np.mean(class_dice_scores)\n            std_dice = np.std(class_dice_scores)\n            print(f\"\\n📊 {class_name}:\")\n            print(f\"   Average Dice: {avg_dice:.4f} ± {std_dice:.4f}\")\n            print(f\"   Slices with this class: {len(slices_with_class)}\")\n            \n            # Performance rating\n            if avg_dice >= 0.9:\n                rating = \"🎉 EXCELLENT\"\n            elif avg_dice >= 0.8:\n                rating = \"✅ VERY GOOD\"\n            elif avg_dice >= 0.7:\n                rating = \"✓ GOOD\"\n            elif avg_dice >= 0.6:\n                rating = \"📈 FAIR\"\n            else:\n                rating = \"⚠️  NEEDS IMPROVEMENT\"\n            \n            print(f\"   Performance: {rating}\")\n\n# Run the enhanced visualizations\nprint(\"🎨 Creating enhanced visualizations...\")\ncreate_enhanced_visualizations(results['X_test'], results['y_test'], results['y_pred'], num_samples=8)\n\nprint(\"\\n🔍 Analyzing per-class performance...\")\nanalyze_per_class_performance(results['X_test'], results['y_test'], results['y_pred'])\n\n# Show some individual examples\nprint(\"\\n👀 Displaying individual examples...\")\ndef show_individual_examples(X_test, y_test, y_pred, num_examples=3):\n    for i in range(min(num_examples, len(X_test))):\n        # Find an example with tumor\n        if np.sum(y_test[i] > 0) > 50:\n            fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n            \n            # Original image\n            axes[0].imshow(X_test[i][:, :, 0], cmap='gray')\n            axes[0].set_title(f'Example {i+1} - Input')\n            axes[0].axis('off')\n            \n            # Ground truth\n            axes[1].imshow(y_test[i], cmap=ListedColormap(['black', 'red', 'green', 'blue']), vmin=0, vmax=3)\n            axes[1].set_title('Ground Truth')\n            axes[1].axis('off')\n            \n            # Prediction\n            axes[2].imshow(y_pred[i], cmap=ListedColormap(['black', 'red', 'green', 'blue']), vmin=0, vmax=3)\n            axes[2].set_title('Prediction')\n            axes[2].axis('off')\n            \n            plt.tight_layout()\n            plt.show()\n\nshow_individual_examples(results['X_test'], results['y_test'], results['y_pred'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T16:19:34.285252Z","iopub.execute_input":"2025-12-09T16:19:34.286167Z","iopub.status.idle":"2025-12-09T16:19:44.7033Z","shell.execute_reply.started":"2025-12-09T16:19:34.286142Z","shell.execute_reply":"2025-12-09T16:19:44.702489Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Quick and simple visualization\nimport matplotlib.pyplot as plt\nimport numpy as np\n\ndef quick_visualize(X_test, y_test, y_pred, num_samples=3):\n    \"\"\"Quick visualization of results\"\"\"\n    indices = np.random.choice(len(X_test), num_samples, replace=False)\n    \n    for i, idx in enumerate(indices):\n        plt.figure(figsize=(15, 5))\n        \n        # Original image\n        plt.subplot(1, 4, 1)\n        plt.imshow(X_test[idx][:, :, 0], cmap='gray')\n        plt.title('Input Image')\n        plt.axis('off')\n        \n        # Ground truth\n        plt.subplot(1, 4, 2)\n        plt.imshow(y_test[idx], cmap='jet', vmin=0, vmax=3)\n        plt.title('Ground Truth')\n        plt.axis('off')\n        \n        # Prediction\n        plt.subplot(1, 4, 3)\n        plt.imshow(y_pred[idx], cmap='jet', vmin=0, vmax=3)\n        plt.title('Prediction')\n        plt.axis('off')\n        \n        # Overlay\n        plt.subplot(1, 4, 4)\n        plt.imshow(X_test[idx][:, :, 0], cmap='gray')\n        plt.imshow(y_pred[idx], cmap='jet', alpha=0.5, vmin=0, vmax=3)\n        plt.title('Overlay')\n        plt.axis('off')\n        \n        plt.tight_layout()\n        plt.show()\n\n# Run quick visualization\nquick_visualize(results['X_test'], results['y_test'], results['y_pred'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T16:29:57.951063Z","iopub.execute_input":"2025-12-09T16:29:57.951364Z","iopub.status.idle":"2025-12-09T16:29:58.973102Z","shell.execute_reply.started":"2025-12-09T16:29:57.951338Z","shell.execute_reply":"2025-12-09T16:29:58.972262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# SIMPLE visualization - Yellow highlights differences\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import ListedColormap\n\ndef simple_visualization(X_test, y_test, y_pred, samples_per_category=4):\n    \"\"\"\n    Simple visualization: Perfect cases, Mixed cases, Worst cases\n    \"\"\"\n    # Prediction colors\n    pred_cmap = ListedColormap(['black', 'red', 'green', 'blue'])\n    \n    # Categorize samples\n    perfect_cases = []      # High accuracy & high dice\n    mixed_cases = []        # Medium performance  \n    worst_cases = []        # Low performance\n    \n    for i in range(len(X_test)):\n        if np.sum(y_test[i] > 0) > 20:  # Only samples with tumors\n            accuracy = np.mean(y_test[i] == y_pred[i])\n            dice = calculate_tumor_dice(y_test[i], y_pred[i])\n            \n            if dice >= 0.9 and accuracy >= 0.95:\n                perfect_cases.append((i, accuracy, dice))\n            elif dice >= 0.7:\n                mixed_cases.append((i, accuracy, dice))\n            else:\n                worst_cases.append((i, accuracy, dice))\n    \n    print(\"📊 Sample Distribution:\")\n    print(f\"   Perfect cases: {len(perfect_cases)}\")\n    print(f\"   Mixed cases: {len(mixed_cases)}\") \n    print(f\"   Worst cases: {len(worst_cases)}\")\n    \n    # Take top samples from each category\n    perfect_samples = perfect_cases[:samples_per_category]\n    mixed_samples = mixed_cases[:samples_per_category] \n    worst_samples = worst_cases[:samples_per_category]\n    \n    # Plot PERFECT CASES\n    if perfect_samples:\n        print(f\"\\n🎯 PERFECT CASES\")\n        print(\"=\"*50)\n        plot_category(perfect_samples, X_test, y_test, y_pred, \"Perfect\", pred_cmap)\n    \n    # Plot MIXED CASES\n    if mixed_samples:\n        print(f\"\\n🔄 MIXED CASES\")\n        print(\"=\"*50)\n        plot_category(mixed_samples, X_test, y_test, y_pred, \"Mixed\", pred_cmap)\n    \n    # Plot WORST CASES  \n    if worst_samples:\n        print(f\"\\n⚠️  WORST CASES\")\n        print(\"=\"*50)\n        plot_category(worst_samples, X_test, y_test, y_pred, \"Worst\", pred_cmap)\n\ndef plot_category(samples, X_test, y_test, y_pred, category_name, cmap):\n    \"\"\"Plot a category of samples\"\"\"\n    n_samples = len(samples)\n    fig, axes = plt.subplots(n_samples, 4, figsize=(20, 5*n_samples))\n    \n    if n_samples == 1:\n        axes = [axes]  # Make it 2D\n    \n    for i, (idx, accuracy, dice) in enumerate(samples):\n        if n_samples == 1:\n            row_axes = axes\n        else:\n            row_axes = axes[i]\n        \n        # Input MRI\n        row_axes[0].imshow(X_test[idx][:, :, 0], cmap='gray')\n        row_axes[0].set_title(f'{category_name} Case {i+1}\\nInput MRI')\n        row_axes[0].axis('off')\n        \n        # Ground Truth\n        row_axes[1].imshow(y_test[idx], cmap=cmap, vmin=0, vmax=3)\n        row_axes[1].set_title('Ground Truth')\n        row_axes[1].axis('off')\n        \n        # Prediction\n        row_axes[2].imshow(y_pred[idx], cmap=cmap, vmin=0, vmax=3)\n        row_axes[2].set_title(f'Prediction\\nDice: {dice:.3f}')\n        row_axes[2].axis('off')\n        \n        # Differences (Yellow highlights errors)\n        differences = (y_test[idx] != y_pred[idx])\n        row_axes[3].imshow(X_test[idx][:, :, 0], cmap='gray')\n        row_axes[3].imshow(differences, cmap='YlOrRd', alpha=0.7)\n        row_axes[3].set_title(f'Errors (Yellow)\\nAccuracy: {accuracy:.3f}')\n        row_axes[3].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\ndef calculate_tumor_dice(true_mask, pred_mask):\n    \"\"\"Calculate Dice coefficient for tumor regions only\"\"\"\n    true_tumor = (true_mask > 0)\n    pred_tumor = (pred_mask > 0)\n    \n    if np.sum(true_tumor) + np.sum(pred_tumor) == 0:\n        return 1.0\n    \n    intersection = np.sum(true_tumor & pred_tumor)\n    return (2. * intersection) / (np.sum(true_tumor) + np.sum(pred_tumor))\n\ndef print_simple_guide():\n    \"\"\"\n    Simple, clear guide\n    \"\"\"\n    print(\"🎯 SIMPLE INTERPRETATION GUIDE\")\n    print(\"=\"*50)\n    print(\"🔴 RED = Necrotic Core (dead tumor)\")\n    print(\"🟢 GREEN = Edema (swelling)\")  \n    print(\"🔵 BLUE = Enhancing Tumor (active tumor)\")\n    print(\"⚫ BLACK = Healthy brain\")\n    print(\"🟡 YELLOW = Model Errors (where prediction is wrong)\")\n    print(\"=\"*50)\n\n# Run the simple visualization\nprint_simple_guide()\n\nprint(\"\\n📈 ORGANIZED RESULTS BY PERFORMANCE\")\nsimple_visualization(results['X_test'], results['y_test'], results['y_pred'], samples_per_category=3)\n\n# Show only errors in a separate view\ndef show_only_errors(X_test, y_test, y_pred, num_samples=6):\n    \"\"\"\n    Show only the error patterns across different samples\n    \"\"\"\n    print(f\"\\n🟡 ERROR PATTERNS ACROSS SAMPLES\")\n    print(\"=\"*50)\n    \n    # Find samples with errors\n    error_samples = []\n    for i in range(len(X_test)):\n        if np.sum(y_test[i] > 0) > 20:  # Has tumor\n            error_pixels = np.sum(y_test[i] != y_pred[i])\n            if error_pixels > 10:  # Has meaningful errors\n                accuracy = np.mean(y_test[i] == y_pred[i])\n                error_samples.append((i, accuracy, error_pixels))\n    \n    error_samples.sort(key=lambda x: x[2], reverse=True)  # Sort by most errors\n    selected_samples = error_samples[:num_samples]\n    \n    n_samples = len(selected_samples)\n    fig, axes = plt.subplots(n_samples, 3, figsize=(15, 5*n_samples))\n    \n    if n_samples == 1:\n        axes = [axes]\n    \n    for i, (idx, accuracy, error_count) in enumerate(selected_samples):\n        if n_samples == 1:\n            row_axes = axes\n        else:\n            row_axes = axes[i]\n        \n        # Input\n        row_axes[0].imshow(X_test[idx][:, :, 0], cmap='gray')\n        row_axes[0].set_title(f'Sample {i+1}\\nInput MRI')\n        row_axes[0].axis('off')\n        \n        # Ground Truth\n        row_axes[1].imshow(y_test[idx], cmap=ListedColormap(['black', 'red', 'green', 'blue']), vmin=0, vmax=3)\n        row_axes[1].set_title('Ground Truth')\n        row_axes[1].axis('off')\n        \n        # Errors Only (Yellow)\n        differences = (y_test[idx] != y_pred[idx])\n        row_axes[2].imshow(X_test[idx][:, :, 0], cmap='gray', alpha=0.5)\n        row_axes[2].imshow(differences, cmap='YlOrRd', alpha=0.8)\n        row_axes[2].set_title(f'Errors: {error_count} pixels\\nAccuracy: {accuracy:.3f}')\n        row_axes[2].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\nprint(\"\\n🟡 FOCUS ON ERROR PATTERNS\")\nshow_only_errors(results['X_test'], results['y_test'], results['y_pred'], num_samples=6)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T16:31:54.946349Z","iopub.execute_input":"2025-12-09T16:31:54.947045Z","iopub.status.idle":"2025-12-09T16:31:59.271009Z","shell.execute_reply.started":"2025-12-09T16:31:54.947019Z","shell.execute_reply":"2025-12-09T16:31:59.270201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, h5py, cv2, gc\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import confusion_matrix\nfrom glob import glob\nimport tensorflow as tf\n\n# ============================================================================\n# Configuration\n# ============================================================================\n\nIMG_SIZE = 128\nNUM_CLASSES = 4\nMAX_TEST_SLICES = 500  # Load 500 test slices\n\n# Paths (from training code)\nDATA_ROOT = \"/kaggle/input/brats2020-training-data/BraTS2020_training_data/content/data\"\nMODEL_PATH = 'best_unet_brats2020.keras'\n\n# ============================================================================\n# Dataset Information\n# ============================================================================\n\nCLASS_NAMES = {\n    0: 'Background',\n    1: 'Necrotic Core (NCR)',\n    2: 'Edema (ED)',\n    3: 'Enhancing Tumor (ET)'\n}\n\nCLASS_COLORS = {\n    0: [0, 0, 0],      # Black\n    1: [255, 0, 0],    # Red\n    2: [0, 255, 0],    # Green\n    3: [0, 0, 255]     # Blue\n}\n\n# ============================================================================\n# Load Test Data\n# ============================================================================\n\ndef load_h5_slice(filepath):\n    \"\"\"Load a single H5 file\"\"\"\n    with h5py.File(filepath, 'r') as f:\n        image = f['image'][:]\n        mask = f['mask'][:]\n    \n    image = cv2.resize(image, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_LINEAR)\n    \n    if len(mask.shape) == 3:\n        mask = np.argmax(mask, axis=-1).astype(np.uint8)\n    \n    mask = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n    \n    image = image.astype(np.float32)\n    if image.max() > 0:\n        image = image / image.max()\n    \n    mask = mask.astype(np.int32)\n    \n    return image, mask\n\ndef load_test_data():\n    \"\"\"Load test dataset\"\"\"\n    print(\"Loading test data...\")\n    \n    h5_files = sorted(glob(os.path.join(DATA_ROOT, \"volume_*_slice_*.h5\")))\n    \n    # Take last 500 files as test set\n    test_files = h5_files[-MAX_TEST_SLICES:]\n    \n    images = []\n    masks = []\n    \n    for idx, filepath in enumerate(test_files):\n        if (idx + 1) % 100 == 0:\n            print(f\"  Loaded {idx+1}/{len(test_files)} slices...\")\n        \n        try:\n            img, mask = load_h5_slice(filepath)\n            if np.sum(mask > 0) > 50:\n                images.append(img)\n                masks.append(mask)\n        except:\n            continue\n    \n    X_test = np.array(images, dtype=np.float32)\n    y_test = np.array(masks, dtype=np.int32)\n    \n    print(f\"Test data loaded: {X_test.shape}\")\n    return X_test, y_test\n\n# ============================================================================\n# Metrics\n# ============================================================================\n\ndef compute_metrics(y_true, y_pred):\n    \"\"\"Compute comprehensive metrics\"\"\"\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"SEGMENTATION METRICS\")\n    print(\"=\"*80)\n    \n    y_true_flat = y_true.flatten()\n    y_pred_flat = y_pred.flatten()\n    \n    overall_acc = np.mean(y_true_flat == y_pred_flat)\n    print(f\"\\nOverall Pixel Accuracy: {overall_acc:.4f} ({overall_acc*100:.2f}%)\")\n    \n    print(\"\\nPER-CLASS PERFORMANCE:\")\n    print(\"-\" * 80)\n    print(f\"{'Class':<25} {'Dice':<12} {'IoU':<12} {'Precision':<12} {'Recall':<12}\")\n    print(\"-\" * 80)\n    \n    dice_scores = []\n    iou_scores = []\n    \n    for class_id in range(NUM_CLASSES):\n        true_mask = (y_true_flat == class_id)\n        pred_mask = (y_pred_flat == class_id)\n        \n        tp = np.sum(true_mask & pred_mask)\n        fp = np.sum(~true_mask & pred_mask)\n        fn = np.sum(true_mask & ~pred_mask)\n        \n        dice = (2 * tp) / (2 * tp + fp + fn + 1e-7)\n        iou = tp / (tp + fp + fn + 1e-7)\n        precision = tp / (tp + fp + 1e-7)\n        recall = tp / (tp + fn + 1e-7)\n        \n        dice_scores.append(dice)\n        iou_scores.append(iou)\n        \n        class_name = CLASS_NAMES[class_id]\n        print(f\"{class_name:<25} {dice:.4f}      {iou:.4f}      {precision:.4f}      {recall:.4f}\")\n    \n    print(\"-\" * 80)\n    \n    avg_dice = np.mean(dice_scores[1:])\n    avg_iou = np.mean(iou_scores[1:])\n    \n    print(f\"\\nAVERAGE TUMOR METRICS (Classes 1-3):\")\n    print(f\"  Mean Dice Score: {avg_dice:.4f} ({avg_dice*100:.2f}%)\")\n    print(f\"  Mean IoU:        {avg_iou:.4f} ({avg_iou*100:.2f}%)\")\n    print(\"=\"*80)\n    \n    return {\n        'overall_accuracy': overall_acc,\n        'dice_scores': dice_scores,\n        'iou_scores': iou_scores,\n        'avg_dice': avg_dice,\n        'avg_iou': avg_iou\n    }\n\n# ============================================================================\n# Visualization\n# ============================================================================\n\ndef mask_to_rgb(mask):\n    \"\"\"Convert mask to RGB\"\"\"\n    h, w = mask.shape\n    rgb = np.zeros((h, w, 3), dtype=np.uint8)\n    for class_id, color in CLASS_COLORS.items():\n        rgb[mask == class_id] = color\n    return rgb\n\ndef create_error_map(y_true, y_pred):\n    \"\"\"Create error visualization\"\"\"\n    h, w = y_true.shape\n    error_map = np.zeros((h, w, 3), dtype=np.uint8)\n    \n    fp = (y_true == 0) & (y_pred > 0)\n    error_map[fp] = [255, 0, 0]  # Red - False Positive\n    \n    fn = (y_true > 0) & (y_pred == 0)\n    error_map[fn] = [255, 255, 0]  # Yellow - False Negative\n    \n    confusion = (y_true != y_pred) & (y_true > 0) & (y_pred > 0)\n    error_map[confusion] = [255, 255, 255]  # White - Wrong Class\n    \n    return error_map\n\ndef visualize_results(X_test, y_test, y_pred, num_samples=7):\n    \"\"\"Create comprehensive visualizations\"\"\"\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"GENERATING VISUALIZATIONS\")\n    print(\"=\"*80)\n    \n    # Select diverse samples\n    tumor_sizes = [np.sum(y_test[i] > 0) for i in range(len(y_test))]\n    sorted_indices = np.argsort(tumor_sizes)\n    step = len(sorted_indices) // num_samples\n    indices = [sorted_indices[i * step] for i in range(num_samples)]\n    \n    for sample_idx, idx in enumerate(indices):\n        fig = plt.figure(figsize=(20, 12))\n        fig.suptitle(f'Sample {sample_idx + 1}: Brain Tumor Segmentation Analysis', \n                     fontsize=16, fontweight='bold')\n        \n        img = X_test[idx]\n        true_mask = y_test[idx]\n        pred_mask = y_pred[idx]\n        \n        correct_pixels = np.sum(true_mask == pred_mask)\n        total_pixels = true_mask.size\n        sample_acc = correct_pixels / total_pixels\n        \n        tumor_pixels_true = np.sum(true_mask > 0)\n        tumor_pixels_pred = np.sum(pred_mask > 0)\n        \n        # Row 1: MRI inputs and basic comparison\n        plt.subplot(3, 5, 1)\n        plt.imshow(img[:, :, 0], cmap='gray')\n        plt.title('MRI - Channel 1', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 2)\n        plt.imshow(img[:, :, min(1, img.shape[2]-1)], cmap='gray')\n        plt.title('MRI - Channel 2', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 3)\n        plt.imshow(img[:, :, min(2, img.shape[2]-1)], cmap='gray')\n        plt.title('MRI - Channel 3', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 4)\n        true_rgb = mask_to_rgb(true_mask)\n        plt.imshow(true_rgb)\n        plt.title(f'Ground Truth\\nTumor: {tumor_pixels_true} px', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 5)\n        pred_rgb = mask_to_rgb(pred_mask)\n        plt.imshow(pred_rgb)\n        plt.title(f'Prediction\\nTumor: {tumor_pixels_pred} px\\nAcc: {sample_acc*100:.1f}%', fontsize=10)\n        plt.axis('off')\n        \n        # Row 2: Overlays and error analysis\n        plt.subplot(3, 5, 6)\n        plt.imshow(img[:, :, 0], cmap='gray')\n        plt.imshow(true_rgb, alpha=0.5)\n        plt.title('Overlay: MRI + Truth', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 7)\n        plt.imshow(img[:, :, 0], cmap='gray')\n        plt.imshow(pred_rgb, alpha=0.5)\n        plt.title('Overlay: MRI + Prediction', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 8)\n        error_map = create_error_map(true_mask, pred_mask)\n        plt.imshow(error_map)\n        plt.title('Error Map\\nRed: FP | Yellow: FN | White: Wrong', fontsize=9)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 9)\n        comparison = np.concatenate([true_rgb, pred_rgb], axis=1)\n        plt.imshow(comparison)\n        plt.title('Side-by-Side\\nLeft: Truth | Right: Pred', fontsize=10)\n        plt.axis('off')\n        \n        plt.subplot(3, 5, 10)\n        plt.imshow(img[:, :, 0], cmap='gray')\n        diff_mask = (true_mask != pred_mask).astype(float)\n        plt.imshow(diff_mask, cmap='Reds', alpha=0.7)\n        plt.title('Difference Highlighting', fontsize=10)\n        plt.axis('off')\n        \n        # Row 3: Per-class analysis\n        class_titles = ['Background', 'Necrotic Core', 'Edema', 'Enhancing', 'Legend']\n        \n        for i, class_id in enumerate([0, 1, 2, 3]):\n            plt.subplot(3, 5, 11 + i)\n            \n            # FIX: Convert to boolean arrays explicitly\n            true_binary = (true_mask == class_id).astype(bool)\n            pred_binary = (pred_mask == class_id).astype(bool)\n            \n            overlay = np.zeros((true_mask.shape[0], true_mask.shape[1], 3))\n            overlay[true_binary & pred_binary] = [0, 1, 0]  # Correct (Green)\n            overlay[(~true_binary) & pred_binary] = [1, 0, 0]  # FP (Red)\n            overlay[true_binary & (~pred_binary)] = [1, 1, 0]  # FN (Yellow)\n            \n            plt.imshow(img[:, :, 0], cmap='gray')\n            plt.imshow(overlay, alpha=0.6)\n            \n            tp = np.sum(true_binary & pred_binary)\n            fp = np.sum((~true_binary) & pred_binary)\n            fn = np.sum(true_binary & (~pred_binary))\n            dice = (2 * tp) / (2 * tp + fp + fn + 1e-7)\n            \n            plt.title(f'{class_titles[i]}\\nDice: {dice:.3f}', fontsize=9)\n            plt.axis('off')\n        \n        # Legend\n        plt.subplot(3, 5, 15)\n        plt.axis('off')\n        legend_text = \"COLOR LEGEND:\\n\\n\"\n        legend_text += \"Classes:\\n\"\n        legend_text += \"Red: Necrotic\\n\"\n        legend_text += \"Green: Edema\\n\"\n        legend_text += \"Blue: Enhancing\\n\"\n        legend_text += \"Black: Background\\n\\n\"\n        legend_text += \"Errors:\\n\"\n        legend_text += \"Green: Correct\\n\"\n        legend_text += \"Red: False Pos\\n\"\n        legend_text += \"Yellow: False Neg\\n\"\n        plt.text(0.1, 0.5, legend_text, fontsize=9, verticalalignment='center',\n                fontfamily='monospace', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))\n        \n        plt.tight_layout()\n        plt.show()\n        \n        print(f\"\\nSample {sample_idx + 1}:\")\n        print(f\"  Accuracy: {sample_acc*100:.2f}%\")\n        print(f\"  Tumor pixels - True: {tumor_pixels_true}, Pred: {tumor_pixels_pred}\")\n\ndef plot_confusion_matrix(y_test, y_pred):\n    \"\"\"Plot confusion matrix\"\"\"\n    \n    print(\"\\nGenerating Confusion Matrix...\")\n    \n    y_true_flat = y_test.flatten()\n    y_pred_flat = y_pred.flatten()\n    \n    cm = confusion_matrix(y_true_flat, y_pred_flat)\n    cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n    \n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))\n    \n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax1,\n                xticklabels=CLASS_NAMES.values(),\n                yticklabels=CLASS_NAMES.values())\n    ax1.set_title('Confusion Matrix (Counts)', fontsize=14, fontweight='bold')\n    ax1.set_ylabel('True Label', fontsize=12)\n    ax1.set_xlabel('Predicted Label', fontsize=12)\n    \n    sns.heatmap(cm_normalized, annot=True, fmt='.2%', cmap='Blues', ax=ax2,\n                xticklabels=CLASS_NAMES.values(),\n                yticklabels=CLASS_NAMES.values())\n    ax2.set_title('Confusion Matrix (Normalized)', fontsize=14, fontweight='bold')\n    ax2.set_ylabel('True Label', fontsize=12)\n    ax2.set_xlabel('Predicted Label', fontsize=12)\n    \n    plt.tight_layout()\n    plt.show()\n\ndef plot_metrics_summary(metrics):\n    \"\"\"Plot metrics bar charts\"\"\"\n    \n    print(\"\\nGenerating Metrics Summary...\")\n    \n    fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n    \n    dice_scores = metrics['dice_scores']\n    classes = list(CLASS_NAMES.values())\n    colors = ['black', 'red', 'green', 'blue']\n    \n    axes[0].bar(classes, dice_scores, color=colors, alpha=0.7, edgecolor='black')\n    axes[0].set_ylabel('Dice Score', fontsize=12)\n    axes[0].set_title('Dice Score per Class', fontsize=14, fontweight='bold')\n    axes[0].set_ylim([0, 1])\n    axes[0].axhline(y=0.85, color='orange', linestyle='--', label='Good (85%)')\n    axes[0].axhline(y=0.90, color='green', linestyle='--', label='Excellent (90%)')\n    axes[0].legend()\n    axes[0].grid(axis='y', alpha=0.3)\n    \n    for i, (cls, score) in enumerate(zip(classes, dice_scores)):\n        axes[0].text(i, score + 0.02, f'{score:.3f}', ha='center', fontsize=10, fontweight='bold')\n    \n    iou_scores = metrics['iou_scores']\n    \n    axes[1].bar(classes, iou_scores, color=colors, alpha=0.7, edgecolor='black')\n    axes[1].set_ylabel('IoU Score', fontsize=12)\n    axes[1].set_title('IoU per Class', fontsize=14, fontweight='bold')\n    axes[1].set_ylim([0, 1])\n    axes[1].axhline(y=0.75, color='orange', linestyle='--', label='Good (75%)')\n    axes[1].axhline(y=0.85, color='green', linestyle='--', label='Excellent (85%)')\n    axes[1].legend()\n    axes[1].grid(axis='y', alpha=0.3)\n    \n    for i, (cls, score) in enumerate(zip(classes, iou_scores)):\n        axes[1].text(i, score + 0.02, f'{score:.3f}', ha='center', fontsize=10, fontweight='bold')\n    \n    plt.tight_layout()\n    plt.show()\n\n# ============================================================================\n# Main Testing\n# ============================================================================\n\nprint(\"=\"*80)\nprint(\"BRATS2020 BRAIN TUMOR SEGMENTATION - COMPREHENSIVE TESTING\")\nprint(\"=\"*80)\n\n# Load model\nprint(f\"\\nLoading model from: {MODEL_PATH}\")\nmodel = tf.keras.models.load_model(MODEL_PATH, compile=False)\nprint(\"Model loaded successfully\")\n\n# Load test data\nX_test, y_test = load_test_data()\n\n# Generate predictions\nprint(\"\\nGenerating predictions...\")\ny_pred_probs = model.predict(X_test, batch_size=32, verbose=1)\ny_pred = np.argmax(y_pred_probs, axis=-1)\nprint(\"Predictions complete\")\n\n# Compute metrics\nmetrics = compute_metrics(y_test, y_pred)\n\n# Generate visualizations\nvisualize_results(X_test, y_test, y_pred, num_samples=7)\nplot_confusion_matrix(y_test, y_pred)\nplot_metrics_summary(metrics)\n\nprint(\"\\n\" + \"=\"*80)\nprint(\"TESTING COMPLETE\")\nprint(\"=\"*80)\nprint(f\"\\nFinal Results:\")\nprint(f\"  Average Dice (Tumor): {metrics['avg_dice']*100:.2f}%\")\nprint(f\"  Overall Accuracy: {metrics['overall_accuracy']*100:.2f}%\")\nprint(\"=\"*80)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-09T17:49:39.633579Z","iopub.execute_input":"2025-12-09T17:49:39.634451Z","iopub.status.idle":"2025-12-09T17:50:04.022043Z","shell.execute_reply.started":"2025-12-09T17:49:39.634424Z","shell.execute_reply":"2025-12-09T17:50:04.021298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport json\nimport albumentations as A\nfrom pycocotools.coco import COCO\nfrom glob import glob\nimport random\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\n\n# Configuration\nIMG_SIZE = 256\nTARGET_SAMPLES = 10000  # We want 10,000+ images\nSEED = 42\n\nprint(\"=\"*70)\nprint(\"🚀 FOOTBALL PLAYER SEGMENTATION - DATA AUGMENTATION PIPELINE\")\nprint(\"=\"*70)\n\nclass FootballDataAugmentor:\n    def __init__(self, data_root, output_dir, img_size=256):\n        self.data_root = data_root\n        self.output_dir = output_dir\n        self.img_size = img_size\n        self.annotations_path = os.path.join(data_root, \"annotations\", \"instances_default.json\")\n        self.images_dir = os.path.join(data_root, \"images\")\n        \n        # Create output directories\n        self.augmented_images_dir = os.path.join(output_dir, \"augmented_images\")\n        self.augmented_masks_dir = os.path.join(output_dir, \"augmented_masks\")\n        os.makedirs(self.augmented_images_dir, exist_ok=True)\n        os.makedirs(self.augmented_masks_dir, exist_ok=True)\n        \n        # Load COCO annotations\n        self.coco = COCO(self.annotations_path)\n        self.image_ids = self.coco.getImgIds()\n        \n        print(f\"📊 Original Dataset: {len(self.image_ids)} images\")\n    \n    def load_image_and_mask(self, image_id):\n        \"\"\"Load original image and mask\"\"\"\n        image_info = self.coco.loadImgs(image_id)[0]\n        image_path = os.path.join(self.images_dir, image_info['file_name'])\n        \n        # Load image\n        image = cv2.imread(image_path)\n        if image is None:\n            raise ValueError(f\"Could not load image: {image_path}\")\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Get annotations and create mask\n        annotation_ids = self.coco.getAnnIds(imgIds=image_id)\n        annotations = self.coco.loadAnns(annotation_ids)\n        \n        mask = np.zeros((image_info['height'], image_info['width']), dtype=np.uint8)\n        \n        for annotation in annotations:\n            if annotation['iscrowd'] == 0 and 'segmentation' in annotation:\n                for seg in annotation['segmentation']:\n                    poly = np.array(seg).reshape((-1, 2))\n                    cv2.fillPoly(mask, [poly.astype(np.int32)], 1)\n        \n        return image, mask, image_info['file_name']\n    \n    def get_advanced_augmentations(self):\n        \"\"\"Create multiple augmentation pipelines for variety\"\"\"\n        \n        # Pipeline 1: Geometric transformations\n        geometric_aug = A.Compose([\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.2),\n            A.RandomRotate90(p=0.5),\n            A.Rotate(limit=30, p=0.6),\n            A.ShiftScaleRotate(\n                shift_limit=0.2, scale_limit=0.3, rotate_limit=25,\n                border_mode=cv2.BORDER_CONSTANT, value=0, p=0.7\n            ),\n            A.Perspective(scale=(0.05, 0.15), keep_size=True, p=0.3),\n            A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, p=0.2),\n        ])\n        \n        # Pipeline 2: Color destruction\n        color_aug = A.Compose([\n            A.OneOf([\n                A.ToGray(p=0.4),\n                A.RGBShift(r_shift_limit=50, g_shift_limit=50, b_shift_limit=50, p=0.4),\n                A.ChannelShuffle(p=0.2),\n            ], p=0.8),\n            A.OneOf([\n                A.RandomBrightnessContrast(brightness_limit=0.5, contrast_limit=0.5, p=0.5),\n                A.RandomGamma(gamma_limit=(70, 130), p=0.3),\n                A.CLAHE(clip_limit=4.0, p=0.2),\n            ], p=0.7),\n            A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20, p=0.4),\n        ])\n        \n        # Pipeline 3: Noise and blur\n        noise_aug = A.Compose([\n            A.OneOf([\n                A.GaussianBlur(blur_limit=(3, 7), p=0.3),\n                A.MotionBlur(blur_limit=(3, 7), p=0.2),\n                A.MedianBlur(blur_limit=3, p=0.2),\n                A.GaussNoise(var_limit=(10.0, 100.0), p=0.3),\n            ], p=0.6),\n            A.ImageCompression(quality_lower=30, quality_upper=80, p=0.3),\n        ])\n        \n        # Pipeline 4: Cropping variations\n        crop_aug = A.Compose([\n            A.OneOf([\n                A.RandomCrop(height=180, width=180, p=0.3),\n                A.RandomCrop(height=200, width=200, p=0.3),\n                A.RandomCrop(height=220, width=220, p=0.2),\n                A.CenterCrop(height=224, width=224, p=0.2),\n            ], p=0.8),\n            A.Resize(IMG_SIZE, IMG_SIZE, always_apply=True),\n        ])\n        \n        return [geometric_aug, color_aug, noise_aug, crop_aug]\n    \n    def change_jersey_colors(self, image, mask):\n        \"\"\"Advanced jersey color changing - multiple players can have different colors\"\"\"\n        player_mask = mask == 1\n        \n        if np.sum(player_mask) > 100:  # Only if there are players\n            # Find connected components to identify individual players\n            num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(\n                player_mask.astype(np.uint8), connectivity=8\n            )\n            \n            augmented_image = image.copy()\n            \n            # Jersey colors (avoid white)\n            jersey_colors = [\n                [255, 0, 0], [0, 255, 0], [0, 0, 255],          # Red, Green, Blue\n                [255, 255, 0], [255, 0, 255], [0, 255, 255],    # Yellow, Magenta, Cyan\n                [128, 0, 0], [0, 128, 0], [0, 0, 128],          # Dark Red, Green, Blue\n                [255, 128, 0], [128, 0, 128], [0, 128, 128],    # Orange, Purple, Teal\n                [200, 100, 0], [100, 200, 0], [0, 100, 200],    # Brown variants\n            ]\n            \n            # Apply different colors to different players\n            for label in range(1, num_labels):  # Skip background\n                if stats[label, cv2.CC_STAT_AREA] > 200:  # Only substantial players\n                    player_region = labels == label\n                    jersey_color = random.choice(jersey_colors)\n                    \n                    # Apply color with some variation\n                    for channel in range(3):\n                        color_variation = random.randint(-30, 30)\n                        actual_color = max(0, min(255, jersey_color[channel] + color_variation))\n                        augmented_image[player_region, channel] = actual_color\n            \n            return augmented_image\n        \n        return image\n    \n    def add_synthetic_text(self, image):\n        \"\"\"Add synthetic text to simulate jersey numbers and text\"\"\"\n        if random.random() < 0.4:  # 40% chance to add text\n            augmented_image = image.copy()\n            h, w = image.shape[:2]\n            \n            # Possible text to add (jersey numbers, team names, etc.)\n            texts = [\n                \"7\", \"9\", \"10\", \"11\", \"17\", \"21\", \"23\", \n                \"Messi\", \"Ronaldo\", \"Neymar\", \"Mbappe\", \"Team A\", \"Team B\",\n                \"FC\", \"United\", \"City\", \"FCB\", \"RM\", \"JUVE\"\n            ]\n            \n            # Add 1-3 text elements\n            for _ in range(random.randint(1, 3)):\n                text = random.choice(texts)\n                font = cv2.FONT_HERSHEY_SIMPLEX\n                font_scale = random.uniform(0.5, 1.5)\n                thickness = random.randint(1, 3)\n                \n                # Random position (avoid edges)\n                x = random.randint(20, w - 100)\n                y = random.randint(30, h - 20)\n                \n                # Random color\n                color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))\n                \n                # Add text\n                cv2.putText(augmented_image, text, (x, y), font, font_scale, color, thickness)\n            \n            return augmented_image\n        \n        return image\n    \n    def copy_paste_players(self, image, mask, all_images, all_masks):\n        \"\"\"Copy players from other images and paste them\"\"\"\n        if random.random() < 0.5 and len(all_images) > 1:  # 50% chance\n            augmented_image = image.copy()\n            augmented_mask = mask.copy()\n            \n            # Select source image\n            src_idx = random.randint(0, len(all_images) - 1)\n            src_image = all_images[src_idx].copy()\n            src_mask = all_masks[src_idx].copy()\n            \n            # Find players in source\n            src_player_coords = np.where(src_mask == 1)\n            if len(src_player_coords[0]) > 100:\n                y_coords, x_coords = src_player_coords\n                y_min, y_max = np.min(y_coords), np.max(y_coords)\n                x_min, x_max = np.min(x_coords), np.max(x_coords)\n                \n                # Extract player\n                player_height = y_max - y_min\n                player_width = x_max - x_min\n                \n                # Find valid position in destination\n                max_y = IMG_SIZE - player_height - 1\n                max_x = IMG_SIZE - player_width - 1\n                \n                if max_y > 0 and max_x > 0:\n                    new_y = random.randint(0, max_y)\n                    new_x = random.randint(0, max_x)\n                    \n                    # Extract player region\n                    player_img = src_image[y_min:y_max, x_min:x_max]\n                    player_mask_region = src_mask[y_min:y_max, x_min:x_max]\n                    \n                    # Paste player\n                    augmented_image[new_y:new_y+player_height, new_x:new_x+player_width] = np.where(\n                        player_mask_region[:, :, np.newaxis] == 1,\n                        player_img,\n                        augmented_image[new_y:new_y+player_height, new_x:new_x+player_width]\n                    )\n                    augmented_mask[new_y:new_y+player_height, new_x:new_x+player_width] = np.where(\n                        player_mask_region == 1,\n                        1,\n                        augmented_mask[new_y:new_y+player_height, new_x:new_x+player_width]\n                    )\n            \n            return augmented_image, augmented_mask\n        \n        return image, mask\n    \n    def generate_augmented_samples(self, target_samples=10000):\n        \"\"\"Generate augmented dataset\"\"\"\n        print(f\"\\n🎯 Generating {target_samples} augmented samples...\")\n        \n        # First, load all original images\n        print(\"📥 Loading original images...\")\n        original_images = []\n        original_masks = []\n        original_filenames = []\n        \n        for image_id in tqdm(self.image_ids, desc=\"Loading originals\"):\n            try:\n                image, mask, filename = self.load_image_and_mask(image_id)\n                # Resize to target size\n                image = cv2.resize(image, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_AREA)\n                mask = cv2.resize(mask, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)\n                original_images.append(image)\n                original_masks.append(mask)\n                original_filenames.append(filename)\n            except Exception as e:\n                print(f\"⚠️ Error loading {image_id}: {e}\")\n                continue\n        \n        print(f\"✅ Loaded {len(original_images)} original images\")\n        \n        # Get augmentation pipelines\n        augmentation_pipelines = self.get_advanced_augmentations()\n        \n        # Calculate how many augmentations per original image\n        augs_per_image = max(1, target_samples // len(original_images))\n        print(f\"🔧 Generating ~{augs_per_image} variations per original image\")\n        \n        generated_count = 0\n        pbar = tqdm(total=target_samples, desc=\"Generating augmented samples\")\n        \n        # Save original images first\n        for i, (image, mask, filename) in enumerate(zip(original_images, original_masks, original_filenames)):\n            base_name = f\"original_{i:04d}\"\n            cv2.imwrite(os.path.join(self.augmented_images_dir, f\"{base_name}.png\"), \n                       cv2.cvtColor(image, cv2.COLOR_RGB2BGR))\n            cv2.imwrite(os.path.join(self.augmented_masks_dir, f\"{base_name}.png\"), mask * 255)\n            generated_count += 1\n            pbar.update(1)\n        \n        # Generate augmented samples\n        for i, (image, mask, filename) in enumerate(zip(original_images, original_masks, original_filenames)):\n            for aug_idx in range(augs_per_image):\n                if generated_count >= target_samples:\n                    break\n                \n                try:\n                    # Apply random augmentation pipeline\n                    aug_pipeline = random.choice(augmentation_pipelines)\n                    \n                    # Convert to uint8 for albumentations\n                    image_uint8 = image.astype(np.uint8)\n                    \n                    # Apply augmentation\n                    augmented = aug_pipeline(image=image_uint8, mask=mask)\n                    aug_image = augmented['image']\n                    aug_mask = augmented['mask']\n                    \n                    # Apply jersey color changes\n                    aug_image = self.change_jersey_colors(aug_image, aug_mask)\n                    \n                    # Add synthetic text\n                    aug_image = self.add_synthetic_text(aug_image)\n                    \n                    # Occasionally copy-paste players from other images\n                    if random.random() < 0.3:  # 30% chance\n                        aug_image, aug_mask = self.copy_paste_players(\n                            aug_image, aug_mask, original_images, original_masks\n                        )\n                    \n                    # Save augmented sample\n                    base_name = f\"aug_{i:04d}_{aug_idx:03d}\"\n                    cv2.imwrite(os.path.join(self.augmented_images_dir, f\"{base_name}.png\"), \n                               cv2.cvtColor(aug_image, cv2.COLOR_RGB2BGR))\n                    cv2.imwrite(os.path.join(self.augmented_masks_dir, f\"{base_name}.png\"), aug_mask * 255)\n                    \n                    generated_count += 1\n                    pbar.update(1)\n                    \n                except Exception as e:\n                    print(f\"⚠️ Augmentation failed for image {i}: {e}\")\n                    continue\n        \n        pbar.close()\n        \n        print(f\"\\n✅ Augmentation complete!\")\n        print(f\"📁 Generated {generated_count} samples\")\n        print(f\"📁 Images saved to: {self.augmented_images_dir}\")\n        print(f\"📁 Masks saved to: {self.augmented_masks_dir}\")\n        \n        return generated_count\n\n# Run the augmentation\ndef main_augmentation():\n    data_root = \"/kaggle/input/football-player-segmentation\"\n    output_dir = \"/kaggle/working/augmented_dataset\"\n    \n    augmentor = FootballDataAugmentor(data_root, output_dir, img_size=IMG_SIZE)\n    total_samples = augmentor.generate_augmented_samples(target_samples=TARGET_SAMPLES)\n    \n    print(f\"\\n🎉 AUGMENTATION PIPELINE COMPLETE!\")\n    print(f\"📊 Total samples generated: {total_samples}\")\n    print(f\"📁 Output directory: {output_dir}\")\n\nif __name__ == \"__main__\":\n    main_augmentation()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T19:34:26.468656Z","iopub.execute_input":"2025-11-29T19:34:26.469275Z","iopub.status.idle":"2025-11-29T19:36:07.888476Z","shell.execute_reply.started":"2025-11-29T19:34:26.469251Z","shell.execute_reply":"2025-11-29T19:36:07.887817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport tensorflow as tf\nfrom glob import glob\nfrom tqdm import tqdm\nimport albumentations as A\n\nprint(\"=\"*70)\nprint(\"🚀 EFFICIENT FOOTBALL PLAYER SEGMENTATION TRAINING\")\nprint(\"=\"*70)\n\n# Configuration\nIMG_SIZE = 256\nBATCH_SIZE = 16\nEPOCHS = 100\nSEED = 42\nNUM_CLASSES = 2\nLEARNING_RATE = 1e-4\n\n# Set seeds for reproducibility\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\n# ============================================================================\n# EFFICIENT DATA GENERATOR\n# ============================================================================\n\nclass EfficientDataGenerator(tf.keras.utils.Sequence):\n    \"\"\"Memory-efficient data generator with on-the-fly augmentation\"\"\"\n    \n    def __init__(self, image_paths, mask_paths, batch_size=16, img_size=256, \n                 augment=True, shuffle=True):\n        self.image_paths = image_paths\n        self.mask_paths = mask_paths\n        self.batch_size = batch_size\n        self.img_size = img_size\n        self.augment = augment\n        self.shuffle = shuffle\n        self.indexes = np.arange(len(self.image_paths))\n        \n        # Online augmentation pipeline (applied during training)\n        if self.augment:\n            self.transform = A.Compose([\n                A.HorizontalFlip(p=0.5),\n                A.VerticalFlip(p=0.2),\n                A.RandomRotate90(p=0.5),\n                A.ShiftScaleRotate(\n                    shift_limit=0.1, scale_limit=0.2, rotate_limit=20,\n                    border_mode=cv2.BORDER_CONSTANT, p=0.5\n                ),\n                A.OneOf([\n                    A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=1),\n                    A.RandomGamma(gamma_limit=(80, 120), p=1),\n                    A.CLAHE(clip_limit=2.0, p=1),\n                ], p=0.5),\n                A.OneOf([\n                    A.GaussianBlur(blur_limit=(3, 5), p=1),\n                    A.GaussNoise(std_limit=(5.0, 30.0), p=1),\n                ], p=0.3),\n                A.HueSaturationValue(\n                    hue_shift_limit=15, sat_shift_limit=25, val_shift_limit=15, p=0.4\n                ),\n                A.RGBShift(r_shift_limit=20, g_shift_limit=20, b_shift_limit=20, p=0.3),\n                A.CoarseDropout(\n                    max_holes=8, max_height=32, max_width=32,\n                    fill_value=0, p=0.3\n                ),\n            ])\n        else:\n            self.transform = None\n        \n        self.on_epoch_end()\n    \n    def __len__(self):\n        return int(np.ceil(len(self.image_paths) / self.batch_size))\n    \n    def __getitem__(self, index):\n        # Get batch indexes\n        indexes = self.indexes[index * self.batch_size:(index + 1) * self.batch_size]\n        \n        # Generate batch\n        X, y = self._generate_batch(indexes)\n        return X, y\n    \n    def on_epoch_end(self):\n        \"\"\"Shuffle indexes after each epoch\"\"\"\n        if self.shuffle:\n            np.random.shuffle(self.indexes)\n    \n    def _load_image_and_mask(self, img_path, mask_path):\n        \"\"\"Load and preprocess single image and mask\"\"\"\n        # Load image\n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = cv2.resize(image, (self.img_size, self.img_size), interpolation=cv2.INTER_AREA)\n        \n        # Load mask\n        mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)\n        mask = cv2.resize(mask, (self.img_size, self.img_size), interpolation=cv2.INTER_NEAREST)\n        mask = (mask > 128).astype(np.uint8)\n        \n        return image, mask\n    \n    def _generate_batch(self, indexes):\n        \"\"\"Generate one batch of data\"\"\"\n        X = np.zeros((len(indexes), self.img_size, self.img_size, 3), dtype=np.float32)\n        y = np.zeros((len(indexes), self.img_size, self.img_size), dtype=np.uint8)\n        \n        for i, idx in enumerate(indexes):\n            image, mask = self._load_image_and_mask(\n                self.image_paths[idx], \n                self.mask_paths[idx]\n            )\n            \n            # Apply augmentation\n            if self.augment and self.transform:\n                augmented = self.transform(image=image, mask=mask)\n                image = augmented['image']\n                mask = augmented['mask']\n            \n            # Normalize image\n            X[i] = image.astype(np.float32) / 255.0\n            y[i] = mask\n        \n        return X, y\n\n# ============================================================================\n# U-NET MODEL (Same architecture)\n# ============================================================================\n\ndef conv_block(x, filters, dropout=0.1):\n    \"\"\"Standard U-Net convolutional block\"\"\"\n    x = tf.keras.layers.Conv2D(filters, 3, padding='same', kernel_initializer='he_normal')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation('relu')(x)\n    x = tf.keras.layers.Dropout(dropout)(x)\n    \n    x = tf.keras.layers.Conv2D(filters, 3, padding='same', kernel_initializer='he_normal')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation('relu')(x)\n    \n    return x\n\ndef build_unet(input_shape=(IMG_SIZE, IMG_SIZE, 3), num_classes=NUM_CLASSES):\n    \"\"\"Build U-Net model for player segmentation\"\"\"\n    \n    inputs = tf.keras.layers.Input(input_shape)\n    \n    # Encoder\n    c1 = conv_block(inputs, 32, dropout=0.1)\n    p1 = tf.keras.layers.MaxPooling2D((2, 2))(c1)\n    \n    c2 = conv_block(p1, 64, dropout=0.1)\n    p2 = tf.keras.layers.MaxPooling2D((2, 2))(c2)\n    \n    c3 = conv_block(p2, 128, dropout=0.2)\n    p3 = tf.keras.layers.MaxPooling2D((2, 2))(c3)\n    \n    c4 = conv_block(p3, 256, dropout=0.2)\n    p4 = tf.keras.layers.MaxPooling2D((2, 2))(c4)\n    \n    # Bottleneck\n    c5 = conv_block(p4, 512, dropout=0.3)\n    \n    # Decoder\n    u6 = tf.keras.layers.Conv2DTranspose(256, (2, 2), strides=(2, 2), padding='same')(c5)\n    u6 = tf.keras.layers.concatenate([u6, c4])\n    c6 = conv_block(u6, 256, dropout=0.2)\n    \n    u7 = tf.keras.layers.Conv2DTranspose(128, (2, 2), strides=(2, 2), padding='same')(c6)\n    u7 = tf.keras.layers.concatenate([u7, c3])\n    c7 = conv_block(u7, 128, dropout=0.2)\n    \n    u8 = tf.keras.layers.Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same')(c7)\n    u8 = tf.keras.layers.concatenate([u8, c2])\n    c8 = conv_block(u8, 64, dropout=0.1)\n    \n    u9 = tf.keras.layers.Conv2DTranspose(32, (2, 2), strides=(2, 2), padding='same')(c8)\n    u9 = tf.keras.layers.concatenate([u9, c1])\n    c9 = conv_block(u9, 32, dropout=0.1)\n    \n    # Output\n    outputs = tf.keras.layers.Conv2D(num_classes, (1, 1), activation='softmax', dtype='float32', name='output')(c9)\n    \n    model = tf.keras.Model(inputs=[inputs], outputs=[outputs], name='UNet-FootballPlayers')\n    \n    return model\n\n# ============================================================================\n# LOSS FUNCTIONS AND METRICS (Same as before)\n# ============================================================================\n\ndef dice_coefficient(y_true, y_pred, smooth=1e-6):\n    y_pred = tf.cast(y_pred, tf.float32)\n    \n    y_true_f = tf.reshape(tf.cast(y_true, tf.int32), [-1])\n    y_pred_f = tf.reshape(y_pred, [-1, NUM_CLASSES])\n    y_true_oh = tf.one_hot(y_true_f, NUM_CLASSES, dtype=tf.float32)\n    \n    intersection = tf.reduce_sum(y_true_oh * y_pred_f, axis=0)\n    union = tf.reduce_sum(y_true_oh, axis=0) + tf.reduce_sum(y_pred_f, axis=0)\n    \n    dice = (2. * intersection + smooth) / (union + smooth)\n    return tf.reduce_mean(dice)\n\ndef dice_loss(y_true, y_pred):\n    return 1 - dice_coefficient(y_true, y_pred)\n\ndef focal_loss(y_true, y_pred, alpha=0.25, gamma=2.0):\n    y_pred = tf.cast(y_pred, tf.float32)\n    \n    y_true = tf.cast(y_true, tf.int32)\n    y_true = tf.one_hot(y_true, NUM_CLASSES, dtype=tf.float32)\n    y_true = tf.reshape(y_true, [-1, NUM_CLASSES])\n    y_pred = tf.reshape(y_pred, [-1, NUM_CLASSES])\n    \n    cross_entropy = -y_true * tf.math.log(y_pred + 1e-8)\n    weight = alpha * tf.pow(1 - y_pred, gamma)\n    focal_loss_val = weight * cross_entropy\n    return tf.reduce_mean(focal_loss_val)\n\ndef combined_loss(y_true, y_pred):\n    \"\"\"Combined Dice + Focal loss\"\"\"\n    dice_loss_val = dice_loss(y_true, y_pred)\n    focal_loss_val = focal_loss(y_true, y_pred)\n    return 0.5 * dice_loss_val + 0.5 * focal_loss_val\n\ndef iou_metric(y_true, y_pred):\n    \"\"\"Intersection over Union metric\"\"\"\n    y_pred = tf.cast(y_pred, tf.float32)\n    y_pred = tf.argmax(y_pred, axis=-1)\n    y_pred = tf.cast(y_pred, tf.int32)\n    y_true = tf.cast(y_true, tf.int32)\n    \n    intersection = tf.reduce_sum(tf.cast(y_true * y_pred, tf.float32))\n    union = tf.reduce_sum(tf.cast(y_true, tf.float32)) + tf.reduce_sum(tf.cast(y_pred, tf.float32)) - intersection\n    \n    return intersection / (union + 1e-8)\n\ndef precision_metric(y_true, y_pred):\n    \"\"\"Precision metric\"\"\"\n    y_pred = tf.cast(y_pred, tf.float32)\n    y_pred = tf.argmax(y_pred, axis=-1)\n    y_pred = tf.cast(y_pred, tf.int32)\n    y_true = tf.cast(y_true, tf.int32)\n    \n    true_positives = tf.reduce_sum(tf.cast(y_true * y_pred, tf.float32))\n    predicted_positives = tf.reduce_sum(tf.cast(y_pred, tf.float32))\n    \n    return true_positives / (predicted_positives + 1e-8)\n\ndef recall_metric(y_true, y_pred):\n    \"\"\"Recall metric\"\"\"\n    y_pred = tf.cast(y_pred, tf.float32)\n    y_pred = tf.argmax(y_pred, axis=-1)\n    y_pred = tf.cast(y_pred, tf.int32)\n    y_true = tf.cast(y_true, tf.int32)\n    \n    true_positives = tf.reduce_sum(tf.cast(y_true * y_pred, tf.float32))\n    actual_positives = tf.reduce_sum(tf.cast(y_true, tf.float32))\n    \n    return true_positives / (actual_positives + 1e-8)\n\n# ============================================================================\n# TRAINING FUNCTION\n# ============================================================================\n\ndef train_efficient():\n    \"\"\"Train model efficiently using data generators\"\"\"\n    \n    augmented_data_dir = \"/kaggle/working/augmented_dataset\"\n    images_dir = os.path.join(augmented_data_dir, \"augmented_images\")\n    masks_dir = os.path.join(augmented_data_dir, \"augmented_masks\")\n    \n    # Get all image and mask paths\n    print(\"📥 Loading file paths...\")\n    image_paths = sorted(glob(os.path.join(images_dir, \"*.png\")))\n    mask_paths = sorted(glob(os.path.join(masks_dir, \"*.png\")))\n    \n    print(f\"📊 Found {len(image_paths)} images and {len(mask_paths)} masks\")\n    \n    # Split dataset\n    from sklearn.model_selection import train_test_split\n    train_img_paths, val_img_paths, train_mask_paths, val_mask_paths = train_test_split(\n        image_paths, mask_paths, test_size=0.15, random_state=SEED, shuffle=True\n    )\n    \n    print(f\"\\n📊 Dataset Split:\")\n    print(f\"   Training: {len(train_img_paths)} images\")\n    print(f\"   Validation: {len(val_img_paths)} images\")\n    \n    # Create data generators\n    print(\"\\n🔧 Creating data generators...\")\n    train_generator = EfficientDataGenerator(\n        train_img_paths, train_mask_paths, \n        batch_size=BATCH_SIZE, \n        img_size=IMG_SIZE,\n        augment=True,  # Enable online augmentation\n        shuffle=True\n    )\n    \n    val_generator = EfficientDataGenerator(\n        val_img_paths, val_mask_paths,\n        batch_size=BATCH_SIZE,\n        img_size=IMG_SIZE,\n        augment=False,  # No augmentation for validation\n        shuffle=False\n    )\n    \n    print(f\"✅ Training batches per epoch: {len(train_generator)}\")\n    print(f\"✅ Validation batches per epoch: {len(val_generator)}\")\n    \n    # Build model\n    print(\"\\n🏗️ Building U-Net model...\")\n    model = build_unet()\n    \n    # Compile model with L2 regularization already in architecture\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n        loss=combined_loss,\n        metrics=['accuracy', dice_coefficient, iou_metric, precision_metric, recall_metric]\n    )\n    \n    print(f\"📋 Model Parameters: {model.count_params():,}\")\n    \n    # Callbacks\n    callbacks = [\n        tf.keras.callbacks.ModelCheckpoint(\n            'best_football_unet_efficient.h5',\n            save_best_only=True,\n            monitor='val_dice_coefficient',\n            mode='max',\n            verbose=1\n        ),\n        tf.keras.callbacks.EarlyStopping(\n            monitor='val_dice_coefficient',\n            patience=20,  # Increased patience\n            mode='max',\n            restore_best_weights=True,\n            verbose=1\n        ),\n        tf.keras.callbacks.ReduceLROnPlateau(\n            monitor='val_loss',\n            factor=0.5,\n            patience=8,\n            min_lr=1e-7,\n            verbose=1\n        ),\n        tf.keras.callbacks.CSVLogger('training_log_efficient.csv'),\n    ]\n    \n    # Train model\n    print(\"\\n🚀 Starting efficient training...\")\n    print(\"💡 Using on-the-fly augmentation - no RAM overload!\")\n    \n    history = model.fit(\n        train_generator,\n        validation_data=val_generator,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=1\n    )\n    \n    # Final evaluation\n    print(\"\\n\" + \"=\"*70)\n    print(\"✅ TRAINING COMPLETE!\")\n    print(\"=\"*70)\n    \n    results = model.evaluate(val_generator, verbose=0)\n    print(f\"\\n📈 Final Evaluation:\")\n    print(f\"   Validation Loss: {results[0]:.4f}\")\n    print(f\"   Validation Accuracy: {results[1]:.4f}\")\n    print(f\"   Validation Dice: {results[2]:.4f}\")\n    print(f\"   Validation IoU: {results[3]:.4f}\")\n    print(f\"   Validation Precision: {results[4]:.4f}\")\n    print(f\"   Validation Recall: {results[5]:.4f}\")\n    \n    # Save final model\n    model.save('football_player_segmentation_efficient.h5')\n    print(f\"\\n💾 Model saved: football_player_segmentation_efficient.h5\")\n    \n    return model, history\n\n# ============================================================================\n# MAIN EXECUTION\n# ============================================================================\n\nif __name__ == \"__main__\":\n    print(\"🎯 Training with efficient memory usage...\")\n    model, history = train_efficient()\n    \n    print(f\"\\n🎉 TRAINING COMPLETE!\")\n    print(\"💾 Models saved:\")\n    print(\"   - best_football_unet_efficient.h5 (best validation)\")\n    print(\"   - football_player_segmentation_efficient.h5 (final)\")\n    print(\"   - training_log_efficient.csv (training history)\")\n    print(\"\\n✨ Key improvements:\")\n    print(\"   ✅ Memory efficient - loads images on demand\")\n    print(\"   ✅ On-the-fly augmentation during training\")\n    print(\"   ✅ No RAM overload even with 10,000+ images\")\n    print(\"   ✅ Faster training with proper batching\")\n    print(\"   ✅ Better generalization with online augmentation\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T19:47:50.762112Z","iopub.execute_input":"2025-11-29T19:47:50.762687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Evaluate the saved model\nimport tensorflow as tf\nmodel = tf.keras.models.load_model('best_football_unet_efficient.h5')\nresults = model.evaluate(val_generator)\nprint(f\"Val Dice: {results[2]:.4f}\")  # Should be ~0.88+","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T22:12:28.799593Z","iopub.execute_input":"2025-11-29T22:12:28.799846Z","iopub.status.idle":"2025-11-29T22:12:29.185286Z","shell.execute_reply.started":"2025-11-29T22:12:28.799826Z","shell.execute_reply":"2025-11-29T22:12:29.184328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom IPython.display import display, clear_output\nimport time\n\n# Load your trained model\nmodel = tf.keras.models.load_model('best_football_unet.h5', compile=False)\n\nprint(\"✅ Model loaded successfully!\")\nprint(\"🎮 Starting football player segmentation demo...\")\n\n# ============================================================================\n# Video Processing Functions - KAGGLE COMPATIBLE\n# ============================================================================\n\ndef preprocess_frame(frame, target_size=256):\n    \"\"\"Preprocess frame for model prediction\"\"\"\n    # Resize frame\n    frame_resized = cv2.resize(frame, (target_size, target_size))\n    # Normalize\n    frame_normalized = frame_resized.astype(np.float32) / 255.0\n    # Add batch dimension\n    frame_batch = np.expand_dims(frame_normalized, axis=0)\n    return frame_batch, frame_resized\n\ndef create_segmentation_overlay(frame, prediction_mask, alpha=0.6):\n    \"\"\"Create a colorful overlay for segmentation\"\"\"\n    # Convert prediction to binary mask\n    player_mask = (prediction_mask == 1).astype(np.uint8)\n    \n    # Create colored overlay (green for players)\n    overlay = frame.copy()\n    overlay[player_mask == 1] = [0, 255, 0]  # Green for players\n    \n    # Blend with original frame\n    result = cv2.addWeighted(frame, 1 - alpha, overlay, alpha, 0)\n    \n    return result\n\ndef process_video_kaggle(video_path, output_path=None, max_frames=100, display_every=10):\n    \"\"\"\n    Process video on Kaggle - saves output and shows previews\n    \"\"\"\n    # Open video\n    cap = cv2.VideoCapture(video_path)\n    \n    if not cap.isOpened():\n        print(\"❌ Error: Could not open video file\")\n        return\n    \n    # Get video properties\n    fps = cap.get(cv2.CAP_PROP_FPS)\n    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    \n    print(f\"📹 Video Info: {width}x{height}, {fps:.1f} FPS, {total_frames} frames\")\n    \n    # Setup output if specified\n    if output_path:\n        fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n        out = cv2.VideoWriter(output_path, fourcc, fps, (width * 2, height))\n    \n    frame_count = 0\n    processing_times = []\n    preview_frames = []\n    \n    print(\"🚀 Starting processing...\")\n    \n    while True:\n        ret, frame = cap.read()\n        if not ret or frame_count >= max_frames:\n            break\n        \n        # Start timer\n        start_time = time.time()\n        \n        # Preprocess frame\n        frame_batch, frame_resized = preprocess_frame(frame)\n        \n        # Model prediction\n        prediction = model.predict(frame_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        \n        # Resize mask back to original frame size\n        prediction_mask_full = cv2.resize(prediction_mask, (width, height), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create overlay\n        overlay_frame = create_segmentation_overlay(frame, prediction_mask_full)\n        \n        # Count players (approximate)\n        players_detected = np.sum(prediction_mask == 1) // 500\n        \n        # Calculate FPS\n        processing_time = time.time() - start_time\n        processing_times.append(processing_time)\n        current_fps = 1.0 / processing_time if processing_time > 0 else 0\n        \n        # Add statistics to frame\n        stats_frame = overlay_frame.copy()\n        cv2.putText(stats_frame, f\"FPS: {current_fps:.1f}\", (10, 30), \n                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)\n        cv2.putText(stats_frame, f\"Players: {players_detected}\", (10, 60), \n                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)\n        cv2.putText(stats_frame, \"AI Player Detection\", (10, 90), \n                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)\n        \n        # Create side-by-side comparison\n        original_with_text = frame.copy()\n        cv2.putText(original_with_text, \"Original\", (10, 30), \n                   cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)\n        \n        combined = np.hstack([original_with_text, stats_frame])\n        \n        # Save output if specified\n        if output_path:\n            out.write(combined)\n        \n        # Store every nth frame for preview\n        if frame_count % display_every == 0:\n            preview_frames.append(combined)\n        \n        frame_count += 1\n        \n        # Print progress\n        if frame_count % 20 == 0:\n            avg_fps = 1.0 / np.mean(processing_times[-20:]) if len(processing_times) >= 20 else current_fps\n            print(f\"📊 Processed {frame_count}/{min(total_frames, max_frames)} frames | Avg FPS: {avg_fps:.1f}\")\n    \n    # Cleanup\n    cap.release()\n    if output_path:\n        out.release()\n    \n    # Display preview of results\n    if preview_frames:\n        print(\"\\n🎬 Displaying preview of processed frames...\")\n        display_preview_frames(preview_frames)\n    \n    # Print summary\n    if processing_times:\n        avg_processing_time = np.mean(processing_times)\n        avg_fps = 1.0 / avg_processing_time\n        print(f\"\\n📈 Processing Summary:\")\n        print(f\"   Total frames processed: {frame_count}\")\n        print(f\"   Average FPS: {avg_fps:.1f}\")\n        print(f\"   Average processing time: {avg_processing_time*1000:.1f}ms per frame\")\n        if output_path:\n            print(f\"   Output saved to: {output_path}\")\n\ndef display_preview_frames(frames, max_display=6):\n    \"\"\"Display a grid of preview frames\"\"\"\n    n_frames = min(len(frames), max_display)\n    \n    fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n    axes = axes.flatten()\n    \n    for i in range(n_frames):\n        # Convert BGR to RGB for matplotlib\n        frame_rgb = cv2.cvtColor(frames[i], cv2.COLOR_BGR2RGB)\n        axes[i].imshow(frame_rgb)\n        axes[i].set_title(f'Frame {i * 10}', fontsize=12)\n        axes[i].axis('off')\n    \n    # Hide unused subplots\n    for i in range(n_frames, len(axes)):\n        axes[i].axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\n# ============================================================================\n# Single Image Demo\n# ============================================================================\n\ndef process_single_image(image_path, output_path=None):\n    \"\"\"\n    Process a single image with detailed visualization\n    \"\"\"\n    # Load image\n    image = cv2.imread(image_path)\n    if image is None:\n        print(f\"❌ Error: Could not load image {image_path}\")\n        return\n    \n    image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    original_height, original_width = image.shape[:2]\n    \n    print(f\"🖼️ Processing image: {image_path}\")\n    print(f\"   Size: {original_width}x{original_height}\")\n    \n    # Preprocess\n    image_batch, image_resized = preprocess_frame(image_rgb)\n    \n    # Predict\n    prediction = model.predict(image_batch, verbose=0)\n    prediction_mask = np.argmax(prediction[0], axis=-1)\n    \n    # Resize mask to original size\n    prediction_mask_full = cv2.resize(prediction_mask, (original_width, original_height), \n                                    interpolation=cv2.INTER_NEAREST)\n    \n    # Create detailed visualization\n    fig, axes = plt.subplots(2, 2, figsize=(15, 12))\n    \n    # Original image\n    axes[0, 0].imshow(image_rgb)\n    axes[0, 0].set_title('Original Image', fontsize=16, fontweight='bold')\n    axes[0, 0].axis('off')\n    \n    # Prediction mask\n    axes[0, 1].imshow(prediction_mask_full, cmap='viridis')\n    axes[0, 1].set_title('AI Segmentation Mask', fontsize=16, fontweight='bold')\n    axes[0, 1].axis('off')\n    \n    # Overlay\n    overlay_image = image_rgb.copy()\n    player_mask = (prediction_mask_full == 1)\n    overlay_image[player_mask] = [255, 0, 0]  # Red overlay\n    \n    axes[1, 0].imshow(overlay_image)\n    axes[1, 0].set_title('Segmentation Overlay', fontsize=16, fontweight='bold')\n    axes[1, 0].axis('off')\n    \n    # Detailed overlay (transparent)\n    detailed_overlay = image_rgb.copy()\n    detailed_overlay[player_mask] = [0, 255, 0]  # Green for players\n    result = cv2.addWeighted(image_rgb, 0.7, detailed_overlay, 0.3, 0)\n    \n    axes[1, 1].imshow(result)\n    axes[1, 1].set_title('Enhanced Visualization', fontsize=16, fontweight='bold')\n    axes[1, 1].axis('off')\n    \n    plt.tight_layout()\n    \n    # Add statistics\n    players_pixels = np.sum(player_mask)\n    total_pixels = original_width * original_height\n    coverage = (players_pixels / total_pixels) * 100\n    \n    plt.figtext(0.5, 0.01, \n               f\"📊 AI Analysis: {players_pixels} player pixels detected | {coverage:.2f}% of image | Model: 92.4% Dice Score\", \n               ha=\"center\", fontsize=12, bbox={\"facecolor\":\"orange\", \"alpha\":0.2, \"pad\":5})\n    \n    plt.show()\n    \n    if output_path:\n        plt.savefig(output_path, dpi=150, bbox_inches='tight')\n        print(f\"💾 Visualization saved to: {output_path}\")\n\n# ============================================================================\n# Main Demo Menu - KAGGLE COMPATIBLE\n# ============================================================================\n\ndef main():\n    print(\"🎯 FOOTBALL PLAYER SEGMENTATION - AI DEMO CENTER\")\n    print(\"=\"*60)\n    print(\"Choose demo mode:\")\n    print(\"1. 📹 Process Video File (Kaggle Compatible)\")\n    print(\"2. 🖼️ Process Single Image\")\n    print(\"=\"*60)\n    \n    choice = input(\"Enter your choice (1-2): \").strip()\n    \n    if choice == \"1\":\n        video_path = input(\"Enter video file path: \").strip()\n        if not video_path:\n            video_path = \"/kaggle/working/dataset_video.mp4\"  # Default path\n            \n        output_path = input(\"Enter output path (optional): \").strip() or \"/kaggle/working/output_video.mp4\"\n        \n        print(\"\\n\" + \"🎬 PROCESSING VIDEO\".center(60, \"=\"))\n        process_video_kaggle(video_path, output_path, max_frames=100)\n        \n    elif choice == \"2\":\n        image_path = input(\"Enter image file path: \").strip()\n        output_path = input(\"Enter output path (optional): \").strip() or \"/kaggle/working/output_image.png\"\n        print(\"\\n\" + \"🖼️ PROCESSING IMAGE\".center(60, \"=\"))\n        process_single_image(image_path, output_path)\n        \n    else:\n        print(\"❌ Invalid choice!\")\n\n# ============================================================================\n# Quick Test Function\n# ============================================================================\n\ndef quick_test():\n    \"\"\"Quick test with default paths\"\"\"\n    print(\"⚡ QUICK TEST MODE\")\n    \n    # Try to process the dataset video\n    video_path = \"/kaggle/working/dataset_video.mp4\"\n    output_path = \"/kaggle/working/ai_segmentation_output.mp4\"\n    \n    if os.path.exists(video_path):\n        print(f\"🎬 Found video: {video_path}\")\n        process_video_kaggle(video_path, output_path, max_frames=50, display_every=5)\n    else:\n        print(\"❌ No video found. Please create a video first using the image-to-video script.\")\n        print(\"💡 Run: create_video_from_images('/kaggle/input/football-player-segmentation/images', '/kaggle/working/dataset_video.mp4')\")\n\nif __name__ == \"__main__\":\n    import os\n    \n    # Check if we're in Kaggle\n    if os.path.exists('/kaggle'):\n        print(\"🏠 Running in Kaggle environment\")\n        quick_test()  # Auto-run quick test in Kaggle\n    else:\n        main()  # Show menu for local execution","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-28T17:04:21.19605Z","iopub.execute_input":"2025-11-28T17:04:21.196563Z","iopub.status.idle":"2025-11-28T17:04:33.065371Z","shell.execute_reply.started":"2025-11-28T17:04:21.196542Z","shell.execute_reply":"2025-11-28T17:04:33.064294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport os\nfrom glob import glob\nimport numpy as np\nfrom tqdm import tqdm\n\ndef create_smooth_optimized_video(image_folder, output_video_path, fps=24, \n                                 target_size_mb=95, quality=85):\n    \"\"\"\n    Create a smooth, high-quality video that stays under size limits\n    \"\"\"\n    # Get all image files\n    image_files = (glob(os.path.join(image_folder, \"*.jpg\")) + \n                   glob(os.path.join(image_folder, \"*.jpeg\")) + \n                   glob(os.path.join(image_folder, \"*.png\")))\n    \n    if not image_files:\n        print(\"❌ No images found in the folder\")\n        return False\n    \n    # Sort images by name for consistent order\n    image_files.sort()\n    \n    print(f\"📸 Found {len(image_files)} images\")\n    print(\"🎬 Creating smooth, optimized video...\")\n    \n    # Read first image to get dimensions\n    first_image = cv2.imread(image_files[0])\n    if first_image is None:\n        print(\"❌ Could not read the first image\")\n        return False\n    \n    height, width = first_image.shape[:2]\n    \n    # Calculate optimal resolution to maintain quality while reducing size\n    # Start with original resolution and only reduce if needed\n    original_width, original_height = width, height\n    scale_factor = calculate_optimal_scale(len(image_files), fps, width * height, target_size_mb)\n    \n    new_width = int(width * scale_factor)\n    new_height = int(height * scale_factor)\n    \n    print(f\"📊 Resolution: {new_width}x{new_height} (scale: {scale_factor:.2f})\")\n    \n    # Create video writer with optimized settings\n    fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n    out = cv2.VideoWriter(output_video_path, fourcc, fps, (new_width, new_height))\n    \n    # Process images with smooth transitions\n    previous_frame = None\n    \n    for i in tqdm(range(len(image_files)), desc=\"Creating smooth video\"):\n        current_image_path = image_files[i]\n        current_frame = cv2.imread(current_image_path)\n        \n        if current_frame is None:\n            continue\n        \n        # Resize if needed\n        if scale_factor < 1.0:\n            current_frame = cv2.resize(current_frame, (new_width, new_height), \n                                     interpolation=cv2.INTER_LANCZOS4)\n        \n        # Apply quality optimization (mild compression)\n        current_frame = optimize_frame_quality(current_frame, quality)\n        \n        # Add smooth transition between frames\n        if previous_frame is not None:\n            # Create 3-frame cross-fade transition for smoothness\n            for t in range(3):\n                alpha = (t + 1) / 4  # Gentle transition\n                blended_frame = cv2.addWeighted(previous_frame, 1 - alpha, \n                                              current_frame, alpha, 0)\n                out.write(blended_frame)\n        \n        # Show each frame for consistent duration (2-3 frames per image)\n        for _ in range(2):\n            out.write(current_frame)\n        \n        previous_frame = current_frame\n    \n    # Release video writer\n    out.release()\n    \n    # Check and optimize file size if needed\n    final_size = optimize_video_size(output_video_path, target_size_mb)\n    \n    if os.path.exists(output_video_path):\n        file_size = os.path.getsize(output_video_path) / (1024 * 1024)\n        duration = (len(image_files) * (2 + 3)) / fps  # frames * (display + transition)\n        \n        print(f\"\\n✅ Smooth video created: {output_video_path}\")\n        print(f\"📊 Video info:\")\n        print(f\"   • Resolution: {new_width}x{new_height}\")\n        print(f\"   • Duration: {duration:.1f} seconds\")\n        print(f\"   • FPS: {fps}\")\n        print(f\"   • File size: {file_size:.1f} MB\")\n        print(f\"   • Smooth transitions: Enabled\")\n        \n        return True\n    else:\n        print(\"❌ Failed to create video\")\n        return False\n\ndef calculate_optimal_scale(num_images, fps, original_pixels, target_size_mb):\n    \"\"\"\n    Calculate the optimal scale factor to stay under size limit\n    while maintaining smoothness\n    \"\"\"\n    # Estimate size based on original parameters\n    estimated_size_mb = (num_images * 5 * original_pixels * 3) / (1024 * 1024)  # Rough estimate\n    \n    if estimated_size_mb <= target_size_mb:\n        return 1.0  # Keep original resolution\n    \n    # Calculate required scale factor\n    scale_factor = np.sqrt(target_size_mb / estimated_size_mb) * 0.9  # Conservative estimate\n    \n    # Don't scale below 40% of original\n    return max(0.4, min(1.0, scale_factor))\n\ndef optimize_frame_quality(frame, quality=85):\n    \"\"\"\n    Optimize frame quality and compression\n    \"\"\"\n    # Apply mild sharpening for clarity\n    kernel = np.array([[0, -0.5, 0],\n                       [-0.5, 3, -0.5],\n                       [0, -0.5, 0]])\n    sharpened = cv2.filter2D(frame, -1, kernel)\n    \n    # Use JPEG compression to reduce size while maintaining quality\n    encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]\n    result, encimg = cv2.imencode('.jpg', sharpened, encode_param)\n    if result:\n        return cv2.imdecode(encimg, 1)\n    \n    return sharpened\n\ndef optimize_video_size(video_path, target_size_mb):\n    \"\"\"\n    Re-encode video with optimal settings if it exceeds size limit\n    \"\"\"\n    if not os.path.exists(video_path):\n        return 0\n    \n    file_size = os.path.getsize(video_path) / (1024 * 1024)\n    \n    if file_size <= target_size_mb:\n        return file_size\n    \n    print(f\"📦 Optimizing video size: {file_size:.1f}MB -> {target_size_mb}MB\")\n    \n    # Re-encode with optimized settings\n    cap = cv2.VideoCapture(video_path)\n    fps = cap.get(cv2.CAP_PROP_FPS)\n    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n    \n    # Calculate new dimensions (reduce slightly)\n    new_width = int(width * 0.8)\n    new_height = int(height * 0.8)\n    \n    temp_path = video_path.replace('.mp4', '_optimized.mp4')\n    \n    # Use better compression\n    fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n    out = cv2.VideoWriter(temp_path, fourcc, fps, (new_width, new_height))\n    \n    frame_count = 0\n    while True:\n        ret, frame = cap.read()\n        if not ret:\n            break\n        \n        frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4)\n        out.write(frame)\n        frame_count += 1\n    \n    cap.release()\n    out.release()\n    \n    # Replace original if smaller\n    new_size = os.path.getsize(temp_path) / (1024 * 1024)\n    if new_size < file_size:\n        os.replace(temp_path, video_path)\n        print(f\"✅ Size optimized: {file_size:.1f}MB -> {new_size:.1f}MB\")\n        return new_size\n    else:\n        os.remove(temp_path)\n        return file_size\n\ndef create_high_quality_variant(image_folder):\n    \"\"\"\n    Create the best possible quality video that fits under 100MB\n    \"\"\"\n    image_files = (glob(os.path.join(image_folder, \"*.jpg\")) + \n                   glob(os.path.join(image_folder, \"*.jpeg\")) + \n                   glob(os.path.join(image_folder, \"*.png\")))\n    \n    if not image_files:\n        return False\n    \n    print(f\"🎯 Creating high-quality smooth video...\")\n    print(f\"📊 Total images: {len(image_files)}\")\n    \n    # Adjust parameters based on dataset size\n    if len(image_files) > 300:\n        fps = 20\n        print(\"   • Using 20 FPS for larger dataset\")\n    elif len(image_files) > 150:\n        fps = 24\n        print(\"   • Using 24 FPS for optimal smoothness\")\n    else:\n        fps = 30\n        print(\"   • Using 30 FPS for maximum smoothness\")\n    \n    return create_smooth_optimized_video(\n        image_folder,\n        \"/kaggle/working/smooth_football_video.mp4\",\n        fps=fps,\n        target_size_mb=95,  # Leave 5MB buffer\n        quality=90\n    )\n\n# Main execution\nif __name__ == \"__main__\":\n    # Configuration\n    image_folder = \"/kaggle/input/football-player-segmentation/images\"\n    \n    print(\"🚀 Football Smooth Video Creator\")\n    print(\"=\" * 50)\n    print(\"🎯 Creating smooth, high-quality video (<100MB)\")\n    \n    success = create_high_quality_variant(image_folder)\n    \n    if success:\n        print(f\"\\n✅ Smooth video ready for download!\")\n        print(\"💡 The video maintains smooth transitions while staying under 100MB\")\n    else:\n        print(\"❌ Video creation failed\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T13:07:11.981579Z","iopub.execute_input":"2025-11-29T13:07:11.981847Z","iopub.status.idle":"2025-11-29T13:07:59.615736Z","shell.execute_reply.started":"2025-11-29T13:07:11.98183Z","shell.execute_reply":"2025-11-29T13:07:59.614851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nimport os\nfrom glob import glob\n\n# Load your trained model\nmodel = tf.keras.models.load_model('best_football_unet.h5', compile=False)\nprint(\"✅ Model loaded successfully!\")\n\n# ============================================================================\n# Image Testing Functions\n# ============================================================================\n\ndef preprocess_image(image, target_size=256):\n    \"\"\"Preprocess image for model prediction\"\"\"\n    # Resize image\n    image_resized = cv2.resize(image, (target_size, target_size))\n    # Normalize\n    image_normalized = image_resized.astype(np.float32) / 255.0\n    # Add batch dimension\n    image_batch = np.expand_dims(image_normalized, axis=0)\n    return image_batch, image_resized\n\ndef create_white_segmentation(original, prediction_mask, alpha=0.7):\n    \"\"\"Create segmentation with white color overlay\"\"\"\n    # Create white overlay\n    white_overlay = original.copy()\n    player_mask = (prediction_mask == 1)\n    \n    # Make detected players white\n    white_overlay[player_mask] = [255, 255, 255]  # White for players\n    \n    # Blend with original\n    blended = cv2.addWeighted(original, 1 - alpha, white_overlay, alpha, 0)\n    \n    return blended, white_overlay\n\ndef test_multiple_images_white_segmentation(image_folder, num_images=6, save_dir=None):\n    \"\"\"\n    Test model on multiple images with white segmentation\n    \"\"\"\n    # Get all image files\n    image_files = glob(os.path.join(image_folder, \"*.jpg\")) + \\\n                  glob(os.path.join(image_folder, \"*.jpeg\")) + \\\n                  glob(os.path.join(image_folder, \"*.png\"))\n    \n    if not image_files:\n        print(f\"❌ No images found in {image_folder}\")\n        return\n    \n    # Select random images\n    if len(image_files) > num_images:\n        selected_indices = np.random.choice(len(image_files), num_images, replace=False)\n        selected_images = [image_files[i] for i in selected_indices]\n    else:\n        selected_images = image_files\n    \n    print(f\"🔍 Testing on {len(selected_images)} images...\")\n    \n    # Create results directory if saving\n    if save_dir and not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n    \n    # Create figure for grid display - 3 columns: Original, Segmentation, Overlay\n    fig, axes = plt.subplots(len(selected_images), 3, figsize=(18, 6 * len(selected_images)))\n    \n    # Handle single row case\n    if len(selected_images) == 1:\n        axes = axes.reshape(1, -1)\n    \n    for i, image_path in enumerate(selected_images):\n        # Load and process image\n        image = cv2.imread(image_path)\n        if image is None:\n            continue\n            \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        original_height, original_width = image.shape[:2]\n        \n        # Preprocess and predict\n        image_batch, _ = preprocess_image(image_rgb)\n        prediction = model.predict(image_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        prediction_mask_full = cv2.resize(prediction_mask, (original_width, original_height), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create white segmentation and overlay\n        blended, white_overlay = create_white_segmentation(image_rgb, prediction_mask_full)\n        \n        # Column 1: Original Image\n        axes[i, 0].imshow(image_rgb)\n        axes[i, 0].set_title(f'🖼️ Original Image\\n{os.path.basename(image_path)}', \n                           fontsize=14, fontweight='bold', pad=10)\n        axes[i, 0].axis('off')\n        \n        # Column 2: White Segmentation Mask\n        axes[i, 1].imshow(white_overlay)\n        player_pixels = np.sum(prediction_mask_full == 1)\n        total_pixels = original_width * original_height\n        coverage_percent = (player_pixels / total_pixels) * 100\n        \n        axes[i, 1].set_title(f'⚪ AI Segmentation\\n{player_pixels:,} white pixels\\n({coverage_percent:.1f}% coverage)', \n                           fontsize=14, fontweight='bold', pad=10)\n        axes[i, 1].axis('off')\n        \n        # Column 3: Blended Overlay\n        axes[i, 2].imshow(blended)\n        axes[i, 2].set_title(f'🎨 White Overlay\\nPlayers highlighted in white', \n                           fontsize=14, fontweight='bold', pad=10)\n        axes[i, 2].axis('off')\n    \n    plt.suptitle('🎯 AI Football Player Segmentation - White Color Detection\\n'\n                '⚪ White areas show detected players | 🎯 Model: 92.4% Dice Score', \n                fontsize=20, fontweight='bold', y=0.98)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(top=0.94)\n    \n    if save_dir:\n        grid_save_path = os.path.join(save_dir, \"white_segmentation_results.png\")\n        plt.savefig(grid_save_path, dpi=150, bbox_inches='tight', facecolor='white')\n        print(f\"💾 Results saved to: {grid_save_path}\")\n    \n    plt.show()\n    \n    print(f\"✅ Tested {len(selected_images)} images successfully!\")\n    if save_dir:\n        print(f\"💾 Results saved to: {save_dir}\")\n\ndef test_10_images_detailed(image_folder, save_dir=None):\n    \"\"\"\n    Test on 10 images with detailed white segmentation\n    \"\"\"\n    # Get all image files\n    image_files = glob(os.path.join(image_folder, \"*.jpg\")) + \\\n                  glob(os.path.join(image_folder, \"*.jpeg\")) + \\\n                  glob(os.path.join(image_folder, \"*.png\"))\n    \n    if not image_files:\n        print(f\"❌ No images found in {image_folder}\")\n        return\n    \n    # Select 10 images (or all if less than 10)\n    if len(image_files) > 10:\n        selected_indices = np.random.choice(len(image_files), 10, replace=False)\n        selected_images = [image_files[i] for i in selected_indices]\n    else:\n        selected_images = image_files\n    \n    print(f\"🔍 Testing on {len(selected_images)} images...\")\n    \n    # Create results directory if saving\n    if save_dir and not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n    \n    # Create figure - 10 rows, 3 columns\n    fig, axes = plt.subplots(len(selected_images), 3, figsize=(20, 5 * len(selected_images)))\n    \n    # Handle single row case\n    if len(selected_images) == 1:\n        axes = axes.reshape(1, -1)\n    \n    total_player_pixels = 0\n    total_pixels = 0\n    \n    for i, image_path in enumerate(selected_images):\n        # Load and process image\n        image = cv2.imread(image_path)\n        if image is None:\n            continue\n            \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        original_height, original_width = image.shape[:2]\n        \n        # Preprocess and predict\n        image_batch, _ = preprocess_image(image_rgb)\n        prediction = model.predict(image_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        prediction_mask_full = cv2.resize(prediction_mask, (original_width, original_height), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create white segmentation and overlay\n        blended, white_overlay = create_white_segmentation(image_rgb, prediction_mask_full, alpha=0.6)\n        \n        # Calculate statistics\n        player_pixels = np.sum(prediction_mask_full == 1)\n        image_pixels = original_width * original_height\n        coverage_percent = (player_pixels / image_pixels) * 100\n        \n        total_player_pixels += player_pixels\n        total_pixels += image_pixels\n        \n        # Column 1: Original Image\n        axes[i, 0].imshow(image_rgb)\n        axes[i, 0].set_title(f'Image {i+1}: Original\\n{original_width}×{original_height}', \n                           fontsize=12, fontweight='bold', pad=10)\n        axes[i, 0].axis('off')\n        \n        # Column 2: Pure White Segmentation\n        axes[i, 1].imshow(white_overlay)\n        axes[i, 1].set_title(f'Segmentation Mask\\n{player_pixels:,} white pixels', \n                           fontsize=12, fontweight='bold', pad=10)\n        axes[i, 1].axis('off')\n        \n        # Column 3: Overlay\n        axes[i, 2].imshow(blended)\n        axes[i, 2].set_title(f'White Overlay\\n{coverage_percent:.1f}% coverage', \n                           fontsize=12, fontweight='bold', pad=10)\n        axes[i, 2].axis('off')\n    \n    # Calculate overall statistics\n    overall_coverage = (total_player_pixels / total_pixels) * 100 if total_pixels > 0 else 0\n    \n    plt.suptitle(f'🎯 AI Football Player Segmentation - 10 Image Analysis\\n'\n                f'⚪ White = Detected Players | 📊 Overall: {overall_coverage:.1f}% player coverage | 🎯 Model Accuracy: 92.4%', \n                fontsize=22, fontweight='bold', y=0.98)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(top=0.95)\n    \n    if save_dir:\n        save_path = os.path.join(save_dir, \"10_images_white_segmentation.png\")\n        plt.savefig(save_path, dpi=150, bbox_inches='tight', facecolor='white')\n        print(f\"💾 Detailed results saved to: {save_path}\")\n    \n    plt.show()\n    \n    # Print summary statistics\n    print(f\"\\n📈 Summary Statistics for {len(selected_images)} images:\")\n    print(f\"   Total player pixels detected: {total_player_pixels:,}\")\n    print(f\"   Total image pixels: {total_pixels:,}\")\n    print(f\"   Overall player coverage: {overall_coverage:.2f}%\")\n    print(f\"   Average per image: {total_player_pixels/len(selected_images):.0f} player pixels\")\n\ndef quick_white_segmentation_preview(image_folder, num_images=6):\n    \"\"\"\n    Quick preview with white segmentation\n    \"\"\"\n    # Get all image files\n    image_files = glob(os.path.join(image_folder, \"*.jpg\")) + \\\n                  glob(os.path.join(image_folder, \"*.jpeg\")) + \\\n                  glob(os.path.join(image_folder, \"*.png\"))\n    \n    if not image_files:\n        print(f\"❌ No images found in {image_folder}\")\n        return\n    \n    # Select images\n    if len(image_files) > num_images:\n        selected_indices = np.random.choice(len(image_files), num_images, replace=False)\n        selected_images = [image_files[i] for i in selected_indices]\n    else:\n        selected_images = image_files\n    \n    print(f\"🔍 Quick preview of {len(selected_images)} images with white segmentation...\")\n    \n    # Create compact figure\n    fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n    axes = axes.flatten()\n    \n    for i, image_path in enumerate(selected_images):\n        if i >= len(axes):\n            break\n            \n        # Load and process image\n        image = cv2.imread(image_path)\n        if image is None:\n            continue\n            \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Preprocess and predict\n        image_batch, _ = preprocess_image(image_rgb)\n        prediction = model.predict(image_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        prediction_mask_full = cv2.resize(prediction_mask, (image.shape[1], image.shape[0]), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create white overlay\n        blended, _ = create_white_segmentation(image_rgb, prediction_mask_full, alpha=0.5)\n        \n        # Display\n        axes[i].imshow(blended)\n        player_pixels = np.sum(prediction_mask_full == 1)\n        \n        axes[i].set_title(f'Image {i+1}\\n⚪ {player_pixels:,} player pixels', \n                         fontsize=12, fontweight='bold')\n        axes[i].axis('off')\n    \n    # Hide unused subplots\n    for i in range(len(selected_images), len(axes)):\n        axes[i].axis('off')\n    \n    plt.suptitle('⚪ AI Player Detection - White Segmentation Preview\\n'\n                'White areas show detected football players', \n                fontsize=16, fontweight='bold', y=0.95)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(top=0.90)\n    plt.show()\n\n# ============================================================================\n# Main Execution\n# ============================================================================\n\ndef main():\n    print(\"🎯 FOOTBALL PLAYER SEGMENTATION - WHITE SEGMENTATION TESTING\")\n    print(\"=\"*60)\n    print(\"Choose test mode:\")\n    print(\"1. 🔍 Test 6 Images (Grid View)\")\n    print(\"2. 📊 Test 10 Images (Detailed)\")\n    print(\"3. ⚡ Quick 6-Image Preview\")\n    print(\"=\"*60)\n    \n    choice = input(\"Enter your choice (1-3): \").strip()\n    \n    # Default paths for Kaggle\n    default_image_folder = \"/kaggle/input/football-player-segmentation/images\"\n    default_output_dir = \"/kaggle/working/white_segmentation_results\"\n    \n    if choice == \"1\":\n        image_folder = input(f\"Enter image folder [default: {default_image_folder}]: \").strip() or default_image_folder\n        save_dir = input(\"Enter save directory [default: /kaggle/working/white_segmentation_results]: \").strip() or default_output_dir\n        test_multiple_images_white_segmentation(image_folder, num_images=6, save_dir=save_dir)\n        \n    elif choice == \"2\":\n        image_folder = input(f\"Enter image folder [default: {default_image_folder}]: \").strip() or default_image_folder\n        save_dir = input(\"Enter save directory [default: /kaggle/working/white_segmentation_results]: \").strip() or default_output_dir\n        test_10_images_detailed(image_folder, save_dir=save_dir)\n        \n    elif choice == \"3\":\n        image_folder = input(f\"Enter image folder [default: {default_image_folder}]: \").strip() or default_image_folder\n        quick_white_segmentation_preview(image_folder, num_images=6)\n        \n    else:\n        print(\"❌ Invalid choice!\")\n\n# Auto-run in Kaggle\nif __name__ == \"__main__\":\n    import os\n    \n    if os.path.exists('/kaggle'):\n        print(\"🏠 Running in Kaggle - Auto testing with white segmentation...\")\n        image_folder = \"/kaggle/input/football-player-segmentation/images\"\n        \n        if os.path.exists(image_folder):\n            # Run all three modes\n            print(\"\\n\" + \"=\"*50)\n            print(\"1. 🔍 Quick 6-Image Preview\")\n            quick_white_segmentation_preview(image_folder, num_images=6)\n            \n            print(\"\\n\" + \"=\"*50)\n            print(\"2. 📊 Detailed 6-Image Analysis\")\n            test_multiple_images_white_segmentation(image_folder, num_images=6, \n                                                   save_dir=\"/kaggle/working/white_segmentation_results\")\n        else:\n            print(\"❌ Dataset not found. Please check the path.\")\n    else:\n        main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-28T17:14:38.951767Z","iopub.execute_input":"2025-11-28T17:14:38.952448Z","iopub.status.idle":"2025-11-28T17:14:58.903877Z","shell.execute_reply.started":"2025-11-28T17:14:38.952422Z","shell.execute_reply":"2025-11-28T17:14:58.903073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nimport os\nfrom glob import glob\n\n# Load your trained model\nmodel = tf.keras.models.load_model('best_football_unet_efficient.h5', compile=False)\nprint(\"✅ Model loaded successfully!\")\n\n# ============================================================================\n# Image Testing Functions\n# ============================================================================\n\ndef preprocess_image(image, target_size=256):\n    \"\"\"Preprocess image for model prediction\"\"\"\n    # Resize image\n    image_resized = cv2.resize(image, (target_size, target_size))\n    # Normalize\n    image_normalized = image_resized.astype(np.float32) / 255.0\n    # Add batch dimension\n    image_batch = np.expand_dims(image_normalized, axis=0)\n    return image_batch, image_resized\n\ndef create_white_segmentation(original, prediction_mask, alpha=0.7):\n    \"\"\"Create segmentation with white color overlay\"\"\"\n    # Create white overlay\n    white_overlay = original.copy()\n    player_mask = (prediction_mask == 1)\n    \n    # Make detected players white\n    white_overlay[player_mask] = [255, 255, 255]  # White for players\n    \n    # Blend with original\n    blended = cv2.addWeighted(original, 1 - alpha, white_overlay, alpha, 0)\n    \n    return blended, white_overlay\n\ndef test_multiple_images_white_segmentation(image_folder, num_images=6, save_dir=None):\n    \"\"\"\n    Test model on multiple images with white segmentation\n    \"\"\"\n    # Get all image files\n    image_files = glob(os.path.join(image_folder, \"*.jpg\")) + \\\n                  glob(os.path.join(image_folder, \"*.jpeg\")) + \\\n                  glob(os.path.join(image_folder, \"*.png\"))\n    \n    if not image_files:\n        print(f\"❌ No images found in {image_folder}\")\n        return\n    \n    # Select random images\n    if len(image_files) > num_images:\n        selected_indices = np.random.choice(len(image_files), num_images, replace=False)\n        selected_images = [image_files[i] for i in selected_indices]\n    else:\n        selected_images = image_files\n    \n    print(f\"🔍 Testing on {len(selected_images)} images...\")\n    \n    # Create results directory if saving\n    if save_dir and not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n    \n    # Create figure for grid display - 3 columns: Original, Segmentation, Overlay\n    fig, axes = plt.subplots(len(selected_images), 3, figsize=(18, 6 * len(selected_images)))\n    \n    # Handle single row case\n    if len(selected_images) == 1:\n        axes = axes.reshape(1, -1)\n    \n    for i, image_path in enumerate(selected_images):\n        # Load and process image\n        image = cv2.imread(image_path)\n        if image is None:\n            continue\n            \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        original_height, original_width = image.shape[:2]\n        \n        # Preprocess and predict\n        image_batch, _ = preprocess_image(image_rgb)\n        prediction = model.predict(image_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        prediction_mask_full = cv2.resize(prediction_mask, (original_width, original_height), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create white segmentation and overlay\n        blended, white_overlay = create_white_segmentation(image_rgb, prediction_mask_full)\n        \n        # Column 1: Original Image\n        axes[i, 0].imshow(image_rgb)\n        axes[i, 0].set_title(f'🖼️ Original Image\\n{os.path.basename(image_path)}', \n                           fontsize=14, fontweight='bold', pad=10)\n        axes[i, 0].axis('off')\n        \n        # Column 2: White Segmentation Mask\n        axes[i, 1].imshow(white_overlay)\n        player_pixels = np.sum(prediction_mask_full == 1)\n        total_pixels = original_width * original_height\n        coverage_percent = (player_pixels / total_pixels) * 100\n        \n        axes[i, 1].set_title(f'⚪ AI Segmentation\\n{player_pixels:,} white pixels\\n({coverage_percent:.1f}% coverage)', \n                           fontsize=14, fontweight='bold', pad=10)\n        axes[i, 1].axis('off')\n        \n        # Column 3: Blended Overlay\n        axes[i, 2].imshow(blended)\n        axes[i, 2].set_title(f'🎨 White Overlay\\nPlayers highlighted in white', \n                           fontsize=14, fontweight='bold', pad=10)\n        axes[i, 2].axis('off')\n    \n    plt.suptitle('🎯 AI Football Player Segmentation - White Color Detection\\n'\n                '⚪ White areas show detected players | 🎯 Model: 92.4% Dice Score', \n                fontsize=20, fontweight='bold', y=0.98)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(top=0.94)\n    \n    if save_dir:\n        grid_save_path = os.path.join(save_dir, \"white_segmentation_results.png\")\n        plt.savefig(grid_save_path, dpi=150, bbox_inches='tight', facecolor='white')\n        print(f\"💾 Results saved to: {grid_save_path}\")\n    \n    plt.show()\n    \n    print(f\"✅ Tested {len(selected_images)} images successfully!\")\n    if save_dir:\n        print(f\"💾 Results saved to: {save_dir}\")\n\ndef test_10_images_detailed(image_folder, save_dir=None):\n    \"\"\"\n    Test on 10 images with detailed white segmentation\n    \"\"\"\n    # Get all image files\n    image_files = glob(os.path.join(image_folder, \"*.jpg\")) + \\\n                  glob(os.path.join(image_folder, \"*.jpeg\")) + \\\n                  glob(os.path.join(image_folder, \"*.png\"))\n    \n    if not image_files:\n        print(f\"❌ No images found in {image_folder}\")\n        return\n    \n    # Select 10 images (or all if less than 10)\n    if len(image_files) > 10:\n        selected_indices = np.random.choice(len(image_files), 10, replace=False)\n        selected_images = [image_files[i] for i in selected_indices]\n    else:\n        selected_images = image_files\n    \n    print(f\"🔍 Testing on {len(selected_images)} images...\")\n    \n    # Create results directory if saving\n    if save_dir and not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n    \n    # Create figure - 10 rows, 3 columns\n    fig, axes = plt.subplots(len(selected_images), 3, figsize=(20, 5 * len(selected_images)))\n    \n    # Handle single row case\n    if len(selected_images) == 1:\n        axes = axes.reshape(1, -1)\n    \n    total_player_pixels = 0\n    total_pixels = 0\n    \n    for i, image_path in enumerate(selected_images):\n        # Load and process image\n        image = cv2.imread(image_path)\n        if image is None:\n            continue\n            \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        original_height, original_width = image.shape[:2]\n        \n        # Preprocess and predict\n        image_batch, _ = preprocess_image(image_rgb)\n        prediction = model.predict(image_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        prediction_mask_full = cv2.resize(prediction_mask, (original_width, original_height), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create white segmentation and overlay\n        blended, white_overlay = create_white_segmentation(image_rgb, prediction_mask_full, alpha=0.6)\n        \n        # Calculate statistics\n        player_pixels = np.sum(prediction_mask_full == 1)\n        image_pixels = original_width * original_height\n        coverage_percent = (player_pixels / image_pixels) * 100\n        \n        total_player_pixels += player_pixels\n        total_pixels += image_pixels\n        \n        # Column 1: Original Image\n        axes[i, 0].imshow(image_rgb)\n        axes[i, 0].set_title(f'Image {i+1}: Original\\n{original_width}×{original_height}', \n                           fontsize=12, fontweight='bold', pad=10)\n        axes[i, 0].axis('off')\n        \n        # Column 2: Pure White Segmentation\n        axes[i, 1].imshow(white_overlay)\n        axes[i, 1].set_title(f'Segmentation Mask\\n{player_pixels:,} white pixels', \n                           fontsize=12, fontweight='bold', pad=10)\n        axes[i, 1].axis('off')\n        \n        # Column 3: Overlay\n        axes[i, 2].imshow(blended)\n        axes[i, 2].set_title(f'White Overlay\\n{coverage_percent:.1f}% coverage', \n                           fontsize=12, fontweight='bold', pad=10)\n        axes[i, 2].axis('off')\n    \n    # Calculate overall statistics\n    overall_coverage = (total_player_pixels / total_pixels) * 100 if total_pixels > 0 else 0\n    \n    plt.suptitle(f'🎯 AI Football Player Segmentation - 10 Image Analysis\\n'\n                f'⚪ White = Detected Players | 📊 Overall: {overall_coverage:.1f}% player coverage | 🎯 Model Accuracy: 92.4%', \n                fontsize=22, fontweight='bold', y=0.98)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(top=0.95)\n    \n    if save_dir:\n        save_path = os.path.join(save_dir, \"10_images_white_segmentation.png\")\n        plt.savefig(save_path, dpi=150, bbox_inches='tight', facecolor='white')\n        print(f\"💾 Detailed results saved to: {save_path}\")\n    \n    plt.show()\n    \n    # Print summary statistics\n    print(f\"\\n📈 Summary Statistics for {len(selected_images)} images:\")\n    print(f\"   Total player pixels detected: {total_player_pixels:,}\")\n    print(f\"   Total image pixels: {total_pixels:,}\")\n    print(f\"   Overall player coverage: {overall_coverage:.2f}%\")\n    print(f\"   Average per image: {total_player_pixels/len(selected_images):.0f} player pixels\")\n\ndef quick_white_segmentation_preview(image_folder, num_images=6):\n    \"\"\"\n    Quick preview with white segmentation\n    \"\"\"\n    # Get all image files\n    image_files = glob(os.path.join(image_folder, \"*.jpg\")) + \\\n                  glob(os.path.join(image_folder, \"*.jpeg\")) + \\\n                  glob(os.path.join(image_folder, \"*.png\"))\n    \n    if not image_files:\n        print(f\"❌ No images found in {image_folder}\")\n        return\n    \n    # Select images\n    if len(image_files) > num_images:\n        selected_indices = np.random.choice(len(image_files), num_images, replace=False)\n        selected_images = [image_files[i] for i in selected_indices]\n    else:\n        selected_images = image_files\n    \n    print(f\"🔍 Quick preview of {len(selected_images)} images with white segmentation...\")\n    \n    # Create compact figure\n    fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n    axes = axes.flatten()\n    \n    for i, image_path in enumerate(selected_images):\n        if i >= len(axes):\n            break\n            \n        # Load and process image\n        image = cv2.imread(image_path)\n        if image is None:\n            continue\n            \n        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Preprocess and predict\n        image_batch, _ = preprocess_image(image_rgb)\n        prediction = model.predict(image_batch, verbose=0)\n        prediction_mask = np.argmax(prediction[0], axis=-1)\n        prediction_mask_full = cv2.resize(prediction_mask, (image.shape[1], image.shape[0]), \n                                        interpolation=cv2.INTER_NEAREST)\n        \n        # Create white overlay\n        blended, _ = create_white_segmentation(image_rgb, prediction_mask_full, alpha=0.5)\n        \n        # Display\n        axes[i].imshow(blended)\n        player_pixels = np.sum(prediction_mask_full == 1)\n        \n        axes[i].set_title(f'Image {i+1}\\n⚪ {player_pixels:,} player pixels', \n                         fontsize=12, fontweight='bold')\n        axes[i].axis('off')\n    \n    # Hide unused subplots\n    for i in range(len(selected_images), len(axes)):\n        axes[i].axis('off')\n    \n    plt.suptitle('⚪ AI Player Detection - White Segmentation Preview\\n'\n                'White areas show detected football players', \n                fontsize=16, fontweight='bold', y=0.95)\n    \n    plt.tight_layout()\n    plt.subplots_adjust(top=0.90)\n    plt.show()\n\n# ============================================================================\n# Main Execution\n# ============================================================================\n\ndef main():\n    print(\"🎯 FOOTBALL PLAYER SEGMENTATION - WHITE SEGMENTATION TESTING\")\n    print(\"=\"*60)\n    print(\"Choose test mode:\")\n    print(\"1. 🔍 Test 6 Images (Grid View)\")\n    print(\"2. 📊 Test 10 Images (Detailed)\")\n    print(\"3. ⚡ Quick 6-Image Preview\")\n    print(\"=\"*60)\n    \n    choice = input(\"Enter your choice (1-3): \").strip()\n    \n    # Default paths for Kaggle\n    default_image_folder = \"/kaggle/input/football-player-segmentation/images\"\n    default_output_dir = \"/kaggle/working/white_segmentation_results\"\n    \n    if choice == \"1\":\n        image_folder = input(f\"Enter image folder [default: {default_image_folder}]: \").strip() or default_image_folder\n        save_dir = input(\"Enter save directory [default: /kaggle/working/white_segmentation_results]: \").strip() or default_output_dir\n        test_multiple_images_white_segmentation(image_folder, num_images=6, save_dir=save_dir)\n        \n    elif choice == \"2\":\n        image_folder = input(f\"Enter image folder [default: {default_image_folder}]: \").strip() or default_image_folder\n        save_dir = input(\"Enter save directory [default: /kaggle/working/white_segmentation_results]: \").strip() or default_output_dir\n        test_10_images_detailed(image_folder, save_dir=save_dir)\n        \n    elif choice == \"3\":\n        image_folder = input(f\"Enter image folder [default: {default_image_folder}]: \").strip() or default_image_folder\n        quick_white_segmentation_preview(image_folder, num_images=6)\n        \n    else:\n        print(\"❌ Invalid choice!\")\n\n# Auto-run in Kaggle\nif __name__ == \"__main__\":\n    import os\n    \n    if os.path.exists('/kaggle'):\n        print(\"🏠 Running in Kaggle - Auto testing with white segmentation...\")\n        image_folder = \"/kaggle/input/football-player-segmentation/images\"\n        \n        if os.path.exists(image_folder):\n            # Run all three modes\n            print(\"\\n\" + \"=\"*50)\n            print(\"1. 🔍 Quick 6-Image Preview\")\n            quick_white_segmentation_preview(image_folder, num_images=6)\n            \n            print(\"\\n\" + \"=\"*50)\n            print(\"2. 📊 Detailed 6-Image Analysis\")\n            test_multiple_images_white_segmentation(image_folder, num_images=6, \n                                                   save_dir=\"/kaggle/working/white_segmentation_results\")\n        else:\n            print(\"❌ Dataset not found. Please check the path.\")\n    else:\n        main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T22:13:45.612809Z","iopub.execute_input":"2025-11-29T22:13:45.613111Z","iopub.status.idle":"2025-11-29T22:14:08.383993Z","shell.execute_reply.started":"2025-11-29T22:13:45.613088Z","shell.execute_reply":"2025-11-29T22:14:08.383231Z"}},"outputs":[],"execution_count":null}]}