{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91249,"databundleVersionId":11294684,"isSourceIdPinned":false,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import cv2\nfrom functools import partial\nimport ipywidgets as widgets\nfrom IPython.display import display\nfrom matplotlib import pyplot as plt\nimport numpy as np # linear algebra\nimport os\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom PIL import Image\nimport random\nimport re\nimport tensorflow as tf\nimport tensorflow.keras.backend as K\nfrom tqdm import tqdm","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.782813Z","iopub.execute_input":"2025-06-07T11:47:06.783120Z","iopub.status.idle":"2025-06-07T11:47:06.787654Z","shell.execute_reply.started":"2025-06-07T11:47:06.783101Z","shell.execute_reply":"2025-06-07T11:47:06.786986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"os.environ[\"PYTHONHASHSEED\"] = \"0\"  # Asegura el hash fijo de strings\nrandom.seed(42)\nnp.random.seed(42)\ntf.random.set_seed(42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.788872Z","iopub.execute_input":"2025-06-07T11:47:06.789262Z","iopub.status.idle":"2025-06-07T11:47:06.890020Z","shell.execute_reply.started":"2025-06-07T11:47:06.789247Z","shell.execute_reply":"2025-06-07T11:47:06.889193Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Helper Functions","metadata":{}},{"cell_type":"code","source":"def path2id_slice(file_path):\n    \"\"\"\n    Extracts identifiers from a given file path of the format '.../tomo_XXXacc/slice_YYYY.jpg'.\n    \n    Args:\n        file_path (tf.Tensor or str): Full path to the image file, e.g., '.../tomo_003acc/slice_0007.jpg'.\n    \n    Returns:\n        tuple: A tuple (tomo, slice_num) where:\n            - tomo (str): The name of the tomography folder, e.g., 'tomo_003acc'.\n            - slice_num (str): The slice number without leading zeros, e.g., '7'.\n    \"\"\"\n    # Split the file path into parts\n    parts = tf.strings.split(file_path, '/')\n\n    # Extract the tomography folder name (e.g., 'tomo_003acc')\n    tomo = parts[-2].numpy().decode(\"utf-8\")\n    # Extract the file name (e.g., 'slice_0007.jpg')\n    slice_file = parts[-1]\n    \n    # Remove the '.jpg' extension\n    slice_name = tf.strings.regex_replace(slice_file, r\"\\.jpg$\", \"\")\n    \n    # Extract the numeric part from the file name and remove leading zeros: 'slice_0007' → '7'\n    slice_num = tf.strings.regex_replace(slice_name, r\"slice_0*(\\d+)\", r\"\\1\").numpy().decode(\"utf-8\")\n    \n    # Return the tuple (e.g., ('tomo_003acc', '7'))\n    combined = (tomo,slice_num)\n    \n    return combined","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.897391Z","iopub.execute_input":"2025-06-07T11:47:06.897693Z","iopub.status.idle":"2025-06-07T11:47:06.913334Z","shell.execute_reply.started":"2025-06-07T11:47:06.897673Z","shell.execute_reply":"2025-06-07T11:47:06.912785Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def expand_labels(dataset,labels):\n        \"\"\"\n        Processes a dataset of image paths and expands the associated motor label information \n        into a structured DataFrame containing slice-level annotations.\n    \n        Args:\n            dataset (list or iterable): Collection of file paths to image slices.\n            labels (pd.DataFrame): DataFrame containing motor annotations with columns:\n                - 'tomo_id'\n                - 'Motor axis 0' (slice number)\n                - 'Motor axis 1' (Y coordinate)\n                - 'Motor axis 2' (X coordinate)\n    \n        Returns:\n            pd.DataFrame: A new DataFrame with columns:\n                - 'tomo_id': Tomography ID.\n                - 'slice': Slice number.\n                - 'Number of motors': Number of motors present in the slice.\n                - 'x': X coordinate of the motor (0 if none).\n                - 'y': Y coordinate of the motor (0 if none).\n        \"\"\"\n    \n        tomo_ids = []\n        slices = []\n        n_motors = []\n        bbox = []\n    \n        for file in tqdm(dataset,desc='Processing'):\n            # Extract tomography ID and slice number from path\n            tomo,slc = path2id_slice(file)\n            slc = float(slc)\n            tomo_ids.append(tomo)\n            slices.append(slc)\n    \n            # Filter labels for the given tomo and slice\n            motors = labels[\n                             (labels['tomo_id']==tomo) & \n                             (labels['Motor axis 0']==slc)\n                             ]\n            # Default coordinates (no motor)\n            x,y = 0.,0.\n            # If there is exactly one motor in this slice, get its coordinates\n            if motors == 1:\n                x = labels[(labels['tomo_id']==tomo) & \n                            (labels['Motor axis 0']==slc)\n                            ]['Motor axis 2']\n                y = labels[(labels['tomo_id']==tomo) & \n                            (labels['Motor axis 0']==slc)\n                            ]['Motor axis 1']\n    \n            # Number of motors (rows) in this slice\n            n_motors.append(motors)\n            # Bounding box coordinates\n            bbox.append((x,y))\n    \n        # Build expanded DataFrame\n        labels_expanded =pd.DataFrame({\n                'tomo_id':tomo_ids,\n                'slice':slices,\n                'Number of motors':n_motors,\n                'x':[i[0] for i in bbox],\n                'y':[i[1] for i in bbox]\n            })\n        \n         # Optional: Add binary label\n        # labels_expanded['has_motors'] = (labels_expanded['Number of motors'] > 0).astype(int)\n    \n        return labels_expanded","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.915025Z","iopub.execute_input":"2025-06-07T11:47:06.915477Z","iopub.status.idle":"2025-06-07T11:47:06.934531Z","shell.execute_reply.started":"2025-06-07T11:47:06.915454Z","shell.execute_reply":"2025-06-07T11:47:06.933778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_image_from_path(path,channels=1):\n        \"\"\"\n        Loads and preprocesses an image from a given file path.\n    \n        Args:\n            path (str or tf.Tensor): Path to the image file.\n            channels (int): Number of channels to decode (1 for grayscale, 3 for RGB). Default is 1.\n    \n        Returns:\n            tf.Tensor: The preprocessed image tensor with shape [256, 256, channels] \n                       and dtype tf.float32, scaled to the [0, 1] range.\n        \"\"\"\n        # Read the image file as a byte string\n        image = tf.io.read_file(path)         \n        # Decode the JPEG image with the specified number of channels\n        image = tf.image.decode_jpeg(image, channels=channels)     \n        # Convert image to float32 in the [0, 1] range\n        image = tf.image.convert_image_dtype(image, tf.float32) \n        # Resize the image to 256x256\n        image = tf.image.resize(image, [256, 256])\n        return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.935391Z","iopub.execute_input":"2025-06-07T11:47:06.935686Z","iopub.status.idle":"2025-06-07T11:47:06.960214Z","shell.execute_reply.started":"2025-06-07T11:47:06.935662Z","shell.execute_reply":"2025-06-07T11:47:06.959440Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_test_split(dataset,ds_size, split=0.8):\n\n    train_size = int(split * ds_size)\n    valid_size = ds_size - train_size\n    \n    # Split the dataset into training and validation sets\n    train_ds = dataset.take(train_size)\n    valid_ds = dataset.skip(train_size)\n    \n    print(train_size)\n    print(f'Train size: {train_size}')\n    print(f'Validation size: {valid_size}')\n\n    BATCH_SIZE = 32\n    # Apply batching, repeating and prefetching on the training dataset\n    train_ds = train_ds.batch(BATCH_SIZE).repeat().prefetch(tf.data.AUTOTUNE)\n     # Apply batching and prefetching on the validation dataset (no repeat)\n    valid_ds = valid_ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\n\n    return train_ds, valid_ds","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 256\n\ndef rotate_coords(x, y, k):\n    # k is an integer in [0, 3], representing the number of 90° counter-clockwise rotations.\n    # Rotates the point (x, y) according to k:\n    # 0: (x, y)\n    # 1: (y, IMG_SIZE - x)\n    # 2: (IMG_SIZE - x, IMG_SIZE - y)\n    # 3: (IMG_SIZE - y, x)\n\n    def rot0():\n        return x, y\n    def rot1():\n        return y, IMG_SIZE - x\n    def rot2():\n        return IMG_SIZE - x, IMG_SIZE - y\n    def rot3():\n        return IMG_SIZE - y, x\n\n    return tf.switch_case(k, branch_fns={0: rot0, 1: rot1, 2: rot2, 3: rot3})\n\ndef augment_sample(image, label):\n    # label: [presence, x, y]\n    presence = label[0]\n    x = label[1]\n    y = label[2]\n\n    # Convert image to float32 for safe processing\n    image = tf.image.convert_image_dtype(image, tf.float32)\n\n    # 1. Random horizontal flip with 50% probability\n    flip_lr = tf.random.uniform([]) > 0.5\n    image = tf.cond(flip_lr, lambda: tf.image.flip_left_right(image), lambda: image)\n\n    # 2. Random vertical flip with 50% probability\n    flip_ud = tf.random.uniform([]) > 0.5\n    image = tf.cond(flip_ud, lambda: tf.image.flip_up_down(image), lambda: image)\n\n    # 3. Random rotation by multiple of 90 degrees\n    k = tf.random.uniform([], minval=0, maxval=4, dtype=tf.int32)\n    image = tf.image.rot90(image, k=k)\n\n    # --- Update coordinates only if presence > 0 ---\n    def transform_coords():\n        # Flip coordinates horizontally if flip_lr is True\n        x_flipped = tf.cond(flip_lr, lambda: IMG_SIZE - x, lambda: x)\n        # Flip coordinates vertically if flip_ud is True\n        y_flipped = tf.cond(flip_ud, lambda: IMG_SIZE - y, lambda: y)\n        # Rotate coordinates\n        x_rot, y_rot = rotate_coords(x_flipped, y_flipped, k)\n        return x_rot, y_rot\n\n    x_new, y_new = tf.cond(presence > 0,\n                           true_fn=transform_coords,\n                           false_fn=lambda: (x, y))\n\n    # Rebuild label tensor\n    label_new = tf.stack([presence, x_new, y_new])\n\n    return image, label_new","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_U(inputs):\n    \"\"\"\n    Builds a custom convolutional neural network with an encoder-like structure,\n    hierarchical feature fusion, and two parallel outputs:\n    - 'presence': binary classification (presence of an object).\n    - 'coords': regression of 2D coordinates (e.g., bounding box center).\n\n    Args:\n        inputs (tf.Tensor): Input tensor (e.g., image batch).\n\n    Returns:\n        tf.keras.Model: A compiled Keras model with two outputs:\n            - 'presence' (1 unit, sigmoid activation)\n            - 'coords' (2 units, sigmoid activation)\n    \"\"\"\n    \n\n    relu = tf.keras.activations.relu\n\n    # ----- Level 0 -----\n    conv0_0 = tf.keras.layers.Conv2D(filters=16,kernel_size=3,strides=1,\n                                 padding=\"same\",kernel_initializer=\"he_normal\")(inputs)\n    x = relu(tf.keras.layers.BatchNormalization()(conv0_0))\n    \n    conv0_1 = tf.keras.layers.Conv2D(filters=16,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv0_1))\n    \n    conv0_2 = tf.keras.layers.Conv2D(filters=16,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv0_2))\n    pooling0 = tf.keras.layers.MaxPooling2D(padding=\"same\")(x)\n    \n    # ----- Level 1 -----\n    conv1_0 = tf.keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(pooling0)\n    x = relu(tf.keras.layers.BatchNormalization()(conv1_0))\n    \n    conv1_1 = tf.keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv1_1))\n    \n    conv1_2 = tf.keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x) \n    x = relu(tf.keras.layers.BatchNormalization()(conv1_2))\n    pooling1 = tf.keras.layers.MaxPooling2D(padding=\"same\")(x)\n    \n    # ----- Level 2 -----\n    conv2_0 = tf.keras.layers.Conv2D(filters=64,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(pooling1)\n    x = relu(tf.keras.layers.BatchNormalization()(conv2_0))\n    \n    conv2_1 = tf.keras.layers.Conv2D(filters=64,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv2_1))\n       \n    conv2_2 = tf.keras.layers.Conv2D(filters=64,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv2_2))\n    pooling2 = tf.keras.layers.MaxPooling2D(padding=\"same\")(x)\n    \n    # ----- Decoder & Feature Fusion -----\n\n    # Upsample and concatenate with Level 1\n    upsamp2 = tf.keras.layers.UpSampling2D(size=(2, 2), \n                                           interpolation=\"bilinear\"\n                                           )(pooling2)\n    concat2_1 = tf.keras.layers.Concatenate()([pooling1,upsamp2])\n    conv1_3 = tf.keras.layers.Conv2D(filters=64,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(concat2_1)\n    x = relu(tf.keras.layers.BatchNormalization()(conv1_3))\n    \n    conv1_4 = tf.keras.layers.Conv2D(filters=64,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv1_4))\n       \n    conv1_5 = tf.keras.layers.Conv2D(filters=64,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv1_5))\n    pooling1_5 = tf.keras.layers.MaxPooling2D(padding=\"same\")(x)\n\n    # Upsample and concatenate with Level 0\n    upsamp1 = tf.keras.layers.UpSampling2D(size=(4, 4), \n                                           interpolation=\"bilinear\"\n                                           )(pooling1_5)\n    concat1_0 = tf.keras.layers.Concatenate()([pooling0,upsamp1])\n    conv0_3 = tf.keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(concat1_0)\n    x = relu(tf.keras.layers.BatchNormalization()(conv0_3))\n         \n    conv0_4 = tf.keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv0_4))\n    \n    conv0_5 = tf.keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,\n                                 padding=\"same\",\n                                kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(conv0_5))\n    pooling0_5 = tf.keras.layers.MaxPooling2D(padding=\"same\")(x)\n\n    # ----- Dense Layers -----\n    flatt = tf.keras.layers.Flatten()(pooling0_5)\n    dense1 = tf.keras.layers.Dense(units=128,\n        kernel_initializer=\"he_normal\")(flatt)\n    \n    # Branch X - for 'presence' output\n    x = relu(tf.keras.layers.BatchNormalization()(dense1))\n    x = tf.keras.layers.Dropout(0.5, seed=42)(x)\n    x = tf.keras.layers.Dense(units=20,\n        kernel_initializer=\"he_normal\")(x)\n    x = relu(tf.keras.layers.BatchNormalization()(x))\n    x = tf.keras.layers.Dropout(0.5, seed=42)(x)\n\n    # Branch Y - for 'coords' output\n    y = relu(tf.keras.layers.BatchNormalization()(dense1))\n    y = tf.keras.layers.Dense(units=50,\n        kernel_initializer=\"he_normal\")(y)\n    y = relu(tf.keras.layers.BatchNormalization()(y))\n    \n    # ----- Output Layers -----\n    presence_output = tf.keras.layers.Dense(units=1, activation=\"sigmoid\",name='presence')(x) \n    coords_output = tf.keras.layers.Dense(units=2,activation='sigmoid',name='coords')(y)\n    \n    return tf.keras.Model(inputs=inputs,outputs=[presence_output,coords_output])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.961202Z","iopub.execute_input":"2025-06-07T11:47:06.961422Z","iopub.status.idle":"2025-06-07T11:47:06.982616Z","shell.execute_reply.started":"2025-06-07T11:47:06.961407Z","shell.execute_reply":"2025-06-07T11:47:06.981869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Set base paths for dataset directories\nbase_path = '/kaggle/input/byu-locating-bacterial-flagellar-motors-2025'\ntrain_path = base_path + '/train'\ntest_path = base_path + '/test'\n\n# Create TensorFlow datasets with image file paths for training and testing\ntraining_paths_dataset = tf.data.Dataset.list_files(train_path +'/*/*.jpg',seed=42,shuffle=False)\ntest_paths_dataset = tf.data.Dataset.list_files(test_path +'/*/*.jpg',seed=42,shuffle=False)\n\n# Load CSV file containing training labels\ntrain_labels = pd.read_csv(base_path +\"/\"+\"train_labels.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:06.983456Z","iopub.execute_input":"2025-06-07T11:47:06.983828Z","iopub.status.idle":"2025-06-07T11:47:55.623157Z","shell.execute_reply.started":"2025-06-07T11:47:06.983806Z","shell.execute_reply":"2025-06-07T11:47:55.622498Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Exploring and preparing the data","metadata":{}},{"cell_type":"code","source":"# Filter out all samples with more than one motor\ntrain_labels = train_labels[train_labels['Number of motors']<=1]\n\ntrain_labels.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:55.624023Z","iopub.execute_input":"2025-06-07T11:47:55.624296Z","iopub.status.idle":"2025-06-07T11:47:55.649318Z","shell.execute_reply.started":"2025-06-07T11:47:55.624274Z","shell.execute_reply":"2025-06-07T11:47:55.648580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Count how many samples have 0 or 1 motor\ntrain_labels['Number of motors'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:55.651676Z","iopub.execute_input":"2025-06-07T11:47:55.651891Z","iopub.status.idle":"2025-06-07T11:47:55.656982Z","shell.execute_reply.started":"2025-06-07T11:47:55.651876Z","shell.execute_reply":"2025-06-07T11:47:55.656331Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Filter labels where a motor is present (slice index != -1)\nlabels_one_motor = train_labels[train_labels['Motor axis 0']!=-1]\n\n# Visualize the first 5 annotated motor positions on their corresponding images\nfor item in labels_one_motor.iloc[:5].iterrows():\n    # Construct full image path based on tomo ID and slice number\n    path = train_path + \"/\" + item[1]['tomo_id'] + f\"/slice_{int(item[1]['Motor axis 0']):04d}.jpg\"\n\n    # Load the image using OpenCV\n    imagen = cv2.imread(path)\n\n    # Optional binarization: normalize and round pixel values\n    imagen = np.array([np.round(x/225.0) for x in imagen])\n\n    # Plot image with motor location highlighted\n    plt.figure(figsize=(12,8))\n    plt.imshow(imagen)\n    plt.axis('off')\n    # Overlay the motor position (x = axis 2, y = axis 1)\n    plt.scatter(item[1]['Motor axis 2'],item[1]['Motor axis 1'],\n                800,facecolors='none',edgecolors='r')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:55.657788Z","iopub.execute_input":"2025-06-07T11:47:55.658009Z","iopub.status.idle":"2025-06-07T11:47:57.685948Z","shell.execute_reply.started":"2025-06-07T11:47:55.657994Z","shell.execute_reply":"2025-06-07T11:47:57.685153Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_labels.columns","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:57.686940Z","iopub.execute_input":"2025-06-07T11:47:57.687406Z","iopub.status.idle":"2025-06-07T11:47:57.692304Z","shell.execute_reply.started":"2025-06-07T11:47:57.687384Z","shell.execute_reply":"2025-06-07T11:47:57.691616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Initialize a list to store all training images\nfull_train_images_set = []\n\nfor label in train_labels.iterrows():\n    tomo = label[1]['tomo_id']                                  # Get tomo ID\n    slc = int(label[1]['Motor axis 0'])                         # Get slice index\n\n    # If the slice index is -1 (no motor), select a random slice from the tomo folder\n    if (slc == -1):\n        slice_list = sorted(os.listdir(train_path + \"/\" + tomo))\n        slc = int(np.random.rand()*len(slice_list))\n    # Load the image from the constructed file path and append to the list\n    full_train_images_set.append(load_image_from_path(f'{train_path}/{tomo}/slice_{slc:04d}.jpg'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:47:57.693107Z","iopub.execute_input":"2025-06-07T11:47:57.693339Z","iopub.status.idle":"2025-06-07T11:48:02.554412Z","shell.execute_reply.started":"2025-06-07T11:47:57.693319Z","shell.execute_reply":"2025-06-07T11:48:02.553647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Extract relevant columns from train_labels to create the full_train_labels dataframe\n# These columns represent the number of motors and their coordinates (axis 2 and axis 1)\nfull_train_labels = train_labels[['Number of motors','Motor axis 2','Motor axis 1']]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:02.555515Z","iopub.execute_input":"2025-06-07T11:48:02.555770Z","iopub.status.idle":"2025-06-07T11:48:02.559976Z","shell.execute_reply.started":"2025-06-07T11:48:02.555746Z","shell.execute_reply":"2025-06-07T11:48:02.559296Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_labels.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:02.560744Z","iopub.execute_input":"2025-06-07T11:48:02.561022Z","iopub.status.idle":"2025-06-07T11:48:02.586028Z","shell.execute_reply.started":"2025-06-07T11:48:02.561007Z","shell.execute_reply":"2025-06-07T11:48:02.585293Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Normalize motor coordinates to the resized image scale (256x256)\nfor label in train_labels.iterrows():\n    # Calculate scaling factors based on the original array shape for width and height\n    width_factor = label[1]['Array shape (axis 2)'] /256.0\n    height_factor = label[1]['Array shape (axis 1)'] /256.0\n\n    # Adjust motor axis by dividing by the appropriate scaling factor\n    full_train_labels.loc[label[0],'Motor axis 2'] = full_train_labels.loc[label[0],'Motor axis 2'] / width_factor \n    full_train_labels.loc[label[0],'Motor axis 1'] = full_train_labels.loc[label[0],'Motor axis 1'] / height_factor ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:02.586722Z","iopub.execute_input":"2025-06-07T11:48:02.586913Z","iopub.status.idle":"2025-06-07T11:48:02.842515Z","shell.execute_reply.started":"2025-06-07T11:48:02.586899Z","shell.execute_reply":"2025-06-07T11:48:02.841971Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_train_labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:02.843214Z","iopub.execute_input":"2025-06-07T11:48:02.843421Z","iopub.status.idle":"2025-06-07T11:48:02.852312Z","shell.execute_reply.started":"2025-06-07T11:48:02.843405Z","shell.execute_reply":"2025-06-07T11:48:02.851743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Combine the normalized labels with their corresponding images into a single DataFrame\nfull_training = full_train_labels\nfull_training.loc[:,'image'] = full_train_images_set","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:02.853016Z","iopub.execute_input":"2025-06-07T11:48:02.853262Z","iopub.status.idle":"2025-06-07T11:48:02.999475Z","shell.execute_reply.started":"2025-06-07T11:48:02.853246Z","shell.execute_reply":"2025-06-07T11:48:02.998757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create a TensorFlow dataset of labels, where each element is a tuple:\n# (Number of motors, Motor axis 2 coordinate, Motor axis 1 coordinate)\nfull_training_labels =  tf.data.Dataset.from_tensor_slices([(row['Number of motors'], row['Motor axis 2'], row['Motor axis 1'])\n                                                            for _,row in full_train_labels.iterrows()])\n# Create a TensorFlow dataset from the list of image tensors\nfull_training_dataset = tf.data.Dataset.from_tensor_slices(full_train_images_set)\n\n# Zip the image dataset and the label dataset together into one dataset\n# Each element is a tuple: (image, (Number of motors, x_coord, y_coord))\nfull_training_dataset = tf.data.Dataset.zip(full_training_dataset,full_training_labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:03.000230Z","iopub.execute_input":"2025-06-07T11:48:03.000980Z","iopub.status.idle":"2025-06-07T11:48:03.230243Z","shell.execute_reply.started":"2025-06-07T11:48:03.000957Z","shell.execute_reply":"2025-06-07T11:48:03.229710Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"training_ds, validation_ds = train_test_split(full_training_dataset,len(full_training_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:03.236604Z","iopub.execute_input":"2025-06-07T11:48:03.236856Z","iopub.status.idle":"2025-06-07T11:48:03.262958Z","shell.execute_reply.started":"2025-06-07T11:48:03.236834Z","shell.execute_reply":"2025-06-07T11:48:03.262385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create 8 augmented datasets with different map calls\naugmented_datasets = [full_training_dataset.map(augment_sample) for _ in range(8)]\n\n# Concatenate all augmented datasets into one big dataset\naugmented_dataset = augmented_datasets[0]\nfor ds in augmented_datasets[1:]:\n    augmented_dataset = augmented_dataset.concatenate(ds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:03.272661Z","iopub.execute_input":"2025-06-07T11:48:03.272940Z","iopub.status.idle":"2025-06-07T11:48:05.421068Z","shell.execute_reply.started":"2025-06-07T11:48:03.272917Z","shell.execute_reply":"2025-06-07T11:48:05.420491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X_train_full = []\ny_clf_full = []\ny_loc_full = []\n            #full_training_dataset\nfor x, y in augmented_dataset.as_numpy_iterator():\n    # Append each image tensor to X_train_full list\n    X_train_full.append(x)\n    # Append classification label (presence) to y_clf_full list\n    y_clf_full.append(y[0])\n    # Append localization labels (x, y coordinates) as a tuple to y_loc_full list\n    y_loc_full.append((y[1],y[2]))\n\n# Combine the three lists into one list of tuples to maintain correspondence\ncombine = list(zip(X_train_full, y_clf_full, y_loc_full))\n\n# Shuffle the combined list to randomize the order while keeping data-label pairs\nrandom.shuffle(combine)\n\n# Unzip the combined list back into separate lists\nX_train_full, y_clf_full, y_loc_full = zip(*combine)\n\n# Convert tuples back to lists if needed\nX_train_full = list(X_train_full)\ny_clf_full = list(y_clf_full)\ny_loc_full = list(y_loc_full)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:05.421754Z","iopub.execute_input":"2025-06-07T11:48:05.421953Z","iopub.status.idle":"2025-06-07T11:48:11.535181Z","shell.execute_reply.started":"2025-06-07T11:48:05.421939Z","shell.execute_reply":"2025-06-07T11:48:11.534605Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(X_train_full)*.8","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:11.538948Z","iopub.execute_input":"2025-06-07T11:48:11.539143Z","iopub.status.idle":"2025-06-07T11:48:11.543915Z","shell.execute_reply.started":"2025-06-07T11:48:11.539128Z","shell.execute_reply":"2025-06-07T11:48:11.543206Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Split the full training data into training and validation sets at 80%\nX_train = X_train_full[:3833]            # Training images\nX_validation = X_train_full[3833:]       # Validation images\n\n# Split the classification labels into training and validation sets\ny_clf_train = y_clf_full[:3833]          # Training classification labels\ny_clf_validation = y_clf_full[3833:]     # Validation classification labels\n\n# Split the localization labels into training and validation sets\ny_loc_train = y_loc_full[:3833]          # Training localization labels (coordinates)\ny_loc_validation = y_loc_full[3833:]     # Validation localization labels (coordinates)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:11.544526Z","iopub.execute_input":"2025-06-07T11:48:11.544729Z","iopub.status.idle":"2025-06-07T11:48:11.567331Z","shell.execute_reply.started":"2025-06-07T11:48:11.544714Z","shell.execute_reply":"2025-06-07T11:48:11.566632Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert training images and labels to NumPy arrays\nX_train = np.array(X_train)\ny_clf_train = np.array(y_clf_train)\ny_loc_train = np.array(y_loc_train)\n\n# Convert validation images and labels to NumPy arrays\nX_validation = np.array(X_validation)\ny_clf_validation = np.array(y_clf_validation)\ny_loc_validation = np.array(y_loc_validation)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:11.568117Z","iopub.execute_input":"2025-06-07T11:48:11.568308Z","iopub.status.idle":"2025-06-07T11:48:11.958513Z","shell.execute_reply.started":"2025-06-07T11:48:11.568293Z","shell.execute_reply":"2025-06-07T11:48:11.957727Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calculate mean of the training images\nmean = np.mean(X_train)\n# Calculate standard deviation of the training images\nstddev = np.std(X_train)\n# Standardize training images using the training mean and std\nX_train_std = (X_train - mean) / stddev\n# Standardize validation images using the same training mean and std\nX_validation_std = (X_validation - mean) / stddev","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:11.959441Z","iopub.execute_input":"2025-06-07T11:48:11.959688Z","iopub.status.idle":"2025-06-07T11:48:13.025890Z","shell.execute_reply.started":"2025-06-07T11:48:11.959671Z","shell.execute_reply":"2025-06-07T11:48:13.025326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Normalize location labels by dividing by image size (256)\ny_loc_train_std = y_loc_train /256.0\ny_loc_validation_std = y_loc_validation /256.0\n# Check if the normalized labels have the same type and shape as the original\ntype(y_loc_train_std) == type(y_loc_train), y_loc_train_std.shape == y_loc_train.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:13.026628Z","iopub.execute_input":"2025-06-07T11:48:13.026843Z","iopub.status.idle":"2025-06-07T11:48:13.031946Z","shell.execute_reply.started":"2025-06-07T11:48:13.026826Z","shell.execute_reply":"2025-06-07T11:48:13.031321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# List available physical GPU devices\ngpus = tf.config.list_physical_devices('GPU')\n# Print how many GPUs are available\nprint(f'Available GPUs: {len(gpus)}')\n# Create a MirroredStrategy for distributed training across GPUs\nstrategy = tf.distribute.MirroredStrategy()\n# Print the number of devices used in the strategy (replicas in sync)\nprint(f\"Number of devices: {strategy.num_replicas_in_sync}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:13.032628Z","iopub.execute_input":"2025-06-07T11:48:13.032829Z","iopub.status.idle":"2025-06-07T11:48:13.060639Z","shell.execute_reply.started":"2025-06-07T11:48:13.032807Z","shell.execute_reply":"2025-06-07T11:48:13.059998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Replace coordinates with [-1, -1] if any coordinate is negative in y_loc_train_std\ny_loc_train_std = np.array([\n    np.array([-1,-1]) if x[0]<0 or x[1]<0 else x\n    for x in y_loc_train_std])\n# Replace coordinates with [-1, -1] if any coordinate is negative in y_loc_validation_std\ny_loc_validation_std = np.array([\n    np.array([-1,-1]) if x[0]<0 or x[1]<0 else x\n    for x in y_loc_validation_std])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:13.061338Z","iopub.execute_input":"2025-06-07T11:48:13.061735Z","iopub.status.idle":"2025-06-07T11:48:13.094390Z","shell.execute_reply.started":"2025-06-07T11:48:13.061708Z","shell.execute_reply":"2025-06-07T11:48:13.093847Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_loc_train_std","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:13.095280Z","iopub.execute_input":"2025-06-07T11:48:13.095497Z","iopub.status.idle":"2025-06-07T11:48:13.100271Z","shell.execute_reply.started":"2025-06-07T11:48:13.095481Z","shell.execute_reply":"2025-06-07T11:48:13.099499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert lists to tf.data.Dataset\ndef create_dataset(X, y_clf, y_loc, batch_size=32, shuffle=True):\n     # Combine labels into a dictionary for multi-output\n    y = {'presence': y_clf, 'coords': y_loc}\n\n    # Create a dataset from the features and labels\n    dataset = tf.data.Dataset.from_tensor_slices((X, y))\n    # Shuffle the dataset if requested\n    if shuffle:\n        dataset = dataset.shuffle(buffer_size=len(X))\n    # Batch the dataset and prefetch for performance optimization\n    dataset = dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return dataset\n\nbatch_size = 32\n# Create training dataset with shuffling enabled\ntrain_dataset = create_dataset(X_train, y_clf_train, y_loc_train_std, batch_size=batch_size, shuffle=True)\n# Create validation dataset without shuffling\nval_dataset = create_dataset(X_validation, y_clf_validation, y_loc_validation_std, batch_size=batch_size, shuffle=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:13.101052Z","iopub.execute_input":"2025-06-07T11:48:13.101275Z","iopub.status.idle":"2025-06-07T11:48:17.498216Z","shell.execute_reply.started":"2025-06-07T11:48:13.101251Z","shell.execute_reply":"2025-06-07T11:48:17.497649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for batch in train_dataset.take(1):\n    images, targets = batch\n    print(\"Images shape:\", images.shape)  # Print the shape of the image batch\n\n    print(\"\\nKeys in targets:\", targets.keys())  # Print the keys in the targets dictionary\n    print(\"Presence shape:\", targets['presence'].shape)  # Shape of the presence labels batch\n    print(\"Coords shape:\", targets['coords'].shape)  # Shape of the coordinates labels batch\n    print(\"\\nExample presence label:\", targets['presence'][0].numpy())  # Print an example presence label\n    print(\"Example coords label:\", targets['coords'][0].numpy())  # Print an example coordinates label\n    break  # Only process one batch for this inspection","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:17.499177Z","iopub.execute_input":"2025-06-07T11:48:17.499360Z","iopub.status.idle":"2025-06-07T11:48:18.428996Z","shell.execute_reply.started":"2025-06-07T11:48:17.499346Z","shell.execute_reply":"2025-06-07T11:48:18.428218Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Training","metadata":{}},{"cell_type":"code","source":"early_cb = tf.keras.callbacks.EarlyStopping(patience=10,\n                                           restore_best_weights=True) # Stop training early if no improvement for 10 epochs, restore best model weights\ncheckpoint_cb = tf.keras.callbacks.ModelCheckpoint(\n    'clf_model.keras',\n    monitor=\"val_loss\",\n    verbose=0,\n    save_best_only=True,         # Save only the best model based on validation loss\n    save_weights_only=False,\n    mode=\"min\",\n    save_freq=\"epoch\"\n) # Checkpoint callback to save model after each epoch if improved\n\nwith strategy.scope(): # Use MirroredStrategy for distributed training on GPUs\n\n    @tf.keras.utils.register_keras_serializable()\n    class ConditionalMAEMetric(tf.keras.metrics.Metric):\n        def __init__(self, name=\"conditional_mae\", **kwargs):\n            super().__init__(name=name, **kwargs)\n            self.total_error = self.add_weight(name=\"total_error\", initializer=\"zeros\") # Accumulated total error\n            self.count = self.add_weight(name=\"count\", initializer=\"zeros\")  # Count of valid samples\n    \n        def update_state(self, y_true, y_pred, sample_weight=None):\n            # Mask: 1 if both coordinates >= 0, else 0 to ignore invalid samples\n            mask = tf.cast(tf.reduce_all(y_true >= 0, axis=1), tf.float32)  # shape: (batch_size,)\n            y_true_cast = tf.cast(y_true,tf.float32)\n            y_pred_cast = tf.cast(y_pred,tf.float32)\n            \n            # Absolute error summed over x and y for each sample\n            error_per_sample = tf.reduce_sum(tf.abs(y_true_cast - y_pred_cast), axis=1)  # shape: (batch_size,)\n            \n            # Apply mask to ignore invalid samples\n            masked_error = error_per_sample * mask  # shape: (batch_size,)\n            \n            # Accumulate total error and count valid samples\n            self.total_error.assign_add(tf.reduce_sum(masked_error))\n            self.count.assign_add(tf.reduce_sum(mask))\n    \n        def result(self):\n            # Return mean absolute error over valid samples, avoiding division by zero\n            return tf.math.divide_no_nan(self.total_error, self.count)\n    \n        def reset_states(self):\n            # Reset metric state at the start of each epoch\n            self.total_error.assign(0.0)\n            self.count.assign(0.0)\n\n    @tf.keras.utils.register_keras_serializable()\n    def conditional_mae_loss(y_true, y_pred):\n        \"\"\"\n        Custom loss that computes MAE ignoring samples where both true coords are negative.\n        y_true and y_pred have shape (batch_size, 2).\n        \"\"\"\n        # Create mask: 1 if both coordinates are >= 0 (object present), else 0\n        mask = K.cast(K.all(y_true >= 0.0, axis=1, keepdims=True), dtype='float32')  # shape (batch_size, 1)\n    \n        # Calculate absolute error and apply mask\n        absolute_error = K.abs(y_true - y_pred)\n        masked_error = mask * absolute_error\n    \n    \n        # Return mean error only over valid samples, avoiding division by zero\n        return K.sum(masked_error) / (K.sum(mask) + K.epsilon())\n\n    # Define learning rate schedule with exponential decay\n    lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(\n        initial_learning_rate=1e-2,\n        decay_steps=1200,\n        decay_rate=0.96,\n        staircase=True)\n\n    # Use SGD optimizer with momentum and the learning rate schedule\n    optimizer = tf.keras.optimizers.SGD(learning_rate=lr_schedule,momentum=0.85)\n\n     # Input layer for grayscale images 256x256x1\n    input_layer = tf.keras.Input(shape=(256, 256,1))  \n    # Build model with the U-Net function\n    model = build_U(input_layer)\n    # Compile model with binary crossentropy for classification and conditional MAE loss for coords\n    model.compile(optimizer=optimizer,\n                  loss={'presence': 'binary_crossentropy', 'coords': conditional_mae_loss},\n                  #loss_weights={'presence': 1.0, 'coords': 1.0},\n                  metrics={'presence': 'accuracy','coords': ConditionalMAEMetric()})\n\n    # Calculate steps per epoch and validation steps for training\n    steps_per_epoch = len(train_dataset) * 8 // 32\n    validation_steps = int((1 - 0.8) * len(train_dataset)) // 32\n\n    # Train the model with the datasets, steps, epochs, and checkpoint callback\n    history = model.fit(train_dataset,\n                        validation_data=val_dataset,\n                        epochs=100,\n                        steps_per_epoch=steps_per_epoch,\n                        validation_steps=validation_steps,\n                        callbacks=[checkpoint_cb])\n\n# Load the best saved model after training finishes\nmodel = tf.keras.models.load_model('clf_model.keras', compile=True, safe_mode=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T11:48:18.429991Z","iopub.execute_input":"2025-06-07T11:48:18.430278Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Evaluate best model on test set","metadata":{}},{"cell_type":"code","source":"paths = [path.numpy().decode() for path in test_paths_dataset]\n\ndata = []\nfor path in paths:\n    parts = path.split(os.sep)\n    tomo_id = parts[-2]  # \"tomo_003acc\"\n    slice_name = os.path.splitext(parts[-1])[0]  # \"slice_0000\"\n    \n    # Extract slice number as a float from the slice name using regex\n    match = re.search(r'slice_(\\d+)', slice_name)\n    slice_id = float(match.group(1)) if match else -1  # Use -1 if no match found\n    \n    data.append({\n        \"tomo_id\": tomo_id,\n        \"slice\" : slice_id,\n        \"Motor axis 0\": None,\n        \"Motor axis 1\": None,\n        \"Motor axis 2\": None\n    })\ntest_df = pd.DataFrame(data)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_images = []\nfor img in test_df.iterrows():\n    tomo = img[1]['tomo_id']         # Get tomo folder ID from dataframe row\n    slc = int(img[1]['slice'])       # Get slice number and convert to int\n    # Load image from constructed path with zero-padded slice number (4 digits)\n    test_images.append(load_image_from_path(f'{test_path}/{tomo}/slice_{slc:04d}.jpg'))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Predict outputs for the test images by stacking them into a tensor and feeding into the model\ny_preds = model.predict(tf.stack(test_images))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Assign predictions to the corresponding columns in test_df:\n# - 'Motor axis 0' gets the classification prediction (presence) from the first output y_preds[0]\n# - 'Motor axis 1' gets the second coordinate (y) from the location predictions y_preds[1]\n# - 'Motor axis 2' gets the first coordinate (x) from the location predictions y_preds[1]\ntest_df['Motor axis 0'] = [x[0] for x in y_preds[0]]\ntest_df['Motor axis 1'] = [x for x in y_preds[1][:,1]]\ntest_df['Motor axis 2'] = [x for x in y_preds[1][:,0]]","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# For each 'tomo_id' group, find the index of the row with the maximum 'Motor axis 0' value (presence score)\nidx = test_df.groupby('tomo_id')['Motor axis 0'].idxmax()\n# Select only those rows with the highest presence per tomo_id and reset the index\ntest_df = test_df.loc[idx].reset_index(drop=True)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"idx","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Round the 'Motor axis 0' values to nearest integer (0 or 1)\ntest_df['Motor axis 0'] = np.round(test_df['Motor axis 0'])\n\n# For rows where 'Motor axis 0' is 0, set it to -1 (indicating absence)\ntest_df.loc[test_df['Motor axis 0'] == 0,'Motor axis 0'] = -1\n\n# For rows where 'Motor axis 0' is 1, replace its value with the corresponding 'slice' number\ntest_df.loc[test_df['Motor axis 0'] == 1,'Motor axis 0'] = test_df.loc[test_df['Motor axis 0'] == 1,'slice']\n\n# For rows where 'Motor axis 0' is -1 (absence), set 'Motor axis 1' and 'Motor axis 2' also to -1\ntest_df.loc[test_df['Motor axis 0'] == -1,'Motor axis 1'] = -1\ntest_df.loc[test_df['Motor axis 0'] == -1,'Motor axis 2'] = -1\n# For rows where 'Motor axis 0' is positive (presence), replace its value again with the 'slice' number \ntest_df.loc[test_df['Motor axis 0'] >0,'Motor axis 0'] = test_df.loc[test_df['Motor axis 0'] >0,'slice']\n\n# Remove the now unnecessary 'slice' column\ntest_df.drop('slice',axis=1,inplace=True)\n\n\ntest_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for item in test_df.iterrows():\n    # Check if 'Motor axis 0' value is positive (indicating presence)\n    if item[1]['Motor axis 0'] > 0:\n        # Build the path to the image file for the corresponding tomo and slice\n        path = tf.constant(f\"{test_path}/{item[1]['tomo_id']}/slice_{int(item[1]['Motor axis 0']):04d}.jpg\")\n\n        # Read the image file as a binary string\n        image_data = tf.io.read_file(path)\n\n        # Decode the JPEG image to a tensor with 1 channel (grayscale)\n        image = tf.image.decode_jpeg(image_data, channels=1)\n        \n        height = tf.shape(image)[0].numpy()\n        width = tf.shape(image)[1].numpy()\n        \n        # Scale 'Motor axis 1' by image height and update the DataFrame in column index 2 (Motor axis 1)\n        test_df.iloc[item[0],2] = np.float32(item[1]['Motor axis 1']*height)\n\n        # Scale 'Motor axis 1' by image width and update the DataFrame in column index 3 (Motor axis 2)\n        test_df.iloc[item[0],3] = np.float32(item[1]['Motor axis 1']*width)\n        \ntest_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert the columns 'Motor axis 0', 'Motor axis 1', and 'Motor axis 2' to integer type\ntest_df[['Motor axis 0', 'Motor axis 1', 'Motor axis 2']] = test_df[['Motor axis 0', 'Motor axis 1', 'Motor axis 2']].astype(int)\n\ntest_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save the DataFrame as a CSV file named 'submission.csv' without including the index column\ntest_df.to_csv('submission.csv', index=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}