{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":117682,"databundleVersionId":15062069},{"sourceType":"datasetVersion","sourceId":14922905,"datasetId":9548626,"databundleVersionId":15789729},{"sourceType":"datasetVersion","sourceId":14245247,"datasetId":9088503,"databundleVersionId":15043865},{"sourceType":"datasetVersion","sourceId":14295835,"datasetId":9125518,"databundleVersionId":15099120},{"sourceType":"modelInstanceVersion","sourceId":760816,"databundleVersionId":15803691,"modelInstanceId":579617,"modelId":591936},{"sourceType":"modelInstanceVersion","sourceId":758741,"databundleVersionId":15790436,"modelInstanceId":579617,"modelId":591936},{"sourceType":"kernelVersion","sourceId":288572598},{"sourceType":"kernelVersion","sourceId":294535277},{"sourceType":"kernelVersion","sourceId":299553657}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# import os\n# # ============================================================================\n# # SETUP PACKAGES\n# # ============================================================================\n# var = \"/kaggle/input/vesuvius25-packages-offline-installer-v20251226/whls\"\n# if os.path.exists(var):\n#     print(f\"Installing packages from: {var}\")\n#     import subprocess\n#     subprocess.run([\n#         \"pip\", \"install\", \"--quiet\",\n#         f\"{var}/keras_nightly-3.12.0.dev2025100703-py3-none-any.whl\",\n#         f\"{var}/tifffile-2025.10.16-py3-none-any.whl\",\n#         f\"{var}/imagecodecs-2025.11.11-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl\",\n#         f\"{var}/medicai-0.0.3-py3-none-any.whl\",\n#         \"--no-index\",\n#         \"--find-links\", var\n#     ], check=False, capture_output=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:40:49.421966Z","iopub.execute_input":"2026-02-23T19:40:49.422224Z","iopub.status.idle":"2026-02-23T19:40:49.426720Z","shell.execute_reply.started":"2026-02-23T19:40:49.422193Z","shell.execute_reply":"2026-02-23T19:40:49.426026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, subprocess\n\nwhl_dir = \"/kaggle/input/vesuvius25-packages-offline-installer-v20251226/whls\"\nif os.path.exists(whl_dir):\n    print(\"Installing offline wheels...\")\n    subprocess.run([\n        \"pip\", \"install\", \"--quiet\", \"--no-index\", \"--no-deps\",\n        f\"{whl_dir}/keras_nightly-3.12.0.dev2025100703-py3-none-any.whl\",\n        f\"{whl_dir}/tifffile-2025.10.16-py3-none-any.whl\",\n        f\"{whl_dir}/imagecodecs-2025.11.11-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl\",\n        f\"{whl_dir}/medicai-0.0.3-py3-none-any.whl\"\n    ], check=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:40:49.428030Z","iopub.execute_input":"2026-02-23T19:40:49.428241Z","iopub.status.idle":"2026-02-23T19:40:53.852306Z","shell.execute_reply.started":"2026-02-23T19:40:49.428221Z","shell.execute_reply":"2026-02-23T19:40:53.851454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import imagecodecs\nimport tifffile\nprint(\"imagecodecs OK\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:40:53.853181Z","iopub.execute_input":"2026-02-23T19:40:53.853588Z","iopub.status.idle":"2026-02-23T19:40:54.086957Z","shell.execute_reply.started":"2026-02-23T19:40:53.853556Z","shell.execute_reply":"2026-02-23T19:40:54.086182Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tifffile\nimport numpy as np\n\ntest = tifffile.imread(\"/kaggle/input/vesuvius-challenge-surface-detection/train_images/1004283650.tif\")\nprint(test.shape, test.dtype)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:40:54.087993Z","iopub.execute_input":"2026-02-23T19:40:54.088309Z","iopub.status.idle":"2026-02-23T19:40:54.611799Z","shell.execute_reply.started":"2026-02-23T19:40:54.088260Z","shell.execute_reply":"2026-02-23T19:40:54.610952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 0. Imports & config\n# ============================================================\nimport os\nimport random\nimport numpy as np\nimport tensorflow as tf\nimport keras\nfrom keras import layers\nimport cv2\nimport matplotlib.pyplot as plt\n\nprint(\"TF:\", tf.__version__)\nprint(\"Keras:\", keras.__version__)\n\nDATA_DIR = \"/kaggle/input/notebooks/crischir/3d-vesuvius-posproctdataset/vesuvius_2d_dataset/\"\n\nTARGET_H, TARGET_W = 320, 320\nPATCH_SIZE = 256\nBATCH_SIZE = 8\nEPOCHS = 45\nVAL_RATIO = 0.1\n\nkeras.mixed_precision.set_global_policy(\"float32\")\ntf.config.optimizer.set_jit(False)\n\n# ============================================================\n# 1. List files and split train/val\n# ============================================================\nall_x = sorted([f for f in os.listdir(DATA_DIR) if f.endswith(\"_x.npy\")])\nbases = [f.replace(\"_x.npy\", \"\") for f in all_x]\n\nrandom.seed(42)\nrandom.shuffle(bases)\n\nn_val = int(len(bases) * VAL_RATIO)\nval_bases = bases[:n_val]\ntrain_bases = bases[n_val:]\n\nprint(\"Train slices:\", len(train_bases))\nprint(\"Val slices:\", len(val_bases))\n\n# ============================================================\n# 2. Numpy loader with resize\n# ============================================================\ndef load_and_resize(base: bytes):\n    base = base.decode(\"utf-8\")\n    x_path = os.path.join(DATA_DIR, base + \"_x.npy\")\n    y_path = os.path.join(DATA_DIR, base + \"_y.npy\")\n\n    x = np.load(x_path)  # (H,W,C)\n    y = np.load(y_path)  # (H,W)\n\n    x_resized = np.stack([\n        cv2.resize(x[..., c], (TARGET_W, TARGET_H), interpolation=cv2.INTER_LINEAR)\n        for c in range(x.shape[-1])\n    ], axis=-1)\n\n    y_resized = cv2.resize(y, (TARGET_W, TARGET_H), interpolation=cv2.INTER_NEAREST)\n\n    mask = (y_resized != 2).astype(np.float32)\n    y_bin = (y_resized == 1).astype(np.float32)\n\n    return (\n        x_resized.astype(np.float32),          # (H,W,C)\n        y_bin[..., None].astype(np.float32),   # (H,W,1)\n        mask[..., None].astype(np.float32),    # (H,W,1)\n    )\n\ndef tf_load_full(base):\n    x, y, m = tf.numpy_function(\n        load_and_resize,\n        [base],\n        [tf.float32, tf.float32, tf.float32]\n    )\n    x.set_shape((TARGET_H, TARGET_W, None))\n    y.set_shape((TARGET_H, TARGET_W, 1))\n    m.set_shape((TARGET_H, TARGET_W, 1))\n    return {\"x_full\": x, \"mask_full\": m}, y\n\n# ============================================================\n# 3. Patch extraction\n# ============================================================\ndef random_patch(x, y, m):\n    h, w = TARGET_H, TARGET_W\n    ph, pw = PATCH_SIZE, PATCH_SIZE\n\n    if h == ph and w == pw:\n        return x, y, m\n\n    max_y = h - ph\n    max_x = w - pw\n    ry = tf.random.uniform([], 0, max_y + 1, dtype=tf.int32)\n    rx = tf.random.uniform([], 0, max_x + 1, dtype=tf.int32)\n\n    x_patch = x[ry:ry+ph, rx:rx+pw, :]\n    y_patch = y[ry:ry+ph, rx:rx+pw, :]\n    m_patch = m[ry:ry+ph, rx:rx+pw, :]\n    return x_patch, y_patch, m_patch\n\ndef to_patches(sample, y):\n    x_full = sample[\"x_full\"]\n    m_full = sample[\"mask_full\"]\n    x, y, m = random_patch(x_full, y, m_full)\n    return {\"x\": x, \"mask\": m}, y\n\n# ============================================================\n# 4. Build tf.data pipelines\n# ============================================================\n# def augment_batch(inputs, y):\n#     x = inputs[\"x\"]\n#     mask = inputs[\"mask\"]\n    \n#     # Combine x, y, and mask to ensure they are transformed identically\n#     # We stack them on the channel axis to apply one transformation\n#     combined = tf.concat([x, y, mask], axis=-1)\n    \n#     # 1. Random Flip (Left-Right)\n#     combined = tf.image.random_flip_left_right(combined)\n    \n#     # 2. Random Flip (Up-Down)\n#     combined = tf.image.random_flip_up_down(combined)\n    \n#     # 3. Random Rotation (0, 90, 180, or 270 degrees)\n#     k = tf.random.uniform([], 0, 4, dtype=tf.int32)\n#     combined = tf.image.rot90(combined, k=k)\n    \n#     # Split back into x, y, and mask\n#     # x has 'in_channels', y has 1, mask has 1\n#     new_x = combined[..., :in_channels]\n#     new_y = combined[..., in_channels:in_channels+1]\n#     new_mask = combined[..., in_channels+1:]\n    \n#     return {\"x\": new_x, \"mask\": new_mask}, new_y\ndef augment_batch(inputs, y):\n    x = inputs[\"x\"]\n    mask = inputs[\"mask\"]\n    \n    # Dynamically determine the number of channels in x\n    # This solves the NameError: name 'in_channels' is not defined\n    in_channels = tf.shape(x)[-1]\n    \n    # Combine x, y, and mask along the channel axis (last axis)\n    # This ensures flips and rotations apply to all three simultaneously\n    combined = tf.concat([x, y, mask], axis=-1)\n    \n    # 1. Random Flip (Left-Right)\n    combined = tf.image.random_flip_left_right(combined)\n    \n    # 2. Random Flip (Up-Down)\n    combined = tf.image.random_flip_up_down(combined)\n    \n    # 3. Random Rotation (0, 90, 180, or 270 degrees)\n    k = tf.random.uniform([], 0, 4, dtype=tf.int32)\n    combined = tf.image.rot90(combined, k=k)\n    \n    # Split back into x, y, and mask using the in_channels variable\n    # We use slicing to recover the original dimensions\n    new_x = combined[..., :in_channels]\n    new_y = combined[..., in_channels : in_channels + 1]\n    new_mask = combined[..., in_channels + 1:]\n    \n    return {\"x\": new_x, \"mask\": new_mask}, new_y\n# ============================================================\n# 4. Build tf.data pipelines (Updated)\n# ============================================================\ntrain_ds = (\n    tf.data.Dataset.from_tensor_slices(train_bases)\n    .shuffle(4096)\n    .map(tf_load_full, num_parallel_calls=tf.data.AUTOTUNE)\n    .map(to_patches, num_parallel_calls=tf.data.AUTOTUNE)\n    .map(augment_batch, num_parallel_calls=tf.data.AUTOTUNE) # <--- New step\n    .batch(BATCH_SIZE)\n    .prefetch(tf.data.AUTOTUNE)\n)\n\n# Keep val_ds as it was (no augmentation)\nval_ds = (\n    tf.data.Dataset.from_tensor_slices(val_bases)\n    .map(tf_load_full, num_parallel_calls=tf.data.AUTOTUNE)\n    .map(to_patches, num_parallel_calls=tf.data.AUTOTUNE)\n    .batch(BATCH_SIZE)\n    .prefetch(tf.data.AUTOTUNE)\n)\nsample_x = np.load(os.path.join(DATA_DIR, train_bases[0] + \"_x.npy\"))\nin_channels = sample_x.shape[-1]\ninput_shape = (PATCH_SIZE, PATCH_SIZE, in_channels)\nprint(\"Input shape:\", input_shape)\n\n\n# ============================================================\n# 5. UNet with BatchNorm (only needed if training from scratch)\n# ============================================================\ndef conv_block_bn(x, filters):\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    return x\n\ndef encoder_block_bn(x, filters):\n    c = conv_block_bn(x, filters)\n    p = layers.MaxPool2D()(c)\n    return c, p\n\ndef build_unet_bn(input_shape):\n    x_in = keras.Input(shape=input_shape, name=\"x\")\n\n    c1, p1 = encoder_block_bn(x_in, 32)\n    c2, p2 = encoder_block_bn(p1, 64)\n    c3, p3 = encoder_block_bn(p2, 128)\n    c4, p4 = encoder_block_bn(p3, 256)\n\n    b = conv_block_bn(p4, 256)\n\n    u4 = layers.UpSampling2D()(b)\n    u4 = layers.Concatenate()([u4, c4])\n    c5 = conv_block_bn(u4, 256)\n\n    u3 = layers.UpSampling2D()(c5)\n    u3 = layers.Concatenate()([u3, c3])\n    c6 = conv_block_bn(u3, 128)\n\n    u2 = layers.UpSampling2D()(c6)\n    u2 = layers.Concatenate()([u2, c2])\n    c7 = conv_block_bn(u2, 64)\n\n    u1 = layers.UpSampling2D()(c7)\n    u1 = layers.Concatenate()([u1, c1])\n    c8 = conv_block_bn(u1, 32)\n\n    y_out = layers.Conv2D(1, 1, activation=None, dtype=\"float32\")(c8)\n    return keras.Model(x_in, y_out)\n\n# ============================================================\n# 6. Load pretrained model\n# ============================================================\nPRETRAINED_PATH = \"/kaggle/input/models/crischir/postproceesingvesuvius3dmodel/keras/default/2/best_model.keras\"\n\nprint(\"Loading pretrained model...\")\nmodel = keras.models.load_model(PRETRAINED_PATH, compile=False)\nprint(\"Loaded pretrained model from:\", PRETRAINED_PATH)\nprint(\"Model input shape:\", model.input_shape)\n\n# ============================================================\n# 7. Loss functions\n# ============================================================\ndef dice_loss(y_true, logits, mask, smooth=1.0):\n    y_pred = tf.sigmoid(logits)\n    y_true = y_true * mask\n    y_pred = y_pred * mask\n\n    intersection = tf.reduce_sum(y_true * y_pred)\n    union = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred)\n    dice = (2.0 * intersection + smooth) / (union + smooth)\n    return 1.0 - dice\n\ndef total_loss(y_true, logits, mask):\n    logits = tf.clip_by_value(logits, -10.0, 10.0)\n    bce = tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true, logits=logits)\n    bce = tf.reduce_sum(bce * mask) / (tf.reduce_sum(mask) + 1e-6)\n    d = dice_loss(y_true, logits, mask)\n    return bce + d\n\n# ============================================================\n# 8. Custom training wrapper to inject mask\n# ============================================================\nclass MaskedLossWrapper(keras.losses.Loss):\n    def call(self, y_true, y_pred):\n        # y_pred are logits from the model\n        # we need the mask from the inputs, so we’ll pass it via model.add_metric? No.\n        # Simpler: we store mask in a global and use a custom train_step.\n        raise NotImplementedError(\"We will use a custom train_step instead of this.\")\n\n# Instead of using model.fit with built-in loss, we’ll write a light custom loop\noptimizer = keras.optimizers.Adam(1e-4, clipnorm=1.0)\n\n@tf.function\ndef train_step(batch_inputs, y_true):\n    x = batch_inputs[\"x\"]\n    mask = batch_inputs[\"mask\"]\n    with tf.GradientTape() as tape:\n        logits = model(x, training=True)\n        loss = total_loss(y_true, logits, mask)\n    grads = tape.gradient(loss, model.trainable_variables)\n    optimizer.apply_gradients(zip(grads, model.trainable_variables))\n    return loss\n\n@tf.function\ndef val_step(batch_inputs, y_true):\n    x = batch_inputs[\"x\"]\n    mask = batch_inputs[\"mask\"]\n    logits = model(x, training=False)\n    loss = total_loss(y_true, logits, mask)\n    return loss\n@tf.function\ndef predict_with_tta(x):\n    \"\"\"\n    Applies 8-fold TTA (Rotations + Flips) and averages results.\n    \"\"\"\n    # 1. Original\n    preds = tf.sigmoid(model(x, training=False))\n    \n    # 2. Horizontal Flip\n    x_hf = tf.image.flip_left_right(x)\n    preds += tf.image.flip_left_right(tf.sigmoid(model(x_hf, training=False)))\n    \n    # 3. Vertical Flip\n    x_vf = tf.image.flip_up_down(x)\n    preds += tf.image.flip_up_down(tf.sigmoid(model(x_vf, training=False)))\n    \n    # 4. Rotate 90\n    x_r90 = tf.image.rot90(x, k=1)\n    preds += tf.image.rot90(tf.sigmoid(model(x_r90, training=False)), k=3)\n    \n    # 5. Rotate 180\n    x_r180 = tf.image.rot90(x, k=2)\n    preds += tf.image.rot90(tf.sigmoid(model(x_r180, training=False)), k=2)\n    \n    # 6. Rotate 270\n    x_r270 = tf.image.rot90(x, k=3)\n    preds += tf.image.rot90(tf.sigmoid(model(x_r270, training=False)), k=1)\n    \n    # 7. Transpose (Flip + Rotate combo)\n    x_trans = tf.image.transpose(x)\n    preds += tf.image.transpose(tf.sigmoid(model(x_trans, training=False)))\n\n    # Average (7 versions + original = 8)\n    return preds / 8.0\n\n@tf.function\ndef val_step_tta(batch_inputs, y_true):\n    x = batch_inputs[\"x\"]\n    mask = batch_inputs[\"mask\"]\n    \n    # Get TTA averaged probabilities\n    y_pred_probs = predict_with_tta(x)\n    \n    # Convert back to logit-space for your existing total_loss function \n    # OR calculate Dice directly from probs. \n    # For consistency with your code, we'll use a small epsilon for logit conversion:\n    logits_tta = tf.math.log(y_pred_probs / (1.0 - y_pred_probs + 1e-7))\n    \n    loss = total_loss(y_true, logits_tta, mask)\n    return loss\n# ============================================================\n# 9. Manual loop with early stopping, LR scheduling, checkpoints\n# ============================================================\nbest_val = np.inf\npatience = 4\nwait = 0\nlr_factor = 0.5\nmin_lr = 1e-6\n\nbest_path = \"/kaggle/working/best_model.keras\"\nlast_path = \"/kaggle/working/last_model.keras\"\n\nfor epoch in range(EPOCHS):\n    # Train\n    train_losses = []\n    for batch_inputs, batch_y in train_ds:\n        loss = train_step(batch_inputs, batch_y)\n        train_losses.append(loss.numpy())\n\n    # Val\n    # val_losses = []\n    # for batch_inputs, batch_y in val_ds:\n    #     loss = val_step(batch_inputs, batch_y)\n    #     val_losses.append(loss.numpy())\n\n    # train_mean = float(np.mean(train_losses))\n    # val_mean = float(np.mean(val_losses))\n\n\n    val_losses = []\n    for batch_inputs, batch_y in val_ds:\n        # Using TTA version for more accurate validation metrics\n        loss = val_step_tta(batch_inputs, batch_y)\n        val_losses.append(loss.numpy())\n    train_mean = float(np.mean(train_losses))\n    val_mean = float(np.mean(val_losses))\n    \n    print(f\"Epoch {epoch+1}/{EPOCHS} - \"\n          f\"train_loss: {train_mean:.4f} - \"\n          f\"val_loss: {val_mean:.4f} - \"\n          f\"lr: {float(optimizer.learning_rate.numpy()):.6f}\")\n\n    # Save last\n    model.save(last_path, overwrite=True)\n\n    # Check best\n    if val_mean < best_val - 1e-4:\n        best_val = val_mean\n        wait = 0\n        model.save(best_path, overwrite=True)\n        print(\"  → New best model saved.\")\n    else:\n        wait += 1\n        print(f\"  → No improvement. Patience {wait}/{patience}\")\n\n    # Reduce LR on plateau\n    if wait > 0 and wait % 2 == 0:\n        old_lr = float(optimizer.learning_rate.numpy())\n        new_lr = max(old_lr * lr_factor, min_lr)\n        optimizer.learning_rate.assign(new_lr)\n        print(f\"  → LR reduced: {old_lr:.6f} → {new_lr:.6f}\")\n\n    # Early stopping\n    if wait >= patience:\n        print(\"Early stopping triggered.\")\n        break\n\n# ============================================================\n# 10. Save final model\n# ============================================================\nmodel.save(\"/kaggle/working/vesuvius_2d_unet_bn_patches_finetuned.keras\")\nprint(\"Saved finetuned model.\")\nprint(\"Best model at:\", best_path)\nprint(\"Last model at:\", last_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:47:52.387942Z","iopub.execute_input":"2026-02-23T19:47:52.388863Z","execution_failed":"2026-02-23T20:02:15.943Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport tensorflow as tf\n\ndef visualize_raw_training_sample(dataset, sample_index=0):\n    \"\"\"\n    Visualizes a raw training sample (input, label, mask) from the dataset.\n    Independent of previous code or variables.\n    \"\"\"\n\n    # Take one batch from the dataset\n    for batch_inputs, batch_y in dataset.take(1):\n        # Extract tensors\n        x_full = batch_inputs[\"x_full\"][sample_index]   # (H, W, C)\n        mask = batch_inputs[\"mask_full\"][sample_index]  # (H, W, 1)\n        y_true = batch_y[sample_index]                  # (H, W, 1)\n\n        # Convert to numpy\n        x_np = x_full.numpy()\n        y_np = y_true.numpy().squeeze()\n        m_np = mask.numpy().squeeze()\n\n        # Plot\n        plt.figure(figsize=(18, 5))\n\n        plt.subplot(1, 3, 1)\n        plt.title(\"Input (channel 0)\")\n        plt.imshow(x_np[..., 0], cmap=\"gray\")\n        plt.axis(\"off\")\n\n        plt.subplot(1, 3, 2)\n        plt.title(\"Label (binary ink)\")\n        plt.imshow(y_np, cmap=\"gray\")\n        plt.axis(\"off\")\n\n        plt.subplot(1, 3, 3)\n        plt.title(\"Mask (valid region)\")\n        plt.imshow(m_np, cmap=\"gray\")\n        plt.axis(\"off\")\n\n        plt.show()\n        break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:41:09.715838Z","iopub.status.idle":"2026-02-23T19:41:09.716228Z","shell.execute_reply.started":"2026-02-23T19:41:09.716034Z","shell.execute_reply":"2026-02-23T19:41:09.716057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport tensorflow as tf\n\ndef visualize_training_patch(dataset, sample_index=0):\n    \"\"\"\n    Visualizes a training PATCH (input, label, mask) from the dataset.\n    Works with your current patched dataset structure.\n    \"\"\"\n\n    for batch_inputs, batch_y in dataset.take(1):\n        x = batch_inputs[\"x\"][sample_index]      # (PATCH, PATCH, C)\n        mask = batch_inputs[\"mask\"][sample_index]# (PATCH, PATCH, 1)\n        y_true = batch_y[sample_index]           # (PATCH, PATCH, 1)\n\n        x_np = x.numpy()\n        y_np = y_true.numpy().squeeze()\n        m_np = mask.numpy().squeeze()\n\n        plt.figure(figsize=(18, 5))\n\n        plt.subplot(1, 3, 1)\n        plt.title(\"Input (channel 0)\")\n        plt.imshow(x_np[..., 0], cmap=\"gray\")\n        plt.axis(\"off\")\n\n        plt.subplot(1, 3, 2)\n        plt.title(\"Label (binary ink)\")\n        plt.imshow(y_np, cmap=\"gray\")\n        plt.axis(\"off\")\n\n        plt.subplot(1, 3, 3)\n        plt.title(\"Mask\")\n        plt.imshow(m_np, cmap=\"gray\")\n        plt.axis(\"off\")\n\n        plt.show()\n        break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:41:09.717366Z","iopub.status.idle":"2026-02-23T19:41:09.717769Z","shell.execute_reply.started":"2026-02-23T19:41:09.717551Z","shell.execute_reply":"2026-02-23T19:41:09.717574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_training_patch(train_ds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:41:09.719612Z","iopub.status.idle":"2026-02-23T19:41:09.719958Z","shell.execute_reply.started":"2026-02-23T19:41:09.719784Z","shell.execute_reply":"2026-02-23T19:41:09.719804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# visualize_raw_training_sample(train_ds)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-23T19:41:09.720962Z","iopub.status.idle":"2026-02-23T19:41:09.721213Z","shell.execute_reply.started":"2026-02-23T19:41:09.721091Z","shell.execute_reply":"2026-02-23T19:41:09.721109Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}