Skip to content

Distances#

A distance function decides, for a given frame, how close each incoming Detection is to every existing TrackedObject. The Tracker then uses these pairwise distances to solve the matching problem (small distance → likely the same object).

Norfair ships with a handful of built-in distances, plus utilities for building your own — either scalar (one detection / one track at a time) or vectorized (all pairs at once, using NumPy).

Built-in distances#

You can pass any of these to Tracker(distance_function=...) by name:

Name Best for Notes
"euclidean" Single-point detections (centroids, keypoints). Vectorized via ScipyDistance (cdist). Mathematically equivalent to frobenius for single-point detections, but faster because it avoids the per-pair Python loop.
"mean_euclidean" Multi-point detections (keypoints, polygons). Averages per-point Euclidean distance.
"mean_manhattan" Multi-point detections, cheaper than Euclidean. Averages per-point L1 distance.
"frobenius" Flattened L2 between all points. Loop-based ScalarDistance. Equivalent to "euclidean" for single-point detections.
"iou" Bounding boxes. 1 - IoU, so smaller is better. Requires (2, 2) top-left / bottom-right point arrays.
"iou_opt" Bounding boxes, large detection counts. Vectorized, faster variant of "iou".

Any name accepted by scipy.spatial.distance.cdist also works and is wrapped automatically as a ScipyDistance.

1
2
3
4
5
6
7
8
from norfair import Tracker

# Centroid tracking with plain Euclidean distance.
tracker = Tracker(distance_function="euclidean", distance_threshold=50)

# Bounding-box tracking with IoU. Threshold is in `1 - IoU` space,
# so 0.7 means "match if IoU >= 0.3".
bbox_tracker = Tracker(distance_function="iou", distance_threshold=0.7)

Parameterized distances#

Two factory helpers produce distances tailored to your data:

1
2
3
4
5
from norfair import Tracker
from norfair.distances import create_normalized_mean_euclidean_distance

distance = create_normalized_mean_euclidean_distance(height=1080, width=1920)
tracker = Tracker(distance_function=distance, distance_threshold=0.05)

Custom distances#

For appearance-aware tracking (embeddings, ReID, color histograms, …) you can pass any Callable[[Detection, TrackedObject], float] as distance_function, or — for bulk performance — subclass VectorizedDistance and compute all pairs at once.

API#

Predefined distance functions and the :class:Distance base class.

Distance #

Bases: ABC

Abstract base class representing a tracker distance.

Subclasses must implement :meth:get_distances, which returns a distance matrix between tracked objects and candidates (detections or other tracked objects, when ReID is in use).

Source code in norfair/distances.py
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
class Distance(ABC):
    """Abstract base class representing a tracker distance.

    Subclasses must implement :meth:`get_distances`, which returns a
    distance matrix between tracked objects and candidates (detections or
    other tracked objects, when ReID is in use).
    """

    @abstractmethod
    def get_distances(
        self,
        objects: Sequence["TrackedObject"],
        candidates: Sequence["Candidate"] | None,
    ) -> NDArray[np.float64]:
        """Return the distance matrix between ``objects`` and ``candidates``.

        Parameters
        ----------
        objects : Sequence[TrackedObject]
            Sequence of [TrackedObject][norfair.tracker.TrackedObject]
            instances currently being tracked.
        candidates : Sequence[Detection or TrackedObject], optional
            Candidates to be compared against the tracked ``objects``.
            Detections are used during the normal matching step; tracked
            objects are used during ReID.

        Returns
        -------
        np.ndarray
            A ``(n_candidates, n_objects)`` matrix of distances.

        """

get_distances(objects, candidates) abstractmethod #

Return the distance matrix between objects and candidates.

Parameters:

Name Type Description Default
objects Sequence[TrackedObject]

Sequence of TrackedObject instances currently being tracked.

required
candidates Sequence[Detection or TrackedObject]

Candidates to be compared against the tracked objects. Detections are used during the normal matching step; tracked objects are used during ReID.

required

Returns:

Type Description
ndarray

A (n_candidates, n_objects) matrix of distances.

