From ba7ae9ee20aaf081ff7688f8ee48e7b15e5282cf Mon Sep 17 00:00:00 2001
From: Tim Laibacher <tim.laibacher@idiap.ch>
Date: Wed, 1 May 2019 12:39:59 +0200
Subject: [PATCH] Add more docs and unittests

---
 bob/ip/binseg/configs/__init__.py             |   3 ++
 bob/ip/binseg/configs/datasets/__init__.py    |   3 ++
 bob/ip/binseg/configs/models/__init__.py      |   3 ++
 bob/ip/binseg/configs/models/driulayerwise.py |  51 ++++++++++++++++++
 bob/ip/binseg/data/binsegdataset.py           |   2 +-
 bob/ip/binseg/data/transforms.py              |  10 +---
 bob/ip/binseg/engine/inferencer.py            |  45 ++++++++++++----
 bob/ip/binseg/engine/trainer.py               |  28 ++++++++--
 bob/ip/binseg/modeling/driu.py                |  25 +++++++--
 bob/ip/binseg/modeling/hed.py                 |  27 +++++++---
 bob/ip/binseg/modeling/m2u.py                 |  21 +++++++-
 bob/ip/binseg/modeling/resunet.py             |  19 +++++--
 bob/ip/binseg/modeling/unet.py                |  19 +++++--
 bob/ip/binseg/script/__init__.py              |   3 ++
 bob/ip/binseg/script/binseg.py                |   7 ++-
 bob/ip/binseg/test/test_basemetrics.py        |  44 +++++++++++++++
 bob/ip/binseg/test/test_batchmetrics.py       |  40 ++++++++++++++
 bob/ip/binseg/test/test_models.py             |  27 +++++++---
 precision_recall_comparison.pdf               | Bin 18677 -> 0 bytes
 setup.py                                      |   1 +
 20 files changed, 325 insertions(+), 53 deletions(-)
 create mode 100644 bob/ip/binseg/configs/__init__.py
 create mode 100644 bob/ip/binseg/configs/datasets/__init__.py
 create mode 100644 bob/ip/binseg/configs/models/__init__.py
 create mode 100644 bob/ip/binseg/configs/models/driulayerwise.py
 create mode 100644 bob/ip/binseg/test/test_basemetrics.py
 create mode 100644 bob/ip/binseg/test/test_batchmetrics.py
 delete mode 100644 precision_recall_comparison.pdf

