File size: 8,400 Bytes
47af768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231

import os
import numpy as np
from .utils import TrackEvalException


def plot_compare_trackers(tracker_folder, tracker_list, cls, output_folder, plots_list=None):
    """Create plots which compare metrics across different trackers."""
    # Define what to plot
    if plots_list is None:
        plots_list = get_default_plots_list()

    # Load data
    data = load_multiple_tracker_summaries(tracker_folder, tracker_list, cls)
    out_loc = os.path.join(output_folder, cls)

    # Plot
    for args in plots_list:
        create_comparison_plot(data, out_loc, *args)


def get_default_plots_list():
    # y_label, x_label, sort_label, bg_label, bg_function
    plots_list = [
        ['AssA', 'DetA', 'HOTA', 'HOTA', 'geometric_mean'],
        ['AssPr', 'AssRe', 'HOTA', 'AssA', 'jaccard'],
        ['DetPr', 'DetRe', 'HOTA', 'DetA', 'jaccard'],
        ['HOTA(0)', 'LocA(0)', 'HOTA', 'HOTALocA(0)', 'multiplication'],
        ['HOTA', 'LocA', 'HOTA', None, None],

        ['HOTA', 'MOTA', 'HOTA', None, None],
        ['HOTA', 'IDF1', 'HOTA', None, None],
        ['IDF1', 'MOTA', 'HOTA', None, None],
    ]
    return plots_list


def load_multiple_tracker_summaries(tracker_folder, tracker_list, cls):
    """Loads summary data for multiple trackers."""
    data = {}
    for tracker in tracker_list:
        with open(os.path.join(tracker_folder, tracker, cls + '_summary.txt')) as f:
            keys = next(f).split(' ')
            done = False
            while not done:
                values = next(f).split(' ')
                if len(values) == len(keys):
                    done = True
            data[tracker] = dict(zip(keys, map(float, values)))
    return data


def create_comparison_plot(data, out_loc, y_label, x_label, sort_label, bg_label=None, bg_function=None, settings=None):
    """ Creates a scatter plot comparing multiple trackers between two metric fields, with one on the x-axis and the
    other on the y axis. Adds pareto optical lines and (optionally) a background contour.

    Inputs:
        data: dict of dicts such that data[tracker_name][metric_field_name] = float
        y_label: the metric_field_name to be plotted on the y-axis
        x_label: the metric_field_name to be plotted on the x-axis
        sort_label: the metric_field_name by which trackers are ordered and ranked
        bg_label: the metric_field_name by which (optional) background contours are plotted
        bg_function: the (optional) function bg_function(x,y) which converts the x_label / y_label values into bg_label.
        settings: dict of plot settings with keys:
            'gap_val': gap between axis ticks and bg curves.
            'num_to_plot': maximum number of trackers to plot
    """

    # Only loaded when run to reduce minimum requirements
    from matplotlib import pyplot as plt

    # Get plot settings
    if settings is None:
        gap_val = 2
        num_to_plot = 20
    else:
        gap_val = settings['gap_val']
        num_to_plot = settings['num_to_plot']

    if (bg_label is None) != (bg_function is None):
        raise TrackEvalException('bg_function and bg_label must either be both given or neither given.')

    # Extract data
    tracker_names = np.array(list(data.keys()))
    sort_index = np.array([data[t][sort_label] for t in tracker_names]).argsort()[::-1]
    x_values = np.array([data[t][x_label] for t in tracker_names])[sort_index][:num_to_plot]
    y_values = np.array([data[t][y_label] for t in tracker_names])[sort_index][:num_to_plot]

    # Print info on what is being plotted
    tracker_names = tracker_names[sort_index][:num_to_plot]
    print('\nPlotting %s vs %s, for the following (ordered) trackers:' % (y_label, x_label))
    for i, name in enumerate(tracker_names):
        print('%i: %s' % (i+1, name))

    # Find best fitting boundaries for data
    boundaries = _get_boundaries(x_values, y_values, round_val=gap_val/2)

    fig = plt.figure()

    # Plot background contour
    if bg_function is not None:
        _plot_bg_contour(bg_function, boundaries, gap_val)

    # Plot pareto optimal lines
    _plot_pareto_optimal_lines(x_values, y_values)

    # Plot data points with number labels
    labels = np.arange(len(y_values)) + 1
    plt.plot(x_values, y_values, 'b.', markersize=15)
    for xx, yy, l in zip(x_values, y_values, labels):
        plt.text(xx, yy, str(l), color="red", fontsize=15)

    # Add extra explanatory text to plots
    plt.text(0, -0.11, 'label order:\nHOTA', horizontalalignment='left', verticalalignment='center',
             transform=fig.axes[0].transAxes, color="red", fontsize=12)
    if bg_label is not None:
        plt.text(1, -0.11, 'curve values:\n' + bg_label, horizontalalignment='right', verticalalignment='center',
                 transform=fig.axes[0].transAxes, color="grey", fontsize=12)

    plt.xlabel(x_label, fontsize=15)
    plt.ylabel(y_label, fontsize=15)
    title = y_label + ' vs ' + x_label
    if bg_label is not None:
        title += ' (' + bg_label + ')'
    plt.title(title, fontsize=17)
    plt.xticks(np.arange(0, 100, gap_val))
    plt.yticks(np.arange(0, 100, gap_val))
    min_x, max_x, min_y, max_y = boundaries
    plt.xlim(min_x, max_x)
    plt.ylim(min_y, max_y)
    plt.gca().set_aspect('equal', adjustable='box')
    plt.tight_layout()

    os.makedirs(out_loc, exist_ok=True)
    filename = os.path.join(out_loc, title.replace(' ', '_'))
    plt.savefig(filename + '.pdf', bbox_inches='tight', pad_inches=0.05)
    plt.savefig(filename + '.png', bbox_inches='tight', pad_inches=0.05)


