Skip to content

metrics

sleap.info.metrics

Module for producing prediction metrics for SLEAP datasets.

Functions:

Name Description
calculate_pairwise_cost

Calculate (a * b) matrix of pairwise costs using cost function.

compare_instance_lists

Given two lists of corresponding Instances, returns

list_points_array

Given list of Instances, returns (instances * nodes * 2) matrix.

match_instance_lists

Sorts two lists of Instances to find best overall correspondence

match_instance_lists_nodewise

For each node for each instance in the first list, pairs it with the

matched_instance_distances

Distances between ground truth and predicted nodes over a set of frames.

nodeless_point_dist

Given two instances, returns array of distances for closest points

point_dist

Given two instances, returns array of distances for corresponding nodes.

point_match_count

Given an array of distances, returns number which are <= threshold.

point_nonmatch_count

Given an array of distances, returns number which are not <= threshold.

calculate_pairwise_cost(instances_a, instances_b, cost_function)

Calculate (a * b) matrix of pairwise costs using cost function.

Source code in sleap/info/metrics.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def calculate_pairwise_cost(
    instances_a: List[Union[Instance, PredictedInstance]],
    instances_b: List[Union[Instance, PredictedInstance]],
    cost_function: Callable,
) -> np.ndarray:
    """Calculate (a * b) matrix of pairwise costs using cost function."""

    matrix_size = (len(instances_a), len(instances_b))
    pairwise_cost_matrix = np.full(matrix_size, np.inf)
    for idx_a, inst_a in enumerate(instances_a):
        for idx_b, inst_b in enumerate(instances_b):
            # cost_function can either take a single input or two inputs
            # single input: ndarray of distances between corresponding nodes
            # two inputs: the pair of instances
            if len(signature(cost_function).parameters) == 1:
                point_dist_array = point_dist(inst_a, inst_b)
                cost = cost_function(point_dist_array)
            else:
                cost = cost_function(inst_a, inst_b)

            pairwise_cost_matrix[idx_a, idx_b] = cost
    return pairwise_cost_matrix

compare_instance_lists(instances_a, instances_b)

Given two lists of corresponding Instances, returns (instances * nodes) matrix of distances between corresponding nodes.

Source code in sleap/info/metrics.py
212
213
214
215
216
217
218
219
220
221
222
223
def compare_instance_lists(
    instances_a: List[Union[Instance, PredictedInstance]],
    instances_b: List[Union[Instance, PredictedInstance]],
) -> np.ndarray:
    """Given two lists of corresponding Instances, returns
    (instances * nodes) matrix of distances between corresponding nodes."""

    paired_points_array_distances = []
    for inst_a, inst_b in zip(instances_a, instances_b):
        paired_points_array_distances.append(point_dist(inst_a, inst_b))

    return np.stack(paired_points_array_distances)

list_points_array(instances)

Given list of Instances, returns (instances * nodes * 2) matrix.

Source code in sleap/info/metrics.py
226
227
228
229
230
231
232
233
def list_points_array(
    instances: List[Union[Instance, PredictedInstance]],
) -> np.ndarray:
    """Given list of Instances, returns (instances * nodes * 2) matrix."""
    from sleap.sleap_io_adaptors.instance_utils import instance_get_points_array

    points_arrays = list(map(lambda inst: instance_get_points_array(inst), instances))
    return np.stack(points_arrays)

match_instance_lists(instances_a, instances_b, cost_function)

Sorts two lists of Instances to find best overall correspondence for a given cost function (e.g., total distance between points).

Source code in sleap/info/metrics.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def match_instance_lists(
    instances_a: List[Union[Instance, PredictedInstance]],
    instances_b: List[Union[Instance, PredictedInstance]],
    cost_function: Callable,
) -> Tuple[
    List[Union[Instance, PredictedInstance]], List[Union[Instance, PredictedInstance]]
]:
    """Sorts two lists of Instances to find best overall correspondence
    for a given cost function (e.g., total distance between points)."""

    pairwise_distance_matrix = calculate_pairwise_cost(
        instances_a, instances_b, cost_function
    )
    match_a, match_b = linear_sum_assignment(pairwise_distance_matrix)

    sorted_a = list(map(lambda idx: instances_a[idx], match_a))
    sorted_b = list(map(lambda idx: instances_b[idx], match_b))
    return sorted_a, sorted_b