diff --git a/bob/ip/binseg/configs/__init__.py b/bob/ip/binseg/configs/__init__.py
new file mode 100644
index 00000000..2ca5e07c
--- /dev/null
+++ b/bob/ip/binseg/configs/__init__.py
@@ -0,0 +1,3 @@
+# see https://docs.python.org/3/library/pkgutil.html
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/bob/ip/binseg/configs/datasets/__init__.py b/bob/ip/binseg/configs/datasets/__init__.py
new file mode 100644
index 00000000..2ca5e07c
--- /dev/null
+++ b/bob/ip/binseg/configs/datasets/__init__.py
@@ -0,0 +1,3 @@
+# see https://docs.python.org/3/library/pkgutil.html
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/bob/ip/binseg/configs/models/__init__.py b/bob/ip/binseg/configs/models/__init__.py
new file mode 100644
index 00000000..2ca5e07c
--- /dev/null
+++ b/bob/ip/binseg/configs/models/__init__.py
@@ -0,0 +1,3 @@
+# see https://docs.python.org/3/library/pkgutil.html
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/bob/ip/binseg/configs/models/driulayerwise.py b/bob/ip/binseg/configs/models/driulayerwise.py
new file mode 100644
index 00000000..390145c5
--- /dev/null
+++ b/bob/ip/binseg/configs/models/driulayerwise.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from torch.optim.lr_scheduler import MultiStepLR
+from bob.ip.binseg.modeling.driu import build_driu
+import torch.optim as optim
+from torch.nn import BCEWithLogitsLoss
+from bob.ip.binseg.utils.model_zoo import modelurls
+from bob.ip.binseg.modeling.losses import WeightedBCELogitsLoss
+from bob.ip.binseg.engine.adabound import AdaBound
+
+##### Config #####
+lr = 0.001
+betas = (0.9, 0.999)
+eps = 1e-08
+weight_decay = 0
+final_lr = 0.1
+gamma = 1e-3
+eps = 1e-8
+amsbound = False
+
+scheduler_milestones = [150]
+scheduler_gamma = 0.1
+
+# model
+model = build_driu()
+
+# pretrained backbone
+pretrained_backbone = modelurls['vgg16']
+
+# optimizer
+optimizer = AdaBound(model.parameters(), lr=lr, betas=betas, final_lr=final_lr, gamma=gamma,
+                 eps=eps, weight_decay=weight_decay, amsbound=amsbound) 
+optim = AdaBound(
+    [
+        {"params": model.backbone.parameters(), "lr": 0.0001,"betas":betas, "final_lr":0.01, "gamma":gamma, "eps" : eps},
+        {"params": model.head.parameters(), "lr": 0.001,"betas":betas, "final_lr":0.1, "gamma":gamma, "eps" : eps},
+    ],
+    betas=betas
+    ,final_lr=final_lr
+    ,gamma=gamma
+    ,eps=eps
+    ,weight_decay=weight_decay
+    ,amsbound=amsbound
+    ,lr=0.00001
+)
+# criterion
+criterion = WeightedBCELogitsLoss(reduction='mean')
+
+# scheduler
+scheduler = MultiStepLR(optimizer, milestones=scheduler_milestones, gamma=scheduler_gamma)
diff --git a/bob/ip/binseg/data/binsegdataset.py b/bob/ip/binseg/data/binsegdataset.py
index 82725efc..27853d14 100644
--- a/bob/ip/binseg/data/binsegdataset.py
+++ b/bob/ip/binseg/data/binsegdataset.py
@@ -11,7 +11,7 @@ class BinSegDataset(Dataset):
     It supports indexing such that dataset[i] can be used to get ith sample, e.g.: 
     img, gt, mask, name = db[0]
     
-    Attributes
+    Parameters
     ----------
     database  : binary segmentation `bob.db.database`
                
diff --git a/bob/ip/binseg/data/transforms.py b/bob/ip/binseg/data/transforms.py
index 4518668d..aa20d5d4 100644
--- a/bob/ip/binseg/data/transforms.py
+++ b/bob/ip/binseg/data/transforms.py
@@ -170,8 +170,8 @@ class ColorJitter(object):
     """ 
     Randomly change the brightness, contrast and saturation of an image.
     
-    Attributes
-    -----------
+    Parameters
+    ----------
 
         brightness : float
                         How much to jitter brightness. brightness_factor
@@ -196,12 +196,6 @@ class ColorJitter(object):
 
     @staticmethod
     def get_params(brightness, contrast, saturation, hue):
-        """Get a randomized transform to be applied on image.
-        Arguments are same as that of __init__.
-        Returns:
-            Transform which randomly adjusts brightness, contrast and
-            saturation in a random order.
-        """
         transforms = []
         if brightness > 0:
             brightness_factor = random.uniform(max(0, 1 - brightness), 1 + brightness)
diff --git a/bob/ip/binseg/engine/inferencer.py b/bob/ip/binseg/engine/inferencer.py
index c8166a47..b57a2de0 100644
--- a/bob/ip/binseg/engine/inferencer.py
+++ b/bob/ip/binseg/engine/inferencer.py
@@ -5,20 +5,20 @@ import os
 import logging
 import time
 import datetime
-from tqdm import tqdm
-import torch
 import numpy as np
-import pickle
+import torch
 import pandas as pd
+import torchvision.transforms.functional as VF
+from tqdm import tqdm
 
 from bob.ip.binseg.utils.metric import SmoothedValue, base_metrics
 from bob.ip.binseg.utils.plot import precision_recall_f1iso
 
-import torchvision.transforms.functional as VF
+
 
 def batch_metrics(predictions, ground_truths, masks, names, output_folder, logger):
     """