Source code in norfair/distances.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@abstractmethod
def get_distances(
    self,
    objects: Sequence["TrackedObject"],
    candidates: Sequence["Candidate"] | None,
) -> NDArray[np.float64]:
    """Return the distance matrix between ``objects`` and ``candidates``.

    Parameters
    ----------
    objects : Sequence[TrackedObject]
        Sequence of [TrackedObject][norfair.tracker.TrackedObject]
        instances currently being tracked.
    candidates : Sequence[Detection or TrackedObject], optional
        Candidates to be compared against the tracked ``objects``.
        Detections are used during the normal matching step; tracked
        objects are used during ReID.

    Returns
    -------
    np.ndarray
        A ``(n_candidates, n_objects)`` matrix of distances.

    """

ScalarDistance #

Bases: Distance

Distance computed pointwise (one pair at a time).

Parameters:

Name Type Description Default
distance_function Callable

Function used to compute the distance between a pair. It must accept two positional arguments — a Detection or TrackedObject and a TrackedObject — and return a float.

required
Source code in norfair/distances.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
 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
class ScalarDistance(Distance):
    """Distance computed pointwise (one pair at a time).

    Parameters
    ----------
    distance_function : Callable
        Function used to compute the distance between a pair. It must
        accept two positional arguments — a ``Detection`` or
        ``TrackedObject`` and a ``TrackedObject`` — and return a ``float``.

    """

    @overload
    def __init__(
        self,
        distance_function: Callable[["Detection", "TrackedObject"], float],
    ): ...

    @overload
    def __init__(
        self,
        distance_function: Callable[["TrackedObject", "TrackedObject"], float],
    ): ...

    def __init__(
        self,
        distance_function: Callable[["Detection", "TrackedObject"], float]
        | Callable[["TrackedObject", "TrackedObject"], float],
    ):
        """Store the per-pair ``distance_function``.

        The two overloads (detection→object and object→object) are both
        valid at runtime — Python's duck typing handles the dispatch.
        """
        self.distance_function: Callable = distance_function

    def get_distances(
        self,
        objects: Sequence["TrackedObject"],
        candidates: Sequence["Candidate"] | None,
    ) -> NDArray[np.float64]:
        """Return a distance matrix by calling ``distance_function`` for every pair.

        Pairs with mismatched labels are skipped and their entries left at
        ``np.inf``.

        Parameters
        ----------
        objects : Sequence[TrackedObject]
            Tracked objects to compare against ``candidates``.
        candidates : Sequence[Detection or TrackedObject], optional
            Candidates. ``None`` or empty sequences return a matrix filled
            with ``np.inf``.

        Returns
        -------
        np.ndarray
            A ``(n_candidates, n_objects)`` matrix of distances.

        """
        if not objects or not candidates:
            # Handle None or empty cases
            num_candidates = len(candidates) if candidates is not None else 0
            distance_matrix = np.full(
                (num_candidates, len(objects)),
                fill_value=np.inf,
                dtype=np.float64,
            )
            return distance_matrix

        distance_matrix = np.full(
            (len(candidates), len(objects)),
            fill_value=np.inf,
            dtype=np.float64,
        )
        for c, candidate in enumerate(candidates):
            for o, obj in enumerate(objects):
                if candidate.label != obj.label:
                    if (candidate.label is None) or (obj.label is None):
                        logger.warning(
                            "Label mismatch between candidate and tracked "
                            "object: candidate.label=%r, object.label=%r. "
                            "Mixing labelled and unlabelled inputs prevents "
                            "these pairs from ever matching.",
                            candidate.label,
                            obj.label,
                        )
                    continue
                distance = self.distance_function(candidate, obj)
                distance_matrix[c, o] = distance
        return distance_matrix

__init__(distance_function) #

__init__(distance_function: Callable[[Detection, TrackedObject], float])
__init__(distance_function: Callable[[TrackedObject, TrackedObject], float])

Store the per-pair distance_function.

The two overloads (detection→object and object→object) are both valid at runtime — Python's duck typing handles the dispatch.

Source code in norfair/distances.py
82
83
84
85
86
87
88
89
90
91
92
def __init__(
    self,
    distance_function: Callable[["Detection", "TrackedObject"], float]
    | Callable[["TrackedObject", "TrackedObject"], float],
):
    """Store the per-pair ``distance_function``.

    The two overloads (detection→object and object→object) are both
    valid at runtime — Python's duck typing handles the dispatch.
    """
    self.distance_function: Callable = distance_function

