from __future__ import absolute_import, division import math import cv2 import numpy as np from skimage.morphology import disk __all__ = ['batched_jaccard', 'batched_f_measure'] def batched_jaccard(y_true, y_pred, average_over_objects=True, nb_objects=None): """ Batch jaccard similarity for multiple instance segmentation. Jaccard similarity over two subsets of binary elements $A$ and $B$: $$ \mathcal{J} = \\frac{A \\cap B}{A \\cup B} $$ # Arguments y_true: Numpy Array. Array of shape (B x H x W) and type integer giving the ground truth of the object instance segmentation. y_pred: Numpy Array. Array of shape (B x H x W) and type integer giving the prediction of the object segmentation. average_over_objects: Boolean. Weather or not to average the jaccard over all the objects in the sequence. Default True. nb_objects: Integer. Number of objects in the ground truth mask. If `None` the value will be infered from `y_true`. Setting this value will speed up the computation. # Returns ndarray: Returns an array of shape (B) with the average jaccard for all instances at each frame if `average_over_objects=True`. If `average_over_objects=False` returns an array of shape (B x nObj) with nObj being the number of objects on `y_true`. """ y_true = np.asarray(y_true, dtype=np.int) y_pred = np.asarray(y_pred, dtype=np.int) if y_true.ndim != 3: raise ValueError('y_true array must have 3 dimensions.') if y_pred.ndim != 3: raise ValueError('y_pred array must have 3 dimensions.') if y_true.shape != y_pred.shape: raise ValueError('y_true and y_pred must have the same shape. {} != {}'.format(y_true.shape, y_pred.shape)) if nb_objects is None: objects_ids = np.unique(y_true[(y_true < 255) & (y_true > 0)]) nb_objects = len(objects_ids) else: objects_ids = [i + 1 for i in range(nb_objects)] objects_ids = np.asarray(objects_ids, dtype=np.int) if nb_objects == 0: raise ValueError('Number of objects in y_true should be higher than 0.') nb_frames = len(y_true) jaccard = np.empty((nb_frames, nb_objects), dtype=np.float) for i, obj_id in enumerate(objects_ids): mask_true, mask_pred = y_true == obj_id, y_pred == obj_id union = (mask_true | mask_pred).sum(axis=(1, 2)) intersection = (mask_true & mask_pred).sum(axis=(1, 2)) for j in range(nb_frames): if np.isclose(union[j], 0): jaccard[j, i] = 1. else: jaccard[j, i] = intersection[j] / union[j] if average_over_objects: jaccard = jaccard.mean(axis=1) return jaccard def _seg2bmap(seg, width=None, height=None): """ From a segmentation, compute a binary boundary map with 1 pixel wide boundaries. The boundary pixels are offset by 1/2 pixel towards the origin from the actual segment boundary. # Arguments seg: Segments labeled from 1..k. width: Width of desired bmap <= seg.shape[1] height: Height of desired bmap <= seg.shape[0] # Returns bmap (ndarray): Binary boundary map. David Martin January 2003 """ seg = seg.astype(np.bool) seg[seg > 0] = 1 assert np.atleast_3d(seg).shape[2] == 1 width = seg.shape[1] if width is None else width height = seg.shape[0] if height is None else height h, w = seg.shape[:2] ar1 = float(width) / float(height) ar2 = float(w) / float(h) assert not (width > w | height > h | abs(ar1 - ar2) > 0.01), "Can't convert %dx%d seg to %dx%d bmap." % (w, h, width, height) e = np.zeros_like(seg) s = np.zeros_like(seg) se = np.zeros_like(seg) e[:, :-1] = seg[:, 1:] s[:-1, :] = seg[1:, :] se[:-1, :-1] = seg[1:, 1:] b = seg ^ e | seg ^ s | seg ^ se b[-1, :] = seg[-1, :] ^ e[-1, :] b[:, -1] = seg[:, -1] ^ s[:, -1] b[-1, -1] = 0 if w == width and h == height: bmap = b else: bmap = np.zeros((height, width)) for x in range(w): for y in range(h): if b[y, x]: j = 1 + math.floor((y - 1) + height / h) i = 1 + math.floor((x - 1) + width / h) bmap[j, i] = 1 return bmap def f_measure(true_mask, pred_mask, bound_th=0.008): """F-measure for two 2D masks. # Arguments true_mask: Numpy Array, Binary array of shape (H x W) representing the ground truth mask. pred_mask: Numpy Array. Binary array of shape (H x W) representing the predicted mask. bound_th: Float. Optional parameter to compute the F-measure. Default is 0.008. # Returns float: F-measure. """ true_mask = np.asarray(true_mask, dtype=np.bool) pred_mask = np.asarray(pred_mask, dtype=np.bool) assert true_mask.shape == pred_mask.shape bound_pix = bound_th if bound_th >= 1 else (np.ceil( bound_th * np.linalg.norm(true_mask.shape))) fg_boundary = _seg2bmap(pred_mask) gt_boundary = _seg2bmap(true_mask) fg_dil = cv2.dilate( fg_boundary.astype(np.uint8), disk(bound_pix).astype(np.uint8)) gt_dil = cv2.dilate( gt_boundary.astype(np.uint8), disk(bound_pix).astype(np.uint8)) # Get the intersection gt_match = gt_boundary * fg_dil fg_match = fg_boundary * gt_dil # Area of the intersection n_fg = np.sum(fg_boundary) n_gt = np.sum(gt_boundary) # Compute precision and recall if n_fg == 0 and n_gt > 0: precision = 1 recall = 0 elif n_fg > 0 and n_gt == 0: precision = 0 recall = 1 elif n_fg == 0 and n_gt == 0: precision = 1 recall = 1 else: precision = np.sum(fg_match) / float(n_fg) recall = np.sum(gt_match) / float(n_gt) # Compute F measure if precision + recall == 0: F = 0 else: F = 2 * precision * recall / (precision + recall) return F def batched_f_measure(y_true, y_pred, average_over_objects=True, nb_objects=None, bound_th=0.008): """ Batch F-measure for multiple instance segmentation. # Arguments y_true: Numpy Array. Array of shape (B x H x W) and type integer giving the ground truth of the object instance segmentation. y_pred: Numpy Array. Array of shape (B x H x W) and type integer giving the prediction of the object segmentation. average_over_objects: Boolean. Weather or not to average the F-measure over all the objects in the sequence. Default True. nb_objects: Integer. Number of objects in the ground truth mask. If `None` the value will be infered from `y_true`. Setting this value will speed up the computation. # Returns ndarray: Returns an array of shape (B) with the average F-measure for all instances at each frame if `average_over_objects=True`. If `average_over_objects=False` returns an array of shape (B x nObj) with nObj being the number of objects on `y_true`. """ y_true = np.asarray(y_true, dtype=np.int) y_pred = np.asarray(y_pred, dtype=np.int) if y_true.ndim != 3: raise ValueError('y_true array must have 3 dimensions.') if y_pred.ndim != 3: raise ValueError('y_pred array must have 3 dimensions.') if y_true.shape != y_pred.shape: raise ValueError('y_true and y_pred must have the same shape. {} != {}'.format(y_true.shape, y_pred.shape)) if nb_objects is None: objects_ids = np.unique(y_true[(y_true < 255) & (y_true > 0)]) nb_objects = len(objects_ids) else: objects_ids = [i + 1 for i in range(nb_objects)] objects_ids = np.asarray(objects_ids, dtype=np.int) if nb_objects == 0: raise ValueError('Number of objects in y_true should be higher than 0.') nb_frames = len(y_true) f_measure_result = np.empty((nb_frames, nb_objects), dtype=np.float) for i, obj_id in enumerate(objects_ids): for frame_id in range(nb_frames): gt_mask = y_true[frame_id, :, :] == obj_id pred_mask = y_pred[frame_id, :, :] == obj_id f_measure_result[frame_id, i] = f_measure( gt_mask, pred_mask, bound_th=bound_th) if average_over_objects: f_measure_result = f_measure_result.mean(axis=1) return f_measure_result