{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"sourceType":"competition"},{"sourceId":1003830,"sourceType":"datasetVersion","datasetId":550917}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<a name='T'>\n\n<p style=\"padding: 20px;\n          background-color: black;\n          font-family: computermodern;\n          color: white;\n          font-size: 200%;\n          text-align: center;\n          border-radius: 40px 20px;\n          \">Resources for Image Dataset Augmentation<br>\n          </p>\n<p style=\"font-family: computermodern;\n          color: #000000;\n          font-size: 175%;\n          text-align: center;\n          \">Created by Alexandre Le Mercier on the 19th of March 2025<br>\n             </p>\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F17037041%2Fd6563a102c24a17131c70b3bd9b66d99%2Ftitle.png?generation=1742408079992349&alt=media)\n\n<a id=\"TOC\"></a>\n\n<div style=\"background-color: #e8f5e9; border-left: 10px solid #66bb6a; padding: 20px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n    <h1 style=\"color: #388e3c;\">Table of Contents</h1>\n    <ul style=\"list-style-type: none;\">\n        <li><a href=\"#s0\" style=\"color: #2e7d32;\"><strong>1. Context and motivation: Why should you care about this notebook?</strong></a></li>\n        <li><a href=\"#sd\" style=\"color: #2e7d32;\"><strong>2. Data augmentation techniques available in this library</strong></a></li>\n        <li><a href=\"#s1\" style=\"color: #2e7d32;\"><strong>3. Environment setup</strong></a></li>\n        <li><a href=\"#s2\" style=\"color: #2e7d32;\"><strong>4. Building a data augmentation class</strong></a></li>\n        <li><a href=\"#s3\" style=\"color: #2e7d32;\"><strong>5. Apply data augmentation to benchmark datasets</strong></a></li>\n        <li><a href=\"#s4\" style=\"color: #2e7d32;\"><strong>6. Compare the performances of XCeption on tiny MNIST and its augmented version</strong></a></li>\n</div>","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"markdown","source":"<a id=\"s0\"></a>\n<div style=\"color: black; background-color: #E6E6FA; padding: 10px; border-left: 5px solid purple; border-radius: 5px;\">\n<h1>1. Context and motivation: Why should you care about this notebook?</h1>\n</div>\n\n[Back to table of contents](#TOC)\n\nHere is why:\n\n![Evasion attack on MNIST](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F17037041%2Fa7718f60e1fb3ca1abd4d42fb6bb3334%2FImage1.png?generation=1742391212319013&alt=media)\n\nThe **robustness** of the models we build has always been an issue:\n- **Natural robustness** means that your model is very likely to make good decisions in \"naturel\" scenarios, i.e. contexts that you would expect to encounter in your everyday life. For example, an animal classifier expects to be given animal pictures as inputs.\n- **Adversarial robustness** means that your model is not vulnerable to *evasion attacks*, i.e. cyberattacks that will slightly change the input image in such way that a near-perfect classifier would fail miserably at each input. How? By adding an imperceptible noise, carefully crafted based on knowledge extracted by querying the model multiple times (see example above, where an XGBoost model is fooled by only changing a single pixel of the original, correctly classified image). Evasion attacks have proven to be very effective on image datasets and consistute obvious security threats.\n\nTherefore, *how should you make your models safe?* One way of doing that is **data augmentation**. This means that you will craft new images for your dataset based on the existing ones, in order to improve the versatility of the dataset and force the trained model to learn more general patterns rather that simplistic ones. Data augmentation considerably rises the robustness of the trained model, although the training time is also longer.\n\n<div style=\"color: black; background-color: #ffcccc; padding: 10px; border-left: 5px solid #ff3333; border-radius: 5px;\">\n           <strong>Remark:</strong> So far, this package can only treat and export jpg images. If your images are png, please consider building a simple loop to change the extensions into jpg. If this is really an issue for you, you can contact me for an update of the fast_skimage library.\n</div>","metadata":{}},{"cell_type":"markdown","source":"<a id=\"sd\"></a>\n<div style=\"color: black; background-color: #E6E6FA; padding: 10px; border-left: 5px solid purple; border-radius: 5px;\">\n<h1>2. Data augmentation techniques available in this library</h1>\n</div>\n\n[Back to table of contents](#TOC)\n\n\n### ✅ **1. Random Rotation**\n**Function:** `random_rotate`\n\nRandomly rotates the image within a user-defined angle range (by default, ±25 degrees). Rotation helps your model become invariant to orientation changes, making it robust to rotated images encountered in real-world scenarios.\n\n\n### ✅ **2. Random Shear**\n**Function:** `random_shear`\n\nRandomly shears (slants) your image by a small angle. This augmentation helps the model to generalize better to slight geometric distortions or perspective changes in real-world images.\n\n\n### ✅ **3. Random Translation**\n**Function:** `random_translate`\n\nRandomly shifts the image horizontally and vertically within user-specified pixel limits. Translation forces your model to become insensitive to the exact placement of the object within the image frame, improving generalization.\n\n\n### ✅ **4. Random Reflection (Flip)**\n**Function:** `random_reflect`\n\nRandomly flips your image either vertically, horizontally, or both. Reflection helps build invariance towards mirrored images and is particularly effective for datasets where orientation does not matter (such as certain natural scenes or objects).\n\n\n### ✅ **5. Random Scale (Zoom)**\n**Function:** `random_scale`\n\nRandomly zooms in or out by a small factor. Scaling simulates objects being observed from different distances, enhancing the model's ability to handle varied object sizes.\n\n\n### ✅ **6. Spatial Padding**\n**Function:** `spatial_pad`\n\nPads the borders of your image with zeros or a mean pixel value to reach a target dimension. Padding is especially useful in convolutional neural networks (CNNs) to preserve the input size after multiple layers of convolutions, helping the model learn stable patterns across image boundaries.\n\n\n### ✅ **7. Ben Graham Transform (APTOS method)**\n**Function:** `ben_graham_transform`\n\nApplies an effective preprocessing step inspired by Ben Graham's winning solution in the APTOS competition (see [here](https://www.kaggle.com/code/nxrprime/panda-image-data-augmentation-techniques-w-monai)). This method emphasizes image features by adding a weighted version of a Gaussian-blurred copy of the image back onto itself. It's particularly powerful for enhancing image contrast and structural clarity, benefiting medical and photographic images alike.\n\n\n### ✅ **8. Random Gaussian Noise**\n**Function:** `random_gaussian_noise`\n\nInjects random Gaussian noise into the image. Adding Gaussian noise emulates real-life sensor noise or environmental variations, making your model robust to noisy input data and less sensitive to small perturbations (important for adversarial robustness).\n\n\n### ✅ **9. Random Salt-and-Pepper Noise**\n**Function:** `random_salt_pepper_noise`\n\nRandomly applies \"salt-and-pepper\" (black-and-white) pixel noise to the image. This augmentation simulates potential corruption or pixel-level faults, training the model to ignore spurious artifacts and handle corrupted data gracefully.\n\n\n### ✅ **10. Random Text Overlay**\n**Function:** `random_text_overlay`\n\nAdds random, semi-transparent text at random positions on your image. This augmentation improves robustness towards textual artifacts or watermarks that might appear in real-world image sources. It's especially useful when models must ignore irrelevant textual content on images.\n\n\n<div style=\"color: black; background-color: #ccffcc; padding: 10px; border-left: 5px solid #33cc33; border-radius: 5px;\">\n<strong>How to use these augmentations:</strong>\n\nSimply create a list of the desired augmentation functions and pass it to the augmentation method in the provided library. You can control the number of augmentations applied randomly per image, ensuring a diverse augmented dataset.\n</div>\n\nHere's an example of how to define and use them:\n\n```python\naugmentations = [\n    random_rotate,              \n    random_shear,               \n    random_translate,           \n    random_reflect,             \n    random_scale,               \n    spatial_pad,                \n    ben_graham_transform,       \n    random_gaussian_noise,\n    random_salt_pepper_noise,\n    random_text_overlay\n]\n\n# Example usage:\naug.build_augmented_dataset_on_disk(\n    dataset_id=0,\n    output_dir=\"augmented_dataset\",\n    scale_factor=2,  # generates 2 new images per original image\n    augmentations=augmentations,\n    min_augmentations=1,   # At least 1 augmentation technique will be applied on each image\n    max_augmentations=5,   # No image can contain more than 5 augmentations simultaneously\n    label=\"cat\",\n    to_df=True,\n    csv_name=\"augmented_dataset.csv\"\n)\n\n","metadata":{}},{"cell_type":"markdown","source":"<a id=\"s1\"></a>\n<div style=\"color: black; background-color: #E6E6FA; padding: 10px; border-left: 5px solid purple; border-radius: 5px;\">  \n    <h1>3. Environment setup</h1>\n</div>\n\n[Back to table of contents](#TOC)","metadata":{}},{"cell_type":"code","source":"#| default_exp core","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:11.616754Z","iopub.execute_input":"2025-03-24T18:39:11.617048Z","iopub.status.idle":"2025-03-24T18:39:11.620886Z","shell.execute_reply.started":"2025-03-24T18:39:11.617015Z","shell.execute_reply":"2025-03-24T18:39:11.619973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\ndef ensure_dependencies_installed():\n    \"\"\"\n    Ensures required dependencies are installed.\n    \"\"\"\n    try:\n        import monai, tqdm, cv2, fast_skimage, skimage, tensorflow, numpy\n    except ImportError as e:\n        print(\"Missing libraries detected. You can download them by runing 'Add-ons/Install Dependencies' on Kaggle or simply uncommenting the line below.\")\n        # !pip install monai tqdm opencv-python fast-skimage scikit-image tensorflow numpy\n\n# Call to make sure libraries are installed\nensure_dependencies_installed()\n\n# Global imports\nimport os\nimport warnings\nimport shutil\nimport sys\nimport random\nfrom skimage.io import imsave\nimport pkg_resources      # Track lib versions\nimport gc                 # Garbage collector\nimport glob\nimport pandas as pd       # Operations on CSV files\nimport cv2                # Operations on images\nimport numpy as np\nimport fast_skimage\nfrom skimage.transform import rotate\nfrom skimage.transform import AffineTransform, warp\nfrom scipy.ndimage import gaussian_filter\nfrom fast_skimage.Image import Image\nfrom fast_skimage.Collection import Collection\nimport torch\nfrom PIL import Image as PILImage, ImageDraw, ImageFont\nimport string\nimport matplotlib.pyplot as plt\n\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import load_img, img_to_array\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Input, Lambda, Flatten, Dropout, Dense\nfrom tensorflow.keras.optimizers import Adamax\nfrom tensorflow.keras.metrics import Precision, Recall\nfrom tensorflow.keras.applications import Xception\nfrom sklearn.model_selection import train_test_split\n\nfrom monai.transforms import (\n    LoadImage,\n    RandAffine,\n    Rand2DElastic,\n    Rand3DElastic,\n    RandFlip,\n    SpatialPad,\n    Transform,\n    Compose\n)\n\nfrom tqdm.notebook import tqdm\n\nwarnings.filterwarnings(\"ignore\")\n\n# Set random seed\nRANDOM_SEED = 5\ntf.random.set_seed(RANDOM_SEED)\nnp.random.seed(RANDOM_SEED)\n\nDEFAULT_OUTPUT_DIR = \"Augmented data\"\n# !mkdir \"Augmented data\"\n\n# Display versions (no re-import necessary)\nprint(f\"Python version: {sys.version}\\n\")\n\nprint(\"Num GPUs Available: \", len(tf.config.list_physical_devices('GPU')), \"\\n\")\n\nprint(\"Installed libraries and versions:\")\ninstalled_packages = pkg_resources.working_set\npackages_list = sorted([f\"{i.key}=={i.version}\" for i in installed_packages])\n\nfor package in packages_list:\n    print(package)\n\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:11.644107Z","iopub.execute_input":"2025-03-24T18:39:11.644400Z","iopub.status.idle":"2025-03-24T18:39:40.486432Z","shell.execute_reply.started":"2025-03-24T18:39:11.644378Z","shell.execute_reply":"2025-03-24T18:39:40.485635Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"s2\"></a>\n<div style=\"color: black; background-color: #E6E6FA; padding: 10px; border-left: 5px solid purple; border-radius: 5px;\">  \n    <h1>4. Building a data augmentation class</h1>\n</div>\n\n[Back to table of contents](#TOC)\n\nNow, we will build the `Augmenter` class in which operations on dataset will be executed. The idea is to input a dataset and list of parameters, and output the augmented dataset. Wraping this into a class makes it easier for you to reuse this in your own work via kagglehub.\n\nThe `Augmenter` class makes an extensive usage of the `Collection` class from `fast_skimage` (which I designed). I built this library to ease up image processing. Each element of a collection is stored in an optimized `Image` class that allows you to perform a series of processing operations very easily. Below is a simple example of how to handle the `Collection` class:\n\n```Python\nfor im in collection_cat.images.values():\n    im.color_to_gray() # This made all cat images turn gray!\n\nfor im in collection_dog.images.values():\n    collection_cat.images[len(collection_cat.images.values())+1] = im # We added all dog images to cat images!\n```\n\nThe `Collection` class is therefore very convenient for image data augmentation tasks.","metadata":{}},{"cell_type":"code","source":"#| export\n\ndef random_rotate(img_obj, angle_range=(-25, 25)):\n    \"\"\"\n    Rotates the image by a random angle (in degrees) within angle_range.\n    Ensures the array is (H, W) or (H, W, 3) to avoid ValueError.\n    \"\"\"\n\n    arr = img_obj.image\n\n    # 1) If we have a channel-first array (C, H, W), transpose to (H, W, C).\n    #    Heuristic: the first dim is small (1..4) and second dim is large => likely (C, H, W).\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) If an alpha channel is present (H, W, 4), drop it (we only need 3 channels).\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) If single-channel last dim => squeeze out to (H, W).\n    #    i.e. shape (H, W, 1) => (H, W).\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]  # or np.squeeze(arr, axis=-1)\n\n    # Overwrite the image in the Image object with our standardized array\n    img_obj.image = arr\n\n    # 4) Rotate by random angle\n    angle = np.random.uniform(angle_range[0], angle_range[1])\n    try:\n        # rotate(...) returns float in [0..1], so preserve_range => keep original range\n        rotated = rotate(img_obj.image, angle, preserve_range=True)\n        rotated = np.clip(rotated, 0, 255).astype(img_obj.image.dtype)\n    \n        # 5) Update the Image object\n        img_obj.set_image(rotated, method=f\"random_rotate_{round(angle,1)}deg\")\n    except Exception as e:\n        print(\"An error occured while applying rotation for an image:\", e)\n    return img_obj\n\n\ndef random_text_overlay(img_obj, prob=0.8,\n                        font_size_range=(12, 40),\n                        opacity_range=(100, 255),\n                        num_words_range=(1, 3)):\n    \"\"\"\n    Overlay random text on the image with a given probability.\n\n    Args:\n        img_obj: Your image object (expects .image as NumPy array).\n        prob: Probability of applying this augmentation.\n        font_size_range: Tuple specifying the min/max font size.\n        opacity_range: Tuple specifying the min/max opacity (alpha channel).\n        num_words_range: Number of words in random text.\n\n    Returns:\n        img_obj with random text overlay.\n    \"\"\"\n\n    if random.random() > prob:\n        return img_obj  # No augmentation applied\n\n    arr = img_obj.image\n\n    # Convert to PIL image\n    if arr.ndim == 2:\n        mode = 'L'  # grayscale\n    elif arr.shape[2] == 3:\n        mode = 'RGB'\n    elif arr.shape[2] == 4:\n        mode = 'RGBA'\n    else:\n        raise ValueError(\"Unexpected image shape for text overlay\")\n\n    pil_img = PILImage.fromarray(arr.astype('uint8'), mode=mode).convert(\"RGBA\")\n\n    # Create overlay for transparency\n    txt_overlay = PILImage.new('RGBA', pil_img.size, (255, 255, 255, 0))\n    draw = ImageDraw.Draw(txt_overlay)\n\n    # Random text generation\n    num_words = random.randint(*num_words_range)\n    random_text = ' '.join(''.join(random.choices(string.ascii_letters, k=random.randint(3,8))) for _ in range(num_words))\n\n    # Random font size\n    font_size = random.randint(*font_size_range)\n    \n    try:\n        font = ImageFont.truetype(\"arial.ttf\", font_size)\n    except:\n        font = ImageFont.load_default()\n\n    # Use textbbox to measure text size instead of deprecated textsize\n    text_bbox = draw.textbbox((0, 0), random_text, font=font)\n    text_width = text_bbox[2] - text_bbox[0]\n    text_height = text_bbox[3] - text_bbox[1]\n\n    # Text position\n    width, height = pil_img.size\n    position = (random.randint(0, max(0, width - text_width)),\n                random.randint(0, max(0, height - text_height)))\n\n    # Random text color\n    text_color = tuple(np.random.randint(0, 256, size=3))\n\n    # Random opacity\n    opacity = random.randint(*opacity_range)\n\n    # Draw the text\n    draw.text(position, random_text, font=font, fill=text_color + (opacity,))\n\n    # Composite the image with overlay\n    combined = PILImage.alpha_composite(pil_img, txt_overlay).convert(mode)\n\n    # Back to NumPy array\n    img_obj.set_image(np.array(combined), method=\"random_text_overlay\")\n\n    return img_obj\n    \n\ndef random_elastic_deformation(img_obj):\n    arr = img_obj.image\n\n    # Channel-first conversion\n    if arr.ndim == 2:\n        arr = arr[None, ...]  # (1,H,W)\n    elif arr.ndim == 3 and arr.shape[-1] == 3:\n        arr = np.transpose(arr, (2, 0, 1))  # (3,H,W)\n\n    # Add dummy third spatial dimension (B, C, D=1, H, W)\n    arr_torch = torch.from_numpy(arr).float().unsqueeze(0).unsqueeze(2)\n\n    # Instantiate Rand3DElastic correctly\n    transform = Rand3DElastic(\n        prob=1.0,\n        sigma_range=(5, 7),               # controls smoothness of deformation\n        magnitude_range=(50, 100),        # deformation magnitude\n        rotate_range=(0, 0, np.pi / 4),   # rotation around the last axis only (Z-axis)\n        scale_range=(0, 0.2, 0.2),        # scale range (D,H,W)\n        translate_range=(0, 50, 50),      # translation (D,H,W)\n        padding_mode='zeros',\n        device=torch.device('cpu')\n    )\n\n    # Apply transform\n    out_torch = transform(arr_torch)\n\n    # Remove batch and dummy dimension: (B,C,D=1,H,W) -> (C,H,W)\n    out_np = out_torch.squeeze(0).squeeze(1).cpu().numpy()\n\n    # Channel-last conversion if needed\n    if out_np.shape[0] == 3:\n        out_np = np.transpose(out_np, (1, 2, 0))\n    elif out_np.shape[0] == 1:\n        out_np = out_np[0]\n\n    # Clip values to valid image range and dtype\n    out_np = np.clip(out_np, 0, 255).astype(img_obj.image.dtype)\n\n    # Update the image object\n    img_obj.set_image(out_np, method=\"random_elastic_deformation_monai\")\n    return img_obj\n\n\ndef random_shear(img_obj, shear_range=(-0.2, 0.2)):\n    \"\"\"\n    Applies a random shear to the image.\n    shear_range: tuple of (min_shear, max_shear) in radians.\n    \"\"\"\n    arr = img_obj.image\n\n    # 1) Handle shape: (C,H,W) => (H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) Drop alpha if present\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) Squeeze single-channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    # Update the Image object with standardized array\n    img_obj.image = arr\n\n    # 4) Random shear factor\n    shear_val = np.random.uniform(shear_range[0], shear_range[1])\n\n    # Construct the AffineTransform\n    transform = AffineTransform(shear=shear_val)\n\n    try:\n        # Warp the image\n        sheared = warp(arr, transform, preserve_range=True, mode='constant', cval=0)\n        sheared = np.clip(sheared, 0, 255).astype(arr.dtype)\n\n        # 5) Update the image\n        img_obj.set_image(sheared, method=f\"random_shear_{round(shear_val, 3)}\")\n    except Exception as e:\n        print(\"An error occurred while applying shear:\", e)\n\n    return img_obj\n\ndef random_translate(img_obj, max_shift=(20, 20)):\n    \"\"\"\n    Translates the image randomly by up to +/- max_shift pixels (y_shift, x_shift).\n    max_shift: tuple (max_shift_y, max_shift_x).\n    \"\"\"\n    arr = img_obj.image\n\n    # 1) Handle shape: (C,H,W) => (H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) Drop alpha if present\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) Squeeze single-channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    img_obj.image = arr\n\n    # 4) Sample random shifts in the range [-max_shift, +max_shift]\n    shift_y = np.random.uniform(-max_shift[0], max_shift[0])\n    shift_x = np.random.uniform(-max_shift[1], max_shift[1])\n\n    # Construct the AffineTransform\n    transform = AffineTransform(translation=(shift_x, shift_y))\n\n    try:\n        translated = warp(arr, transform, preserve_range=True, mode='constant', cval=0)\n        translated = np.clip(translated, 0, 255).astype(arr.dtype)\n        img_obj.set_image(translated, method=f\"random_translate_{(round(shift_x), round(shift_y))}\")\n    except Exception as e:\n        print(\"An error occurred while applying translation:\", e)\n\n    return img_obj\n\ndef random_reflect(img_obj, prob=0.5, reflect_axes=('vertical', 'horizontal')):\n    \"\"\"\n    Randomly reflects (flips) the image across specified axes.\n    reflect_axes can be a tuple/list of any combination of 'horizontal' / 'vertical'.\n    For example, ('vertical',) only does vertical flips, ('horizontal',) only horizontal, \n    ('vertical', 'horizontal') might do both with some probability logic, etc.\n    prob is the probability that a reflection will happen at all.\n    \"\"\"\n    arr = img_obj.image\n\n    # 1) Handle shape: (C,H,W) => (H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) Drop alpha if present\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) Squeeze single-channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    img_obj.image = arr\n\n    if np.random.rand() < prob:\n        # Decide which axis to flip\n        # E.g., do vertical if 'vertical' in reflect_axes, do horizontal if 'horizontal' in reflect_axes\n        flipped = arr.copy()\n        description = []\n\n        if 'horizontal' in reflect_axes and np.random.rand() < 0.5:\n            flipped = np.flip(flipped, axis=1)\n            description.append(\"H\")\n\n        if 'vertical' in reflect_axes and np.random.rand() < 0.5:\n            flipped = np.flip(flipped, axis=0)\n            description.append(\"V\")\n\n        img_obj.set_image(flipped, method=f\"random_reflect_{''.join(description)}\")\n\n    return img_obj\n\ndef random_scale(img_obj, scale_range=(0.8, 1.2)):\n    \"\"\"\n    Scales (zooms) the image by a random factor in scale_range.\n    e.g., (0.8, 1.2) means possible 20% contraction up to 20% expansion.\n    \"\"\"\n    arr = img_obj.image\n\n    # 1) Handle shape: (C,H,W) => (H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) Drop alpha if present\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) Squeeze single-channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    img_obj.image = arr\n\n    # 4) Random scale factor\n    scale_factor = np.random.uniform(scale_range[0], scale_range[1])\n    # If you want isotropic scaling, use the same factor for x and y:\n    transform = AffineTransform(scale=(scale_factor, scale_factor))\n\n    try:\n        scaled = warp(arr, transform, preserve_range=True, mode='constant', cval=0)\n        scaled = np.clip(scaled, 0, 255).astype(arr.dtype)\n        img_obj.set_image(scaled, method=f\"random_scale_{round(scale_factor, 2)}\")\n    except Exception as e:\n        print(\"An error occurred while applying scaling:\", e)\n\n    return img_obj\n    \n\ndef spatial_pad(img_obj, new_size=(300, 300), mode=\"mean\"):\n    \"\"\"\n    Pad the image spatially to new_size = (target_height, target_width).\n    If mode=\"mean\", pad with the mean pixel value.\n    If mode=\"constant\", pad with 0 (or you can add another param).\n    \"\"\"\n    arr = img_obj.image\n\n    # 1) Convert (C,H,W)->(H,W,C) if needed\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) Drop alpha channel if present\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) Squeeze single-channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    # Overwrite in the Image object\n    img_obj.image = arr\n\n    orig_h, orig_w = arr.shape[:2]\n    pad_h, pad_w = new_size\n\n    # If the image is already bigger than new_size, you can either skip\n    # or do a center-crop, etc. For now, we'll just skip if bigger:\n    if orig_h >= pad_h and orig_w >= pad_w:\n        # no padding needed\n        return img_obj\n\n    # Determine pad values on each side so the image is (roughly) centered\n    pad_top = (pad_h - orig_h) // 2 if pad_h > orig_h else 0\n    pad_bottom = pad_h - orig_h - pad_top if pad_h > orig_h else 0\n    pad_left = (pad_w - orig_w) // 2 if pad_w > orig_w else 0\n    pad_right = pad_w - orig_w - pad_left if pad_w > orig_w else 0\n\n    if mode == \"mean\":\n        fill_value = arr.mean().astype(arr.dtype)\n    else:\n        # default to 0 if \"constant\" or unknown\n        fill_value = 0\n\n    # For grayscale: shape=(H,W), fill_value is scalar\n    # For RGB: shape=(H,W,3), fill_value should be shape=(3,) or broadcastable\n    if arr.ndim == 3:\n        padded = np.full((pad_h, pad_w, arr.shape[2]), fill_value, dtype=arr.dtype)\n        padded[pad_top:pad_top+orig_h, pad_left:pad_left+orig_w, :] = arr\n    else:\n        padded = np.full((pad_h, pad_w), fill_value, dtype=arr.dtype)\n        padded[pad_top:pad_top+orig_h, pad_left:pad_left+orig_w] = arr\n\n    img_obj.set_image(padded, method=f\"spatial_pad_{new_size}_{mode}\")\n    return img_obj\n\n\ndef ben_graham_transform(img_obj, blur_ratio=0.1):\n    \"\"\"\n    Ben Graham's APTOS transform:\n    1) Optionally convert to HSV (depends on your preference).\n    2) Gaussian blur the image.\n    3) Merge original & blurred with addWeighted(...).\n    \"\"\"\n    arr = img_obj.image\n\n    # Convert (C,H,W)->(H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # If there's an alpha channel, drop it\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # If single-channel last dim => (H,W)\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    # Overwrite\n    img_obj.image = arr\n\n    # Ensure we have color data for OpenCV. If grayscale, we'll skip HSV step\n    # because HSV conversion requires 3 channels. \n    is_color = (arr.ndim == 3 and arr.shape[-1] == 3)\n\n    # Convert from RGB to BGR for OpenCV (if needed)  \n    # (OpenCV uses BGR ordering by default.)\n    if is_color:\n        arr_bgr = arr[..., ::-1].copy().astype(np.uint8)  # RGB->BGR\n    else:\n        arr_bgr = arr.astype(np.uint8)\n\n    # (Optionally) convert to HSV\n    # arr_hsv = cv2.cvtColor(arr_bgr, cv2.COLOR_BGR2HSV) # uncomment if you want HSV\n    \n    # Let's just do the final \"Ben Graham\" line with BGR itself\n    # Gaussian Blur\n    radius = max(arr_bgr.shape[:2]) * blur_ratio  # e.g. 0.1 of max dimension\n    # OpenCV's GaussianBlur ksize must be odd & > 0. If radius=25.6 => ksize=25 or 27\n    # We'll guess a kernel near radius*2+1 if it’s not huge\n    k = int(radius*2 + 1)\n    if k < 1:\n        k = 1\n    if k % 2 == 0:\n        k += 1\n\n    blurred = cv2.GaussianBlur(arr_bgr, (k, k), radius)\n\n    # addWeighted => result = alpha*src1 + beta*src2 + gamma\n    # from your snippet: 4*image + -4*blurred + 128\n    # We must cast to float to avoid overflow\n    out = cv2.addWeighted(arr_bgr, 4, blurred, -4, 128)\n\n    # Convert back to RGB if color\n    if is_color:\n        out_rgb = out[..., ::-1]\n    else:\n        out_rgb = out\n\n    out_rgb = out_rgb.astype(arr.dtype)\n\n    # Put into image object\n    img_obj.set_image(out_rgb, method=f\"ben_graham_transform\")\n\n    return img_obj\n\ndef random_gaussian_noise(img_obj, mean=0, std=10, prob=0.5):\n    \"\"\"\n    Adds Gaussian noise (mean, std) to the image with probability = prob.\n    \"\"\"\n    arr = img_obj.image\n\n    # Convert (C,H,W)->(H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # Drop alpha\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # Squeeze single channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    img_obj.image = arr\n\n    if np.random.rand() < prob:\n        # Generate noise\n        noise = np.random.normal(mean, std, arr.shape)\n        noisy = arr + noise\n        noisy = np.clip(noisy, 0, 255).astype(arr.dtype)\n        img_obj.set_image(noisy, method=f\"random_gaussian_noise_mean{mean}_std{std}\")\n\n    return img_obj\n\n\ndef random_salt_pepper_noise(img_obj, amount=0.02, salt_vs_pepper=0.5, prob=0.5):\n    \"\"\"\n    Adds salt-and-pepper noise to the image.\n    \n    Parameters:\n    - amount: fraction of pixels affected (e.g., 0.02 = 2% of pixels)\n    - salt_vs_pepper: ratio of salt (white) pixels. 0.5 = equal salt and pepper.\n    - prob: probability of applying this noise at all.\n    \"\"\"\n    arr = img_obj.image\n\n    # 1) Handle (C,H,W) => (H,W,C)\n    if arr.ndim == 3 and arr.shape[0] <= 4 and arr.shape[1] > 10:\n        arr = np.transpose(arr, (1, 2, 0))\n\n    # 2) Drop alpha channel if present\n    if arr.ndim == 3 and arr.shape[-1] == 4:\n        arr = arr[..., :3]\n\n    # 3) Squeeze single-channel\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n\n    img_obj.image = arr\n\n    # Decide if noise is applied based on prob\n    if np.random.rand() < prob:\n        noisy_img = np.copy(arr)\n        total_pixels = arr.shape[0] * arr.shape[1]\n        num_salt = np.ceil(amount * total_pixels * salt_vs_pepper).astype(int)\n        num_pepper = np.ceil(amount * total_pixels * (1.0 - salt_vs_pepper)).astype(int)\n\n        # Apply salt noise (white pixels)\n        coords_salt = (\n            np.random.randint(0, arr.shape[0], num_salt),\n            np.random.randint(0, arr.shape[1], num_salt),\n        )\n        if arr.ndim == 2:\n            noisy_img[coords_salt] = 255\n        else:\n            noisy_img[coords_salt[0], coords_salt[1], :] = 255\n\n        # Apply pepper noise (black pixels)\n        coords_pepper = (\n            np.random.randint(0, arr.shape[0], num_pepper),\n            np.random.randint(0, arr.shape[1], num_pepper),\n        )\n        if arr.ndim == 2:\n            noisy_img[coords_pepper] = 0\n        else:\n            noisy_img[coords_pepper[0], coords_pepper[1], :] = 0\n\n        img_obj.set_image(noisy_img, method=f\"random_salt_pepper_{amount}\")\n\n    return img_obj","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:40.487874Z","iopub.execute_input":"2025-03-24T18:39:40.488524Z","iopub.status.idle":"2025-03-24T18:39:40.526348Z","shell.execute_reply.started":"2025-03-24T18:39:40.488498Z","shell.execute_reply":"2025-03-24T18:39:40.525621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Augmenter:\n    def __init__(self, verbose=True):\n        \"\"\"\n        Initializes the Augmenter.\n        \n        Args:\n            verbose (bool): If True, prints out details about operations.\n        \"\"\"\n        self.verbose = verbose\n        self.last_id = 0\n        # Store dataset (Collection) objects keyed by unique integer IDs\n        self.datasets_dic = {}\n        self.names_dic = {}   # store corresponding dataset names\n\n    def isunique(self, dataset_id):\n        \"\"\"Check if a dataset_id is already used or not.\"\"\"\n        return dataset_id not in self.datasets_dic\n\n    def load_dataset_from_path(self, input_dir, dataset_name=None, dataset_id=None):\n        \"\"\"\n        Loads a dataset from a directory path into a Collection object\n        and stores it in self.datasets_dic with a unique integer ID.\n\n        Args:\n            input_dir (str): File path to the dataset folder.\n            dataset_name (str, optional): Name for the dataset. Defaults to folder name.\n            dataset_id (int, optional): Custom ID for the dataset. If None,\n                we attempt to use self.last_id (and increment as needed).\n        \"\"\"\n        if dataset_name is None:\n            dataset_name = os.path.basename(input_dir.rstrip(\"/\"))\n\n        # Auto-assign ID if not specified\n        if dataset_id is None:\n            while not self.isunique(self.last_id):\n                self.last_id += 1\n            dataset_id = self.last_id\n\n        self.names_dic[dataset_id] = dataset_name\n\n        dataset = Collection(\n            directory_path=input_dir,\n            name=dataset_name,\n            verbose=self.verbose\n        )\n        self.datasets_dic[dataset_id] = dataset\n\n        if self.verbose:\n            print(f\"[Augmenter] Loaded dataset '{dataset_name}' \"\n                  f\"with ID={dataset_id} from '{input_dir}'.\")\n\n    def display_first_images(self, n=1, ID=None):\n        \"\"\"\n        Convenience function: displays the first `n` images from each dataset in memory.\n        \"\"\"\n        for key, dataset in self.datasets_dic.items():\n            if ID is None or key == ID:\n                name = self.names_dic.get(key, f\"Unamed_{key}\")\n                print(f\"--- Showing first {n} images for dataset \\\"{name}\\\" (ID: {key}) ---\")\n                for x in range(n):\n                    image = dataset.images[x]\n                    image.show(title=f\"Dataset \\\"{name}\\\": Image #{x}\", axis=True)\n\n    def create_new_dataset_id(self, proposed_name=\"augmented_dataset\"):\n        \"\"\"\n        Helper to create a new dataset ID (and store a name).\n        Returns an integer dataset_id that is unique.\n        \"\"\"\n        while not self.isunique(self.last_id):\n            self.last_id += 1\n        new_id = self.last_id\n        self.last_id += 1\n        self.names_dic[new_id] = proposed_name\n        return new_id\n\n    # -----------------------------------------------------------------------\n    #  \"All-in-one\" method that:\n    #      1) extracts raw images from an existing dataset,\n    #      2) applies transformations behind the scenes,\n    #      3) writes all results to disk in a new folder,\n    #      4) optionally writes a CSV of filenames & label.\n    #\n    #   The user only needs to know `dataset_id` and a few parameters.\n    #   They don't see any direct usage of 'extract_images' or how Collection\n    #   organizes images internally.\n    # -----------------------------------------------------------------------\n    def build_augmented_dataset_on_disk(self,\n                                        dataset_id,\n                                        output_dir,\n                                        scale_factor=2.0,\n                                        augmentations=None,\n                                        label=None,\n                                        to_df=True,\n                                        debug=False,\n                                        min_augmentations=1,\n                                        max_augmentations=None,\n                                        csv_name=\"dataset.csv\"):\n        \"\"\"\n        Higher-level convenience method:\n         - Grabs raw images from a dataset (via extract_images).\n         - Applies augmentations multiple times (scale_factor).\n         - Writes each augmented image to a physical folder (output_dir).\n         - If `to_df` is True, also writes a CSV with columns:\n                [filename, label] if a label is provided\n            or just [filename] if label=None.\n\n        Args:\n            dataset_id (int): Which dataset to augment/convert into a new disk folder.\n            output_dir (str): Folder in which to store the augmented images.\n            scale_factor (float): E.g. 2.0 => create 2 augmented images per original.\n            augmentations (list, optional): List of augmentation functions \n                (defaults to random_rotate + random_horizontal_flip).\n            label (str, optional): If provided, put a 'label' column in the CSV.\n            to_df (bool): If True, also generate a CSV of filenames (+ optional label).\n            debug (bool): If each error should stop the program and display details\n            min_augmentations (int): lower boundary for number of simultaneous augmentations per image\n            max_augmentations (int or None): upper boundary\n            csv_name (str): The CSV filename. Defaults to \"dataset.csv\".\n        \"\"\"\n        if max_augmentations is None:\n            max_augmentations = len(augmentations)\n\n        if max_augmentations > len(augmentations):\n            max_augmentations = len(augmentations)\n            \n        if dataset_id not in self.datasets_dic:\n            raise ValueError(f\"[Augmenter] Dataset with ID={dataset_id} not found.\")\n\n        if not os.path.exists(output_dir):\n            os.makedirs(output_dir, exist_ok=True)\n\n        if augmentations is None:\n            # Provide some default augmentations\n            augmentations = [\n                random_rotate,              \n                random_shear,               \n                random_translate,           \n                random_reflect,             \n                random_scale,               \n                #random_elastic_deformation, <-- WIP\n                spatial_pad,                \n                ben_graham_transform,       \n                random_gaussian_noise,\n                random_salt_pepper_noise,\n                random_text_overlay\n            ]\n\n        collection_obj = self.datasets_dic[dataset_id]\n        dataset_name = self.names_dic.get(dataset_id, f\"Dataset_{dataset_id}\")\n\n        # 1) Extract the raw ND-arrays behind the scenes (the user doesn't see this)\n        #    If to_df=True, we get a DataFrame with each row being an array, plus a 'label' column if passed.\n        #    But in the standard implementation of Collection, extract_images(to_df=False)\n        #    simply returns a list of ND-arrays. We'll do that for simpler iteration.\n        #    (If you want label-based DataFrame usage, see the docstring in your Collection.)\n        raw_images = collection_obj.extract_images(label=None, to_df=False)\n        # raw_images is a list of NumPy arrays. \n        # The dictionary keys are not returned, but we can keep a consistent naming approach.\n\n        # 2) For each raw image, we generate `scale_factor` augmentations\n        #    and write them to disk. We also keep track of filenames for CSV if needed.\n        n_aug_per_img = int(scale_factor)\n        csv_rows = []  # to store (filename, label)\n\n        for idx, np_img in enumerate(tqdm(raw_images)):\n            base_name = f\"{dataset_name}_img{idx}\"\n        \n            for i in range(n_aug_per_img):\n                try:\n                    # Make a fresh Image (from fast_skimage) for each iteration\n                    new_img = Image(np.copy(np_img), chatty_mode=False)\n                    new_img.name = base_name\n            \n                    # Apply random subset of augmentations\n                    num_transforms_to_apply = random.randint(min_augmentations, max_augmentations)\n                    random_transforms = random.sample(augmentations, num_transforms_to_apply)\n                    for aug_func in random_transforms:\n                        new_img = aug_func(new_img)\n            \n                    # Instead of new_img.save_image_data(...), \n                    # just call imsave on new_img.image:\n                    out_filename = f\"{new_img.name}_aug_{idx}_{i}.jpg\"\n                    out_path = os.path.join(output_dir, out_filename)\n            \n                    # Write as a standard PNG:\n                    imsave(out_path, new_img.image)\n            \n                    # Optionally record in CSV:\n                    if to_df:\n                        if label is not None:\n                            csv_rows.append((out_filename, label))\n                        else:\n                            csv_rows.append((out_filename,))\n                except Exception as e:\n                    if debug:\n                        raise(e)\n                    print(\"Could not augment image\", i, \"because of an error:\", e)\n                        \n        # 3) Optionally write a CSV summarizing all output filenames\n        if to_df:\n            if label is not None:\n                df = pd.DataFrame(csv_rows, columns=[\"filename\", \"label\"])\n            else:\n                df = pd.DataFrame(csv_rows, columns=[\"filename\"])\n\n            csv_out_path = os.path.join(output_dir, csv_name)\n            df.to_csv(csv_out_path, index=False)\n            if self.verbose:\n                print(f\"[Augmenter] CSV with {len(csv_rows)} entries saved to '{csv_out_path}'.\")\n\n        if self.verbose:\n            print(f\"[Augmenter] Built augmented dataset from dataset_id={dataset_id}\")\n            print(f\"            => {output_dir} (scale_factor={scale_factor}, #images={len(raw_images)*n_aug_per_img}).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:40.528036Z","iopub.execute_input":"2025-03-24T18:39:40.528355Z","iopub.status.idle":"2025-03-24T18:39:40.552240Z","shell.execute_reply.started":"2025-03-24T18:39:40.528322Z","shell.execute_reply":"2025-03-24T18:39:40.551590Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"s3\"></a>\n<div style=\"color: black; background-color: #E6E6FA; padding: 10px; border-left: 5px solid purple; border-radius: 5px;\">  \n    <h1>5. Apply data augmentation to benchmark datasets</h1>\n</div>\n\n[Back to table of contents](#TOC)\n\nMNIST is loaded in a csv file, so first we need to extract it:","metadata":{}},{"cell_type":"code","source":"#| export\n\ndata = pd.read_csv(\"/kaggle/input/digit-recognizer/train.csv\")\ndata.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:40.553545Z","iopub.execute_input":"2025-03-24T18:39:40.553817Z","iopub.status.idle":"2025-03-24T18:39:45.001790Z","shell.execute_reply.started":"2025-03-24T18:39:40.553784Z","shell.execute_reply":"2025-03-24T18:39:45.001129Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\n# This function was initially built to load MNIST, but you can use it on other datasets\n\ndef load_images_from_csv(\n    csv_path,\n    label_column=\"label\",\n    output_data_dir=\"Dataset\",\n    limit=None,\n    normalize=False\n):\n    \"\"\"\n    Loads a dataset from a CSV with one 'label' (or custom label_column) \n    and multiple pixel columns (e.g., pixel0..pixel783), then writes each \n    row to an image file in <output_data_dir>/<label>/img_<idx>.png\n\n    Args:\n        csv_path (str): Path to the CSV file.\n        label_column (str): Name of the column containing labels.\n        output_data_dir (str): Directory in which to store the images.\n        limit (int): limit to a certain number of images\n        normalize (bool): Whether to scale pixel values to [0, 1].\n\n    Example CSV for MNIST-like data:\n        label,pixel0,pixel1,...,pixel783\n        1,0,0,0,...,255\n        0,255,255,255,...,0\n    \"\"\"\n    \n    # 1. Load the CSV\n    data = pd.read_csv(csv_path)\n\n    if limit is not None:\n        data = data[:limit]\n    \n    # 2. Create the root directory if not existing\n    os.makedirs(output_data_dir, exist_ok=True)\n    \n    # 3. Create subdirectories for each label (e.g., 0..9 for MNIST)\n    labels = sorted(data[label_column].unique())\n    for lbl in labels:\n        label_dir = os.path.join(output_data_dir, str(lbl))\n        os.makedirs(label_dir, exist_ok=True)\n    \n    # How many total columns do we have? One for label, the rest are pixel columns.\n    total_columns = len(data.columns)\n    # Subtract 1 for the label column to get the number of pixel columns\n    num_pixel_cols = total_columns - 1\n\n    # For MNIST, 784 columns = 28 x 28\n    im_length = int(np.sqrt(num_pixel_cols))\n    \n    # Check if the number of columns is a perfect square\n    if im_length * im_length != num_pixel_cols:\n        raise ValueError(\n            f\"The number of columns ({num_pixel_cols}) for pixel data is not a perfect square.\\n\" \n            f\"Please specify how to reshape the image or verify the CSV format.\"\n        )\n    \n    # 4. Convert each row to an image and save it\n    for idx, row in data.iterrows():\n        # Extract the label and pixel values\n        label = row[label_column]\n        pixels = row.drop(label_column).values  # all pixel columns\n        \n        # Reshape to (im_length, im_length), convert to uint8\n        img_arr = pixels.reshape(im_length, im_length).astype(np.uint8)\n        \n        # If requested, normalize to [0, 1] for floating-point images\n        if normalize:\n            # But be aware that cv2.imwrite expects 0..255 for 8-bit images\n            # If you truly want 0..1 range, you'd need a different approach or scaled back to 0..255.\n            img_arr = np.clip(img_arr / 255.0, 0, 1)\n            # Converting back to uint8 if you want to save as 8-bit PNG\n            img_arr = (img_arr * 255).astype(np.uint8)\n        \n        # Save with a name like 'img_1234.png'\n        outpath = os.path.join(output_data_dir, str(label), f\"img_{idx}.jpg\")\n        cv2.imwrite(outpath, img_arr)\n    \n    print(f\"All images saved in '{output_data_dir}/' directory.\")\n\nload_images_from_csv(\"/kaggle/input/digit-recognizer/train.csv\", output_data_dir = \"MNIST\", limit=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:45.002505Z","iopub.execute_input":"2025-03-24T18:39:45.002731Z","iopub.status.idle":"2025-03-24T18:39:47.177099Z","shell.execute_reply.started":"2025-03-24T18:39:45.002712Z","shell.execute_reply":"2025-03-24T18:39:47.176274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\n# Initialize the augmenter\n\naug = Augmenter()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:47.178083Z","iopub.execute_input":"2025-03-24T18:39:47.178363Z","iopub.status.idle":"2025-03-24T18:39:47.181632Z","shell.execute_reply.started":"2025-03-24T18:39:47.178341Z","shell.execute_reply":"2025-03-24T18:39:47.180838Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\n# Load cats and dogs images\n\ndef copy_first_images_in_folder(source_folder, dest_folder, n=500):\n    os.makedirs(dest_folder, exist_ok=True)\n\n    # List all items in the source folder\n    all_files = sorted(os.listdir(source_folder))  # sort by filename\n    \n    # Copy the first 500 valid files \n    count = 0\n    for file_name in all_files:\n        # Stop if we've copied 500 already\n        if count >= n:\n            break\n        \n        src_path = os.path.join(source_folder, file_name)\n        dst_path = os.path.join(dest_folder, file_name)\n        \n        # (Optional) Skip subdirectories, hidden files, or any non-image \n        # For instance, to skip if it doesn't look like an image:\n        if file_name.lower().endswith((\".jpg\", \".jpeg\", \".png\")):\n            shutil.copy2(src_path, dst_path)\n            count += 1\n    \n    print(f\"Copied {count} images to '{dest_folder}'.\")\n\ncopy_first_images_in_folder(\"/kaggle/input/microsoft-catsvsdogs-dataset/PetImages/Cat\", \"Pets/Cats\")\ncopy_first_images_in_folder(\"/kaggle/input/microsoft-catsvsdogs-dataset/PetImages/Dog\", \"Pets/Dogs\")\n\naug.load_dataset_from_path(\"Pets/Cats\", \"Cats\")\naug.load_dataset_from_path(\"Pets/Dogs\", \"Dogs\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:47.182438Z","iopub.execute_input":"2025-03-24T18:39:47.182735Z","iopub.status.idle":"2025-03-24T18:39:57.643666Z","shell.execute_reply.started":"2025-03-24T18:39:47.182709Z","shell.execute_reply":"2025-03-24T18:39:57.642740Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\n# Load MNIST\n\nfor n in range(0, 10):\n    aug.load_dataset_from_path(f\"/kaggle/working/MNIST/{n}\", f\"Label {n}\")\n\naug.names_dic","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:57.646011Z","iopub.execute_input":"2025-03-24T18:39:57.646276Z","iopub.status.idle":"2025-03-24T18:39:59.205624Z","shell.execute_reply.started":"2025-03-24T18:39:57.646251Z","shell.execute_reply":"2025-03-24T18:39:59.204975Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\n# aug.display_first_images() # <-- uncomment to display first images of each dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:59.206595Z","iopub.execute_input":"2025-03-24T18:39:59.206859Z","iopub.status.idle":"2025-03-24T18:39:59.209901Z","shell.execute_reply.started":"2025-03-24T18:39:59.206836Z","shell.execute_reply":"2025-03-24T18:39:59.209292Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"⚠️ It is important to note that image classifiers are built differently depending on whether images are grayscale or not. Data augmentation on grayscale images would typically be different from color images.","metadata":{}},{"cell_type":"code","source":"#| export\n\n# Create a header for the printing table\nprint(f\"{'Collection Name':<25} {'Content Type':<10} {'Format':<20}\")\nprint('-' * 60)  # Print a dividing line for headers\n\n# Loop through each collection and print the details in a formatted row\nfor col in aug.datasets_dic.values():\n    col.check_dimension_consistency()\n    col.check_image_format_consistency()\n\n    # Determine content type\n    if col.dimension_consistency and not col.iscolor:\n        content_type = \"Fully Grayscale\"\n    elif col.dimension_consistency and col.iscolor:\n        content_type = \"Fully Colored\"\n    else:\n        content_type = \"Mixed\"\n\n    # Determine format consistency\n    format_consistency = f\"Unique Shape {col.format}\" if col.format_consistency else \"Different Shapes\"\n\n    # Print the collection details in a formatted row\n    print(f\"{col.name:<25} {content_type:<10} {format_consistency:<20}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:59.210805Z","iopub.execute_input":"2025-03-24T18:39:59.211121Z","iopub.status.idle":"2025-03-24T18:39:59.229548Z","shell.execute_reply.started":"2025-03-24T18:39:59.211093Z","shell.execute_reply":"2025-03-24T18:39:59.228718Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The cats and dogs dataset are all colored but with different formats, while MNIST is full grayscale with the same format. ","metadata":{}},{"cell_type":"code","source":"#| export\n\nfor n, col in enumerate(aug.datasets_dic.values()):\n    if n >= 2:\n        break # no need of evaluating MNIST here\n    displayed_features = []\n    images_to_display = []\n    \n    for im in tqdm(col.images.values()):\n        if (im.iscolor, (im.width, im.height)) not in displayed_features:\n            displayed_features.append((im.iscolor, (im.width, im.height)))\n            images_to_display.append(im)\n            \n    n_im = len(images_to_display)\n    to_display = min(16-1, n_im)\n    images_to_display = images_to_display[:to_display]\n    \n    \n    for k, im in enumerate(images_to_display):\n        im.show(size=12, subplots=(4, 4, k+1), title=str((im.width, im.height, im.iscolor, n)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:39:59.230325Z","iopub.execute_input":"2025-03-24T18:39:59.230615Z","iopub.status.idle":"2025-03-24T18:40:02.247841Z","shell.execute_reply.started":"2025-03-24T18:39:59.230594Z","shell.execute_reply":"2025-03-24T18:40:02.246893Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\naugmentations = [\n    random_rotate,              \n    random_shear,               \n    random_translate,           \n    random_reflect,             \n    random_scale,               \n    spatial_pad,                \n    ben_graham_transform,       \n    random_gaussian_noise,\n    random_salt_pepper_noise,\n    random_text_overlay\n]\n\nN = 1 # produce 1 new image per original\n\naug.build_augmented_dataset_on_disk(\n    dataset_id=0,\n    output_dir=\"augmented_cats\",\n    scale_factor=N,  \n    augmentations=augmentations,\n    label=\"cat\",\n    to_df=True,\n    #debug=True,    # <-- uncomment if you want see more details on the errors\n    min_augmentations=2,\n    max_augmentations=5, # Limit number of simultaneous augmentations\n    csv_name=\"cats_augmented.csv\"\n)\n# => This will produce a folder \"augmented_cats_folder/\" \n#    with N times the number of original images, \n#    plus a CSV listing filenames & label=\"cat\".","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:40:02.248766Z","iopub.execute_input":"2025-03-24T18:40:02.249022Z","iopub.status.idle":"2025-03-24T18:40:20.761598Z","shell.execute_reply.started":"2025-03-24T18:40:02.248999Z","shell.execute_reply":"2025-03-24T18:40:20.760664Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"augmentations = [\n    random_rotate,              \n    random_shear,                \n    #random_translate, If you miss half of the \"5\", this is no longer a \"5\"           \n    #random_reflect,  If you return a \"5\", it becomes a \"2\"           \n    random_scale,            \n    #spatial_pad, Makes images look way too small because base resolution is small                \n    ben_graham_transform,       \n    random_gaussian_noise,\n    random_salt_pepper_noise\n    #random_text_overlay  If you add another character on a \"5\", it is no longer a \"5\"\n]\n\n\nfor label in range(0, 10):\n    aug.build_augmented_dataset_on_disk(\n        dataset_id=2+label,\n        output_dir=f\"augmented_{label}\",\n        scale_factor=3,  \n        augmentations=augmentations,\n        label=f\"Label {label}\",\n        to_df=True,\n        min_augmentations=2,\n        max_augmentations=5,\n        csv_name=f\"{label}_augmented.csv\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:40:20.762693Z","iopub.execute_input":"2025-03-24T18:40:20.763034Z","iopub.status.idle":"2025-03-24T18:40:26.690463Z","shell.execute_reply.started":"2025-03-24T18:40:20.763000Z","shell.execute_reply":"2025-03-24T18:40:26.689724Z"},"_kg_hide-output":false},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"This was a simple example with several different operations. You can select more of them, or even build your own (as calls for external functions is allowed). Please keep in mind that so far, `Collection` can only handle jpg images. Future updates could include png as well (tell me in the comments of this notebook if you would like that).","metadata":{}},{"cell_type":"code","source":"#| export\n\naugmented_cats = Collection(directory_path=\"/kaggle/working/augmented_cats\")\nfor k, im in enumerate(list(augmented_cats.images.values())):\n    if k < 10:\n        im.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:40:26.691349Z","iopub.execute_input":"2025-03-24T18:40:26.691601Z","iopub.status.idle":"2025-03-24T18:40:30.338427Z","shell.execute_reply.started":"2025-03-24T18:40:26.691580Z","shell.execute_reply":"2025-03-24T18:40:30.337574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#| export\n\nfig, axes = plt.subplots(nrows=10, ncols=4, figsize=(8, 20))\n\nfor digit in range(10):\n    folder = f\"/kaggle/working/augmented_{digit}\"\n    if not os.path.isdir(folder):\n        print(f\"Folder not found: {folder}\")\n        continue\n\n    # Gather all JPG file names in this folder\n    images = [fn for fn in os.listdir(folder) if fn.lower().endswith('.jpg')]\n    # Sort or shuffle if desired, but we'll just take the first 4\n    images = images[:4]\n\n    for i, img_name in enumerate(images):\n        # Safely handle cases with fewer than 4 images\n        if i >= 4:\n            break\n        \n        img_path = os.path.join(folder, img_name)\n        # Load in grayscale\n        img = load_img(img_path, color_mode='grayscale')\n\n        # Display on the correct subplot\n        ax = axes[digit, i]\n        ax.imshow(img, cmap='gray')\n        ax.axis('off')\n        # Optional: label each image with the digit\n        if i == 0:\n            ax.set_title(f\"Digit: {digit}\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:40:30.339331Z","iopub.execute_input":"2025-03-24T18:40:30.339576Z","iopub.status.idle":"2025-03-24T18:40:32.059076Z","shell.execute_reply.started":"2025-03-24T18:40:30.339555Z","shell.execute_reply":"2025-03-24T18:40:32.058249Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:40:32.060111Z","iopub.execute_input":"2025-03-24T18:40:32.060474Z","iopub.status.idle":"2025-03-24T18:40:32.462160Z","shell.execute_reply.started":"2025-03-24T18:40:32.060438Z","shell.execute_reply":"2025-03-24T18:40:32.461385Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"s4\"></a>\n<div style=\"color: black; background-color: #E6E6FA; padding: 10px; border-left: 5px solid purple; border-radius: 5px;\">  \n    <h1>6. Compare the performances of XCeption on tiny MNIST and its augmented version</h1>\n</div>\n\n[Back to table of contents](#TOC)\n\nFor this section, we will use the [XCeption model](https://keras.io/api/applications/xception/) from Keras for building and comparing two classifiers on tiny MNIST (i.e. the 1,000 first images from MNIST).","metadata":{}},{"cell_type":"code","source":"#| export\n\nTEST_WITH_XCEPTION = True  # set to False if you don't need this part\n\nif TEST_WITH_XCEPTION:\n    ################################################################################\n    # 1) LOAD ONLY 1000 ROWS OF ORIGINAL MNIST CSV\n    ################################################################################\n    data = pd.read_csv(\"/kaggle/input/digit-recognizer/train.csv\").iloc[:1000]\n    print(\"Original data size:\", data.shape)\n\n    original_images = []\n    original_labels = data[\"label\"].values  # shape (1000,)\n\n    X_orig = data.drop(\"label\", axis=1).values / 255.0  # shape (1000, 784)\n    for i in range(len(X_orig)):\n        arr_2d = X_orig[i].reshape((28, 28))   # raw 28×28\n        original_images.append(arr_2d)\n\n    gc.collect()\n\n    ################################################################################\n    # 2) LOAD AUGMENTED IMAGES (up to max_augmented)\n    ################################################################################\n    # Instead of 1,000, let's allow up to 3,000 to see more augmentation.\n    max_augmented = 3000  \n    augmented_arrays = []\n    augmented_labels = []\n    current_count = 0\n\n    for digit in range(10):\n        aug_dir = f\"/kaggle/working/augmented_{digit}\"\n        if not os.path.isdir(aug_dir):\n            continue\n\n        for fname in os.listdir(aug_dir):\n            if current_count >= max_augmented:\n                break\n            if fname.lower().endswith(\".jpg\"):\n                fpath = os.path.join(aug_dir, fname)\n                img = load_img(fpath, color_mode='grayscale')  # native size\n                img_array = img_to_array(img)  # (H, W, 1)\n                img_array = np.squeeze(img_array, axis=-1)  # shape (H, W)\n                augmented_arrays.append(img_array)\n                augmented_labels.append(digit)\n                current_count += 1\n            if current_count >= max_augmented:\n                break\n\n    print(\"Augmented images:\", len(augmented_arrays))\n    gc.collect()\n\n    ################################################################################\n    # 3) RESIZE EVERYTHING TO A SMALLER FIXED SIZE (e.g. 64×64) TO AVOID\n    #    RAM PROBLEMS\n    ################################################################################\n    NEW_H, NEW_W = 64, 64\n\n    def resize_to_fixed(img_2d, new_h=NEW_H, new_w=NEW_W):\n        \"\"\"\n        Resize a 2D grayscale array to (new_h, new_w).\n        \"\"\"\n        tensor = tf.expand_dims(img_2d, axis=-1)   # (H, W, 1)\n        tensor = tf.image.resize(tensor, (new_h, new_w), method='bilinear')\n        tensor = tf.squeeze(tensor, axis=-1)       # (new_h, new_w)\n        return tensor.numpy()\n\n    # Resize original images\n    resized_orig = []\n    for arr in original_images:\n        resized_arr = resize_to_fixed(arr, NEW_H, NEW_W)\n        resized_orig.append(resized_arr)\n\n    # Resize augmented images\n    resized_aug = []\n    for arr in augmented_arrays:\n        resized_arr = resize_to_fixed(arr, NEW_H, NEW_W)\n        resized_aug.append(resized_arr)\n\n    resized_orig = np.array(resized_orig)  # shape (N_orig, 64, 64)\n    resized_aug  = np.array(resized_aug)   # shape (N_aug, 64, 64)\n\n    print(\"resized_orig.shape =\", resized_orig.shape)\n    print(\"resized_aug.shape  =\", resized_aug.shape)\n\n    # Expand dims for channels => (H, W, 1)\n    resized_orig = np.expand_dims(resized_orig, axis=-1)\n    resized_aug  = np.expand_dims(resized_aug, axis=-1)\n\n    gc.collect()\n\n    ################################################################################\n    # 4) SPLIT THE ORIGINAL 1000 INTO (TRAIN+VAL) + TEST\n    #    - We'll keep 10% (100 images) as a final test set that BOTH models\n    #      will be evaluated on.\n    ################################################################################\n    y_orig = original_labels\n\n    X_temp_orig, X_test_orig, y_temp_orig, y_test_orig = train_test_split(\n        resized_orig, y_orig, test_size=0.1, random_state=42\n    )\n    print(\"After splitting original -> train/val candidate:\", X_temp_orig.shape, \"| test:\", X_test_orig.shape)\n\n    # For the ORIGINAL model: further split X_temp_orig into train & val\n    X_train_orig, X_val_orig, y_train_orig, y_val_orig = train_test_split(\n        X_temp_orig, y_temp_orig, test_size=0.1, random_state=42\n    )\n    print(\"Original model -> train:\", X_train_orig.shape, \", val:\", X_val_orig.shape)\n\n    # For the AUGMENTED model: combine X_temp_orig with the augmented images\n    X_combined = np.concatenate([X_temp_orig, resized_aug], axis=0)\n    y_combined = np.concatenate([y_temp_orig, np.array(augmented_labels)], axis=0)\n\n    X_train_aug, X_val_aug, y_train_aug, y_val_aug = train_test_split(\n        X_combined, y_combined, test_size=0.1, random_state=42\n    )\n    print(\"Augmented model -> train:\", X_train_aug.shape, \", val:\", X_val_aug.shape)\n\n    gc.collect()\n\n    ################################################################################\n    # 5) ONE-HOT ENCODE LABELS\n    ################################################################################\n    num_classes = 10\n\n    # Original\n    y_train_orig_oh = tf.keras.utils.to_categorical(y_train_orig, num_classes=num_classes)\n    y_val_orig_oh   = tf.keras.utils.to_categorical(y_val_orig,   num_classes=num_classes)\n\n    # Combined\n    y_train_aug_oh = tf.keras.utils.to_categorical(y_train_aug, num_classes=num_classes)\n    y_val_aug_oh   = tf.keras.utils.to_categorical(y_val_aug,   num_classes=num_classes)\n\n    # Final test set from the original data\n    y_test_orig_oh = tf.keras.utils.to_categorical(y_test_orig, num_classes=num_classes)\n    gc.collect()\n\n    ################################################################################\n    # 6) BUILD XCEPTION MODEL (use 10 outputs, 'categorical_crossentropy')\n    #    with a smaller input (64×64, 1 channel)\n    ################################################################################\n    def build_xception_model(img_shape=(64,64,1), n_classes=10):\n        input_layer = Input(shape=img_shape)\n        # Duplicate 1 channel => 3 for Xception\n        x = Lambda(lambda x: tf.tile(x, [1,1,1,3]))(input_layer)\n\n        base_model = Xception(include_top=False,\n                              weights=\"imagenet\",\n                              input_tensor=x,\n                              pooling='max')  # global max pooling\n\n        model = Sequential([\n            base_model,\n            Flatten(),\n            Dropout(0.3),\n            Dense(128, activation='relu'),\n            Dropout(0.25),\n            Dense(n_classes, activation='softmax')\n        ])\n\n        model.compile(\n            optimizer=Adamax(learning_rate=0.001),\n            loss='categorical_crossentropy',\n            metrics=[\n                'accuracy',\n                Precision(name='precision'),\n                Recall(name='recall')\n            ]\n        )\n        return model\n\n    def plot_history(history, title=\"Model Performance\"):\n        training_loss = history.history['loss']\n        training_accuracy = history.history['accuracy']\n        validation_loss = history.history['val_loss']\n        validation_accuracy = history.history['val_accuracy']\n        epochs = range(1, len(training_loss) + 1)\n\n        plt.figure(figsize=(14, 6))\n        # Plot Training/Val Loss\n        plt.subplot(1, 2, 1)\n        plt.plot(epochs, training_loss, 'bo-', label='Training Loss')\n        plt.plot(epochs, validation_loss, 'ro-', label='Validation Loss')\n        plt.title(f'{title}: Loss')\n        plt.xlabel('Epochs')\n        plt.ylabel('Loss')\n        plt.legend()\n\n        # Plot Training/Val Accuracy\n        plt.subplot(1, 2, 2)\n        plt.plot(epochs, training_accuracy, 'bo-', label='Training Accuracy')\n        plt.plot(epochs, validation_accuracy, 'ro-', label='Validation Accuracy')\n        plt.title(f'{title}: Accuracy')\n        plt.xlabel('Epochs')\n        plt.ylabel('Accuracy')\n        plt.legend()\n        plt.tight_layout()\n        plt.show()\n\n    gc.collect()\n\n    ################################################################################\n    # 7) TRAIN ON ORIGINAL-ONLY\n    ################################################################################\n    model_orig = build_xception_model(img_shape=(NEW_H, NEW_W, 1), n_classes=num_classes)\n\n    history_orig = model_orig.fit(\n        X_train_orig, y_train_orig_oh,\n        validation_data=(X_val_orig, y_val_orig_oh),\n        epochs=10, # 10 is enough\n        batch_size=16,\n        verbose=1\n    )\n\n    plot_history(history_orig, title=\"Original (One-Hot)\")\n\n    print(\"\\n-- Evaluating Original model on (original) VAL set --\")\n    val_loss_orig, val_acc_orig, val_prec_orig, val_recall_orig = model_orig.evaluate(\n        X_val_orig, y_val_orig_oh, verbose=0\n    )\n    print(f\"Val set -> loss={val_loss_orig:.4f}, acc={val_acc_orig:.4f}, \"\n          f\"precision={val_prec_orig:.4f}, recall={val_recall_orig:.4f}\")\n\n    gc.collect()\n\n    ################################################################################\n    # 8) TRAIN ON COMBINED (ORIGINAL + AUGMENTED)\n    ################################################################################\n    model_aug = build_xception_model(img_shape=(NEW_H, NEW_W, 1), n_classes=num_classes)\n\n    history_aug = model_aug.fit(\n        X_train_aug, y_train_aug_oh,\n        validation_data=(X_val_aug, y_val_aug_oh),\n        epochs=20,\n        batch_size=16,\n        verbose=1\n    )\n\n    plot_history(history_aug, title=\"Augmented (One-Hot)\")\n\n    print(\"\\n-- Evaluating Augmented model on (original) VAL set --\")\n    val_loss_aug, val_acc_aug, val_prec_aug, val_recall_aug = model_aug.evaluate(\n        X_val_aug, y_val_aug_oh, verbose=0\n    )\n    print(f\"Val set -> loss={val_loss_aug:.4f}, acc={val_acc_aug:.4f}, \"\n          f\"precision={val_prec_aug:.4f}, recall={val_recall_aug:.4f}\")\n\n    print(\"\\nComparison of validation accuracy on each approach's val:\")\n    print(f\"  Original model:   {val_acc_orig:.4f}\")\n    print(f\"  Augmented model:  {val_acc_aug:.4f}\")\n\n    gc.collect()\n\n    ###########################################################################\n    # 9) EVALUATE BOTH MODELS ON THE SAME FINAL TEST SPLIT\n    ###########################################################################\n    print(\"\\n-- Evaluating BOTH models on the same final test set (from original data) --\")\n\n    test_loss_orig, test_acc_orig, test_prec_orig, test_recall_orig = model_orig.evaluate(\n        X_test_orig, y_test_orig_oh, verbose=0\n    )\n    print(f\"Original model -> loss={test_loss_orig:.4f}, acc={test_acc_orig:.4f}, \"\n          f\"precision={test_prec_orig:.4f}, recall={test_recall_orig:.4f}\")\n\n    test_loss_aug, test_acc_aug, test_prec_aug, test_recall_aug = model_aug.evaluate(\n        X_test_orig, y_test_orig_oh, verbose=0\n    )\n    print(f\"Augmented model -> loss={test_loss_aug:.4f}, acc={test_acc_aug:.4f}, \"\n          f\"precision={test_prec_aug:.4f}, recall={test_recall_aug:.4f}\")\n\n    print(\"\\nFinal test-set comparison:\")\n    print(f\"  Original model test acc:   {test_acc_orig:.4f}\")\n    print(f\"  Augmented model test acc:  {test_acc_aug:.4f}\")\n\n    gc.collect()\n\n    ################################################################################\n    # 10) PREDICT ON THE KAGGLE COMPETITION test.csv AND BUILD submission.csv\n    #     (using the AUGMENTED MODEL).\n    ################################################################################\n    test_df = pd.read_csv(\"/kaggle/input/digit-recognizer/test.csv\")  # shape (28000, 784)\n    X_test_raw = test_df.values / 255.0\n\n    test_resized = []\n    for i in range(len(X_test_raw)):\n        arr_2d = X_test_raw[i].reshape((28, 28))\n        # Now just resize to (64×64) or whatever we used\n        arr_2d = resize_to_fixed(arr_2d, NEW_H, NEW_W)\n        test_resized.append(arr_2d)\n\n    gc.collect()\n\n    test_resized = np.array(test_resized)  # (28000, 64, 64)\n    test_resized = np.expand_dims(test_resized, axis=-1)  # (28000, 64, 64, 1)\n\n    predictions = model_aug.predict(test_resized)\n    predicted_labels = np.argmax(predictions, axis=1)\n\n    submission = pd.DataFrame({\n        \"ImageId\": np.arange(1, len(predicted_labels)+1),\n        \"Label\": predicted_labels\n    })\n    submission.to_csv(\"submission.csv\", index=False)\n    print(\"\\nsubmission.csv has been created using the AUGMENTED model.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:40:32.463225Z","iopub.execute_input":"2025-03-24T18:40:32.463577Z","iopub.status.idle":"2025-03-24T18:46:11.905811Z","shell.execute_reply.started":"2025-03-24T18:40:32.463543Z","shell.execute_reply":"2025-03-24T18:46:11.904973Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Of course, the task of training the augmented dataset is **considerably more difficult** because it forces the model to focus on much more complex patterns (similarly to what a human would do). Testing on the augmented dataset will therefore lead to less good results.\n\nNow, the interesting thing is to test this model in **real case scenarios**. Further work could also include testing the **adversarial robustness** of this model.","metadata":{}},{"cell_type":"code","source":"#| export\n\n# Free some space\n\ndel data, aug, model_aug, model_orig, predictions\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T18:46:31.462912Z","iopub.execute_input":"2025-03-24T18:46:31.463257Z","iopub.status.idle":"2025-03-24T18:46:32.255867Z","shell.execute_reply.started":"2025-03-24T18:46:31.463205Z","shell.execute_reply":"2025-03-24T18:46:32.254901Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Special thanks to Trigram for the notebook about [data augmentation for PANDA](https://www.kaggle.com/code/nxrprime/panda-image-data-augmentation-techniques-w-monai) which inspired some of the augmentation techniques in this package.\n\n<p style=\"padding: 20px;\n          background-color: green;\n          font-family: computermodern;\n          color: white;\n          font-size: 200%;\n          text-align: center;\n          border-radius: 40px 20px;\n          \">Thank you! If you found this useful please like 👍</p>","metadata":{}}]}