get_distances(objects, candidates) #

Return a distance matrix by calling distance_function for every pair.

Pairs with mismatched labels are skipped and their entries left at np.inf.

Parameters:

Name Type Description Default
objects Sequence[TrackedObject]

Tracked objects to compare against candidates.

required
candidates Sequence[Detection or TrackedObject]

Candidates. None or empty sequences return a matrix filled with np.inf.

required

Returns:

Type Description
ndarray

A (n_candidates, n_objects) matrix of distances.

Source code in norfair/distances.py
 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
def get_distances(
    self,
    objects: Sequence["TrackedObject"],
    candidates: Sequence["Candidate"] | None,
) -> NDArray[np.float64]:
    """Return a distance matrix by calling ``distance_function`` for every pair.

    Pairs with mismatched labels are skipped and their entries left at
    ``np.inf``.

    Parameters
    ----------
    objects : Sequence[TrackedObject]
        Tracked objects to compare against ``candidates``.
    candidates : Sequence[Detection or TrackedObject], optional
        Candidates. ``None`` or empty sequences return a matrix filled
        with ``np.inf``.

    Returns
    -------
    np.ndarray
        A ``(n_candidates, n_objects)`` matrix of distances.

    """
    if not objects or not candidates:
        # Handle None or empty cases
        num_candidates = len(candidates) if candidates is not None else 0
        distance_matrix = np.full(
            (num_candidates, len(objects)),
            fill_value=np.inf,
            dtype=np.float64,
        )
        return distance_matrix

    distance_matrix = np.full(
        (len(candidates), len(objects)),
        fill_value=np.inf,
        dtype=np.float64,
    )
    for c, candidate in enumerate(candidates):
        for o, obj in enumerate(objects):
            if candidate.label != obj.label:
                if (candidate.label is None) or (obj.label is None):
                    logger.warning(
                        "Label mismatch between candidate and tracked "
                        "object: candidate.label=%r, object.label=%r. "
                        "Mixing labelled and unlabelled inputs prevents "
                        "these pairs from ever matching.",
                        candidate.label,
                        obj.label,
                    )
                continue
            distance = self.distance_function(candidate, obj)
            distance_matrix[c, o] = distance
    return distance_matrix

VectorizedDistance #

Bases: Distance

Distance computed in a single vectorized operation.

Rather than iterating over every pair of candidate and tracked object, VectorizedDistance stacks their coordinates and hands the whole batch to distance_function in one call — much faster for large numbers of objects.

Parameters:

Name Type Description Default
distance_function Callable[[NDArray[float64], NDArray[float64]], NDArray[float64]]

Distance function that accepts two 2D arrays (candidates, objects) and returns a (n_candidates, n_objects) distance matrix.

