|
import os |
|
import numpy as np |
|
from scipy.optimize import linear_sum_assignment |
|
from ._base_metric import _BaseMetric |
|
from .. import _timing |
|
|
|
|
|
class avisA(_BaseMetric): |
|
def __init__(self, config=None): |
|
super().__init__() |
|
self.plottable = True |
|
self.array_labels = np.arange(0.05, 0.99, 0.05) |
|
self.integer_array_fields = ['HOTA_TP', 'HOTA_FN', 'HOTA_FP'] |
|
self.float_array_fields = ['AssA', 'AssRe', 'AssPr', 'SegA'] |
|
self.float_fields = ['SegA(0)'] |
|
self.fields = self.float_array_fields + self.integer_array_fields + self.float_fields |
|
self.summary_fields = self.float_array_fields + self.float_fields |
|
|
|
@_timing.time |
|
def eval_sequence(self, data): |
|
"""Calculates the AssA and SegA metrics for one sequence""" |
|
|
|
|
|
res = {} |
|
for field in self.float_array_fields + self.integer_array_fields: |
|
res[field] = np.zeros((len(self.array_labels)), dtype=float) |
|
for field in self.float_fields: |
|
res[field] = 0 |
|
|
|
|
|
if data['num_tracker_dets'] == 0: |
|
res['HOTA_FN'] = data['num_gt_dets'] * np.ones((len(self.array_labels)), dtype=float) |
|
res['SegA'] = np.ones((len(self.array_labels)), dtype=float) |
|
res['SegA(0)'] = 1.0 |
|
return res |
|
if data['num_gt_dets'] == 0: |
|
res['HOTA_FP'] = data['num_tracker_dets'] * np.ones((len(self.array_labels)), dtype=float) |
|
res['SegA'] = np.ones((len(self.array_labels)), dtype=float) |
|
res['SegA(0)'] = 1.0 |
|
return res |
|
|
|
|
|
potential_matches_count = np.zeros((data['num_gt_ids'], data['num_tracker_ids'])) |
|
gt_id_count = np.zeros((data['num_gt_ids'], 1)) |
|
tracker_id_count = np.zeros((1, data['num_tracker_ids'])) |
|
|
|
|
|
for t, (gt_ids_t, tracker_ids_t) in enumerate(zip(data['gt_ids'], data['tracker_ids'])): |
|
|
|
|
|
similarity = data['similarity_scores'][t] |
|
sim_iou_denom = similarity.sum(0)[np.newaxis, :] + similarity.sum(1)[:, np.newaxis] - similarity |
|
sim_iou = np.zeros_like(similarity) |
|
sim_iou_mask = sim_iou_denom > 0 + np.finfo('float').eps |
|
sim_iou[sim_iou_mask] = similarity[sim_iou_mask] / sim_iou_denom[sim_iou_mask] |
|
potential_matches_count[gt_ids_t[:, np.newaxis], tracker_ids_t[np.newaxis, :]] += sim_iou |
|
|
|
|
|
gt_id_count[gt_ids_t] += 1 |
|
tracker_id_count[0, tracker_ids_t] += 1 |
|
|
|
|
|
global_alignment_score = potential_matches_count / (gt_id_count + tracker_id_count - potential_matches_count) |
|
matches_counts = [np.zeros_like(potential_matches_count) for _ in self.array_labels] |
|
|
|
|
|
for t, (gt_ids_t, tracker_ids_t) in enumerate(zip(data['gt_ids'], data['tracker_ids'])): |
|
|
|
if len(gt_ids_t) == 0: |
|
for a, alpha in enumerate(self.array_labels): |
|
res['HOTA_FP'][a] += len(tracker_ids_t) |
|
continue |
|
if len(tracker_ids_t) == 0: |
|
for a, alpha in enumerate(self.array_labels): |
|
res['HOTA_FN'][a] += len(gt_ids_t) |
|
continue |
|
|
|
|
|
similarity = data['similarity_scores'][t] |
|
score_mat = global_alignment_score[gt_ids_t[:, np.newaxis], tracker_ids_t[np.newaxis, :]] * similarity |
|
|
|
|
|
match_rows, match_cols = linear_sum_assignment(-score_mat) |
|
|
|
|
|
for a, alpha in enumerate(self.array_labels): |
|
actually_matched_mask = similarity[match_rows, match_cols] >= alpha - np.finfo('float').eps |
|
alpha_match_rows = match_rows[actually_matched_mask] |
|
alpha_match_cols = match_cols[actually_matched_mask] |
|
num_matches = len(alpha_match_rows) |
|
res['HOTA_TP'][a] += num_matches |
|
res['HOTA_FN'][a] += len(gt_ids_t) - num_matches |
|
res['HOTA_FP'][a] += len(tracker_ids_t) - num_matches |
|
if num_matches > 0: |
|
res['SegA'][a] += sum(similarity[alpha_match_rows, alpha_match_cols]) |
|
matches_counts[a][gt_ids_t[alpha_match_rows], tracker_ids_t[alpha_match_cols]] += 1 |
|
|
|
|
|
|
|
for a, alpha in enumerate(self.array_labels): |
|
matches_count = matches_counts[a] |
|
ass_a = matches_count / np.maximum(1, gt_id_count + tracker_id_count - matches_count) |
|
res['AssA'][a] = np.sum(matches_count * ass_a) / np.maximum(1, res['HOTA_TP'][a]) |
|
ass_re = matches_count / np.maximum(1, gt_id_count) |
|
res['AssRe'][a] = np.sum(matches_count * ass_re) / np.maximum(1, res['HOTA_TP'][a]) |
|
ass_pr = matches_count / np.maximum(1, tracker_id_count) |
|
res['AssPr'][a] = np.sum(matches_count * ass_pr) / np.maximum(1, res['HOTA_TP'][a]) |
|
|
|
|
|
res['SegA'] = np.maximum(1e-10, res['SegA']) / np.maximum(1e-10, res['HOTA_TP']) |
|
res = self._compute_final_fields(res) |
|
return res |
|
|
|
def combine_sequences(self, all_res): |
|
"""Combines metrics across all sequences""" |
|
res = {} |
|
for field in self.integer_array_fields: |
|
res[field] = self._combine_sum(all_res, field) |
|
for field in ['AssRe', 'AssPr', 'AssA']: |
|
res[field] = self._combine_weighted_av(all_res, field, res, weight_field='HOTA_TP') |
|
sega_weighted_sum = sum([all_res[k]['SegA'] * all_res[k]['HOTA_TP'] for k in all_res.keys()]) |
|
res['SegA'] = np.maximum(1e-10, sega_weighted_sum) / np.maximum(1e-10, res['HOTA_TP']) |
|
res = self._compute_final_fields(res) |
|
return res |
|
|
|
def combine_classes_class_averaged(self, all_res, ignore_empty_classes=False): |
|
"""Combines metrics across all classes by averaging over the class values. |
|
If 'ignore_empty_classes' is True, then it only sums over classes with at least one gt or predicted detection. |
|
""" |
|
res = {} |
|
for field in self.integer_array_fields: |
|
if ignore_empty_classes: |
|
res[field] = self._combine_sum( |
|
{k: v for k, v in all_res.items() |
|
if (v['HOTA_TP'] + v['HOTA_FN'] + v['HOTA_FP'] > 0 + np.finfo('float').eps).any()}, field) |
|
else: |
|
res[field] = self._combine_sum({k: v for k, v in all_res.items()}, field) |
|
|
|
for field in self.float_fields + self.float_array_fields: |
|
if ignore_empty_classes: |
|
res[field] = np.mean([v[field] for v in all_res.values() if |
|
(v['HOTA_TP'] + v['HOTA_FN'] + v['HOTA_FP'] > 0 + np.finfo('float').eps).any()], |
|
axis=0) |
|
else: |
|
res[field] = np.mean([v[field] for v in all_res.values()], axis=0) |
|
return res |
|
|
|
def combine_classes_det_averaged(self, all_res): |
|
"""Combines metrics across all classes by averaging over the detection values""" |
|
res = {} |
|
for field in self.integer_array_fields: |
|
res[field] = self._combine_sum(all_res, field) |
|
for field in ['AssRe', 'AssPr', 'AssA']: |
|
res[field] = self._combine_weighted_av(all_res, field, res, weight_field='HOTA_TP') |
|
sega_weighted_sum = sum([all_res[k]['SegA'] * all_res[k]['HOTA_TP'] for k in all_res.keys()]) |
|
res['SegA'] = np.maximum(1e-10, sega_weighted_sum) / np.maximum(1e-10, res['HOTA_TP']) |
|
res = self._compute_final_fields(res) |
|
return res |
|
|
|
@staticmethod |
|
def _compute_final_fields(res): |
|
"""Calculate sub-metric ('field') values which only depend on other sub-metric values. |
|
This function is used both for both per-sequence calculation, and in combining values across sequences. |
|
""" |
|
res['SegA(0)'] = res['SegA'][0] |
|
return res |
|
|
|
def plot_single_tracker_results(self, table_res, tracker, cls, output_folder): |
|
"""Create plot of results""" |
|
|
|
|
|
from matplotlib import pyplot as plt |
|
|
|
res = table_res['COMBINED_SEQ'] |
|
styles_to_plot = ['r', 'b', 'g', 'b--', 'b:', 'g--', 'g:', 'm'] |
|
for name, style in zip(self.float_array_fields, styles_to_plot): |
|
plt.plot(self.array_labels, res[name], style) |
|
plt.xlabel('alpha') |
|
plt.ylabel('score') |
|
plt.title(tracker + ' - ' + cls) |
|
plt.axis([0, 1, 0, 1]) |
|
legend = [] |
|
for name in self.float_array_fields: |
|
legend += [name + ' (' + str(np.round(np.mean(res[name]), 2)) + ')'] |
|
plt.legend(legend, loc='lower left') |
|
out_file = os.path.join(output_folder, cls + '_plot.pdf') |
|
os.makedirs(os.path.dirname(out_file), exist_ok=True) |
|
plt.savefig(out_file) |
|
plt.savefig(out_file.replace('.pdf', '.png')) |
|
plt.clf() |
|
|