-    calculates metrics on the batch and saves it to disc
+    Calculates metrics on the batch and saves it to disc
 
     Parameters
     ----------
@@ -27,7 +27,7 @@ def batch_metrics(predictions, ground_truths, masks, names, output_folder, logge
     mask : :py:class:torch.Tensor
     names : list
     output_folder : str
-    logger : logger
+    logger : :py:class:logging
 
     Returns
     -------
@@ -86,24 +86,44 @@ def batch_metrics(predictions, ground_truths, masks, names, output_folder, logge
     return batch_metrics
 
 
-
 def save_probability_images(predictions, names, output_folder, logger):
+    """
+    Saves probability maps as tif image
+
+    Parameters
+    ----------
+    predictions : :py:class:torch.Tensor
+    names : list
+    output_folder : str
+    logger :  :py:class:logging
+    """
     images_subfolder = os.path.join(output_folder,'images') 
     if not os.path.exists(images_subfolder): os.makedirs(images_subfolder)
     for j in range(predictions.size()[0]):
         img = VF.to_pil_image(predictions.cpu().data[j])
-        filename = '{}_prob.gif'.format(names[j])
+        filename = '{}.tif'.format(names[j])
         logger.info("saving {}".format(filename))
         img.save(os.path.join(images_subfolder, filename))
 
 
-
 def do_inference(
     model,
     data_loader,
     device,
     output_folder = None
 ):
+
+    """
+    Run inference and calculate metrics
+    
+    Paramters
+    ---------
+    model : :py:class:torch.nn.Module
+    data_loader : py:class:torch.torch.utils.data.DataLoader
+    device : str
+                'cpu' or 'cuda'
+    output_folder : str
+    """
     logger = logging.getLogger("bob.ip.binseg.engine.inference")
     logger.info("Start evaluation")
     logger.info("Split: {}, Output folder: {}, Device: {}".format(data_loader.dataset.split, output_folder, device))
@@ -128,10 +148,12 @@ def do_inference(
             start_time = time.perf_counter()
 
             outputs = model(images)
+            
             # necessary check for hed architecture that uses several outputs 
             # for loss calculation instead of just the last concatfuse block
             if isinstance(outputs,list):
                 outputs = outputs[-1]
+            
             probabilities = sigmoid(outputs)
             
             batch_time = time.perf_counter() - start_time
@@ -140,10 +162,11 @@ def do_inference(
             
             b_metrics = batch_metrics(probabilities, ground_truths, masks, names,results_subfolder, logger)
             metrics.extend(b_metrics)
+            
             # Create probability images
             save_probability_images(probabilities, names, output_folder, logger)
 
-
+    # DataFrame 
     df_metrics = pd.DataFrame(metrics,columns= \
                            ["name",
                             "threshold",
@@ -187,7 +210,7 @@ def do_inference(
 
     times_file = "Times.txt".format(model.name)
     logger.info("saving {}".format(times_file))
-        
+ 
     with open (os.path.join(results_subfolder,times_file), "w+") as outfile:
         date = datetime.datetime.now()
         outfile.write("Date: {} \n".format(date.strftime("%Y-%m-%d %H:%M:%S")))
diff --git a/bob/ip/binseg/engine/trainer.py b/bob/ip/binseg/engine/trainer.py
index c84a0733..b5a60bb2 100644
--- a/bob/ip/binseg/engine/trainer.py
+++ b/bob/ip/binseg/engine/trainer.py
@@ -1,17 +1,18 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+import os 
 import logging
 import time
 import datetime
-from tqdm import tqdm
 import torch
-import os 
 import pandas as pd
+from tqdm import tqdm
 
 from bob.ip.binseg.utils.metric import SmoothedValue
 from bob.ip.binseg.utils.plot import loss_curve
 
+
 def do_train(
     model,
     data_loader,
@@ -24,7 +25,24 @@ def do_train(
     arguments,
     output_folder
 ):
-    """ Trains the model """
+    """ 
+    Trains the model
+    
+    Parameters
+    ----------
+    model : :py:class:torch.nn.Module
+    data_loader : py:class:torch.torch.utils.data.DataLoader
+    optimizer : py:class.torch.torch.optim.Optimizer
+    criterion : py:class.torch.nn.modules.loss._Loss
+    scheduler : py:class.torch.torch.optim._LRScheduler
+    checkpointer : bob.ip.binseg.utils.checkpointer.DetectronCheckpointer
+    checkpoint_period : int
+    device : str
+                'cpu' or 'cuda'
+    arguments : dict
+    output_folder : str
+
+    """
     logger = logging.getLogger("bob.ip.binseg.engine.trainer")
     logger.info("Start training")
     start_epoch = arguments["epoch"]
@@ -42,15 +60,17 @@ def do_train(
             losses = SmoothedValue(len(data_loader))
             epoch = epoch + 1
             arguments["epoch"] = epoch
+            
+            # Epoch time
             start_epoch_time = time.time()
 
             for images, ground_truths, masks, _ in tqdm(data_loader):
 
                 images = images.to(device)
                 ground_truths = ground_truths.to(device)
-                #masks = masks.to(device) 
 
                 outputs = model(images)
+
                 loss = criterion(outputs, ground_truths)
                 optimizer.zero_grad()
                 loss.backward()
diff --git a/bob/ip/binseg/modeling/driu.py b/bob/ip/binseg/modeling/driu.py
index b1478f63..fa367e61 100644
--- a/bob/ip/binseg/modeling/driu.py
+++ b/bob/ip/binseg/modeling/driu.py
@@ -8,6 +8,10 @@ from bob.ip.binseg.modeling.backbones.vgg import vgg16
 from bob.ip.binseg.modeling.make_layers import conv_with_kaiming_uniform,convtrans_with_kaiming_uniform, UpsampleCropBlock
 
 class ConcatFuseBlock(nn.Module):
+    """ 
+    Takes in four feature maps with 16 channels each, concatenates them 
+    and applies a 1x1 convolution with 1 output channel. 
+    """
     def __init__(self):
         super().__init__()
         self.conv = conv_with_kaiming_uniform(4*16,1,1,1,0)
@@ -20,16 +24,18 @@ class ConcatFuseBlock(nn.Module):
 class DRIU(nn.Module):
     """
     DRIU head module
-    Attributes
+    
+    Parameters
     ----------
-        in_channels_list (list[int]): number of channels for each feature map that is returned from backbone
+    in_channels_list : list
+                        number of channels for each feature map that is returned from backbone
     """
     def __init__(self, in_channels_list=None):
         super(DRIU, self).__init__()
         in_conv_1_2_16, in_upsample2, in_upsample_4, in_upsample_8 = in_channels_list
 
         self.conv1_2_16 = nn.Conv2d(in_conv_1_2_16, 16, 3, 1, 1)
-        # Upsample
+        # Upsample layers
         self.upsample2 = UpsampleCropBlock(in_upsample2, 16, 4, 2, 0)
         self.upsample4 = UpsampleCropBlock(in_upsample_4, 16, 8, 4, 0)
         self.upsample8 = UpsampleCropBlock(in_upsample_8, 16, 16, 8, 0)
@@ -39,8 +45,10 @@ class DRIU(nn.Module):
 
     def forward(self,x):
         """
-        Arguments:
-            x (list[Tensor]): tensor as returned from the backbone network.
+        Parameters
+        ----------
+        x : list
+                list of tensors as returned from the backbone network.
                 First element: height and width of input image. 
                 Remaining elements: feature maps for each feature level.
         """
@@ -53,6 +61,13 @@ class DRIU(nn.Module):
         return out
 
 def build_driu():
+    """ 
+    Adds backbone and head together
+
+    Returns
+    -------
+    model : :py:class:torch.nn.Module
+    """
     backbone = vgg16(pretrained=False, return_features = [3, 8, 14, 22])
     driu_head = DRIU([64, 128, 256, 512])
 
diff --git a/bob/ip/binseg/modeling/hed.py b/bob/ip/binseg/modeling/hed.py
index 6a3e1d8c..6a8349fd 100644
--- a/bob/ip/binseg/modeling/hed.py
+++ b/bob/ip/binseg/modeling/hed.py
@@ -8,6 +8,10 @@ from bob.ip.binseg.modeling.backbones.vgg import vgg16
 from bob.ip.binseg.modeling.make_layers import conv_with_kaiming_uniform, convtrans_with_kaiming_uniform, UpsampleCropBlock
 
 class ConcatFuseBlock(nn.Module):
+    """ 
+    Takes in five feature maps with one channel each, concatenates thems 
+    and applies a 1x1 convolution with 1 output channel. 
+    """
     def __init__(self):
         super().__init__()
         self.conv = conv_with_kaiming_uniform(5,1,1,1,0)
@@ -20,11 +24,11 @@ class ConcatFuseBlock(nn.Module):
 class HED(nn.Module):
     """
     HED head module
-    Attributes
+    
+    Parameters
     ----------
-        in_channels_list (list[int]): number of channels for each feature map that
-        will be fed
-        
+    in_channels_list : list
+                        number of channels for each feature map that is returned from backbone
     """
     def __init__(self, in_channels_list=None):
         super(HED, self).__init__()
@@ -41,8 +45,12 @@ class HED(nn.Module):
 
     def forward(self,x):
         """
-        Arguments:
-            x (list[Tensor]): feature maps for each feature level.
+        Parameters
+        ----------
+        x : list
+                list of tensors as returned from the backbone network.
+                First element: height and width of input image. 
+                Remaining elements: feature maps for each feature level.
         """
         hw = x[0]
         conv1_2_16 = self.conv1_2_16(x[1])  
@@ -56,6 +64,13 @@ class HED(nn.Module):
         return out
 
 def build_hed():
+    """ 
+    Adds backbone and head together
+
+    Returns
+    -------
+    model : :py:class:torch.nn.Module
+    """
     backbone = vgg16(pretrained=False, return_features = [3, 8, 14, 22, 29])
     hed_head = HED([64, 128, 256, 512, 512])
 
diff --git a/bob/ip/binseg/modeling/m2u.py b/bob/ip/binseg/modeling/m2u.py
index e3936252..13602eb3 100644
--- a/bob/ip/binseg/modeling/m2u.py
+++ b/bob/ip/binseg/modeling/m2u.py
@@ -40,9 +40,11 @@ class LastDecoderBlock(nn.Module):
 class M2U(nn.Module):
     """
     M2U-Net head module
-    Attributes
+    
+    Parameters
     ----------
-        in_channels_list (list[int]): number of channels for each feature map that is returned from backbone
+    in_channels_list : list
+                        number of channels for each feature map that is returned from backbone
     """
     def __init__(self, in_channels_list=None,upsamplemode='bilinear',expand_ratio=0.15):
         super(M2U, self).__init__()
@@ -67,6 +69,14 @@ class M2U(nn.Module):
                 m.bias.data.zero_()
     
     def forward(self,x):
+        """
+        Parameters
+        ----------
+        x : list
+                list of tensors as returned from the backbone network.
+                First element: height and width of input image. 
+                Remaining elements: feature maps for each feature level.
+        """
         decode4 = self.decode4(x[5],x[4])    # 96, 32
         decode3 = self.decode3(decode4,x[3]) # 64, 24
         decode2 = self.decode2(decode3,x[2]) # 44, 16
@@ -75,6 +85,13 @@ class M2U(nn.Module):
         return decode1
 
 def build_m2unet():
+    """ 
+    Adds backbone and head together
+
+    Returns
+    -------
+    model : :py:class:torch.nn.Module
+    """
     backbone = MobileNetV2(return_features = [1,3,6,13], m2u=True)
     m2u_head = M2U(in_channels_list=[16, 24, 32, 96])
 
diff --git a/bob/ip/binseg/modeling/resunet.py b/bob/ip/binseg/modeling/resunet.py
index 7ee94961..38f66cdd 100644
--- a/bob/ip/binseg/modeling/resunet.py
+++ b/bob/ip/binseg/modeling/resunet.py
@@ -12,9 +12,11 @@ from bob.ip.binseg.modeling.backbones.resnet import resnet50
 class ResUNet(nn.Module):
     """
     UNet head module for ResNet backbones
-    Attributes
+    
+    Parameters
     ----------
-        in_channels_list (list[int]): number of channels for each feature map that is returned from backbone
+    in_channels_list : list
+                        number of channels for each feature map that is returned from backbone
     """
     def __init__(self, in_channels_list=None, pixel_shuffle=False):
         super(ResUNet, self).__init__()
@@ -36,8 +38,10 @@ class ResUNet(nn.Module):
 
     def forward(self,x):
         """
-        Arguments:
-            x (list[Tensor]): tensor as returned from the backbone network.
+        Parameters
+        ----------
+        x : list
+                list of tensors as returned from the backbone network.
                 First element: height and width of input image. 
                 Remaining elements: feature maps for each feature level.
         """
@@ -51,6 +55,13 @@ class ResUNet(nn.Module):
         return out
 
 def build_res50unet():
+    """ 
+    Adds backbone and head together
+
+    Returns
+    -------
+    model : :py:class:torch.nn.Module
+    """
     backbone = resnet50(pretrained=False, return_features = [2, 4, 5, 6, 7])
     unet_head  = ResUNet([64, 256, 512, 1024, 2048],pixel_shuffle=False)
     model = nn.Sequential(OrderedDict([("backbone", backbone), ("head", unet_head)]))
diff --git a/bob/ip/binseg/modeling/unet.py b/bob/ip/binseg/modeling/unet.py
index 9be2b498..d0db666b 100644
--- a/bob/ip/binseg/modeling/unet.py
+++ b/bob/ip/binseg/modeling/unet.py
@@ -12,9 +12,11 @@ from bob.ip.binseg.modeling.backbones.vgg import vgg16
 class UNet(nn.Module):
     """
     UNet head module
-    Attributes
+    
+    Parameters
     ----------
-        in_channels_list (list[int]): number of channels for each feature map that is returned from backbone
+    in_channels_list : list
+                        number of channels for each feature map that is returned from backbone
     """
     def __init__(self, in_channels_list=None, pixel_shuffle=False):
         super(UNet, self).__init__()
@@ -30,8 +32,10 @@ class UNet(nn.Module):
 
     def forward(self,x):
         """
-        Arguments:
-            x (list[Tensor]): tensor as returned from the backbone network.
+        Parameters
+        ----------
+        x : list
+                list of tensors as returned from the backbone network.
                 First element: height and width of input image. 
                 Remaining elements: feature maps for each feature level.
         """
@@ -44,6 +48,13 @@ class UNet(nn.Module):
         return out
 
 def build_unet():
+    """ 
+    Adds backbone and head together
+
+    Returns
+    -------
+    model : :py:class:torch.nn.Module
+    """
     backbone = vgg16(pretrained=False, return_features = [3, 8, 14, 22, 29])
     unet_head = UNet([64, 128, 256, 512, 512], pixel_shuffle=False)
 
diff --git a/bob/ip/binseg/script/__init__.py b/bob/ip/binseg/script/__init__.py
index e69de29b..2ca5e07c 100644
--- a/bob/ip/binseg/script/__init__.py
+++ b/bob/ip/binseg/script/__init__.py
@@ -0,0 +1,3 @@
+# see https://docs.python.org/3/library/pkgutil.html
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/bob/ip/binseg/script/binseg.py b/bob/ip/binseg/script/binseg.py
index d57406f7..8fc667b1 100644
--- a/bob/ip/binseg/script/binseg.py
+++ b/bob/ip/binseg/script/binseg.py
@@ -75,6 +75,7 @@ def binseg():
     )
 @click.option(
     '--pretrained-backbone',
+    '-t',
     required=True,
     cls=ResourceOption
     )
@@ -122,6 +123,8 @@ def train(model
         ,checkpoint_period
         ,device
         ,**kwargs):
+    """ Train a model """
+    
     if not os.path.exists(output_path): os.makedirs(output_path)
     
     # PyTorch dataloader
@@ -198,7 +201,7 @@ def test(model
         ,batch_size
         ,dataset
         , **kwargs):
-
+    """ Run inference and evalaute the model performance """
 
     # PyTorch dataloader
     data_loader = DataLoader(
@@ -257,7 +260,7 @@ def testcheckpoints(model
         ,dataset
         , **kwargs):
 
-
+    """ Run inference and evaluate all checkpoints saved for a model"""
     # PyTorch dataloader
     data_loader = DataLoader(
         dataset = dataset
diff --git a/bob/ip/binseg/test/test_basemetrics.py b/bob/ip/binseg/test/test_basemetrics.py
new file mode 100644
index 00000000..bf478ac7
--- /dev/null
+++ b/bob/ip/binseg/test/test_basemetrics.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import unittest
+import numpy as np
+from bob.ip.binseg.utils.metric import base_metrics
+import random
+
+class Tester(unittest.TestCase):
+    """
+    Unit test for base metrics
+    """
+    def setUp(self):
+        self.tp = random.randint(1, 100)
+        self.fp = random.randint(1, 100)
+        self.tn = random.randint(1, 100)
+        self.fn = random.randint(1, 100)
+    
+    def test_precision(self):
+        precision = base_metrics(self.tp, self.fp, self.tn, self.fn)[0]
+        self.assertEqual((self.tp)/(self.tp + self.fp),precision)
+
+    def test_recall(self):
+        recall = base_metrics(self.tp, self.fp, self.tn, self.fn)[1]
+        self.assertEqual((self.tp)/(self.tp + self.fn),recall)
+
+    def test_specificity(self):
+        specificity = base_metrics(self.tp, self.fp, self.tn, self.fn)[2]
+        self.assertEqual((self.tn)/(self.tn + self.fp),specificity)
+    
+    def test_accuracy(self):
+        accuracy = base_metrics(self.tp, self.fp, self.tn, self.fn)[3]
+        self.assertEqual((self.tp + self.tn)/(self.tp + self.tn + self.fp + self.fn), accuracy)
+
+    def test_jaccard(self):
+        jaccard = base_metrics(self.tp, self.fp, self.tn, self.fn)[4]
+        self.assertEqual(self.tp / (self.tp+self.fp+self.fn), jaccard)
+
+    def test_f1(self):
+        f1 = base_metrics(self.tp, self.fp, self.tn, self.fn)[5]
+        self.assertEqual((2.0 * self.tp ) / (2.0 * self.tp + self.fp + self.fn ),f1)
+        
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/bob/ip/binseg/test/test_batchmetrics.py b/bob/ip/binseg/test/test_batchmetrics.py
new file mode 100644
index 00000000..93d573e8
--- /dev/null
+++ b/bob/ip/binseg/test/test_batchmetrics.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import unittest
+import numpy as np
+from bob.ip.binseg.engine.inferencer import batch_metrics
+import random
+import shutil, tempfile
+import logging
+import torch
+
+class Tester(unittest.TestCase):
+    """
+    Unit test for batch metrics
+    """
+    def setUp(self):
+        self.tp = random.randint(1, 100)
+        self.fp = random.randint(1, 100)
+        self.tn = random.randint(1, 100)
+        self.fn = random.randint(1, 100)
+        self.predictions = torch.rand(size=(2,1,420,420))
+        self.ground_truths = torch.randint(low=0, high=2, size=(2,1,420,420))
+        self.masks = None
+        self.names = ['Bob','Tim'] 
+        self.output_folder = tempfile.mkdtemp()
+        self.logger = logging.getLogger(__name__)
+
+    def tearDown(self):
+        # Remove the temporary folder after the test
+        shutil.rmtree(self.output_folder)
+    
+    def test_batch_metrics(self):
+        bm = batch_metrics(self.predictions, self.ground_truths, self.masks, self.names, self.output_folder, self.logger)
+        self.assertEqual(len(bm),2*100)
+        for metric in bm:
+            # check whether f1 score agree
+            self.assertAlmostEqual(metric[-1],2*(metric[-6]*metric[-5])/(metric[-6]+metric[-5]))
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/bob/ip/binseg/test/test_models.py b/bob/ip/binseg/test/test_models.py
index bf796565..a5f37d3f 100644
--- a/bob/ip/binseg/test/test_models.py
+++ b/bob/ip/binseg/test/test_models.py
@@ -6,27 +6,42 @@ import unittest
 import numpy as np
 from bob.ip.binseg.modeling.driu import build_driu
 from bob.ip.binseg.modeling.hed import build_hed
+from bob.ip.binseg.modeling.unet import build_unet
+from bob.ip.binseg.modeling.resunet import build_res50unet
 
 class Tester(unittest.TestCase):
     """
     Unit test for model architectures
     """
-    x = torch.randn(1, 3, 544, 544)
-    hw = np.array(x.shape)[[2,3]]
+    def setUp(self):
+        self.x = torch.randn(1, 3, 544, 544)
+        self.hw = np.array(self.x.shape)[[2,3]]
     
     def test_driu(self):
         model = build_driu()
-        out = model(Tester.x)
+        out = model(self.x)
         out_hw = np.array(out.shape)[[2,3]]
-        self.assertEqual(Tester.hw.all(), out_hw.all())
+        self.assertEqual(self.hw.all(), out_hw.all())
 
 
     def test_hed(self):
         model = build_hed()
-        out = model(Tester.x)
+        out = model(self.x)
         # NOTE: HED outputs a list of length 4. We test only for the last concat-fuse layer
         out_hw = np.array(out[4].shape)[[2,3]]
-        self.assertEqual(Tester.hw.all(), out_hw.all())
+        self.assertEqual(self.hw.all(), out_hw.all())
+
+    def test_unet(self):
+        model = build_unet()
+        out = model(self.x)
+        out_hw = np.array(out.shape)[[2,3]]
+        self.assertEqual(self.hw.all(), out_hw.all())
+
+    def test_resunet(self):
+        model = build_res50unet()
+        out = model(self.x)
+        out_hw = np.array(out.shape)[[2,3]]
+        self.assertEqual(self.hw.all(), out_hw.all())
 
 
 if __name__ == '__main__':
diff --git a/precision_recall_comparison.pdf b/precision_recall_comparison.pdf
deleted file mode 100644
index 1568e9c1a33e6421a2fd77af3bb6d720f1af156d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 18677
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
Tbm@QqV=Yd^&8?($p6LGoqvm7{

diff --git a/setup.py b/setup.py
index 8c8bb0bf..eb8429e7 100644
--- a/setup.py
+++ b/setup.py
@@ -59,6 +59,7 @@ setup(
           'M2UNet = bob.ip.binseg.configs.models.m2unet',
           'UNet = bob.ip.binseg.configs.models.unet',
           'ResUNet = bob.ip.binseg.configs.models.resunet',
+          'ShapeResUNet = bob.ip.binseg.configs.models.shaperesunet',
           'DRIUADABOUND = bob.ip.binseg.configs.models.driuadabound',
           'DRIVETRAIN = bob.ip.binseg.configs.datasets.drivetrain',
           'DRIVECROPTRAIN = bob.ip.binseg.configs.datasets.drivecroptrain',
-- 
GitLab