match_instance_lists_nodewise(instances_a, instances_b, thresh=5)

For each node for each instance in the first list, pairs it with the closest corresponding node from any instance in the second list.

Source code in sleap/info/metrics.py
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
def match_instance_lists_nodewise(
    instances_a: List[Union[Instance, PredictedInstance]],
    instances_b: List[Union[Instance, PredictedInstance]],
    thresh: float = 5,
) -> Tuple[
    List[Union[Instance, PredictedInstance]], List[Union[Instance, PredictedInstance]]
]:
    """For each node for each instance in the first list, pairs it with the
    closest corresponding node from *any* instance in the second list."""

    node_count = len(instances_a[0].skeleton.nodes)
    b_points_arrays = list_points_array(instances_b)

    best_points_array = []

    for inst_a in instances_a:
        # Calculate distance from nodes in A to nodes for each B
        dist_array = []
        for inst_b in instances_b:
            dist_array.append(point_dist(inst_a, inst_b))
        dist_array = np.stack(dist_array)

        # Construct matrix with closest node from any B
        closest_point_array = np.full((node_count, 2), np.nan)
        for node_idx in range(node_count):
            # Make sure there's some prediction for this node
            if any(~np.isnan(dist_array[:, node_idx])):
                best_idx = np.nanargmin(dist_array[:, node_idx])

                # Ignore closest point if distance is beyond threshold
                if dist_array[best_idx, node_idx] <= thresh:
                    closest_point_array[node_idx] = b_points_arrays[best_idx, node_idx]

        # Add matrix of points to compare against ground truth instance
        best_points_array.append(closest_point_array)

    return instances_a, best_points_array

matched_instance_distances(labels_gt, labels_pr, match_lists_function=match_instance_lists_nodewise, frame_range=None)

Distances between ground truth and predicted nodes over a set of frames.

Parameters:

Name Type Description Default
labels_gt Labels

the Labels object with ground truth data

required
labels_pr Labels

the Labels object with predicted data

required
match_lists_function Callable

function for determining corresponding instances Takes two lists of instances and returns "sorted" lists.

match_instance_lists_nodewise
frame_range optional

range of frames for which to compare data If None, we compare every frame in labels_gt with corresponding frame in labels_pr.

None
Source code in sleap/info/metrics.py
 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
def matched_instance_distances(
    labels_gt: Labels,
    labels_pr: Labels,
    match_lists_function: Callable = match_instance_lists_nodewise,
    frame_range: Optional[range] = None,
) -> Tuple[List[int], np.ndarray, np.ndarray, np.ndarray]:
    """
    Distances between ground truth and predicted nodes over a set of frames.

    Args:
        labels_gt: the `Labels` object with ground truth data
        labels_pr: the `Labels` object with predicted data
        match_lists_function: function for determining corresponding instances
            Takes two lists of instances and returns "sorted" lists.
        frame_range (optional): range of frames for which to compare data
            If None, we compare every frame in labels_gt with corresponding
            frame in labels_pr.
    Returns:
        Tuple:
        * frame indices map: instance idx (for other matrices) -> frame idx
        * distance matrix: (instances * nodes)
        * ground truth points matrix: (instances * nodes * 2)
        * predicted points matrix: (instances * nodes * 2)
    """

    frame_idxs = []
    points_gt = []
    points_pr = []
    for lf_gt in labels_gt.find(labels_gt.videos[0]):
        frame_idx = lf_gt.frame_idx

        # Get instances from ground truth/predicted labels
        instances_gt = lf_gt.instances
        lfs_pr = labels_pr.find(labels_pr.videos[0], frame_idx=frame_idx)
        if len(lfs_pr):
            instances_pr = lfs_pr[0].instances
        else:
            instances_pr = []

        # Sort ground truth and predicted instances.
        # We'll then compare points between corresponding items in lists.
        # We can use different "match" functions depending on what we want.
        sorted_gt, sorted_pr = match_lists_function(instances_gt, instances_pr)

        # Convert lists of instances to (instances, nodes, 2) matrices.
        # This allows match_lists_function to return data as either
        # a list of Instances or a (instances, nodes, 2) matrix.
        if type(sorted_gt[0]) != np.ndarray:
            sorted_gt = list_points_array(sorted_gt)
        if type(sorted_pr[0]) != np.ndarray:
            sorted_pr = list_points_array(sorted_pr)

        points_gt.append(sorted_gt)
        points_pr.append(sorted_pr)
        frame_idxs.extend([frame_idx] * len(sorted_gt))

    # Convert arrays to numpy matrixes
    # instances * nodes * (x,y)
    points_gt = np.concatenate(points_gt)
    points_pr = np.concatenate(points_pr)

    # Calculate distances between corresponding nodes for all corresponding
    # ground truth and predicted instances.
    D = np.linalg.norm(points_gt - points_pr, axis=2)

    return frame_idxs, D, points_gt, points_pr

