{"metadata":{"accelerator":"GPU","colab":{"provenance":[],"gpuType":"T4"},"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":8089,"databundleVersionId":44321,"sourceType":"competition"}],"dockerImageVersionId":30636,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Overview\n\nThis notebook follows the steps taken in the [Nuclei Overview to Submission](https://www.kaggle.com/code/kmader/nuclei-overview-to-submission/notebook) notebook by [K Scott Mader](https://www.kaggle.com/kmader), performing cell nuclei segmentation in microscope images with neural networks. In addition, it implements the UNet++ model from the [UNet++: A Nested U-Net TensorFlow Architecture](https://www.kaggle.com/code/abdualimov/unet-a-nested-u-net-tensorflow-architecture/notebook) notebook by [Timur Abdualimov](https://www.kaggle.com/abdualimov), carrying out cell nuclei segmentation with the mentioned architecture. We will take an extra step to find the optimal threshold for the model predictions by investigating the TPR and FPR for different threshold values.\n\nStages:<br>\n1. Data preparation\n<br>1.1. Load data (images)\n<br>1.2. Process data\n<br>1.3. Visualise data\n<br>1.4. Split data (by modality)\n<br>1.5. Process data (2)\n2. Choosing the model\n<br>2.1. Loss and metrics\n<br>2.2. Model (UNet++) [1]\n3. Training the model\n<br>3.1. Training\n4. Evaluating the model\n<br>4.1. Testing\n<br>4.2. Optimal threshold\n<br>4.3. Visualise data (2)","metadata":{"id":"peLAmSNrUmOa"}},{"cell_type":"code","source":"# Unzipping competition file containing data\n\nimport zipfile\nwith zipfile.ZipFile('/kaggle/input/data-science-bowl-2018/stage1_train.zip', 'r') as zip_ref:\n    zip_ref.extractall('/kaggle/working/stage1_train')\n    \nimport os\nos.chdir('/kaggle/working/')","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:14:46.801906Z","iopub.execute_input":"2024-01-26T16:14:46.802267Z","iopub.status.idle":"2024-01-26T16:14:51.461565Z","shell.execute_reply.started":"2024-01-26T16:14:46.802235Z","shell.execute_reply":"2024-01-26T16:14:51.460724Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Importing main data processing and visualisation libraries\n\nfrom glob import glob\nimport numpy as np\nimport pandas as pd\nfrom skimage.io import imread\nfrom skimage.transform import resize\nimport matplotlib.pyplot as plt\n%matplotlib inline\nimport seaborn as sns\nimport plotly.express as px\n\npd.set_option('display.max_colwidth', None)","metadata":{"id":"LxX8LDDyUmOf","execution":{"iopub.status.busy":"2024-01-26T16:14:51.463229Z","iopub.execute_input":"2024-01-26T16:14:51.463536Z","iopub.status.idle":"2024-01-26T16:14:53.811708Z","shell.execute_reply.started":"2024-01-26T16:14:51.463508Z","shell.execute_reply":"2024-01-26T16:14:53.810816Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# %%capture\n# !pip install \\\n#     --extra-index-url=https://pypi.nvidia.com \\\n#     cudf-cu12==23.12.* dask-cudf-cu12==23.12.* cuml-cu12==23.12.* \\\n#     cugraph-cu12==23.12.* cuspatial-cu12==23.12.* cuproj-cu12==23.12.* \\\n#     cuxfilter-cu12==23.12.* cucim-cu12==23.12.* pylibraft-cu12==23.12.* \\\n#     raft-dask-cu12==23.12.*\n# import cudf","metadata":{"id":"HzRT3E4Wm_vG","execution":{"iopub.status.busy":"2024-01-26T16:14:53.813125Z","iopub.execute_input":"2024-01-26T16:14:53.814088Z","iopub.status.idle":"2024-01-26T16:14:53.818517Z","shell.execute_reply.started":"2024-01-26T16:14:53.814050Z","shell.execute_reply":"2024-01-26T16:14:53.817525Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Load data (images)","metadata":{"id":"xo4COH8nUmOm"}},{"cell_type":"code","source":"# Obtaining paths for all image files in the dataset\n\ntrain_dir = [x for x in glob('stage1_train/*/*/*')]","metadata":{"id":"hefDg84dUmOo","execution":{"iopub.status.busy":"2024-01-26T16:14:53.821059Z","iopub.execute_input":"2024-01-26T16:14:53.821332Z","iopub.status.idle":"2024-01-26T16:14:54.005308Z","shell.execute_reply.started":"2024-01-26T16:14:53.821309Z","shell.execute_reply":"2024-01-26T16:14:54.004324Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Extracting image information such as id and type (image or mask) from file paths\n\nimg_id_fn = lambda x: x.split('/')[-3]\nimg_type_fn = lambda x: x.split('/')[-2]\nimg_group_fn = lambda x: x.split('/')[-4].split('_')[1]\n\nimg_df = pd.DataFrame({'Path': train_dir})\nimg_df['ImageId'] = img_df.Path.map(img_id_fn)\nimg_df['ImageType'] = img_df.Path.map(img_type_fn)\nimg_df['TrainingSplit'] = img_df.Path.map(img_group_fn)\n\nimg_df.head()","metadata":{"executionInfo":{"elapsed":21,"status":"ok","timestamp":1705419928712,"user":{"displayName":"Alejandro Avila Carrión","userId":"04765895392467710014"},"user_tz":-60},"id":"KQAw23MwjX_s","outputId":"3dbaec8a-b8b9-4ebb-85e0-65815af25c80","execution":{"iopub.status.busy":"2024-01-26T16:14:54.006470Z","iopub.execute_input":"2024-01-26T16:14:54.006769Z","iopub.status.idle":"2024-01-26T16:14:54.113107Z","shell.execute_reply.started":"2024-01-26T16:14:54.006743Z","shell.execute_reply":"2024-01-26T16:14:54.112197Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Process data","metadata":{"id":"6t-Gd7gIUmOs"}},{"cell_type":"code","source":"# Keeping only images belonging to the stage1_train dataset\n\nimg_df = img_df[img_df.TrainingSplit == 'train']","metadata":{"id":"6_o8aCEyYLtR","execution":{"iopub.status.busy":"2024-01-26T16:14:54.114345Z","iopub.execute_input":"2024-01-26T16:14:54.115061Z","iopub.status.idle":"2024-01-26T16:14:54.131419Z","shell.execute_reply.started":"2024-01-26T16:14:54.115026Z","shell.execute_reply":"2024-01-26T16:14:54.130692Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Aggregating files with the same image id and file\n\nimg_df = img_df.groupby(['ImageId', 'ImageType']).agg({'Path': list}).reset_index()\n\n# Saving masks and images in new separate columns. Dropping original path column\n\nimg_df['Masks'] = img_df[img_df.ImageType == 'masks'].Path\nimg_df['Images'] = img_df[img_df.ImageType == 'images'].Path\nimg_df.drop(columns=['ImageType', 'Path'], inplace=True)\n\nimg_df = img_df.groupby('ImageId').agg('sum').reset_index()","metadata":{"id":"Y8HjeiDxYVE8","execution":{"iopub.status.busy":"2024-01-26T16:14:54.132471Z","iopub.execute_input":"2024-01-26T16:14:54.132745Z","iopub.status.idle":"2024-01-26T16:14:54.221458Z","shell.execute_reply.started":"2024-01-26T16:14:54.132721Z","shell.execute_reply":"2024-01-26T16:14:54.220623Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"img_df.head(1)","metadata":{"executionInfo":{"elapsed":17,"status":"ok","timestamp":1705419928713,"user":{"displayName":"Alejandro Avila Carrión","userId":"04765895392467710014"},"user_tz":-60},"id":"hROG8AN8igz2","outputId":"b6abea83-4402-4493-f076-ad7fe298a6ee","execution":{"iopub.status.busy":"2024-01-26T16:14:54.222547Z","iopub.execute_input":"2024-01-26T16:14:54.222823Z","iopub.status.idle":"2024-01-26T16:14:54.233843Z","shell.execute_reply.started":"2024-01-26T16:14:54.222799Z","shell.execute_reply":"2024-01-26T16:14:54.232925Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Visualising one image and four of its masks\n\nfig, ax = plt.subplots(1, 6, figsize=(10, 3))\nfig.suptitle('Visualising Raw Images')\n\nax[0].imshow(imread(img_df.loc[0, 'Images'][0]))\nax[0].axis('off')\nax[0].set_title('Image')\n\nfor i in range(5):\n    ax[i + 1].imshow(imread(img_df.loc[0, 'Masks'][i]))\n    ax[i + 1].axis('off')\n    ax[i + 1].set_title(f'Mask {i}')","metadata":{"executionInfo":{"elapsed":735,"status":"ok","timestamp":1705419929432,"user":{"displayName":"Alejandro Avila Carrión","userId":"04765895392467710014"},"user_tz":-60},"id":"sGCPostTsund","outputId":"15c2c43d-c597-496a-a283-6b6b22c93453","execution":{"iopub.status.busy":"2024-01-26T16:14:54.235101Z","iopub.execute_input":"2024-01-26T16:14:54.235500Z","iopub.status.idle":"2024-01-26T16:14:54.820105Z","shell.execute_reply.started":"2024-01-26T16:14:54.235466Z","shell.execute_reply":"2024-01-26T16:14:54.819111Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The Parallel-pandas library allows to parallelise Pandas computations on all available CPUs of the Kaggle kernel, greatly reducing the image processing time.","metadata":{}},{"cell_type":"code","source":"%%capture\n# Installing Parallel-pandas\n\n!pip install --upgrade parallel-pandas\nfrom parallel_pandas import ParallelPandas","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:14:54.823785Z","iopub.execute_input":"2024-01-26T16:14:54.824132Z","iopub.status.idle":"2024-01-26T16:15:09.908558Z","shell.execute_reply.started":"2024-01-26T16:14:54.824105Z","shell.execute_reply":"2024-01-26T16:15:09.907683Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Importing masks and image data as NumPy arrays. Combining (summing) all masks into a single array\n\nIMG_CHANNELS = 3\n\nParallelPandas.initialize()\n\nimg_df = img_df.copy()\n\ndef read_stack(img_list):\n    return np.sum(np.stack([imread(img) for img in img_list], 0), 0) / 255.0  # Normalising pixel values (to [0, 1] range)\n\nimg_df['Images'] = img_df.Images.p_map(read_stack).p_map(lambda x: x[:, :, :IMG_CHANNELS])  # Removing the opacity (always = 1) values\nimg_df['Masks'] = img_df.Masks.p_map(read_stack)","metadata":{"id":"e3h2pDS-UmO0","execution":{"iopub.status.busy":"2024-01-26T16:15:09.909892Z","iopub.execute_input":"2024-01-26T16:15:09.910603Z","iopub.status.idle":"2024-01-26T16:15:37.639602Z","shell.execute_reply.started":"2024-01-26T16:15:09.910572Z","shell.execute_reply":"2024-01-26T16:15:37.638331Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualise data\nIn the following images, we observe the cell nuclei in the brightfield and fluorescence microscope modalities.","metadata":{"id":"fmDyTC4mUmO0"}},{"cell_type":"code","source":"fig, ax = plt.subplots(2, 3)\nfor i, row in img_df.head(3).iterrows():\n\n    ax[0, i].imshow(row.Images)\n    ax[0, i].axis('off')\n    ax[0, i].set_title('Microscope Image')\n\n    ax[1, i].imshow(row.Masks)\n    ax[1, i].axis('off')\n    ax[1, i].set_title('Labelled')","metadata":{"executionInfo":{"elapsed":1192,"status":"ok","timestamp":1705419976486,"user":{"displayName":"Alejandro Avila Carrión","userId":"04765895392467710014"},"user_tz":-60},"id":"eRWAXNQCUmO1","outputId":"de1fb9fc-fa28-4c3b-a03e-d8a14d2f5aa9","execution":{"iopub.status.busy":"2024-01-26T16:15:37.641647Z","iopub.execute_input":"2024-01-26T16:15:37.641949Z","iopub.status.idle":"2024-01-26T16:15:38.269395Z","shell.execute_reply.started":"2024-01-26T16:15:37.641914Z","shell.execute_reply":"2024-01-26T16:15:38.268448Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Split data (by modality)\nHere we look at the distribution of mean gray values for all images. We then partition the data based on the gray values into two separate dataframes, one for the fluorescence modality and another for the brightfield modality.","metadata":{"id":"x306n4PWUmO2"}},{"cell_type":"code","source":"# Visualising the mean gray values\n\ngray = img_df.Images.map(lambda x: np.mean(x))\n\nax = sns.displot(gray, height=3)\nax.set(xlabel='Gray_Value');","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:38.270809Z","iopub.execute_input":"2024-01-26T16:15:38.271115Z","iopub.status.idle":"2024-01-26T16:15:39.753738Z","shell.execute_reply.started":"2024-01-26T16:15:38.271088Z","shell.execute_reply":"2024-01-26T16:15:39.752889Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"As seen in the histogram above, the data could be split by the Gray_Value=0.4 line. We will therefore separate the data using this line where gray values < 0.4 correspond to fluorescence and gray values ≥ 0.4 correspond to brightfield.","metadata":{}},{"cell_type":"code","source":"img_df1 = img_df[gray < 0.4]  # (Fluorescence images)\nimg_df2 = img_df[gray >= 0.4]  # (Brightfield images)","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:39.755017Z","iopub.execute_input":"2024-01-26T16:15:39.756689Z","iopub.status.idle":"2024-01-26T16:15:39.764914Z","shell.execute_reply.started":"2024-01-26T16:15:39.756658Z","shell.execute_reply":"2024-01-26T16:15:39.763955Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Visualising fluorescence and brightfield images\n\nfig, ax = plt.subplots(1, 2, figsize=(6, 2))\n\nax[0].imshow(img_df1.iloc[-1].Images)\nax[0].axis('off')\nax[0].set_title('Fluorescence Microscope')\n\nax[1].imshow(img_df2.iloc[-1].Images)\nax[1].axis('off')\nax[1].set_title('Brighfield Microscope');\n","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:39.766108Z","iopub.execute_input":"2024-01-26T16:15:39.766372Z","iopub.status.idle":"2024-01-26T16:15:40.072331Z","shell.execute_reply.started":"2024-01-26T16:15:39.766348Z","shell.execute_reply":"2024-01-26T16:15:40.071445Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Process data (2)\nWe will now resize some of the images, ensuring the image dimensions on both datasets are uniform.","metadata":{"id":"QEAFXkJlUmO2"}},{"cell_type":"code","source":"img_df1.Images.map(lambda x: x.shape).value_counts()","metadata":{"executionInfo":{"elapsed":35,"status":"ok","timestamp":1705419987464,"user":{"displayName":"Alejandro Avila Carrión","userId":"04765895392467710014"},"user_tz":-60},"id":"53ce8k5QUmO3","outputId":"4684d4b0-71cd-413e-b21e-ea5a26e55ec2","execution":{"iopub.status.busy":"2024-01-26T16:15:40.073657Z","iopub.execute_input":"2024-01-26T16:15:40.073964Z","iopub.status.idle":"2024-01-26T16:15:40.088003Z","shell.execute_reply.started":"2024-01-26T16:15:40.073932Z","shell.execute_reply":"2024-01-26T16:15:40.087036Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"img_df2.Images.map(lambda x: x.shape).value_counts()","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:40.089295Z","iopub.execute_input":"2024-01-26T16:15:40.089999Z","iopub.status.idle":"2024-01-26T16:15:40.097779Z","shell.execute_reply.started":"2024-01-26T16:15:40.089966Z","shell.execute_reply":"2024-01-26T16:15:40.096944Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(img_df1[img_df1.Images.map(lambda x: x.shape) == (603, 1272, 3)].index)","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:40.098897Z","iopub.execute_input":"2024-01-26T16:15:40.099124Z","iopub.status.idle":"2024-01-26T16:15:40.109063Z","shell.execute_reply.started":"2024-01-26T16:15:40.099103Z","shell.execute_reply":"2024-01-26T16:15:40.108161Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(1, 2, figsize=(8, 3))\nax[0].imshow(img_df1.loc[89].Images)\nax[0].axis('off')\nax[1].imshow(img_df1.loc[89].Masks)\nax[1].axis('off');","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:40.110245Z","iopub.execute_input":"2024-01-26T16:15:40.110528Z","iopub.status.idle":"2024-01-26T16:15:40.525101Z","shell.execute_reply.started":"2024-01-26T16:15:40.110505Z","shell.execute_reply":"2024-01-26T16:15:40.524147Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# img_df1 = img_df1[img_df1.Images.map(lambda x: x.shape) == (256, 256, 3)]\n# img_df2 = img_df2[img_df2.Images.map(lambda x: x.shape) == (256, 320, 3)]","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:40.526542Z","iopub.execute_input":"2024-01-26T16:15:40.527190Z","iopub.status.idle":"2024-01-26T16:15:40.531478Z","shell.execute_reply.started":"2024-01-26T16:15:40.527151Z","shell.execute_reply":"2024-01-26T16:15:40.530468Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def resize1(img, mask=False):\n    output_shape = (256, 256, 3) if not mask else (256, 256)\n    return resize(img, output_shape, mode='constant', preserve_range=True)\n\ndef resize2(img, mask=False):\n    output_shape = (256, 320, 3) if not mask else (256, 320)\n    return resize(img, output_shape, mode='constant', preserve_range=True)","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:40.532851Z","iopub.execute_input":"2024-01-26T16:15:40.533195Z","iopub.status.idle":"2024-01-26T16:15:40.541870Z","shell.execute_reply.started":"2024-01-26T16:15:40.533161Z","shell.execute_reply":"2024-01-26T16:15:40.541142Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"img_df1 = img_df1.copy()\nimg_df2 = img_df2.copy()\n\nimg_df1['Images'] = img_df1.Images.p_map(resize1)\nimg_df1['Masks'] = img_df1.Masks.p_map(lambda x: resize1(x, mask=True))\n\nimg_df2['Images'] = img_df2.Images.p_map(resize2)\nimg_df2['Masks'] = img_df2.Masks.p_map(lambda x: resize2(x, mask=True))","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:40.543044Z","iopub.execute_input":"2024-01-26T16:15:40.543335Z","iopub.status.idle":"2024-01-26T16:15:52.608572Z","shell.execute_reply.started":"2024-01-26T16:15:40.543311Z","shell.execute_reply":"2024-01-26T16:15:52.607288Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, ax = plt.subplots(1, 2, figsize=(5, 5))\nax[0].imshow(img_df1.loc[89].Images)\nax[0].axis('off')\nax[1].imshow(img_df1.loc[89].Masks)\nax[1].axis('off');","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:15:52.610286Z","iopub.execute_input":"2024-01-26T16:15:52.610644Z","iopub.status.idle":"2024-01-26T16:15:52.767285Z","shell.execute_reply.started":"2024-01-26T16:15:52.610611Z","shell.execute_reply":"2024-01-26T16:15:52.766071Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Loss and metrics\n\nNext, we define some hyperparameters for our model, with a combined BCE-DICE loss function [[1]](https://www.kaggle.com/code/abdualimov/unet-a-nested-u-net-tensorflow-architecture/notebook) and TPR and FPR as metrics.\n\n<!--\n\"In particular, BCE Loss is used for pixel-level classification, Focal Loss is used for learning hard examples, and Dice Loss is used for learning better boundary representation. By applying these loss functions in combination, segmentation models can improve their accuracy and deliver outstanding results.\" [[2]](https://serp.ai/multi-loss-bce-loss-focal-loss-dice-loss/)\n-->","metadata":{"id":"Vda5az9PUmO4"}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import backend as K\n\n# Learning rate\nLR = 0.001\n\n# Loss function [1]\ndef dice_coef(y_true, y_pred):\n    smooth = 1.\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.flatten(y_pred)\n    intersection = K.sum(y_true_f * y_pred_f)\n    return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)\n\ndef bce_dice_loss(y_true, y_pred):\n    return 0.5 * tf.keras.losses.binary_crossentropy(y_true, y_pred) - dice_coef(y_true, y_pred)\n\n# True positive rate (TPR) and false positive rate (FPR) - threshold at 0.5\ndef tpr(y_true, y_pred):\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.round(K.flatten(y_pred))\n    tp = K.sum(y_true_f * y_pred_f)\n    p = K.sum(y_true_f)\n    return tp / p\n\ndef fpr(y_true, y_pred):\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.round(K.flatten(y_pred))\n    fp = K.sum((1 - y_true_f) * y_pred_f)\n    n = K.sum(1 - y_true_f)\n    return fp / n\n\n# Epoch number\nNUM_EPOCHS = 3","metadata":{"id":"uU8o3u3XUmO4","execution":{"iopub.status.busy":"2024-01-26T16:15:52.768904Z","iopub.execute_input":"2024-01-26T16:15:52.772007Z","iopub.status.idle":"2024-01-26T16:16:05.719764Z","shell.execute_reply.started":"2024-01-26T16:15:52.771972Z","shell.execute_reply":"2024-01-26T16:16:05.718846Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model (UNet++) [[1]](https://www.kaggle.com/code/abdualimov/unet-a-nested-u-net-tensorflow-architecture/notebook)\n<!--\n\"U-Net is an image segmentation CNN architecture designed specifically for analyzing biomedical images, meaning that it can be trained given a small number of images and still achieve incredible performance.\" [[4]](https://medium.com/analytics-vidhya/real-time-cell-counting-in-microscopy-images-with-neural-networks-d630c2a5b6c4)\n\n\"UNet++ was designed to improve the segmentation accuracy and capture more fine-grained details compared to the original UNet model.\" [[5]](https://medium.com/@saba99/unet-443b429ae0be)\n-->","metadata":{"id":"DQtMRm2OUmO3"}},{"cell_type":"code","source":"from tensorflow.keras.layers import *\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.callbacks import LearningRateScheduler, ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom sklearn.model_selection import train_test_split\n\ntf.keras.backend.clear_session()\nnb_filter = [32,64,128,256,512]\n\n# Build U-Net++ model\ninputs = Input((None, None, IMG_CHANNELS))\n\nc1 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (inputs)\nc1 = Dropout(0.5) (c1)\nc1 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (c1)\nc1 = Dropout(0.5) (c1)\np1 = MaxPooling2D((2, 2), strides=(2, 2)) (c1)\n\nc2 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (p1)\nc2 = Dropout(0.5) (c2)\nc2 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (c2)\nc2 = Dropout(0.5) (c2)\np2 = MaxPooling2D((2, 2), strides=(2, 2)) (c2)\n\nup1_2 = Conv2DTranspose(nb_filter[0], (2, 2), strides=(2, 2), name='up12', padding='same')(c2)\nconv1_2 = concatenate([up1_2, c1], name='merge12', axis=3)\nc3 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_2)\nc3 = Dropout(0.5) (c3)\nc3 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (c3)\nc3 = Dropout(0.5) (c3)\n\nconv3_1 = Conv2D(128, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (p2)\nconv3_1 = Dropout(0.5) (conv3_1)\nconv3_1 = Conv2D(128, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv3_1)\nconv3_1 = Dropout(0.5) (conv3_1)\npool3 = MaxPooling2D((2, 2), strides=(2, 2), name='pool3')(conv3_1)\n\nup2_2 = Conv2DTranspose(nb_filter[1], (2, 2), strides=(2, 2), name='up22', padding='same')(conv3_1)\nconv2_2 = concatenate([up2_2, c2], name='merge22', axis=3) #x10\nconv2_2 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv2_2)\nconv2_2 = Dropout(0.5) (conv2_2)\nconv2_2 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv2_2)\nconv2_2 = Dropout(0.5) (conv2_2)\n\nup1_3 = Conv2DTranspose(nb_filter[0], (2, 2), strides=(2, 2), name='up13', padding='same')(conv2_2)\nconv1_3 = concatenate([up1_3, c1, c3], name='merge13', axis=3)\nconv1_3 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_3)\nconv1_3 = Dropout(0.5) (conv1_3)\nconv1_3 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_3)\nconv1_3 = Dropout(0.5) (conv1_3)\n\nconv4_1 = Conv2D(256, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (pool3)\nconv4_1 = Dropout(0.5) (conv4_1)\nconv4_1 = Conv2D(256, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv4_1)\nconv4_1 = Dropout(0.5) (conv4_1)\npool4 = MaxPooling2D((2, 2), strides=(2, 2), name='pool4')(conv4_1)\n\nup3_2 = Conv2DTranspose(nb_filter[2], (2, 2), strides=(2, 2), name='up32', padding='same')(conv4_1)\nconv3_2 = concatenate([up3_2, conv3_1], name='merge32', axis=3) #x20\nconv3_2 = Conv2D(128, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv3_2)\nconv3_2 = Dropout(0.5) (conv3_2)\nconv3_2 = Conv2D(128, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv3_2)\nconv3_2 = Dropout(0.5) (conv3_2)\n\nup2_3 = Conv2DTranspose(nb_filter[1], (2, 2), strides=(2, 2), name='up23', padding='same')(conv3_2)\nconv2_3 = concatenate([up2_3, c2, conv2_2], name='merge23', axis=3)\nconv2_3 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv2_3)\nconv2_3 = Dropout(0.5) (conv2_3)\nconv2_3 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv2_3)\nconv2_3 = Dropout(0.5) (conv2_3)\n\nup1_4 = Conv2DTranspose(nb_filter[0], (2, 2), strides=(2, 2), name='up14', padding='same')(conv2_3)\nconv1_4 = concatenate([up1_4, c1, c3, conv1_3], name='merge14', axis=3)\nconv1_4 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_4)\nconv1_4 = Dropout(0.5) (conv1_4)\nconv1_4 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_4)\nconv1_4 = Dropout(0.5) (conv1_4)\n\nconv5_1 = Conv2D(512, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (pool4)\nconv5_1 = Dropout(0.5) (conv5_1)\nconv5_1 = Conv2D(512, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv5_1)\nconv5_1 = Dropout(0.5) (conv5_1)\n\nup4_2 = Conv2DTranspose(nb_filter[3], (2, 2), strides=(2, 2), name='up42', padding='same')(conv5_1)\nconv4_2 = concatenate([up4_2, conv4_1], name='merge42', axis=3) #x30\nconv4_2 = Conv2D(256, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv4_2)\nconv4_2 = Dropout(0.5) (conv4_2)\nconv4_2 = Conv2D(256, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv4_2)\nconv4_2 = Dropout(0.5) (conv4_2)\n\nup3_3 = Conv2DTranspose(nb_filter[2], (2, 2), strides=(2, 2), name='up33', padding='same')(conv4_2)\nconv3_3 = concatenate([up3_3, conv3_1, conv3_2], name='merge33', axis=3)\nconv3_3 = Conv2D(128, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv3_3)\nconv3_3 = Dropout(0.5) (conv3_3)\nconv3_3 = Conv2D(128, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv3_3)\nconv3_3 = Dropout(0.5) (conv3_3)\n\nup2_4 = Conv2DTranspose(nb_filter[1], (2, 2), strides=(2, 2), name='up24', padding='same')(conv3_3)\nconv2_4 = concatenate([up2_4, c2, conv2_2, conv2_3], name='merge24', axis=3)\nconv2_4 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv2_4)\nconv2_4 = Dropout(0.5) (conv2_4)\nconv2_4 = Conv2D(64, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv2_4)\nconv2_4 = Dropout(0.5) (conv2_4)\n\nup1_5 = Conv2DTranspose(nb_filter[0], (2, 2), strides=(2, 2), name='up15', padding='same')(conv2_4)\nconv1_5 = concatenate([up1_5, c1, c3, conv1_3, conv1_4], name='merge15', axis=3)\nconv1_5 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_5)\nconv1_5 = Dropout(0.5) (conv1_5)\nconv1_5 = Conv2D(32, (3, 3), activation='elu', kernel_initializer='he_normal', padding='same') (conv1_5)\nconv1_5 = Dropout(0.5) (conv1_5)\n\nnestnet_output_4 = Conv2D(1, (1, 1), activation='sigmoid', kernel_initializer = 'he_normal',  name='output_4', padding='same')(conv1_5)\n\nmodel = Model([inputs], [nestnet_output_4])","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:16:05.721321Z","iopub.execute_input":"2024-01-26T16:16:05.722033Z","iopub.status.idle":"2024-01-26T16:16:08.151978Z","shell.execute_reply.started":"2024-01-26T16:16:05.722003Z","shell.execute_reply":"2024-01-26T16:16:08.151138Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"tf.keras.utils.plot_model(\n    model,\n    show_shapes=False,\n    show_dtype=False,\n    show_layer_names=True,\n    rankdir='TB',\n    expand_nested=False,\n    dpi=46,\n    layer_range=None\n)","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:16:08.153130Z","iopub.execute_input":"2024-01-26T16:16:08.153437Z","iopub.status.idle":"2024-01-26T16:16:08.687881Z","shell.execute_reply.started":"2024-01-26T16:16:08.153398Z","shell.execute_reply":"2024-01-26T16:16:08.686751Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Training\n<!--\n\"Here we run a simple training, with each image being it's own batch (not a very good idea), but it keeps the code simple.\" [[1]](https://www.kaggle.com/code/kmader/nuclei-overview-to-submission/notebook)\n-->","metadata":{"id":"k1JTvbkUUmO5"}},{"cell_type":"code","source":"print('Df1:', img_df1.shape)\nprint('Df2:', img_df2.shape)","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:16:08.689313Z","iopub.execute_input":"2024-01-26T16:16:08.689682Z","iopub.status.idle":"2024-01-26T16:16:08.695389Z","shell.execute_reply.started":"2024-01-26T16:16:08.689652Z","shell.execute_reply":"2024-01-26T16:16:08.694447Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Splitting training and test set on a 80%-20% basis. Training a model for each dataset\n\nfrom tensorflow.keras.models import clone_model\n\ntrain_img_df1 = img_df1.iloc[:int(img_df1.shape[0] * 0.8)]  \ntrain_img_df2 = img_df2.iloc[:int(img_df2.shape[0] * 0.8)]\n\nmodel1 = clone_model(model)\nmodel2 = clone_model(model)\n\nfor modelx, train_img_dfx in zip([model1, model2], [train_img_df1, train_img_df2]):\n\n    modelx.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=LR), loss=bce_dice_loss, metrics=['acc', tpr, fpr, 'mae'])\n    \n    def simple_gen():\n        while True:\n            for _, row in train_img_dfx.iterrows():\n                yield np.expand_dims(row.Images, 0), np.expand_dims(np.expand_dims(row.Masks, -1), 0)\n\n    modelx.fit(simple_gen(), steps_per_epoch=train_img_dfx.shape[0], epochs = 3)","metadata":{"id":"2lDOlqiWUmO5","outputId":"311d6c0b-26c4-49f6-b000-ee9b545ab9bb","execution":{"iopub.status.busy":"2024-01-26T16:16:08.702563Z","iopub.execute_input":"2024-01-26T16:16:08.703134Z","iopub.status.idle":"2024-01-26T16:19:05.478359Z","shell.execute_reply.started":"2024-01-26T16:16:08.703100Z","shell.execute_reply":"2024-01-26T16:19:05.477397Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Testing","metadata":{"id":"Piapo1xpUmO5"}},{"cell_type":"code","source":"test_img_df1 = img_df1.iloc[-int(img_df1.shape[0] * 0.2):]\ntest_img_df2 = img_df2.iloc[-int(img_df2.shape[0] * 0.2):]","metadata":{"id":"UOyoj7vDUmO5","execution":{"iopub.status.busy":"2024-01-26T16:19:05.479678Z","iopub.execute_input":"2024-01-26T16:19:05.479970Z","iopub.status.idle":"2024-01-26T16:19:05.485467Z","shell.execute_reply.started":"2024-01-26T16:19:05.479944Z","shell.execute_reply":"2024-01-26T16:19:05.484532Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%capture\nfor model, test_img_df in zip([model1, model2], [test_img_df1, test_img_df2]):\n    test_img_df['Predictions'] = test_img_df.Images.map(lambda x: model.predict(np.expand_dims(x, 0))[0, :, :, 0])","metadata":{"id":"0TJ8yinlUmO6","execution":{"iopub.status.busy":"2024-01-26T16:19:05.486523Z","iopub.execute_input":"2024-01-26T16:19:05.486789Z","iopub.status.idle":"2024-01-26T16:19:17.889615Z","shell.execute_reply.started":"2024-01-26T16:19:05.486766Z","shell.execute_reply":"2024-01-26T16:19:17.888674Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Optimal threshold\n\nWe will now find a threshold for determining whether the predicted pixel values are considered as belonging to a nucleus or not. We will obtain the ROC curve for varying threshold values and subsquently determine a final threhold.","metadata":{}},{"cell_type":"code","source":"# Custom rounding function, for example:\n# >>> round_thr(np.array([0.1, 0.2, 0.3]), 0.1)\n# array([1., 1., 1.])\n#\n# >>> round_thr(np.array([0.1, 0.2, 0.3]), 0.25)\n# array([0., 0., 1.])\n\ndef round_thr(arr, thr):\n    return np.where(arr < np.floor(arr) + thr, np.floor(arr), np.ceil(arr))","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:19:17.890916Z","iopub.execute_input":"2024-01-26T16:19:17.891308Z","iopub.status.idle":"2024-01-26T16:19:17.897104Z","shell.execute_reply.started":"2024-01-26T16:19:17.891268Z","shell.execute_reply":"2024-01-26T16:19:17.896110Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# True positive rate (TPR) and false positive rate (FPR) for numpy arrays -\n# with custom rounding function, works for different thresholds\n\ndef tpr_(y_true, y_pred, thr):\n    y_pred = round_thr(y_pred, thr)\n    tp = np.sum(y_true * y_pred)\n    p = np.sum(y_true)\n    return tp / p\n\ndef fpr_(y_true, y_pred, thr):\n    y_pred = round_thr(y_pred, thr)\n    fp = np.sum((1 - y_true) * y_pred)\n    n = np.sum(1 - y_true)\n    return fp / n","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:19:17.898278Z","iopub.execute_input":"2024-01-26T16:19:17.898607Z","iopub.status.idle":"2024-01-26T16:19:17.909088Z","shell.execute_reply.started":"2024-01-26T16:19:17.898582Z","shell.execute_reply":"2024-01-26T16:19:17.908108Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Obtaining ROC curve data for both datasets\n\nroc_curve1 = {'Threshold': [], 'TPR': [], 'FPR': []}\nroc_curve2 = {'Threshold': [], 'TPR': [], 'FPR': []}\n\nfor thr in [x / 500 for x in range(0, 501, 1)]:\n    tpr_s1 = test_img_df1.apply(lambda x: tpr_(x.Masks, x.Predictions, thr), axis=1)\n    fpr_s1 = test_img_df1.apply(lambda x: fpr_(x.Masks, x.Predictions, thr), axis=1)\n    \n    roc_curve1['Threshold'] += [thr]\n    roc_curve1['TPR'] += [tpr_s1.mean()]\n    roc_curve1['FPR'] += [fpr_s1.mean()]\n    \n    tpr_s2 = test_img_df2.apply(lambda x: tpr_(x.Masks, x.Predictions, thr), axis=1)\n    fpr_s2 = test_img_df2.apply(lambda x: fpr_(x.Masks, x.Predictions, thr), axis=1)\n    \n    roc_curve2['Threshold'] += [thr]\n    roc_curve2['TPR'] += [tpr_s2.mean()]\n    roc_curve2['FPR'] += [fpr_s2.mean()]\n    \nroc_df1 = pd.DataFrame(roc_curve1)\nroc_df2 = pd.DataFrame(roc_curve2)","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:19:17.910261Z","iopub.execute_input":"2024-01-26T16:19:17.910544Z","iopub.status.idle":"2024-01-26T16:20:27.559921Z","shell.execute_reply.started":"2024-01-26T16:19:17.910520Z","shell.execute_reply":"2024-01-26T16:20:27.559057Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Visualising ROC curve for fluorescence modality\n\nfig = px.line(roc_df1, x='FPR', y='TPR', title='Fluorescence Microscope', hover_data='Threshold', width=600, height=400)\nfig.update_layout(hovermode=\"x unified\")\nfig.show()","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:20:27.561317Z","iopub.execute_input":"2024-01-26T16:20:27.562059Z","iopub.status.idle":"2024-01-26T16:20:29.196622Z","shell.execute_reply.started":"2024-01-26T16:20:27.562020Z","shell.execute_reply":"2024-01-26T16:20:29.195611Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Visualising ROC curve for brightfield modality\n\nfig = px.line(roc_df2, x='FPR', y='TPR', title='Brightfield Microscope', hover_data='Threshold', width=600, height=400)\nfig.update_layout(hovermode=\"x unified\")\nfig.show()","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:20:29.197804Z","iopub.execute_input":"2024-01-26T16:20:29.198076Z","iopub.status.idle":"2024-01-26T16:20:29.266282Z","shell.execute_reply.started":"2024-01-26T16:20:29.198052Z","shell.execute_reply":"2024-01-26T16:20:29.265315Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's say that a FPR at 5 % or above is unnaceptable for both brightfield and fluorescence modalities.","metadata":{}},{"cell_type":"code","source":"print('Minimum acceptable threshold for fluorescence:')\nprint(roc_df1[roc_df1.FPR < 0.05].sort_values('FPR').tail(1).Threshold.values[0])\nprint('Minimum acceptable threshold for brightfield:')\nprint(roc_df2[roc_df2.FPR < 0.05].sort_values('FPR').tail(1).Threshold.values[0])","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:20:29.267844Z","iopub.execute_input":"2024-01-26T16:20:29.268794Z","iopub.status.idle":"2024-01-26T16:20:29.289625Z","shell.execute_reply.started":"2024-01-26T16:20:29.268766Z","shell.execute_reply":"2024-01-26T16:20:29.288687Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualise data (2)\n\nHere we observe the original images, along with the raw predicted masks, processed predicted masks and ground truth masks. An additional post-processing step was introduced, with an opening operation followed by a closing operation [[2]](https://www.kaggle.com/code/kmader/nuclei-overview-to-submission/notebook).","metadata":{"id":"XoU5yRRzUmO6"}},{"cell_type":"code","source":"# Importing morphological operation functions\n\nfrom scipy.spatial.qhull import QhullError # https://stackoverflow.com/a/77754773/18683993\nfrom scipy import spatial\nspatial.QhullError = QhullError\n\nfrom skimage.morphology import closing, opening, disk","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:20:29.290955Z","iopub.execute_input":"2024-01-26T16:20:29.291366Z","iopub.status.idle":"2024-01-26T16:20:29.367903Z","shell.execute_reply.started":"2024-01-26T16:20:29.291334Z","shell.execute_reply":"2024-01-26T16:20:29.367163Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Defining cleaning (extra post-processing) function\n\ndef clean_img(x):\n   return opening(closing(x, disk(1)), disk(1))","metadata":{"execution":{"iopub.status.busy":"2024-01-26T16:20:29.369082Z","iopub.execute_input":"2024-01-26T16:20:29.369437Z","iopub.status.idle":"2024-01-26T16:20:29.374002Z","shell.execute_reply.started":"2024-01-26T16:20:29.369382Z","shell.execute_reply":"2024-01-26T16:20:29.373185Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Visualising results for six samples\n\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\nfig, ax = plt.subplots(4, 6, figsize=(14, 8))\n\ntest_img_plt = pd.concat([test_img_df1.sample(3, random_state=7), test_img_df2.sample(3, random_state=13)])\ntest_img_plt['Modalities'] = ['brightfield', 'brightfield', 'brightfield', 'fluorescence', 'fluorescence', 'fluorescence']\ntest_img_plt['Thresholds'] = [0.008, 0.008, 0.008, 0.556, 0.556, 0.556]\n\nfor i, row in test_img_plt.reset_index(drop=True).iterrows():\n    \n    ax[0, i].imshow(row.Images)\n    ax[0, i].axis('off')\n    ax[0, i].set_title('Microscope Image')\n\n    ax[1, i].imshow(row.Predictions)\n    ax[1, i].axis('off')\n    ax[1, i].set_title('Raw prediction')\n\n    ax[2, i].imshow(np.round(clean_img(round_thr(row.Predictions, row.Thresholds))))\n    ax[2, i].axis('off')\n    ax[2, i].set_title('Clean')\n\n    ax[3, i].imshow(row.Masks)\n    ax[3, i].axis('off')\n    ax[3, i].set_title('Label')\n","metadata":{"id":"4_cTPBykEiXJ","execution":{"iopub.status.busy":"2024-01-26T16:20:29.375126Z","iopub.execute_input":"2024-01-26T16:20:29.375429Z","iopub.status.idle":"2024-01-26T16:20:31.804238Z","shell.execute_reply.started":"2024-01-26T16:20:29.375376Z","shell.execute_reply":"2024-01-26T16:20:31.803358Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# References\n\n[1]  [Abdualimov T](https://www.kaggle.com/abdualimov). [UNet++: A Nested U-Net TensorFlow Architecture](https://www.kaggle.com/code/abdualimov/unet-a-nested-u-net-tensorflow-architecture/notebook)\n<br>[2] [Mader K](https://www.kaggle.com/kmader). [Nuclei Overview to Submission](https://www.kaggle.com/code/kmader/nuclei-overview-to-submission/notebook)","metadata":{}}]}