my-cool-model / utils /eval_functions.py
crapthings's picture
Upload folder using huggingface_hub
f7f604d
raw
history blame
28.9 kB
# -*- 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)