nodeless_point_dist(inst_a, inst_b)

Given two instances, returns array of distances for closest points ignoring node identities.

Source code in sleap/info/metrics.py
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
def nodeless_point_dist(
    inst_a: Union[Instance, PredictedInstance],
    inst_b: Union[Instance, PredictedInstance],
) -> np.ndarray:
    """Given two instances, returns array of distances for closest points
    ignoring node identities."""

    matrix_size = (len(inst_a.skeleton.nodes), len(inst_b.skeleton.nodes))
    pairwise_distance_matrix = np.full(matrix_size, 0)

    from sleap.sleap_io_adaptors.instance_utils import instance_get_points_array

    points_a = instance_get_points_array(inst_a)
    points_b = instance_get_points_array(inst_b)

    # Calculate the distance between any pair of inst A and inst B points
    for idx_a in range(points_a.shape[0]):
        for idx_b in range(points_b.shape[0]):
            pair_distance = np.linalg.norm(points_a[idx_a] - points_b[idx_b])
            if not np.isnan(pair_distance):
                pairwise_distance_matrix[idx_a, idx_b] = pair_distance

    # Match A and B points to sum of distances
    match_a, match_b = linear_sum_assignment(pairwise_distance_matrix)

    # Sort points by this match and calculate overall distance
    points_a[match_a, :]
    points_b[match_b, :]
    point_dist = np.linalg.norm(points_a - points_b, axis=1)

    return point_dist

point_dist(inst_a, inst_b)

Given two instances, returns array of distances for corresponding nodes.

Source code in sleap/info/metrics.py
165
166
167
168
169
170
171
172
173
174
175
176
def point_dist(
    inst_a: Union[Instance, PredictedInstance],
    inst_b: Union[Instance, PredictedInstance],
) -> np.ndarray:
    """Given two instances, returns array of distances for corresponding nodes."""

    from sleap.sleap_io_adaptors.instance_utils import instance_get_points_array

    points_a = instance_get_points_array(inst_a)
    points_b = instance_get_points_array(inst_b)
    point_dist = np.linalg.norm(points_a - points_b, axis=1)
    return point_dist

point_match_count(dist_array, thresh=5)

Given an array of distances, returns number which are <= threshold.

Source code in sleap/info/metrics.py
236
237
238
def point_match_count(dist_array: np.ndarray, thresh: float = 5) -> int:
    """Given an array of distances, returns number which are <= threshold."""
    return np.sum(dist_array[~np.isnan(dist_array)] <= thresh)

point_nonmatch_count(dist_array, thresh=5)

Given an array of distances, returns number which are not <= threshold.

Source code in sleap/info/metrics.py
241
242
243
def point_nonmatch_count(dist_array: np.ndarray, thresh: float = 5) -> int:
    """Given an array of distances, returns number which are not <= threshold."""
    return dist_array.shape[0] - point_match_count(dist_array, thresh)