# -*- coding: utf-8 -*- # @Time : 2021/1/4 # @Author : Lart Pang # @GitHub : https://github.com/lartpang/PySODMetrics import numpy as np from scipy.ndimage import convolve from scipy.ndimage import distance_transform_edt as bwdist import cv2 from PIL import Image _EPS = 1e-16 _TYPE = np.float64 def _prepare_data(pred: np.ndarray, gt: np.ndarray) -> tuple: """ A numpy-based function for preparing ``pred`` and ``gt``. - for ``pred``, it looks like ``mapminmax(im2double(...))`` of matlab; - ``gt`` will be binarized by 128. :param pred: prediction :param gt: mask :return: pred, gt """ gt = gt > 128 pred = pred / 255 if pred.max() != pred.min(): pred = (pred - pred.min()) / (pred.max() - pred.min()) return pred, gt def _get_adaptive_threshold(matrix: np.ndarray, max_value: float = 1) -> float: """ Return an adaptive threshold, which is equal to twice the mean of ``matrix``. :param matrix: a data array :param max_value: the upper limit of the threshold :return: min(2 * matrix.mean(), max_value) """ return min(2 * matrix.mean(), max_value) class IoU(object): def __init__(self): self.ious = [] def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred, gt) ious = self.cal_iou(pred=pred, gt=gt) self.ious.append(ious) def cal_iou(self, pred, gt): pred = (pred * 255).astype(np.uint8) bins = np.linspace(0, 256, 257) fg_hist, _ = np.histogram(pred[gt], bins=bins) # ture positive bg_hist, _ = np.histogram(pred[~gt], bins=bins) # false positive fg_w_thrs = np.cumsum(np.flip(fg_hist), axis=0) bg_w_thrs = np.cumsum(np.flip(bg_hist), axis=0) TPs = fg_w_thrs Ps = fg_w_thrs + bg_w_thrs # positives Ps[Ps == 0] = 1 T = max(np.count_nonzero(gt), 1) ious = TPs / (T + bg_w_thrs) return ious def get_results(self) -> dict: iou = np.mean(np.array(self.ious, dtype=_TYPE), axis=0) return dict(iou=dict(curve=iou)) class BIoU(object): def __init__(self, dilation_ratio=0.02): self.bious = [] self.dilation_ratio = dilation_ratio def mask_to_boundary(self, mask): h, w = mask.shape img_diag = np.sqrt(h ** 2 + w ** 2) dilation = int(round(self.dilation_ratio * img_diag)) if dilation < 1: dilation = 1 # Pad image so mask truncated by the image border is also considered as boundary. new_mask = cv2.copyMakeBorder(mask, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0) kernel = np.ones((3, 3), dtype=np.uint8) new_mask_erode = cv2.erode(new_mask, kernel, iterations=dilation) mask_erode = new_mask_erode[1 : h + 1, 1 : w + 1] # G_d intersects G in the paper. return mask - mask_erode def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred, gt) bious = self.cal_biou(pred=pred, gt=gt) self.bious.append(bious) def cal_biou(self, pred, gt): pred = (pred * 255).astype(np.uint8) pred = self.mask_to_boundary(pred) gt = (gt * 255).astype(np.uint8) gt = self.mask_to_boundary(gt) gt = gt > 128 bins = np.linspace(0, 256, 257) fg_hist, _ = np.histogram(pred[gt], bins=bins) # ture positive bg_hist, _ = np.histogram(pred[~gt], bins=bins) # false positive fg_w_thrs = np.cumsum(np.flip(fg_hist), axis=0) bg_w_thrs = np.cumsum(np.flip(bg_hist), axis=0) TPs = fg_w_thrs Ps = fg_w_thrs + bg_w_thrs # positives Ps[Ps == 0] = 1 T = max(np.count_nonzero(gt), 1) ious = TPs / (T + bg_w_thrs) return ious def get_results(self) -> dict: biou = np.mean(np.array(self.bious, dtype=_TYPE), axis=0) return dict(biou=dict(curve=biou)) class TIoU(object): def __init__(self, dilation_ratio=0.001): self.tious = [] self.dilation_ratio = dilation_ratio def mask_to_boundary(self, mask): h, w = mask.shape img_diag = np.sqrt(h ** 2 + w ** 2) dilation = int(round(self.dilation_ratio * img_diag)) if dilation < 1: dilation = 1 # Pad image so mask truncated by the image border is also considered as boundary. new_mask = cv2.copyMakeBorder(mask, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0) kernel = np.ones((3, 3), dtype=np.uint8) new_mask_erode = cv2.erode(new_mask, kernel, iterations=dilation) mask_erode = new_mask_erode[1 : h + 1, 1 : w + 1] # G_d intersects G in the paper. return mask - mask_erode def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred, gt) tious = self.cal_tiou(pred=pred, gt=gt) self.tious.append(tious) def cal_tiou(self, pred, gt): pred = (pred * 255).astype(np.uint8) gt = (gt * 255).astype(np.uint8) gt = self.mask_to_boundary(gt) gt = gt > 128 pred = pred * gt bins = np.linspace(0, 256, 257) fg_hist, _ = np.histogram(pred[gt], bins=bins) # ture positive bg_hist, _ = np.histogram(pred[~gt], bins=bins) # false positive fg_w_thrs = np.cumsum(np.flip(fg_hist), axis=0) bg_w_thrs = np.cumsum(np.flip(bg_hist), axis=0) TPs = fg_w_thrs Ps = fg_w_thrs + bg_w_thrs # positives Ps[Ps == 0] = 1 T = max(np.count_nonzero(gt), 1) ious = TPs / (T + bg_w_thrs) return ious def get_results(self) -> dict: tiou = np.mean(np.array(self.tious, dtype=_TYPE), axis=0) return dict(tiou=dict(curve=tiou)) class Fmeasure(object): def __init__(self, beta: float = 0.3): """ F-measure for SOD. :: @inproceedings{Fmeasure, title={Frequency-tuned salient region detection}, author={Achanta, Radhakrishna and Hemami, Sheila and Estrada, Francisco and S{\"u}sstrunk, Sabine}, booktitle=CVPR, number={CONF}, pages={1597--1604}, year={2009} } :param beta: the weight of the precision """ self.beta = beta self.precisions = [] self.recalls = [] self.adaptive_fms = [] self.changeable_fms = [] def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred, gt) adaptive_fm = self.cal_adaptive_fm(pred=pred, gt=gt) self.adaptive_fms.append(adaptive_fm) precisions, recalls, changeable_fms = self.cal_pr(pred=pred, gt=gt) self.precisions.append(precisions) self.recalls.append(recalls) self.changeable_fms.append(changeable_fms) def cal_adaptive_fm(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the adaptive F-measure. :return: adaptive_fm """ adaptive_threshold = _get_adaptive_threshold(pred, max_value=1) binary_predcition = pred >= adaptive_threshold area_intersection = binary_predcition[gt].sum() if area_intersection == 0: adaptive_fm = 0 else: pre = area_intersection / np.count_nonzero(binary_predcition) rec = area_intersection / np.count_nonzero(gt) adaptive_fm = (1 + self.beta) * pre * rec / (self.beta * pre + rec) return adaptive_fm def cal_pr(self, pred: np.ndarray, gt: np.ndarray) -> tuple: """ Calculate the corresponding precision and recall when the threshold changes from 0 to 255. These precisions and recalls can be used to obtain the mean F-measure, maximum F-measure, precision-recall curve and F-measure-threshold curve. For convenience, ``changeable_fms`` is provided here, which can be used directly to obtain the mean F-measure, maximum F-measure and F-measure-threshold curve. :return: precisions, recalls, changeable_fms """ pred = (pred * 255).astype(np.uint8) bins = np.linspace(0, 256, 257) fg_hist, _ = np.histogram(pred[gt], bins=bins) bg_hist, _ = np.histogram(pred[~gt], bins=bins) fg_w_thrs = np.cumsum(np.flip(fg_hist), axis=0) bg_w_thrs = np.cumsum(np.flip(bg_hist), axis=0) TPs = fg_w_thrs Ps = fg_w_thrs + bg_w_thrs Ps[Ps == 0] = 1 T = max(np.count_nonzero(gt), 1) precisions = TPs / Ps recalls = TPs / T numerator = (1 + self.beta) * precisions * recalls denominator = np.where(numerator == 0, 1, self.beta * precisions + recalls) changeable_fms = numerator / denominator return precisions, recalls, changeable_fms def get_results(self) -> dict: """ Return the results about F-measure. :return: dict(fm=dict(adp=adaptive_fm, curve=changeable_fm), pr=dict(p=precision, r=recall)) """ adaptive_fm = np.mean(np.array(self.adaptive_fms, _TYPE)) changeable_fm = np.mean(np.array(self.changeable_fms, dtype=_TYPE), axis=0) precision = np.mean(np.array(self.precisions, dtype=_TYPE), axis=0) # N, 256 recall = np.mean(np.array(self.recalls, dtype=_TYPE), axis=0) # N, 256 return dict(fm=dict(adp=adaptive_fm, curve=changeable_fm), pr=dict(p=precision, r=recall)) class Mae(object): def __init__(self): """ MAE(mean absolute error) for SOD. :: @inproceedings{MAE, title={Saliency filters: Contrast based filtering for salient region detection}, author={Perazzi, Federico and Kr{\"a}henb{\"u}hl, Philipp and Pritch, Yael and Hornung, Alexander}, booktitle=CVPR, pages={733--740}, year={2012} } """ self.maes = [] def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred, gt) mae = self.cal_mae(pred, gt) self.maes.append(mae) def cal_mae(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray: """ Calculate the mean absolute error. :return: mae """ mae = np.mean(np.abs(pred - gt)) return mae def get_results(self) -> dict: """ Return the results about MAE. :return: dict(mae=mae) """ mae = np.mean(np.array(self.maes, _TYPE)) return dict(mae=mae) class Mse(object): def __init__(self): """ MAE(mean absolute error) for SOD. :: @inproceedings{MAE, title={Saliency filters: Contrast based filtering for salient region detection}, author={Perazzi, Federico and Kr{\"a}henb{\"u}hl, Philipp and Pritch, Yael and Hornung, Alexander}, booktitle=CVPR, pages={733--740}, year={2012} } """ self.mses = [] def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred, gt) mse = self.cal_mse(pred, gt) self.mses.append(mse) def cal_mse(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray: """ Calculate the mean absolute error. :return: mse """ mse = np.mean((pred - gt) ** 2) return mse def get_results(self) -> dict: """ Return the results about MSE. :return: dict(mse=mse) """ mse = np.mean(np.array(self.mses, _TYPE)) return dict(mse=mse) class Smeasure(object): def __init__(self, alpha: float = 0.5): """ S-measure(Structure-measure) of SOD. :: @inproceedings{Smeasure, title={Structure-measure: A new way to eval foreground maps}, author={Fan, Deng-Ping and Cheng, Ming-Ming and Liu, Yun and Li, Tao and Borji, Ali}, booktitle=ICCV, pages={4548--4557}, year={2017} } :param alpha: the weight for balancing the object score and the region score """ self.sms = [] self.alpha = alpha def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred=pred, gt=gt) sm = self.cal_sm(pred, gt) self.sms.append(sm) def cal_sm(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the S-measure. :return: s-measure """ y = np.mean(gt) if y == 0: sm = 1 - np.mean(pred) elif y == 1: sm = np.mean(pred) else: sm = self.alpha * self.object(pred, gt) + (1 - self.alpha) * self.region(pred, gt) sm = max(0, sm) return sm def object(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the object score. """ fg = pred * gt bg = (1 - pred) * (1 - gt) u = np.mean(gt) object_score = u * self.s_object(fg, gt) + (1 - u) * self.s_object(bg, 1 - gt) return object_score def s_object(self, pred: np.ndarray, gt: np.ndarray) -> float: x = np.mean(pred[gt == 1]) sigma_x = np.std(pred[gt == 1]) score = 2 * x / (np.power(x, 2) + 1 + sigma_x + _EPS) return score def region(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the region score. """ x, y = self.centroid(gt) part_info = self.divide_with_xy(pred, gt, x, y) w1, w2, w3, w4 = part_info["weight"] pred1, pred2, pred3, pred4 = part_info["pred"] gt1, gt2, gt3, gt4 = part_info["gt"] score1 = self.ssim(pred1, gt1) score2 = self.ssim(pred2, gt2) score3 = self.ssim(pred3, gt3) score4 = self.ssim(pred4, gt4) return w1 * score1 + w2 * score2 + w3 * score3 + w4 * score4 def centroid(self, matrix: np.ndarray) -> tuple: """ To ensure consistency with the matlab code, one is added to the centroid coordinate, so there is no need to use the redundant addition operation when dividing the region later, because the sequence generated by ``1:X`` in matlab will contain ``X``. :param matrix: a data array :return: the centroid coordinate """ h, w = matrix.shape if matrix.sum() == 0: x = np.round(w / 2) y = np.round(h / 2) else: area_object = np.sum(matrix) row_ids = np.arange(h) col_ids = np.arange(w) x = np.round(np.sum(np.sum(matrix, axis=0) * col_ids) / area_object) y = np.round(np.sum(np.sum(matrix, axis=1) * row_ids) / area_object) return int(x) + 1, int(y) + 1 def divide_with_xy(self, pred: np.ndarray, gt: np.ndarray, x: int, y: int) -> dict: """ Use (x,y) to divide the ``pred`` and the ``gt`` into four submatrices, respectively. """ h, w = gt.shape area = h * w gt_LT = gt[0:y, 0:x] gt_RT = gt[0:y, x:w] gt_LB = gt[y:h, 0:x] gt_RB = gt[y:h, x:w] pred_LT = pred[0:y, 0:x] pred_RT = pred[0:y, x:w] pred_LB = pred[y:h, 0:x] pred_RB = pred[y:h, x:w] w1 = x * y / area w2 = y * (w - x) / area w3 = (h - y) * x / area w4 = 1 - w1 - w2 - w3 return dict( gt=(gt_LT, gt_RT, gt_LB, gt_RB), pred=(pred_LT, pred_RT, pred_LB, pred_RB), weight=(w1, w2, w3, w4), ) def ssim(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the ssim score. """ h, w = pred.shape N = h * w x = np.mean(pred) y = np.mean(gt) sigma_x = np.sum((pred - x) ** 2) / (N - 1) sigma_y = np.sum((gt - y) ** 2) / (N - 1) sigma_xy = np.sum((pred - x) * (gt - y)) / (N - 1) alpha = 4 * x * y * sigma_xy beta = (x ** 2 + y ** 2) * (sigma_x + sigma_y) if alpha != 0: score = alpha / (beta + _EPS) elif alpha == 0 and beta == 0: score = 1 else: score = 0 return score def get_results(self) -> dict: """ Return the results about S-measure. :return: dict(sm=sm) """ sm = np.mean(np.array(self.sms, dtype=_TYPE)) return dict(sm=sm) class Emeasure(object): def __init__(self): """ E-measure(Enhanced-alignment Measure) for SOD. More details about the implementation can be found in https://www.yuque.com/lart/blog/lwgt38 :: @inproceedings{Emeasure, title="Enhanced-alignment Measure for Binary Foreground Map Evaluation", author="Deng-Ping {Fan} and Cheng {Gong} and Yang {Cao} and Bo {Ren} and Ming-Ming {Cheng} and Ali {Borji}", booktitle=IJCAI, pages="698--704", year={2018} } """ self.adaptive_ems = [] self.changeable_ems = [] def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred=pred, gt=gt) self.gt_fg_numel = np.count_nonzero(gt) self.gt_size = gt.shape[0] * gt.shape[1] changeable_ems = self.cal_changeable_em(pred, gt) self.changeable_ems.append(changeable_ems) adaptive_em = self.cal_adaptive_em(pred, gt) self.adaptive_ems.append(adaptive_em) def cal_adaptive_em(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the adaptive E-measure. :return: adaptive_em """ adaptive_threshold = _get_adaptive_threshold(pred, max_value=1) adaptive_em = self.cal_em_with_threshold(pred, gt, threshold=adaptive_threshold) return adaptive_em def cal_changeable_em(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray: """ Calculate the changeable E-measure, which can be used to obtain the mean E-measure, the maximum E-measure and the E-measure-threshold curve. :return: changeable_ems """ changeable_ems = self.cal_em_with_cumsumhistogram(pred, gt) return changeable_ems def cal_em_with_threshold(self, pred: np.ndarray, gt: np.ndarray, threshold: float) -> float: """ Calculate the E-measure corresponding to the specific threshold. Variable naming rules within the function: ``[pred attribute(foreground fg, background bg)]_[gt attribute(foreground fg, background bg)]_[meaning]`` If only ``pred`` or ``gt`` is considered, another corresponding attribute location is replaced with '``_``'. """ binarized_pred = pred >= threshold fg_fg_numel = np.count_nonzero(binarized_pred & gt) fg_bg_numel = np.count_nonzero(binarized_pred & ~gt) fg___numel = fg_fg_numel + fg_bg_numel bg___numel = self.gt_size - fg___numel if self.gt_fg_numel == 0: enhanced_matrix_sum = bg___numel elif self.gt_fg_numel == self.gt_size: enhanced_matrix_sum = fg___numel else: parts_numel, combinations = self.generate_parts_numel_combinations( fg_fg_numel=fg_fg_numel, fg_bg_numel=fg_bg_numel, pred_fg_numel=fg___numel, pred_bg_numel=bg___numel, ) results_parts = [] for i, (part_numel, combination) in enumerate(zip(parts_numel, combinations)): align_matrix_value = ( 2 * (combination[0] * combination[1]) / (combination[0] ** 2 + combination[1] ** 2 + _EPS) ) enhanced_matrix_value = (align_matrix_value + 1) ** 2 / 4 results_parts.append(enhanced_matrix_value * part_numel) enhanced_matrix_sum = sum(results_parts) em = enhanced_matrix_sum / (self.gt_size - 1 + _EPS) return em def cal_em_with_cumsumhistogram(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray: """ Calculate the E-measure corresponding to the threshold that varies from 0 to 255.. Variable naming rules within the function: ``[pred attribute(foreground fg, background bg)]_[gt attribute(foreground fg, background bg)]_[meaning]`` If only ``pred`` or ``gt`` is considered, another corresponding attribute location is replaced with '``_``'. """ pred = (pred * 255).astype(np.uint8) bins = np.linspace(0, 256, 257) fg_fg_hist, _ = np.histogram(pred[gt], bins=bins) fg_bg_hist, _ = np.histogram(pred[~gt], bins=bins) fg_fg_numel_w_thrs = np.cumsum(np.flip(fg_fg_hist), axis=0) fg_bg_numel_w_thrs = np.cumsum(np.flip(fg_bg_hist), axis=0) fg___numel_w_thrs = fg_fg_numel_w_thrs + fg_bg_numel_w_thrs bg___numel_w_thrs = self.gt_size - fg___numel_w_thrs if self.gt_fg_numel == 0: enhanced_matrix_sum = bg___numel_w_thrs elif self.gt_fg_numel == self.gt_size: enhanced_matrix_sum = fg___numel_w_thrs else: parts_numel_w_thrs, combinations = self.generate_parts_numel_combinations( fg_fg_numel=fg_fg_numel_w_thrs, fg_bg_numel=fg_bg_numel_w_thrs, pred_fg_numel=fg___numel_w_thrs, pred_bg_numel=bg___numel_w_thrs, ) results_parts = np.empty(shape=(4, 256), dtype=np.float64) for i, (part_numel, combination) in enumerate(zip(parts_numel_w_thrs, combinations)): align_matrix_value = ( 2 * (combination[0] * combination[1]) / (combination[0] ** 2 + combination[1] ** 2 + _EPS) ) enhanced_matrix_value = (align_matrix_value + 1) ** 2 / 4 results_parts[i] = enhanced_matrix_value * part_numel enhanced_matrix_sum = results_parts.sum(axis=0) em = enhanced_matrix_sum / (self.gt_size - 1 + _EPS) return em def generate_parts_numel_combinations( self, fg_fg_numel, fg_bg_numel, pred_fg_numel, pred_bg_numel ): bg_fg_numel = self.gt_fg_numel - fg_fg_numel bg_bg_numel = pred_bg_numel - bg_fg_numel parts_numel = [fg_fg_numel, fg_bg_numel, bg_fg_numel, bg_bg_numel] mean_pred_value = pred_fg_numel / self.gt_size mean_gt_value = self.gt_fg_numel / self.gt_size demeaned_pred_fg_value = 1 - mean_pred_value demeaned_pred_bg_value = 0 - mean_pred_value demeaned_gt_fg_value = 1 - mean_gt_value demeaned_gt_bg_value = 0 - mean_gt_value combinations = [ (demeaned_pred_fg_value, demeaned_gt_fg_value), (demeaned_pred_fg_value, demeaned_gt_bg_value), (demeaned_pred_bg_value, demeaned_gt_fg_value), (demeaned_pred_bg_value, demeaned_gt_bg_value), ] return parts_numel, combinations def get_results(self) -> dict: """ Return the results about E-measure. :return: dict(em=dict(adp=adaptive_em, curve=changeable_em)) """ adaptive_em = np.mean(np.array(self.adaptive_ems, dtype=_TYPE)) changeable_em = np.mean(np.array(self.changeable_ems, dtype=_TYPE), axis=0) return dict(em=dict(adp=adaptive_em, curve=changeable_em)) class WeightedFmeasure(object): def __init__(self, beta: float = 1): """ Weighted F-measure for SOD. :: @inproceedings{wFmeasure, title={How to eval foreground maps?}, author={Margolin, Ran and Zelnik-Manor, Lihi and Tal, Ayellet}, booktitle=CVPR, pages={248--255}, year={2014} } :param beta: the weight of the precision """ self.beta = beta self.weighted_fms = [] def step(self, pred: np.ndarray, gt: np.ndarray): pred, gt = _prepare_data(pred=pred, gt=gt) if np.all(~gt): wfm = 0 else: wfm = self.cal_wfm(pred, gt) self.weighted_fms.append(wfm) def cal_wfm(self, pred: np.ndarray, gt: np.ndarray) -> float: """ Calculate the weighted F-measure. """ Dst, Idxt = bwdist(gt == 0, return_indices=True) E = np.abs(pred - gt) Et = np.copy(E) Et[gt == 0] = Et[Idxt[0][gt == 0], Idxt[1][gt == 0]] K = self.matlab_style_gauss2D((7, 7), sigma=5) EA = convolve(Et, weights=K, mode="constant", cval=0) MIN_E_EA = np.where(gt & (EA < E), EA, E) B = np.where(gt == 0, 2 - np.exp(np.log(0.5) / 5 * Dst), np.ones_like(gt)) Ew = MIN_E_EA * B TPw = np.sum(gt) - np.sum(Ew[gt == 1]) FPw = np.sum(Ew[gt == 0]) R = 1 - np.mean(Ew[gt == 1]) P = TPw / (TPw + FPw + _EPS) Q = (1 + self.beta) * R * P / (R + self.beta * P + _EPS) return Q def matlab_style_gauss2D(self, shape: tuple = (7, 7), sigma: int = 5) -> np.ndarray: """ 2D gaussian mask - should give the same result as MATLAB's fspecial('saliency',[shape],[sigma]) """ m, n = [(ss - 1) / 2 for ss in shape] y, x = np.ogrid[-m : m + 1, -n : n + 1] h = np.exp(-(x * x + y * y) / (2 * sigma * sigma)) h[h < np.finfo(h.dtype).eps * h.max()] = 0 sumh = h.sum() if sumh != 0: h /= sumh return h def get_results(self) -> dict: """ Return the results about weighted F-measure. :return: dict(wfm=weighted_fm) """ weighted_fm = np.mean(np.array(self.weighted_fms, dtype=_TYPE)) return dict(wfm=weighted_fm) class BoundaryAccuracy(object): def __init__(self): """ MAE(mean absolute error) for SOD. :: @inproceedings{MAE, title={Saliency filters: Contrast based filtering for salient region detection}, author={Perazzi, Federico and Kr{\"a}henb{\"u}hl, Philipp and Pritch, Yael and Hornung, Alexander}, booktitle=CVPR, pages={733--740}, year={2012} } """ self.bas = [] self.all_h = 0 self.all_w = 0 self.all_max = 0 def step(self, pred: np.ndarray, gt: np.ndarray): # pred, gt = _prepare_data(pred, gt) refined = gt.copy() rmin = cmin = 0 rmax, cmax = gt.shape self.all_h += rmax self.all_w += cmax self.all_max += max(rmax, cmax) refined_h, refined_w = refined.shape if refined_h != cmax: refined = np.array(Image.fromarray(pred).resize((cmax, rmax), Image.BILINEAR)) if not(gt.sum() < 32*32): if not((cmax==cmin) or (rmax==rmin)): class_refined_prob = np.array(Image.fromarray(pred).resize((cmax-cmin, rmax-rmin), Image.BILINEAR)) refined[rmin:rmax, cmin:cmax] = class_refined_prob pred = pred > 128 gt = gt > 128 ba = self.cal_ba(pred, gt) self.bas.append(ba) def get_disk_kernel(self, radius): return cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (radius*2+1, radius*2+1)) def cal_ba(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray: """ Calculate the mean absolute error. :return: ba """ gt = gt.astype(np.uint8) pred = pred.astype(np.uint8) h, w = gt.shape min_radius = 1 max_radius = (w+h)/300 num_steps = 5 pred_acc = [None] * num_steps for i in range(num_steps): curr_radius = min_radius + int((max_radius-min_radius)/num_steps*i) kernel = self.get_disk_kernel(curr_radius) boundary_region = cv2.morphologyEx(gt, cv2.MORPH_GRADIENT, kernel) > 0 gt_in_bound = gt[boundary_region] pred_in_bound = pred[boundary_region] num_edge_pixels = (boundary_region).sum() num_pred_gd_pix = ((gt_in_bound) * (pred_in_bound) + (1-gt_in_bound) * (1-pred_in_bound)).sum() pred_acc[i] = num_pred_gd_pix / num_edge_pixels ba = sum(pred_acc)/num_steps return ba def get_results(self) -> dict: """ Return the results about MAE. :return: dict(mae=mae) """ mba = np.mean(np.array(self.bas, _TYPE)) return dict(mba=mba)