required
Source code in norfair/distances.py
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
class VectorizedDistance(Distance):
    """Distance computed in a single vectorized operation.

    Rather than iterating over every pair of candidate and tracked object,
    ``VectorizedDistance`` stacks their coordinates and hands the whole
    batch to ``distance_function`` in one call — much faster for large
    numbers of objects.

    Parameters
    ----------
    distance_function : Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
        Distance function that accepts two 2D arrays ``(candidates,
        objects)`` and returns a ``(n_candidates, n_objects)`` distance
        matrix.

    """

    def __init__(
        self,
        distance_function: Callable[
            [NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
        ],
    ):
        """Store the vectorized ``distance_function``."""
        self.distance_function = distance_function

    def get_distances(
        self,
        objects: Sequence["TrackedObject"],
        candidates: Sequence["Candidate"] | None,
    ) -> NDArray[np.float64]:
        """Return the distance matrix computed per label group.

        Objects and candidates are grouped by label; for each label the
        corresponding sub-block of the distance matrix is filled by
        ``distance_function`` called on the stacked coordinates. Entries
        across different labels remain ``np.inf``.

        Parameters
        ----------
        objects : Sequence[TrackedObject]
            Tracked objects to compare against ``candidates``.
        candidates : Sequence[Detection or TrackedObject], optional
            Candidates. ``None`` or empty sequences return a matrix filled
            with ``np.inf``.

        Returns
        -------
        np.ndarray
            A ``(n_candidates, n_objects)`` matrix of distances.

        """
        if not objects or not candidates:
            # Handle None or empty cases
            num_candidates = len(candidates) if candidates is not None else 0
            distance_matrix = np.full(
                (num_candidates, len(objects)),
                fill_value=np.inf,
                dtype=np.float64,
            )
            return distance_matrix

        distance_matrix = np.full(
            (len(candidates), len(objects)),
            fill_value=np.inf,
            dtype=np.float64,
        )

        from .tracker import Detection

        # Group by the raw Hashable label so ``None`` and the string
        # ``"None"`` remain distinct keys.
        obj_groups: dict = defaultdict(list)
        for i, o in enumerate(objects):
            obj_groups[o.label].append(i)
        cand_groups: dict = defaultdict(list)
        for i, c in enumerate(candidates):
            cand_groups[c.label].append(i)

        # iterate over labels that are present both in objects and detections
        for label, obj_idx in obj_groups.items():
            cand_idx = cand_groups.get(label)
            if not cand_idx:
                continue

            stacked_objects = np.stack([objects[i].estimate.ravel() for i in obj_idx])
            stacked_candidates = np.stack(
                [
                    candidates[i].points.ravel()
                    if isinstance(candidates[i], Detection)
                    else candidates[i].estimate.ravel()
                    for i in cand_idx
                ]
            )

            # calculate the pairwise distances between objects and candidates with this label
            # and assign the result to the correct positions inside distance_matrix
            distance_matrix[np.ix_(cand_idx, obj_idx)] = self._compute_distance(
                stacked_candidates, stacked_objects
            )

        return distance_matrix

    def _compute_distance(
        self,
        stacked_candidates: NDArray[np.float64],
        stacked_objects: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        """Compute the pairwise distance between stacked candidates and objects.

        Parameters
        ----------
        stacked_candidates : np.ndarray
            Stacked candidate coordinates.
        stacked_objects : np.ndarray
            Stacked object coordinates.

        Returns
        -------
        np.ndarray
            A ``(n_candidates, n_objects)`` matrix of distances.

        """
        return self.distance_function(stacked_candidates, stacked_objects)

__init__(distance_function) #

Store the vectorized distance_function.

Source code in norfair/distances.py
168
169
170
171
172
173
174
175
def __init__(
    self,
    distance_function: Callable[
        [NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]
    ],
):
    """Store the vectorized ``distance_function``."""
    self.distance_function = distance_function

get_distances(objects, candidates) #

Return the distance matrix computed per label group.

Objects and candidates are grouped by label; for each label the corresponding sub-block of the distance matrix is filled by distance_function called on the stacked coordinates. Entries across different labels remain np.inf.

Parameters:

Name Type Description Default
objects Sequence[TrackedObject]

Tracked objects to compare against candidates.

required
candidates Sequence[Detection or TrackedObject]

Candidates. None or empty sequences return a matrix filled with np.inf.

required

Returns:

Type Description
ndarray

A (n_candidates, n_objects) matrix of distances.

Source code in norfair/distances.py
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def get_distances(
    self,
    objects: Sequence["TrackedObject"],
    candidates: Sequence["Candidate"] | None,
) -> NDArray[np.float64]:
    """Return the distance matrix computed per label group.

    Objects and candidates are grouped by label; for each label the
    corresponding sub-block of the distance matrix is filled by
    ``distance_function`` called on the stacked coordinates. Entries
    across different labels remain ``np.inf``.

    Parameters
    ----------
    objects : Sequence[TrackedObject]
        Tracked objects to compare against ``candidates``.
    candidates : Sequence[Detection or TrackedObject], optional
        Candidates. ``None`` or empty sequences return a matrix filled
        with ``np.inf``.

    Returns
    -------
    np.ndarray
        A ``(n_candidates, n_objects)`` matrix of distances.

    """
    if not objects or not candidates:
        # Handle None or empty cases
        num_candidates = len(candidates) if candidates is not None else 0
        distance_matrix = np.full(
            (num_candidates, len(objects)),
            fill_value=np.inf,
            dtype=np.float64,
        )
        return distance_matrix

    distance_matrix = np.full(
        (len(candidates), len(objects)),
        fill_value=np.inf,
        dtype=np.float64,
    )

    from .tracker import Detection

    # Group by the raw Hashable label so ``None`` and the string
    # ``"None"`` remain distinct keys.
    obj_groups: dict = defaultdict(list)
    for i, o in enumerate(objects):
        obj_groups[o.label].append(i)
    cand_groups: dict = defaultdict(list)
    for i, c in enumerate(candidates):
        cand_groups[c.label].append(i)

    # iterate over labels that are present both in objects and detections
    for label, obj_idx in obj_groups.items():
        cand_idx = cand_groups.get(label)
        if not cand_idx:
            continue

        stacked_objects = np.stack([objects[i].estimate.ravel() for i in obj_idx])
        stacked_candidates = np.stack(
            [
                candidates[i].points.ravel()
                if isinstance(candidates[i], Detection)
                else candidates[i].estimate.ravel()
                for i in cand_idx
            ]
        )

        # calculate the pairwise distances between objects and candidates with this label
        # and assign the result to the correct positions inside distance_matrix
        distance_matrix[np.ix_(cand_idx, obj_idx)] = self._compute_distance(
            stacked_candidates, stacked_objects
        )

    return distance_matrix

ScipyDistance #

Bases: VectorizedDistance

Vectorized distance backed by scipy.spatial.distance.cdist.

Uses scipy.spatial.distance.cdist to calculate distances between two np.ndarray batches.

Parameters:

Name Type Description Default
metric str

Defines the specific Scipy metric to use to calculate the pairwise distances between new candidates and objects.

'euclidean'
**kwargs

Additional keyword arguments forwarded to scipy.spatial.distance.cdist.

{}
See Also

scipy.spatial.distance.cdist

Source code in norfair/distances.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
class ScipyDistance(VectorizedDistance):
    """Vectorized distance backed by ``scipy.spatial.distance.cdist``.

    Uses [`scipy.spatial.distance.cdist`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html)
    to calculate distances between two ``np.ndarray`` batches.

    Parameters
    ----------
    metric : str, optional
        Defines the specific Scipy metric to use to calculate the pairwise distances between
        new candidates and objects.
    **kwargs
        Additional keyword arguments forwarded to
        `scipy.spatial.distance.cdist`.

    See Also
    --------
    [`scipy.spatial.distance.cdist`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html)

    """

    def __init__(self, metric: str = "euclidean", **kwargs):
        """Configure the scipy metric and any extra ``cdist`` keyword arguments."""
        self.metric = metric
        self._cdist = partial(cdist, metric=self.metric, **kwargs)
        super().__init__(distance_function=self._cdist_float64)

    def _cdist_float64(
        self,
        candidates: NDArray[np.float64],
        objects: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        """Wrap ``cdist`` and ensure the result is ``float64``."""
        return np.asarray(self._cdist(candidates, objects), dtype=np.float64)

__init__(metric='euclidean', **kwargs) #

Configure the scipy metric and any extra cdist keyword arguments.

Source code in norfair/distances.py
298
299
300
301
302
def __init__(self, metric: str = "euclidean", **kwargs):
    """Configure the scipy metric and any extra ``cdist`` keyword arguments."""
    self.metric = metric
    self._cdist = partial(cdist, metric=self.metric, **kwargs)
    super().__init__(distance_function=self._cdist_float64)

frobenius(detection, tracked_object) #

Frobenius norm of the difference between detection points and tracked-object estimates.

The Frobenius distance and norm are given by:

\[ d_f(a, b) = ||a - b||_F \]
\[ ||A||_F = [\\sum_{i,j} abs(a_{i,j})^2]^{1/2} \]

Parameters:

Name Type Description Default
detection Detection

A detection.

required
tracked_object TrackedObject

A tracked object.

required

Returns:

Type Description
float

The distance.

See Also

np.linalg.norm

Source code in norfair/distances.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def frobenius(detection: "Detection", tracked_object: "TrackedObject") -> float:
    r"""Frobenius norm of the difference between detection points and tracked-object estimates.

    The Frobenius distance and norm are given by:

    $$
    d_f(a, b) = ||a - b||_F
    $$

    $$
    ||A||_F = [\\sum_{i,j} abs(a_{i,j})^2]^{1/2}
    $$

    Parameters
    ----------
    detection : Detection
        A detection.
    tracked_object : TrackedObject
        A tracked object.

    Returns
    -------
    float
        The distance.

    See Also
    --------
    [`np.linalg.norm`](https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html)

    """
    return float(np.linalg.norm(detection.points - tracked_object.estimate))

mean_euclidean(detection, tracked_object) #

Average Euclidean distance between detection points and tracked-object estimates.

\[ d(a, b) = \frac{\sum_{i=0}^N ||a_i - b_i||_2}{N} \]

Parameters:

Name Type Description Default
detection Detection

A detection.

required
tracked_object TrackedObject

A tracked object.

required

Returns:

Type Description
float

The distance.

Raises:

Type Description
ValueError

If either input is empty or contains non-finite (NaN/Inf) values.

See Also

np.linalg.norm

Source code in norfair/distances.py
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
def mean_euclidean(detection: "Detection", tracked_object: "TrackedObject") -> float:
    r"""Average Euclidean distance between detection points and tracked-object estimates.

    $$
    d(a, b) = \frac{\sum_{i=0}^N ||a_i - b_i||_2}{N}
    $$

    Parameters
    ----------
    detection : Detection
        A detection.
    tracked_object : TrackedObject
        A tracked object.

    Returns
    -------
    float
        The distance.

    Raises
    ------
    ValueError
        If either input is empty or contains non-finite (NaN/Inf) values.

    See Also
    --------
    [`np.linalg.norm`](https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html)

    """
    points = detection.points
    estimate = tracked_object.estimate
    if points.size == 0 or estimate.size == 0:
        raise ValueError(
            "mean_euclidean received empty points array; "
            "check that the detection/tracked object has valid points."
        )
    if not np.all(np.isfinite(points)) or not np.all(np.isfinite(estimate)):
        raise ValueError(
            "mean_euclidean received non-finite (NaN/Inf) input; "
            "check that the detection/tracked object has valid points."
        )
    return np.linalg.norm(points - estimate, axis=1).mean()

mean_manhattan(detection, tracked_object) #

Average Manhattan distance between detection points and tracked-object estimates.

\[ d(a, b) = \frac{\sum_{i=0}^N ||a_i - b_i||_1}{N} \]

Where \(||a||_1\) is the Manhattan norm.

Parameters:

Name Type Description Default
detection Detection

A detection.

required
tracked_object TrackedObject

A tracked object.

required

Returns:

Type Description
float

The distance.

Raises:

Type Description
ValueError

If either input is empty or contains non-finite (NaN/Inf) values.

See Also

np.linalg.norm

Source code in norfair/distances.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def mean_manhattan(detection: "Detection", tracked_object: "TrackedObject") -> float:
    r"""Average Manhattan distance between detection points and tracked-object estimates.

    $$
    d(a, b) = \frac{\sum_{i=0}^N ||a_i - b_i||_1}{N}
    $$

    Where $||a||_1$ is the Manhattan norm.

    Parameters
    ----------
    detection : Detection
        A detection.
    tracked_object : TrackedObject
        A tracked object.

    Returns
    -------
    float
        The distance.

    Raises
    ------
    ValueError
        If either input is empty or contains non-finite (NaN/Inf) values.

    See Also
    --------
    [`np.linalg.norm`](https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html)

    """
    points = detection.points
    estimate = tracked_object.estimate
    if points.size == 0 or estimate.size == 0:
        raise ValueError(
            "mean_manhattan received empty points array; "
            "check that the detection/tracked object has valid points."
        )
    if not np.all(np.isfinite(points)) or not np.all(np.isfinite(estimate)):
        raise ValueError(
            "mean_manhattan received non-finite (NaN/Inf) input; "
            "check that the detection/tracked object has valid points."
        )
    return np.linalg.norm(points - estimate, ord=1, axis=1).mean()

iou(candidates, objects) #

Compute 1 - IoU between two sets of bounding boxes.

Both sets of boxes are expected to be in [x_min, y_min, x_max, y_max] format.

Normal IoU is 1 when the boxes are identical and 0 when they don't overlap; to turn this into a distance the function returns 1 - IoU.

Parameters:

Name Type Description Default
candidates ndarray

(N, 4) array of candidate bounding boxes.

required
objects ndarray

(K, 4) array of object bounding boxes.

required

Returns:

Type Description
ndarray

(N, K) array of 1 - IoU values between candidates and objects.

Source code in norfair/distances.py
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def iou(
    candidates: NDArray[np.float64], objects: NDArray[np.float64]
) -> NDArray[np.float64]:
    """Compute ``1 - IoU`` between two sets of bounding boxes.

    Both sets of boxes are expected to be in
    ``[x_min, y_min, x_max, y_max]`` format.

    Normal IoU is ``1`` when the boxes are identical and ``0`` when they
    don't overlap; to turn this into a distance the function returns
    ``1 - IoU``.

    Parameters
    ----------
    candidates : np.ndarray
        ``(N, 4)`` array of candidate bounding boxes.
    objects : np.ndarray
        ``(K, 4)`` array of object bounding boxes.

    Returns
    -------
    np.ndarray
        ``(N, K)`` array of ``1 - IoU`` values between candidates and
        objects.

    """
    _validate_bboxes(candidates)
    _validate_bboxes(objects)

    area_candidates = _boxes_area(candidates.T)
    area_objects = _boxes_area(objects.T)

    top_left = np.maximum(candidates[:, None, :2], objects[:, :2])
    bottom_right = np.minimum(candidates[:, None, 2:], objects[:, 2:])

    area_intersection = np.prod(
        np.clip(bottom_right - top_left, a_min=0, a_max=None), 2
    )
    union = area_candidates[:, None] + area_objects - area_intersection
    # Guard against zero-area (degenerate) bounding boxes: use np.divide with
    # where= to avoid evaluating the division when union is zero.
    iou_values = np.divide(
        area_intersection,
        union,
        out=np.zeros_like(area_intersection, dtype=float),
        where=union > 0,
    )
    return 1 - iou_values

get_distance_by_name(name) #

Return a predefined :class:Distance by name.

Accepts the names of Norfair's built-in scalar and vectorized distances, as well as any metric supported by scipy.spatial.distance.cdist.

Parameters:

Name Type Description Default
name str

Name of the distance to look up.

required

Returns:

Type Description
Distance

A distance object ready to be passed to Tracker.

Raises:

Type Description
ValueError

If name is not a known distance.

Source code in norfair/distances.py
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
def get_distance_by_name(name: str) -> Distance:
    """Return a predefined :class:`Distance` by name.

    Accepts the names of Norfair's built-in scalar and vectorized
    distances, as well as any metric supported by
    [`scipy.spatial.distance.cdist`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html).

    Parameters
    ----------
    name : str
        Name of the distance to look up.

    Returns
    -------
    Distance
        A distance object ready to be passed to ``Tracker``.

    Raises
    ------
    ValueError
        If ``name`` is not a known distance.

    """
    distance_function: Distance
    if name in _SCALAR_DISTANCE_FUNCTIONS:
        logger.warning(
            "You are using a scalar distance function. If you want to speed up the"
            " tracking process please consider using a vectorized distance function"
            f" such as {AVAILABLE_VECTORIZED_DISTANCES}."
        )
        distance_function = ScalarDistance(_SCALAR_DISTANCE_FUNCTIONS[name])
    elif name in _SCIPY_DISTANCE_FUNCTIONS:
        distance_function = ScipyDistance(name)
    elif name in _VECTORIZED_DISTANCE_FUNCTIONS:
        if name == "iou_opt":
            logger.warning("iou_opt is deprecated, use iou instead")
        distance_function = VectorizedDistance(_VECTORIZED_DISTANCE_FUNCTIONS[name])
    else:
        raise ValueError(
            f"Invalid distance '{name}', expecting one of"
            f" {list(_SCALAR_DISTANCE_FUNCTIONS.keys()) + AVAILABLE_VECTORIZED_DISTANCES}"
        )

    return distance_function

create_keypoints_voting_distance(keypoint_distance_threshold, detection_threshold) #

Build a keypoint-voting scalar distance bound to the given thresholds.

The returned distance counts how many points in a detection match the points in a tracked object. A point counts as a match when the distance between it and its peer is below keypoint_distance_threshold and both detection and tracked-object scores exceed detection_threshold. The i-th point in a detection can only match the i-th point in a tracked object.

The distance is 1 when nothing matches and tends towards 0 as more points match.

Parameters:

Name Type Description Default
keypoint_distance_threshold float

Points closer than this threshold count as a match.

required
detection_threshold float

Points with score at or below this threshold are ignored.

required

Returns:

Type Description
Callable

A scalar distance function that can be passed to Tracker.

Source code in norfair/distances.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
def create_keypoints_voting_distance(
    keypoint_distance_threshold: float, detection_threshold: float
) -> Callable[["Detection", "TrackedObject"], float]:
    """Build a keypoint-voting scalar distance bound to the given thresholds.

    The returned distance counts how many points in a detection match the
    points in a tracked object. A point counts as a match when the
    distance between it and its peer is below
    ``keypoint_distance_threshold`` and both detection and tracked-object
    scores exceed ``detection_threshold``. The ``i``-th point in a
    detection can only match the ``i``-th point in a tracked object.

    The distance is ``1`` when nothing matches and tends towards ``0`` as
    more points match.

    Parameters
    ----------
    keypoint_distance_threshold : float
        Points closer than this threshold count as a match.
    detection_threshold : float
        Points with score at or below this threshold are ignored.

    Returns
    -------
    Callable
        A scalar distance function that can be passed to ``Tracker``.

    """

    def keypoints_voting_distance(
        detection: "Detection", tracked_object: "TrackedObject"
    ) -> float:
        """Return a voting-based distance in ``[0, 1]`` using nearby keypoints.

        Parameters
        ----------
        detection : Detection
            Incoming detection whose keypoints and scores are compared.
        tracked_object : TrackedObject
            Existing tracked object providing the estimated keypoints.

        Returns
        -------
        float
            Distance in ``[0, 1]``. Returns ``1.0`` when scores are
            unavailable or no keypoints match, tends towards ``0`` as more
            keypoints match.
        """
        if detection.scores is None or tracked_object.last_detection.scores is None:
            return 1.0
        distances = np.linalg.norm(detection.points - tracked_object.estimate, axis=1)
        match_num = np.count_nonzero(
            (distances < keypoint_distance_threshold)
            * (detection.scores > detection_threshold)
            * (tracked_object.last_detection.scores > detection_threshold)
        )
        return 1 / (1 + match_num)

    return keypoints_voting_distance

create_normalized_mean_euclidean_distance(height, width) #

Build a normalized mean Euclidean distance bound to the image size.

The returned distance is normalized so it lies in [0, 1], where 1 corresponds to opposite corners of the image.

Parameters:

Name Type Description Default
height int

Height of the image.

required
width int

Width of the image.

required

Returns:

Type Description
Callable

A scalar distance function that can be passed to Tracker.

Raises:

Type Description
ValueError

If height or width is not positive.

Source code in norfair/distances.py
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
def create_normalized_mean_euclidean_distance(
    height: int, width: int
) -> Callable[["Detection", "TrackedObject"], float]:
    """Build a normalized mean Euclidean distance bound to the image size.

    The returned distance is normalized so it lies in ``[0, 1]``, where
    ``1`` corresponds to opposite corners of the image.

    Parameters
    ----------
    height : int
        Height of the image.
    width : int
        Width of the image.

    Returns
    -------
    Callable
        A scalar distance function that can be passed to ``Tracker``.

    Raises
    ------
    ValueError
        If ``height`` or ``width`` is not positive.

    """
    if width <= 0 or height <= 0:
        raise ValueError(
            f"width and height must be positive, got width={width}, height={height}"
        )

    def normalized__mean_euclidean_distance(
        detection: "Detection", tracked_object: "TrackedObject"
    ) -> float:
        """Compute the normalized mean Euclidean distance between detection and tracked object.

        Parameters
        ----------
        detection : Detection
            Incoming detection whose points are compared.
        tracked_object : TrackedObject
            Existing tracked object providing the estimated points.

        Returns
        -------
        float
            Mean Euclidean distance across all point pairs, normalized by
            image width and height so it lies in ``[0, 1]``.
        """
        # calculate distances and normalized it by width and height
        difference = (detection.points - tracked_object.estimate).astype(float)
        difference[:, 0] /= width
        difference[:, 1] /= height

        # calculate eucledean distance and average
        return np.linalg.norm(difference, axis=1).mean()

    return normalized__mean_euclidean_distance

See also#

  • Tracker — how the distance is consumed each frame.
  • Filter — the Kalman filter that produces the estimate used by most distance functions.