metrics.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. import os
  2. import time
  3. import concurrent.futures
  4. from tqdm import tqdm
  5. import cv2
  6. from PIL import Image
  7. import numpy as np
  8. from scipy.ndimage import convolve, distance_transform_edt as bwdist
  9. from skimage.morphology import skeletonize
  10. from skimage.morphology import disk
  11. from skimage.measure import label
  12. _EPS = np.spacing(1)
  13. _TYPE = np.float64
  14. def load_single_image_pair(args):
  15. gt_path, pred_path, idx = args
  16. try:
  17. pred = pred_path[:-4] + '.png'
  18. valid_extensions = ['.png', '.jpg', '.PNG', '.JPG', '.JPEG']
  19. file_exists = False
  20. for ext in valid_extensions:
  21. if os.path.exists(pred[:-4] + ext):
  22. pred = pred[:-4] + ext
  23. file_exists = True
  24. break
  25. if not file_exists:
  26. print(f'Not exists: {pred}')
  27. return None
  28. gt_ary = cv2.imread(gt_path, cv2.IMREAD_GRAYSCALE)
  29. pred_ary = cv2.imread(pred, cv2.IMREAD_GRAYSCALE)
  30. if gt_ary is None or pred_ary is None:
  31. return None
  32. pred_ary = cv2.resize(pred_ary, (gt_ary.shape[1], gt_ary.shape[0]))
  33. return {
  34. 'idx': idx,
  35. 'gt': gt_ary,
  36. 'pred': pred_ary,
  37. 'gt_path': gt_path,
  38. 'pred_path': pred
  39. }
  40. except Exception as e:
  41. print(f"Error loading {gt_path}: {e}")
  42. return None
  43. def load_images_parallel(gt_paths, pred_paths, num_workers, verbose):
  44. args_list = [(gt_path, pred_path, idx)
  45. for idx, (gt_path, pred_path) in enumerate(zip(gt_paths, pred_paths))]
  46. with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
  47. if verbose:
  48. results = list(tqdm(executor.map(load_single_image_pair, args_list),
  49. total=len(args_list), desc="Loading images"))
  50. else:
  51. results = list(executor.map(load_single_image_pair, args_list))
  52. # 过滤掉加载失败的图像
  53. valid_results = [r for r in results if r is not None]
  54. return valid_results
  55. def process_metrics_single(data, measures, metrics):
  56. gt_ary = data['gt']
  57. pred_ary = data['pred']
  58. gt_path = data['gt_path']
  59. if 'E' in metrics:
  60. measures['E'].step(pred=pred_ary, gt=gt_ary)
  61. if 'S' in metrics:
  62. measures['S'].step(pred=pred_ary, gt=gt_ary)
  63. if 'F' in metrics:
  64. measures['F'].step(pred=pred_ary, gt=gt_ary)
  65. if 'MAE' in metrics:
  66. measures['MAE'].step(pred=pred_ary, gt=gt_ary)
  67. if 'MSE' in metrics:
  68. measures['MSE'].step(pred=pred_ary, gt=gt_ary)
  69. if 'WF' in metrics:
  70. measures['WF'].step(pred=pred_ary, gt=gt_ary)
  71. if 'HCE' in metrics:
  72. # HCE需要特殊处理
  73. ske_path = gt_path.replace('/gt/', '/ske/')
  74. if os.path.exists(ske_path):
  75. ske_ary = cv2.imread(ske_path, cv2.IMREAD_GRAYSCALE)
  76. ske_ary = ske_ary > 128
  77. else:
  78. ske_ary = skeletonize(gt_ary > 128)
  79. ske_save_dir = os.path.join(*ske_path.split(os.sep)[:-1])
  80. if ske_path[0] == os.sep:
  81. ske_save_dir = os.sep + ske_save_dir
  82. os.makedirs(ske_save_dir, exist_ok=True)
  83. cv2.imwrite(ske_path, ske_ary.astype(np.uint8) * 255)
  84. measures['HCE'].step(pred=pred_ary, gt=gt_ary, gt_ske=ske_ary)
  85. if 'MBA' in metrics:
  86. measures['MBA'].step(pred=pred_ary, gt=gt_ary)
  87. if 'BIoU' in metrics:
  88. measures['BIoU'].step(pred=pred_ary, gt=gt_ary)
  89. return measures
  90. def process_with_measures(args):
  91. """为并行处理创建独立的 measures 对象"""
  92. data, metrics = args
  93. # 创建临时的 measures 对象
  94. temp_measures = {}
  95. if 'E' in metrics:
  96. temp_measures['E'] = EMeasure()
  97. if 'S' in metrics:
  98. temp_measures['S'] = SMeasure()
  99. if 'F' in metrics:
  100. temp_measures['F'] = FMeasure()
  101. if 'MAE' in metrics:
  102. temp_measures['MAE'] = MAEMeasure()
  103. if 'MSE' in metrics:
  104. temp_measures['MSE'] = MSEMeasure()
  105. if 'WF' in metrics:
  106. temp_measures['WF'] = WeightedFMeasure()
  107. if 'HCE' in metrics:
  108. temp_measures['HCE'] = HCEMeasure()
  109. if 'MBA' in metrics:
  110. temp_measures['MBA'] = MBAMeasure()
  111. if 'BIoU' in metrics:
  112. temp_measures['BIoU'] = BIoUMeasure()
  113. # 处理单个图像
  114. process_metrics_single(data, temp_measures, metrics)
  115. return temp_measures
  116. def process_metrics_batch(image_data, measures, metrics, verbose, num_workers=10):
  117. if num_workers:
  118. # 并行处理
  119. num_workers = min(8, len(image_data))
  120. args_list = [(data, metrics) for data in image_data]
  121. with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
  122. if verbose:
  123. results = list(tqdm(executor.map(process_with_measures, args_list),
  124. total=len(args_list), desc="Computing metrics"))
  125. else:
  126. results = list(executor.map(process_with_measures, args_list))
  127. # 合并结果到主 measures
  128. for temp_measures in results:
  129. if temp_measures is not None:
  130. for metric_name in metrics:
  131. if metric_name in measures and metric_name in temp_measures:
  132. # 直接合并各个指标的列表
  133. if hasattr(measures[metric_name], 'sms'):
  134. measures[metric_name].sms.extend(temp_measures[metric_name].sms)
  135. if hasattr(measures[metric_name], 'maes'):
  136. measures[metric_name].maes.extend(temp_measures[metric_name].maes)
  137. if hasattr(measures[metric_name], 'mses'):
  138. measures[metric_name].mses.extend(temp_measures[metric_name].mses)
  139. if hasattr(measures[metric_name], 'adaptive_fms'):
  140. measures[metric_name].adaptive_fms.extend(temp_measures[metric_name].adaptive_fms)
  141. measures[metric_name].precisions.extend(temp_measures[metric_name].precisions)
  142. measures[metric_name].recalls.extend(temp_measures[metric_name].recalls)
  143. measures[metric_name].changeable_fms.extend(temp_measures[metric_name].changeable_fms)
  144. if hasattr(measures[metric_name], 'adaptive_ems'):
  145. measures[metric_name].adaptive_ems.extend(temp_measures[metric_name].adaptive_ems)
  146. measures[metric_name].changeable_ems.extend(temp_measures[metric_name].changeable_ems)
  147. if hasattr(measures[metric_name], 'weighted_fms'):
  148. measures[metric_name].weighted_fms.extend(temp_measures[metric_name].weighted_fms)
  149. if hasattr(measures[metric_name], 'hces'):
  150. measures[metric_name].hces.extend(temp_measures[metric_name].hces)
  151. if hasattr(measures[metric_name], 'bas'):
  152. measures[metric_name].bas.extend(temp_measures[metric_name].bas)
  153. # MBA 需要合并累积统计
  154. measures[metric_name].all_h += temp_measures[metric_name].all_h
  155. measures[metric_name].all_w += temp_measures[metric_name].all_w
  156. measures[metric_name].all_max += temp_measures[metric_name].all_max
  157. if hasattr(measures[metric_name], 'bious'):
  158. measures[metric_name].bious.extend(temp_measures[metric_name].bious)
  159. else:
  160. # 串行处理
  161. iterator = tqdm(image_data, desc="Computing metrics") if verbose else image_data
  162. for data in iterator:
  163. measures = process_metrics_single(data, measures, metrics)
  164. return measures
  165. def collect_results(measures, metrics):
  166. em = measures['E'].get_results()['em'] if 'E' in metrics else {'curve': np.array([np.float64(-1)]), 'adp': np.float64(-1)}
  167. sm = measures['S'].get_results()['sm'] if 'S' in metrics else np.float64(-1)
  168. fm = measures['F'].get_results()['fm'] if 'F' in metrics else {'curve': np.array([np.float64(-1)]), 'adp': np.float64(-1)}
  169. mae = measures['MAE'].get_results()['mae'] if 'MAE' in metrics else np.float64(-1)
  170. mse = measures['MSE'].get_results()['mse'] if 'MSE' in metrics else np.float64(-1)
  171. wfm = measures['WF'].get_results()['wfm'] if 'WF' in metrics else np.float64(-1)
  172. hce = measures['HCE'].get_results()['hce'] if 'HCE' in metrics else np.float64(-1)
  173. mba = measures['MBA'].get_results()['mba'] if 'MBA' in metrics else np.float64(-1)
  174. biou = measures['BIoU'].get_results()['biou'] if 'BIoU' in metrics else {'curve': np.array([np.float64(-1)])}
  175. return (em, sm, fm, mae, mse, wfm, hce, mba, biou)
  176. def evaluator(gt_paths, pred_paths, metrics=['S', 'MAE', 'E', 'F', 'WF', 'MBA', 'BIoU', 'MSE', 'HCE'], verbose=False, num_workers=8):
  177. start_time = time.time()
  178. measures = {}
  179. if 'E' in metrics:
  180. measures['E'] = EMeasure()
  181. if 'S' in metrics:
  182. measures['S'] = SMeasure()
  183. if 'F' in metrics:
  184. measures['F'] = FMeasure()
  185. if 'MAE' in metrics:
  186. measures['MAE'] = MAEMeasure()
  187. if 'MSE' in metrics:
  188. measures['MSE'] = MSEMeasure()
  189. if 'WF' in metrics:
  190. measures['WF'] = WeightedFMeasure()
  191. if 'HCE' in metrics:
  192. measures['HCE'] = HCEMeasure()
  193. if 'MBA' in metrics:
  194. measures['MBA'] = MBAMeasure()
  195. if 'BIoU' in metrics:
  196. measures['BIoU'] = BIoUMeasure()
  197. if isinstance(gt_paths, list) and isinstance(pred_paths, list):
  198. assert len(gt_paths) == len(pred_paths)
  199. if num_workers is None:
  200. num_workers = 1
  201. use_parallel = len(gt_paths) > 50 and num_workers > 1
  202. if use_parallel:
  203. if verbose:
  204. print(f"Loading {len(gt_paths)} images in parallel with {num_workers} workers...")
  205. image_data = load_images_parallel(gt_paths, pred_paths, num_workers, verbose)
  206. if verbose:
  207. print(f"Successfully loaded {len(image_data)}/{len(gt_paths)} images")
  208. measures = process_metrics_batch(image_data, measures, metrics, verbose, num_workers)
  209. else:
  210. iterator = tqdm(range(len(gt_paths)), total=len(gt_paths), desc="Loading images and computing metrics") if verbose else range(len(gt_paths))
  211. for idx_sample in iterator:
  212. gt = gt_paths[idx_sample]
  213. pred = pred_paths[idx_sample]
  214. pred = pred[:-4] + '.png'
  215. valid_extensions = ['.png', '.jpg', '.PNG', '.JPG', '.JPEG']
  216. file_exists = False
  217. for ext in valid_extensions:
  218. if os.path.exists(pred[:-4] + ext):
  219. pred = pred[:-4] + ext
  220. file_exists = True
  221. break
  222. if not file_exists:
  223. print('Not exists:', pred)
  224. continue
  225. gt_ary = cv2.imread(gt, cv2.IMREAD_GRAYSCALE)
  226. pred_ary = cv2.imread(pred, cv2.IMREAD_GRAYSCALE)
  227. if gt_ary is None or pred_ary is None:
  228. continue
  229. pred_ary = cv2.resize(pred_ary, (gt_ary.shape[1], gt_ary.shape[0]))
  230. if 'E' in metrics:
  231. measures['E'].step(pred=pred_ary, gt=gt_ary)
  232. if 'S' in metrics:
  233. measures['S'].step(pred=pred_ary, gt=gt_ary)
  234. if 'F' in metrics:
  235. measures['F'].step(pred=pred_ary, gt=gt_ary)
  236. if 'MAE' in metrics:
  237. measures['MAE'].step(pred=pred_ary, gt=gt_ary)
  238. if 'MSE' in metrics:
  239. measures['MSE'].step(pred=pred_ary, gt=gt_ary)
  240. if 'WF' in metrics:
  241. measures['WF'].step(pred=pred_ary, gt=gt_ary)
  242. if 'HCE' in metrics:
  243. ske_path = gt.replace('/gt/', '/ske/')
  244. if os.path.exists(ske_path):
  245. ske_ary = cv2.imread(ske_path, cv2.IMREAD_GRAYSCALE)
  246. ske_ary = ske_ary > 128
  247. else:
  248. ske_ary = skeletonize(gt_ary > 128)
  249. ske_save_dir = os.path.join(*ske_path.split(os.sep)[:-1])
  250. if ske_path[0] == os.sep:
  251. ske_save_dir = os.sep + ske_save_dir
  252. os.makedirs(ske_save_dir, exist_ok=True)
  253. cv2.imwrite(ske_path, ske_ary.astype(np.uint8) * 255)
  254. measures['HCE'].step(pred=pred_ary, gt=gt_ary, gt_ske=ske_ary)
  255. if 'MBA' in metrics:
  256. measures['MBA'].step(pred=pred_ary, gt=gt_ary)
  257. if 'BIoU' in metrics:
  258. measures['BIoU'].step(pred=pred_ary, gt=gt_ary)
  259. results = collect_results(measures, metrics)
  260. if verbose:
  261. total_time = time.time() - start_time
  262. processing_method = "parallel" if use_parallel else "serial"
  263. print(f"Evaluation completed in {total_time:.2f}s using {processing_method} processing")
  264. return results
  265. ## >>>>>>>>>>>> Definitions of metrics:
  266. def _prepare_data(pred: np.ndarray, gt: np.ndarray) -> tuple:
  267. gt = gt > 128
  268. pred = pred / 255
  269. if pred.max() != pred.min():
  270. pred = (pred - pred.min()) / (pred.max() - pred.min())
  271. return pred, gt
  272. def _get_adaptive_threshold(matrix: np.ndarray, max_value: float = 1) -> float:
  273. return min(2 * matrix.mean(), max_value)
  274. class FMeasure(object):
  275. def __init__(self, beta: float = 0.3):
  276. self.beta = beta
  277. self.precisions = []
  278. self.recalls = []
  279. self.adaptive_fms = []
  280. self.changeable_fms = []
  281. def step(self, pred: np.ndarray, gt: np.ndarray):
  282. pred, gt = _prepare_data(pred, gt)
  283. adaptive_fm = self.cal_adaptive_fm(pred=pred, gt=gt)
  284. self.adaptive_fms.append(adaptive_fm)
  285. precisions, recalls, changeable_fms = self.cal_pr(pred=pred, gt=gt)
  286. self.precisions.append(precisions)
  287. self.recalls.append(recalls)
  288. self.changeable_fms.append(changeable_fms)
  289. def cal_adaptive_fm(self, pred: np.ndarray, gt: np.ndarray) -> float:
  290. adaptive_threshold = _get_adaptive_threshold(pred, max_value=1)
  291. binary_predcition = pred >= adaptive_threshold
  292. area_intersection = binary_predcition[gt].sum()
  293. if area_intersection == 0:
  294. adaptive_fm = 0
  295. else:
  296. pre = area_intersection / np.count_nonzero(binary_predcition)
  297. rec = area_intersection / np.count_nonzero(gt)
  298. adaptive_fm = (1 + self.beta) * pre * rec / (self.beta * pre + rec)
  299. return adaptive_fm
  300. def cal_pr(self, pred: np.ndarray, gt: np.ndarray) -> tuple:
  301. pred = (pred * 255).astype(np.uint8)
  302. bins = np.linspace(0, 256, 257)
  303. fg_hist, _ = np.histogram(pred[gt], bins=bins)
  304. bg_hist, _ = np.histogram(pred[~gt], bins=bins)
  305. fg_w_thrs = np.cumsum(np.flip(fg_hist), axis=0)
  306. bg_w_thrs = np.cumsum(np.flip(bg_hist), axis=0)
  307. TPs = fg_w_thrs
  308. Ps = fg_w_thrs + bg_w_thrs
  309. Ps[Ps == 0] = 1
  310. T = max(np.count_nonzero(gt), 1)
  311. precisions = TPs / Ps
  312. recalls = TPs / T
  313. numerator = (1 + self.beta) * precisions * recalls
  314. denominator = np.where(numerator == 0, 1, self.beta * precisions + recalls)
  315. changeable_fms = numerator / denominator
  316. return precisions, recalls, changeable_fms
  317. def get_results(self) -> dict:
  318. adaptive_fm = np.mean(np.array(self.adaptive_fms, _TYPE))
  319. changeable_fm = np.mean(np.array(self.changeable_fms, dtype=_TYPE), axis=0)
  320. precision = np.mean(np.array(self.precisions, dtype=_TYPE), axis=0) # N, 256
  321. recall = np.mean(np.array(self.recalls, dtype=_TYPE), axis=0) # N, 256
  322. return dict(fm=dict(adp=adaptive_fm, curve=changeable_fm),
  323. pr=dict(p=precision, r=recall))
  324. class MAEMeasure(object):
  325. def __init__(self):
  326. self.maes = []
  327. def step(self, pred: np.ndarray, gt: np.ndarray):
  328. pred, gt = _prepare_data(pred, gt)
  329. mae = self.cal_mae(pred, gt)
  330. self.maes.append(mae)
  331. def cal_mae(self, pred: np.ndarray, gt: np.ndarray) -> float:
  332. mae = np.mean(np.abs(pred - gt))
  333. return mae
  334. def get_results(self) -> dict:
  335. mae = np.mean(np.array(self.maes, _TYPE))
  336. return dict(mae=mae)
  337. class MSEMeasure(object):
  338. def __init__(self):
  339. self.mses = []
  340. def step(self, pred: np.ndarray, gt: np.ndarray):
  341. pred, gt = _prepare_data(pred, gt)
  342. mse = self.cal_mse(pred, gt)
  343. self.mses.append(mse)
  344. def cal_mse(self, pred: np.ndarray, gt: np.ndarray) -> float:
  345. mse = np.mean((pred - gt) ** 2)
  346. return mse
  347. def get_results(self) -> dict:
  348. mse = np.mean(np.array(self.mses, _TYPE))
  349. return dict(mse=mse)
  350. class SMeasure(object):
  351. def __init__(self, alpha: float = 0.5):
  352. self.sms = []
  353. self.alpha = alpha
  354. def step(self, pred: np.ndarray, gt: np.ndarray):
  355. pred, gt = _prepare_data(pred=pred, gt=gt)
  356. sm = self.cal_sm(pred, gt)
  357. self.sms.append(sm)
  358. def cal_sm(self, pred: np.ndarray, gt: np.ndarray) -> float:
  359. y = np.mean(gt)
  360. if y == 0:
  361. sm = 1 - np.mean(pred)
  362. elif y == 1:
  363. sm = np.mean(pred)
  364. else:
  365. sm = self.alpha * self.object(pred, gt) + (1 - self.alpha) * self.region(pred, gt)
  366. sm = max(0, sm)
  367. return sm
  368. def object(self, pred: np.ndarray, gt: np.ndarray) -> float:
  369. fg = pred * gt
  370. bg = (1 - pred) * (1 - gt)
  371. u = np.mean(gt)
  372. object_score = u * self.s_object(fg, gt) + (1 - u) * self.s_object(bg, 1 - gt)
  373. return object_score
  374. def s_object(self, pred: np.ndarray, gt: np.ndarray) -> float:
  375. x = np.mean(pred[gt == 1])
  376. sigma_x = np.std(pred[gt == 1], ddof=1)
  377. score = 2 * x / (np.power(x, 2) + 1 + sigma_x + _EPS)
  378. return score
  379. def region(self, pred: np.ndarray, gt: np.ndarray) -> float:
  380. x, y = self.centroid(gt)
  381. part_info = self.divide_with_xy(pred, gt, x, y)
  382. w1, w2, w3, w4 = part_info['weight']
  383. pred1, pred2, pred3, pred4 = part_info['pred']
  384. gt1, gt2, gt3, gt4 = part_info['gt']
  385. score1 = self.ssim(pred1, gt1)
  386. score2 = self.ssim(pred2, gt2)
  387. score3 = self.ssim(pred3, gt3)
  388. score4 = self.ssim(pred4, gt4)
  389. return w1 * score1 + w2 * score2 + w3 * score3 + w4 * score4
  390. def centroid(self, matrix: np.ndarray) -> tuple:
  391. h, w = matrix.shape
  392. area_object = np.count_nonzero(matrix)
  393. if area_object == 0:
  394. x = np.round(w / 2)
  395. y = np.round(h / 2)
  396. else:
  397. # More details can be found at: https://www.yuque.com/lart/blog/gpbigm
  398. y, x = np.argwhere(matrix).mean(axis=0).round()
  399. return int(x) + 1, int(y) + 1
  400. def divide_with_xy(self, pred: np.ndarray, gt: np.ndarray, x, y) -> dict:
  401. h, w = gt.shape
  402. area = h * w
  403. gt_LT = gt[0:y, 0:x]
  404. gt_RT = gt[0:y, x:w]
  405. gt_LB = gt[y:h, 0:x]
  406. gt_RB = gt[y:h, x:w]
  407. pred_LT = pred[0:y, 0:x]
  408. pred_RT = pred[0:y, x:w]
  409. pred_LB = pred[y:h, 0:x]
  410. pred_RB = pred[y:h, x:w]
  411. w1 = x * y / area
  412. w2 = y * (w - x) / area
  413. w3 = (h - y) * x / area
  414. w4 = 1 - w1 - w2 - w3
  415. return dict(gt=(gt_LT, gt_RT, gt_LB, gt_RB),
  416. pred=(pred_LT, pred_RT, pred_LB, pred_RB),
  417. weight=(w1, w2, w3, w4))
  418. def ssim(self, pred: np.ndarray, gt: np.ndarray) -> float:
  419. h, w = pred.shape
  420. N = h * w
  421. x = np.mean(pred)
  422. y = np.mean(gt)
  423. sigma_x = np.sum((pred - x) ** 2) / (N - 1)
  424. sigma_y = np.sum((gt - y) ** 2) / (N - 1)
  425. sigma_xy = np.sum((pred - x) * (gt - y)) / (N - 1)
  426. alpha = 4 * x * y * sigma_xy
  427. beta = (x ** 2 + y ** 2) * (sigma_x + sigma_y)
  428. if alpha != 0:
  429. score = alpha / (beta + _EPS)
  430. elif alpha == 0 and beta == 0:
  431. score = 1
  432. else:
  433. score = 0
  434. return score
  435. def get_results(self) -> dict:
  436. sm = np.mean(np.array(self.sms, dtype=_TYPE))
  437. return dict(sm=sm)
  438. class EMeasure(object):
  439. def __init__(self):
  440. self.adaptive_ems = []
  441. self.changeable_ems = []
  442. def step(self, pred: np.ndarray, gt: np.ndarray):
  443. pred, gt = _prepare_data(pred=pred, gt=gt)
  444. self.gt_fg_numel = np.count_nonzero(gt)
  445. self.gt_size = gt.shape[0] * gt.shape[1]
  446. changeable_ems = self.cal_changeable_em(pred, gt)
  447. self.changeable_ems.append(changeable_ems)
  448. adaptive_em = self.cal_adaptive_em(pred, gt)
  449. self.adaptive_ems.append(adaptive_em)
  450. def cal_adaptive_em(self, pred: np.ndarray, gt: np.ndarray) -> float:
  451. adaptive_threshold = _get_adaptive_threshold(pred, max_value=1)
  452. adaptive_em = self.cal_em_with_threshold(pred, gt, threshold=adaptive_threshold)
  453. return adaptive_em
  454. def cal_changeable_em(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray:
  455. changeable_ems = self.cal_em_with_cumsumhistogram(pred, gt)
  456. return changeable_ems
  457. def cal_em_with_threshold(self, pred: np.ndarray, gt: np.ndarray, threshold: float) -> float:
  458. binarized_pred = pred >= threshold
  459. fg_fg_numel = np.count_nonzero(binarized_pred & gt)
  460. fg_bg_numel = np.count_nonzero(binarized_pred & ~gt)
  461. fg___numel = fg_fg_numel + fg_bg_numel
  462. bg___numel = self.gt_size - fg___numel
  463. if self.gt_fg_numel == 0:
  464. enhanced_matrix_sum = bg___numel
  465. elif self.gt_fg_numel == self.gt_size:
  466. enhanced_matrix_sum = fg___numel
  467. else:
  468. parts_numel, combinations = self.generate_parts_numel_combinations(
  469. fg_fg_numel=fg_fg_numel, fg_bg_numel=fg_bg_numel,
  470. pred_fg_numel=fg___numel, pred_bg_numel=bg___numel,
  471. )
  472. results_parts = []
  473. for i, (part_numel, combination) in enumerate(zip(parts_numel, combinations)):
  474. align_matrix_value = 2 * (combination[0] * combination[1]) / \
  475. (combination[0] ** 2 + combination[1] ** 2 + _EPS)
  476. enhanced_matrix_value = (align_matrix_value + 1) ** 2 / 4
  477. results_parts.append(enhanced_matrix_value * part_numel)
  478. enhanced_matrix_sum = sum(results_parts)
  479. em = enhanced_matrix_sum / (self.gt_size - 1 + _EPS)
  480. return em
  481. def cal_em_with_cumsumhistogram(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray:
  482. pred = (pred * 255).astype(np.uint8)
  483. bins = np.linspace(0, 256, 257)
  484. fg_fg_hist, _ = np.histogram(pred[gt], bins=bins)
  485. fg_bg_hist, _ = np.histogram(pred[~gt], bins=bins)
  486. fg_fg_numel_w_thrs = np.cumsum(np.flip(fg_fg_hist), axis=0)
  487. fg_bg_numel_w_thrs = np.cumsum(np.flip(fg_bg_hist), axis=0)
  488. fg___numel_w_thrs = fg_fg_numel_w_thrs + fg_bg_numel_w_thrs
  489. bg___numel_w_thrs = self.gt_size - fg___numel_w_thrs
  490. if self.gt_fg_numel == 0:
  491. enhanced_matrix_sum = bg___numel_w_thrs
  492. elif self.gt_fg_numel == self.gt_size:
  493. enhanced_matrix_sum = fg___numel_w_thrs
  494. else:
  495. parts_numel_w_thrs, combinations = self.generate_parts_numel_combinations(
  496. fg_fg_numel=fg_fg_numel_w_thrs, fg_bg_numel=fg_bg_numel_w_thrs,
  497. pred_fg_numel=fg___numel_w_thrs, pred_bg_numel=bg___numel_w_thrs,
  498. )
  499. results_parts = np.empty(shape=(4, 256), dtype=np.float64)
  500. for i, (part_numel, combination) in enumerate(zip(parts_numel_w_thrs, combinations)):
  501. align_matrix_value = 2 * (combination[0] * combination[1]) / \
  502. (combination[0] ** 2 + combination[1] ** 2 + _EPS)
  503. enhanced_matrix_value = (align_matrix_value + 1) ** 2 / 4
  504. results_parts[i] = enhanced_matrix_value * part_numel
  505. enhanced_matrix_sum = results_parts.sum(axis=0)
  506. em = enhanced_matrix_sum / (self.gt_size - 1 + _EPS)
  507. return em
  508. def generate_parts_numel_combinations(self, fg_fg_numel, fg_bg_numel, pred_fg_numel, pred_bg_numel):
  509. bg_fg_numel = self.gt_fg_numel - fg_fg_numel
  510. bg_bg_numel = pred_bg_numel - bg_fg_numel
  511. parts_numel = [fg_fg_numel, fg_bg_numel, bg_fg_numel, bg_bg_numel]
  512. mean_pred_value = pred_fg_numel / self.gt_size
  513. mean_gt_value = self.gt_fg_numel / self.gt_size
  514. demeaned_pred_fg_value = 1 - mean_pred_value
  515. demeaned_pred_bg_value = 0 - mean_pred_value
  516. demeaned_gt_fg_value = 1 - mean_gt_value
  517. demeaned_gt_bg_value = 0 - mean_gt_value
  518. combinations = [
  519. (demeaned_pred_fg_value, demeaned_gt_fg_value),
  520. (demeaned_pred_fg_value, demeaned_gt_bg_value),
  521. (demeaned_pred_bg_value, demeaned_gt_fg_value),
  522. (demeaned_pred_bg_value, demeaned_gt_bg_value)
  523. ]
  524. return parts_numel, combinations
  525. def get_results(self) -> dict:
  526. adaptive_em = np.mean(np.array(self.adaptive_ems, dtype=_TYPE))
  527. changeable_em = np.mean(np.array(self.changeable_ems, dtype=_TYPE), axis=0)
  528. return dict(em=dict(adp=adaptive_em, curve=changeable_em))
  529. class WeightedFMeasure(object):
  530. def __init__(self, beta: float = 1):
  531. self.beta = beta
  532. self.weighted_fms = []
  533. def step(self, pred: np.ndarray, gt: np.ndarray):
  534. pred, gt = _prepare_data(pred=pred, gt=gt)
  535. if np.all(~gt):
  536. wfm = 0
  537. else:
  538. wfm = self.cal_wfm(pred, gt)
  539. self.weighted_fms.append(wfm)
  540. def cal_wfm(self, pred: np.ndarray, gt: np.ndarray) -> float:
  541. # [Dst,IDXT] = bwdist(dGT);
  542. Dst, Idxt = bwdist(gt == 0, return_indices=True)
  543. # %Pixel dependency
  544. # E = abs(FG-dGT);
  545. E = np.abs(pred - gt)
  546. Et = np.copy(E)
  547. Et[gt == 0] = Et[Idxt[0][gt == 0], Idxt[1][gt == 0]]
  548. # K = fspecial('gaussian',7,5);
  549. # EA = imfilter(Et,K);
  550. K = self.matlab_style_gauss2D((7, 7), sigma=5)
  551. EA = convolve(Et, weights=K, mode="constant", cval=0)
  552. # MIN_E_EA = E;
  553. # MIN_E_EA(GT & EA<E) = EA(GT & EA<E);
  554. MIN_E_EA = np.where(gt & (EA < E), EA, E)
  555. # %Pixel importance
  556. B = np.where(gt == 0, 2 - np.exp(np.log(0.5) / 5 * Dst), np.ones_like(gt))
  557. Ew = MIN_E_EA * B
  558. TPw = np.sum(gt) - np.sum(Ew[gt == 1])
  559. FPw = np.sum(Ew[gt == 0])
  560. R = 1 - np.mean(Ew[gt == 1])
  561. P = TPw / (TPw + FPw + _EPS)
  562. # % Q = (1+Beta^2)*(R*P)./(eps+R+(Beta.*P));
  563. Q = (1 + self.beta) * R * P / (R + self.beta * P + _EPS)
  564. return Q
  565. def matlab_style_gauss2D(self, shape: tuple = (7, 7), sigma: int = 5) -> np.ndarray:
  566. """
  567. 2D gaussian mask - should give the same result as MATLAB's
  568. fspecial('gaussian',[shape],[sigma])
  569. """
  570. m, n = [(ss - 1) / 2 for ss in shape]
  571. y, x = np.ogrid[-m: m + 1, -n: n + 1]
  572. h = np.exp(-(x * x + y * y) / (2 * sigma * sigma))
  573. h[h < np.finfo(h.dtype).eps * h.max()] = 0
  574. sumh = h.sum()
  575. if sumh != 0:
  576. h /= sumh
  577. return h
  578. def get_results(self) -> dict:
  579. weighted_fm = np.mean(np.array(self.weighted_fms, dtype=_TYPE))
  580. return dict(wfm=weighted_fm)
  581. class HCEMeasure(object):
  582. def __init__(self):
  583. self.hces = []
  584. def step(self, pred: np.ndarray, gt: np.ndarray, gt_ske):
  585. # pred, gt = _prepare_data(pred, gt)
  586. hce = self.cal_hce(pred, gt, gt_ske)
  587. self.hces.append(hce)
  588. def get_results(self) -> dict:
  589. hce = np.mean(np.array(self.hces, _TYPE))
  590. return dict(hce=hce)
  591. def cal_hce(self, pred: np.ndarray, gt: np.ndarray, gt_ske: np.ndarray, relax=5, epsilon=2.0) -> float:
  592. # Binarize gt
  593. if(len(gt.shape)>2):
  594. gt = gt[:, :, 0]
  595. epsilon_gt = 128#(np.amin(gt)+np.amax(gt))/2.0
  596. gt = (gt>epsilon_gt).astype(np.uint8)
  597. # Binarize pred
  598. if(len(pred.shape)>2):
  599. pred = pred[:, :, 0]
  600. epsilon_pred = 128#(np.amin(pred)+np.amax(pred))/2.0
  601. pred = (pred>epsilon_pred).astype(np.uint8)
  602. Union = np.logical_or(gt, pred)
  603. TP = np.logical_and(gt, pred)
  604. FP = pred - TP
  605. FN = gt - TP
  606. # relax the Union of gt and pred
  607. Union_erode = Union.copy()
  608. Union_erode = cv2.erode(Union_erode.astype(np.uint8), disk(1), iterations=relax)
  609. # --- get the relaxed False Positive regions for computing the human efforts in correcting them ---
  610. FP_ = np.logical_and(FP, Union_erode) # get the relaxed FP
  611. for i in range(0, relax):
  612. FP_ = cv2.dilate(FP_.astype(np.uint8), disk(1))
  613. FP_ = np.logical_and(FP_, 1-np.logical_or(TP, FN))
  614. FP_ = np.logical_and(FP, FP_)
  615. # --- get the relaxed False Negative regions for computing the human efforts in correcting them ---
  616. FN_ = np.logical_and(FN, Union_erode) # preserve the structural components of FN
  617. ## recover the FN, where pixels are not close to the TP borders
  618. for i in range(0, relax):
  619. FN_ = cv2.dilate(FN_.astype(np.uint8), disk(1))
  620. FN_ = np.logical_and(FN_, 1-np.logical_or(TP, FP))
  621. FN_ = np.logical_and(FN, FN_)
  622. FN_ = np.logical_or(FN_, np.logical_xor(gt_ske, np.logical_and(TP, gt_ske))) # preserve the structural components of FN
  623. ## 2. =============Find exact polygon control points and independent regions==============
  624. ## find contours from FP_
  625. ctrs_FP, hier_FP = cv2.findContours(FP_.astype(np.uint8), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
  626. ## find control points and independent regions for human correction
  627. bdies_FP, indep_cnt_FP = self.filter_bdy_cond(ctrs_FP, FP_, np.logical_or(TP,FN_))
  628. ## find contours from FN_
  629. ctrs_FN, hier_FN = cv2.findContours(FN_.astype(np.uint8), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
  630. ## find control points and independent regions for human correction
  631. bdies_FN, indep_cnt_FN = self.filter_bdy_cond(ctrs_FN, FN_, 1-np.logical_or(np.logical_or(TP, FP_), FN_))
  632. poly_FP, poly_FP_len, poly_FP_point_cnt = self.approximate_RDP(bdies_FP, epsilon=epsilon)
  633. poly_FN, poly_FN_len, poly_FN_point_cnt = self.approximate_RDP(bdies_FN, epsilon=epsilon)
  634. # FP_points+FP_indep+FN_points+FN_indep
  635. return poly_FP_point_cnt+indep_cnt_FP+poly_FN_point_cnt+indep_cnt_FN
  636. def filter_bdy_cond(self, bdy_, mask, cond):
  637. cond = cv2.dilate(cond.astype(np.uint8), disk(1))
  638. labels = label(mask) # find the connected regions
  639. lbls = np.unique(labels) # the indices of the connected regions
  640. indep = np.ones(lbls.shape[0]) # the label of each connected regions
  641. indep[0] = 0 # 0 indicate the background region
  642. boundaries = []
  643. h,w = cond.shape[0:2]
  644. ind_map = np.zeros((h, w))
  645. indep_cnt = 0
  646. for i in range(0, len(bdy_)):
  647. tmp_bdies = []
  648. tmp_bdy = []
  649. for j in range(0, bdy_[i].shape[0]):
  650. r, c = bdy_[i][j,0,1],bdy_[i][j,0,0]
  651. if(np.sum(cond[r, c])==0 or ind_map[r, c]!=0):
  652. if(len(tmp_bdy)>0):
  653. tmp_bdies.append(tmp_bdy)
  654. tmp_bdy = []
  655. continue
  656. tmp_bdy.append([c, r])
  657. ind_map[r, c] = ind_map[r, c] + 1
  658. indep[labels[r, c]] = 0 # indicates part of the boundary of this region needs human correction
  659. if(len(tmp_bdy)>0):
  660. tmp_bdies.append(tmp_bdy)
  661. # check if the first and the last boundaries are connected
  662. # if yes, invert the first boundary and attach it after the last boundary
  663. if(len(tmp_bdies)>1):
  664. first_x, first_y = tmp_bdies[0][0]
  665. last_x, last_y = tmp_bdies[-1][-1]
  666. if((abs(first_x-last_x)==1 and first_y==last_y) or
  667. (first_x==last_x and abs(first_y-last_y)==1) or
  668. (abs(first_x-last_x)==1 and abs(first_y-last_y)==1)
  669. ):
  670. tmp_bdies[-1].extend(tmp_bdies[0][::-1])
  671. del tmp_bdies[0]
  672. for k in range(0, len(tmp_bdies)):
  673. tmp_bdies[k] = np.array(tmp_bdies[k])[:, np.newaxis, :]
  674. if(len(tmp_bdies)>0):
  675. boundaries.extend(tmp_bdies)
  676. return boundaries, np.sum(indep)
  677. # this function approximate each boundary by DP algorithm
  678. # https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
  679. def approximate_RDP(self, boundaries, epsilon=1.0):
  680. boundaries_ = []
  681. boundaries_len_ = []
  682. pixel_cnt_ = 0
  683. # polygon approximate of each boundary
  684. for i in range(0, len(boundaries)):
  685. boundaries_.append(cv2.approxPolyDP(boundaries[i], epsilon, False))
  686. # count the control points number of each boundary and the total control points number of all the boundaries
  687. for i in range(0, len(boundaries_)):
  688. boundaries_len_.append(len(boundaries_[i]))
  689. pixel_cnt_ = pixel_cnt_ + len(boundaries_[i])
  690. return boundaries_, boundaries_len_, pixel_cnt_
  691. class MBAMeasure(object):
  692. def __init__(self):
  693. self.bas = []
  694. self.all_h = 0
  695. self.all_w = 0
  696. self.all_max = 0
  697. def step(self, pred: np.ndarray, gt: np.ndarray):
  698. # pred, gt = _prepare_data(pred, gt)
  699. refined = gt.copy()
  700. rmin = cmin = 0
  701. rmax, cmax = gt.shape
  702. self.all_h += rmax
  703. self.all_w += cmax
  704. self.all_max += max(rmax, cmax)
  705. refined_h, refined_w = refined.shape
  706. if refined_h != cmax:
  707. refined = np.array(Image.fromarray(pred).resize((cmax, rmax), Image.BILINEAR))
  708. if not(gt.sum() < 32*32):
  709. if not((cmax==cmin) or (rmax==rmin)):
  710. class_refined_prob = np.array(Image.fromarray(pred).resize((cmax-cmin, rmax-rmin), Image.BILINEAR))
  711. refined[rmin:rmax, cmin:cmax] = class_refined_prob
  712. pred = pred > 128
  713. gt = gt > 128
  714. ba = self.cal_ba(pred, gt)
  715. self.bas.append(ba)
  716. def get_disk_kernel(self, radius):
  717. return cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (radius*2+1, radius*2+1))
  718. def cal_ba(self, pred: np.ndarray, gt: np.ndarray) -> np.ndarray:
  719. """
  720. Calculate the mean absolute error.
  721. :return: ba
  722. """
  723. gt = gt.astype(np.uint8)
  724. pred = pred.astype(np.uint8)
  725. h, w = gt.shape
  726. min_radius = 1
  727. max_radius = (w+h)/300
  728. num_steps = 5
  729. pred_acc = [None] * num_steps
  730. for i in range(num_steps):
  731. curr_radius = min_radius + int((max_radius-min_radius)/num_steps*i)
  732. kernel = self.get_disk_kernel(curr_radius)
  733. boundary_region = cv2.morphologyEx(gt, cv2.MORPH_GRADIENT, kernel) > 0
  734. gt_in_bound = gt[boundary_region]
  735. pred_in_bound = pred[boundary_region]
  736. num_edge_pixels = (boundary_region).sum()
  737. num_pred_gd_pix = ((gt_in_bound) * (pred_in_bound) + (1-gt_in_bound) * (1-pred_in_bound)).sum()
  738. pred_acc[i] = num_pred_gd_pix / num_edge_pixels
  739. ba = sum(pred_acc)/num_steps
  740. return ba
  741. def get_results(self) -> dict:
  742. mba = np.mean(np.array(self.bas, _TYPE))
  743. return dict(mba=mba)
  744. class BIoUMeasure(object):
  745. def __init__(self, dilation_ratio=0.02):
  746. self.bious = []
  747. self.dilation_ratio = dilation_ratio
  748. def mask_to_boundary(self, mask):
  749. h, w = mask.shape
  750. img_diag = np.sqrt(h ** 2 + w ** 2)
  751. dilation = int(round(self.dilation_ratio * img_diag))
  752. if dilation < 1:
  753. dilation = 1
  754. # Pad image so mask truncated by the image border is also considered as boundary.
  755. new_mask = cv2.copyMakeBorder(mask, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0)
  756. kernel = np.ones((3, 3), dtype=np.uint8)
  757. new_mask_erode = cv2.erode(new_mask, kernel, iterations=dilation)
  758. mask_erode = new_mask_erode[1 : h + 1, 1 : w + 1]
  759. # G_d intersects G in the paper.
  760. return mask - mask_erode
  761. def step(self, pred: np.ndarray, gt: np.ndarray):
  762. pred, gt = _prepare_data(pred, gt)
  763. bious = self.cal_biou(pred=pred, gt=gt)
  764. self.bious.append(bious)
  765. def cal_biou(self, pred, gt):
  766. pred = (pred * 255).astype(np.uint8)
  767. pred = self.mask_to_boundary(pred)
  768. gt = (gt * 255).astype(np.uint8)
  769. gt = self.mask_to_boundary(gt)
  770. gt = gt > 128
  771. bins = np.linspace(0, 256, 257)
  772. fg_hist, _ = np.histogram(pred[gt], bins=bins) # ture positive
  773. bg_hist, _ = np.histogram(pred[~gt], bins=bins) # false positive
  774. fg_w_thrs = np.cumsum(np.flip(fg_hist), axis=0)
  775. bg_w_thrs = np.cumsum(np.flip(bg_hist), axis=0)
  776. TPs = fg_w_thrs
  777. Ps = fg_w_thrs + bg_w_thrs # positives
  778. Ps[Ps == 0] = 1
  779. T = max(np.count_nonzero(gt), 1)
  780. ious = TPs / (T + bg_w_thrs)
  781. return ious
  782. def get_results(self) -> dict:
  783. biou = np.mean(np.array(self.bious, dtype=_TYPE), axis=0)
  784. return dict(biou=dict(curve=biou))
  785. def sort_and_round_scores(task, scores, r=3):
  786. em, sm, fm, mae, mse, wfm, hce, mba, biou = scores
  787. if task == 'DIS5K':
  788. scores = [fm['curve'].max().round(r), wfm.round(r), mae.round(r), sm.round(r), em['curve'].mean().round(r), int(hce.round()),
  789. em['curve'].max().round(r), fm['curve'].mean().round(r), em['adp'].round(r), fm['adp'].round(r),
  790. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  791. elif task == 'COD':
  792. scores = [sm.round(r), wfm.round(r), fm['curve'].mean().round(r), em['curve'].mean().round(r), em['curve'].max().round(r), mae.round(r),
  793. fm['curve'].max().round(r), em['adp'].round(r), fm['adp'].round(r), int(hce.round()),
  794. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  795. elif task == 'HRSOD':
  796. scores = [sm.round(r), fm['curve'].max().round(r), em['curve'].mean().round(r), mae.round(r),
  797. em['curve'].max().round(r), fm['curve'].mean().round(r), wfm.round(r), em['adp'].round(r), fm['adp'].round(r), int(hce.round()),
  798. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  799. elif task == 'General':
  800. scores = [fm['curve'].max().round(r), wfm.round(r), mae.round(r), sm.round(r), em['curve'].mean().round(r), int(hce.round()),
  801. em['curve'].max().round(r), fm['curve'].mean().round(r), em['adp'].round(r), fm['adp'].round(r),
  802. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  803. elif task == 'General-2K':
  804. scores = [fm['curve'].max().round(r), wfm.round(r), mae.round(r), sm.round(r), em['curve'].mean().round(r), int(hce.round()),
  805. em['curve'].max().round(r), fm['curve'].mean().round(r), em['adp'].round(r), fm['adp'].round(r),
  806. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  807. elif task == 'Matting':
  808. scores = [sm.round(r), fm['curve'].max().round(r), em['curve'].mean().round(r), mse.round(5),
  809. em['curve'].max().round(r), fm['curve'].mean().round(r), wfm.round(r), em['adp'].round(r), fm['adp'].round(r), int(hce.round()),
  810. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  811. else:
  812. scores = [sm.round(r), mae.round(r), em['curve'].max().round(r), em['curve'].mean().round(r),
  813. fm['curve'].max().round(r), fm['curve'].mean().round(r), wfm.round(r),
  814. em['adp'].round(r), fm['adp'].round(r), int(hce.round()),
  815. mba.round(r), biou['curve'].max().round(r), biou['curve'].mean().round(r),]
  816. return scores