def _get_boundaries(x_values, y_values, round_val):
    x1 = np.min(np.floor((x_values - 0.5) / round_val) * round_val)
    x2 = np.max(np.ceil((x_values + 0.5) / round_val) * round_val)
    y1 = np.min(np.floor((y_values - 0.5) / round_val) * round_val)
    y2 = np.max(np.ceil((y_values + 0.5) / round_val) * round_val)
    x_range = x2 - x1
    y_range = y2 - y1
    max_range = max(x_range, y_range)
    x_center = (x1 + x2) / 2
    y_center = (y1 + y2) / 2
    min_x = max(x_center - max_range / 2, 0)
    max_x = min(x_center + max_range / 2, 100)
    min_y = max(y_center - max_range / 2, 0)
    max_y = min(y_center + max_range / 2, 100)
    return min_x, max_x, min_y, max_y


def geometric_mean(x, y):
    return np.sqrt(x * y)


def jaccard(x, y):
    x = x / 100
    y = y / 100
    return 100 * (x * y) / (x + y - x * y)


def multiplication(x, y):
    return x * y / 100


bg_function_dict = {
    "geometric_mean": geometric_mean,
    "jaccard": jaccard,
    "multiplication": multiplication,
    }


def _plot_bg_contour(bg_function, plot_boundaries, gap_val):
    """ Plot background contour. """

    # Only loaded when run to reduce minimum requirements
    from matplotlib import pyplot as plt

    # Plot background contour
    min_x, max_x, min_y, max_y = plot_boundaries
    x = np.arange(min_x, max_x, 0.1)
    y = np.arange(min_y, max_y, 0.1)
    x_grid, y_grid = np.meshgrid(x, y)
    if bg_function in bg_function_dict.keys():
        z_grid = bg_function_dict[bg_function](x_grid, y_grid)
    else:
        raise TrackEvalException("background plotting function '%s' is not defined." % bg_function)
    levels = np.arange(0, 100, gap_val)
    con = plt.contour(x_grid, y_grid, z_grid, levels, colors='grey')

    def bg_format(val):
        s = '{:1f}'.format(val)
        return '{:.0f}'.format(val) if s[-1] == '0' else s

    con.levels = [bg_format(val) for val in con.levels]
    plt.clabel(con, con.levels, inline=True, fmt='%r', fontsize=8)


def _plot_pareto_optimal_lines(x_values, y_values):
    """ Plot pareto optimal lines """

    # Only loaded when run to reduce minimum requirements
    from matplotlib import pyplot as plt

    # Plot pareto optimal lines
    cxs = x_values
    cys = y_values
    best_y = np.argmax(cys)
    x_pareto = [0, cxs[best_y]]
    y_pareto = [cys[best_y], cys[best_y]]
    t = 2
    remaining = cxs > x_pareto[t - 1]
    cys = cys[remaining]
    cxs = cxs[remaining]
    while len(cxs) > 0 and len(cys) > 0:
        best_y = np.argmax(cys)
        x_pareto += [x_pareto[t - 1], cxs[best_y]]
        y_pareto += [cys[best_y], cys[best_y]]
        t += 2
        remaining = cxs > x_pareto[t - 1]
        cys = cys[remaining]
        cxs = cxs[remaining]
    x_pareto.append(x_pareto[t - 1])
    y_pareto.append(0)
    plt.plot(np.array(x_pareto), np.array(y_pareto), '--r')