Skip to content

Engine

Engine module for detection tracking.

This module provides utilities for bounding box interpolation, IoU calculations, clustering, and IoB (Intersection over Background) operations.

interpolate_bbox

interpolate_bbox(
    boxes: ndarray,
    frames: ndarray,
    target_frame: int,
    method="cubic",
) -> np.ndarray

Interpolates bounding boxes using cubic splines.

Args: boxes: a (n, 4) array of bounding box coordinates (x, y, width, height). frames: a (n,) array of frame indices corresponding to the bounding boxes. target_frame: the frame index at which to interpolate the bounding box. method: the spline function, default is 'cubic', 'nearest', 'linear'

Returns: A 1d array (x, y, width, height) representing the interpolated bounding box.

Source code in src/dnt/engine/bbox_interp.py
 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
def interpolate_bbox(boxes: np.ndarray, frames: np.ndarray, target_frame: int, method="cubic") -> np.ndarray:
    """Interpolates bounding boxes using cubic splines.

    Args:
        boxes: a (n, 4) array of bounding box coordinates (x, y, width, height).
        frames: a (n,) array of frame indices corresponding to the bounding boxes.
        target_frame: the frame index at which to interpolate the bounding box.
        method: the spline function, default is 'cubic', 'nearest', 'linear'

    Returns:
        A 1d array (x, y, width, height) representing the interpolated bounding box.

    """
    n = boxes.shape[0]

    if n != frames.shape[0]:
        raise ValueError("Length of boxes and frames must be equal.")

    if n == 0:
        raise ValueError("Input arrays must not be empty.")

    if target_frame < frames[0] or target_frame > frames[-1]:
        raise ValueError("Target frame is out of bounds.")

    # Unpack the boxes into separate arrays for x, y, width, and height
    x = boxes[:, 0]
    y = boxes[:, 1]
    w = boxes[:, 2]
    h = boxes[:, 3]

    # Create the cubic splines for each parameter
    if method == "cubic":
        spline_x = CubicSpline(frames, x)
        spline_y = CubicSpline(frames, y)
        spline_w = CubicSpline(frames, w)
        spline_h = CubicSpline(frames, h)
    elif method == "nearest":
        spline_x = interp1d(frames, x, kind="nearest")
        spline_y = interp1d(frames, y, kind="nearest")
        spline_w = interp1d(frames, w, kind="nearest")
        spline_h = interp1d(frames, h, kind="nearest")
    else:
        spline_x = interp1d(frames, x, kind="linear")
        spline_y = interp1d(frames, y, kind="linear")
        spline_w = interp1d(frames, w, kind="linear")
        spline_h = interp1d(frames, h, kind="linear")

    # Evaluate the splines at the target frame
    x_t = int(spline_x(target_frame))
    y_t = int(spline_y(target_frame))
    w_t = int(spline_w(target_frame))
    h_t = int(spline_h(target_frame))

    return np.array([x_t, y_t, w_t, h_t])

interpolate_bboxes

interpolate_bboxes(
    boxes: ndarray,
    frames: ndarray,
    target_frames: ndarray,
    method="cubic",
) -> np.ndarray

Interpolates bounding boxes using cubic splines.

Args: boxes: a (n, 4) array of bounding box coordinates (x, y, width, height). frames: a (n,) array of frame indices corresponding to the bounding boxes. target_frames: the frame indexes at which to interpolate the bounding boxes. method: the spline function, default is 'cubic', 'nearest', 'linear'

Returns: A (m, 4) array of interpolated bounding boxes for the target frames.

Source code in src/dnt/engine/bbox_interp.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def interpolate_bboxes(boxes: np.ndarray, frames: np.ndarray, target_frames: np.ndarray, method="cubic") -> np.ndarray:
    """Interpolates bounding boxes using cubic splines.

    Args:
        boxes: a (n, 4) array of bounding box coordinates (x, y, width, height).
        frames: a (n,) array of frame indices corresponding to the bounding boxes.
        target_frames: the frame indexes at which to interpolate the bounding boxes.
        method: the spline function, default is 'cubic', 'nearest', 'linear'

    Returns:
        A (m, 4) array of interpolated bounding boxes for the target frames.

    """
    n_frames = target_frames.shape[0]
    results = []

    for i in range(n_frames):
        target_frame = target_frames[i]
        results.append(interpolate_bbox(boxes, frames, target_frame, method))

    return np.vstack(results)

ious

ious(atlbrs, btlbrs)

Compute cost based on IoU.

:type atlbrs: list[tlbr] | np.ndarray :type atlbrs: list[tlbr] | np.ndarray

:rtype ious np.ndarray

Source code in src/dnt/engine/bbox_iou.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def ious(atlbrs, btlbrs):
    """Compute cost based on IoU.

    :type atlbrs: list[tlbr] | np.ndarray
    :type atlbrs: list[tlbr] | np.ndarray

    :rtype ious np.ndarray
    """
    ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float64)
    if ious.size == 0:
        return ious

    ious = bbox_overlaps(np.ascontiguousarray(atlbrs, dtype=np.float64), np.ascontiguousarray(btlbrs, dtype=np.float64))

    return ious

cluster_by_gap

cluster_by_gap(arr: ndarray, gap: int) -> list[list]

Group array elements into clusters based on a gap threshold.

Parameters:

Name Type Description Default
arr ndarray

A 1D numpy array of numbers to be clustered.

required
gap int

The maximum allowed difference between consecutive elements in a cluster. Must be non-negative.

required

Returns:

Type Description
list[list]

A list of clusters, where each cluster is a list of numbers from the input array.

Raises:

Type Description
ValueError

If the input array is empty, not 1-dimensional, or gap is negative.

Examples:

>>> cluster_by_gap(np.array([1, 2, 5, 6, 10]), gap=2)
[[1, 2], [5, 6], [10]]
>>> cluster_by_gap(np.array([1, 2, 5, 6, 10]), gap=1)
[[1, 2], [5, 6], [10]]
>>> cluster_by_gap(np.array([1, 2, 5, 6, 10]), gap=4)
[[1, 2, 5, 6, 10]]
Source code in src/dnt/engine/cluster.py
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
def cluster_by_gap(arr: np.ndarray, gap: int) -> list[list]:
    """Group array elements into clusters based on a gap threshold.

    Parameters
    ----------
    arr : np.ndarray
        A 1D numpy array of numbers to be clustered.
    gap : int
        The maximum allowed difference between consecutive elements in a cluster.
        Must be non-negative.

    Returns
    -------
    list[list]
        A list of clusters, where each cluster is a list of numbers from the input array.

    Raises
    ------
    ValueError
        If the input array is empty, not 1-dimensional, or gap is negative.

    Examples
    --------
    >>> cluster_by_gap(np.array([1, 2, 5, 6, 10]), gap=2)
    [[1, 2], [5, 6], [10]]
    >>> cluster_by_gap(np.array([1, 2, 5, 6, 10]), gap=1)
    [[1, 2], [5, 6], [10]]
    >>> cluster_by_gap(np.array([1, 2, 5, 6, 10]), gap=4)
    [[1, 2, 5, 6, 10]]

    """
    if arr.size == 0:
        raise ValueError("Input array cannot be empty.")
    if arr.ndim != 1:
        raise ValueError("Input array must be 1-dimensional.")
    if gap < 0:
        raise ValueError("Gap threshold must be non-negative.")

    arr = np.sort(arr)
    clusters = []
    current_cluster = [arr[0]]

    for i in range(1, len(arr)):
        if arr[i] - arr[i - 1] <= gap:
            current_cluster.append(arr[i])
        else:
            clusters.append(current_cluster)
            current_cluster = [arr[i]]

    clusters.append(current_cluster)
    return clusters

iobs

iobs(
    alrbs: ndarray, blrbs: ndarray
) -> tuple[np.ndarray, np.ndarray]

Calculate the IoB matrix for multiple bounding boxes.

Parameters:

Name Type Description Default
alrbs ndarray

Array of shape (N, 4) containing N bounding boxes with format [left, top, width, height].

required
blrbs ndarray

Array of shape (M, 4) containing M bounding boxes with format [left, top, width, height].

required

Returns:

Type Description
tuple[ndarray, ndarray]

A tuple (iobs_a, iobs_b) where: - iobs_a: array of shape (N, M) with IoB values relative to alrbs - iobs_b: array of shape (N, M) with IoB values relative to blrbs

Raises:

Type Description
ValueError

If arrays do not have shape (N, 4) and (M, 4) respectively, or contain negative width/height values.

Notes

Bounding box format is [left, top, width, height] where: - left: x-coordinate of top-left corner - top: y-coordinate of top-left corner - width: box width (must be non-negative) - height: box height (must be non-negative)

Examples:

>>> boxes1 = np.array([[0, 0, 10, 10], [5, 5, 10, 10]])
>>> boxes2 = np.array([[0, 0, 5, 5], [10, 10, 5, 5]])
>>> iobs_a, iobs_b = iobs(boxes1, boxes2)
>>> iobs_a.shape
(2, 2)
Source code in src/dnt/engine/iob.py
 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
def iobs(alrbs: np.ndarray, blrbs: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Calculate the IoB matrix for multiple bounding boxes.

    Parameters
    ----------
    alrbs : np.ndarray
        Array of shape (N, 4) containing N bounding boxes with format
        [left, top, width, height].
    blrbs : np.ndarray
        Array of shape (M, 4) containing M bounding boxes with format
        [left, top, width, height].

    Returns
    -------
    tuple[np.ndarray, np.ndarray]
        A tuple (iobs_a, iobs_b) where:
        - iobs_a: array of shape (N, M) with IoB values relative to alrbs
        - iobs_b: array of shape (N, M) with IoB values relative to blrbs

    Raises
    ------
    ValueError
        If arrays do not have shape (N, 4) and (M, 4) respectively, or contain
        negative width/height values.

    Notes
    -----
    Bounding box format is [left, top, width, height] where:
    - left: x-coordinate of top-left corner
    - top: y-coordinate of top-left corner
    - width: box width (must be non-negative)
    - height: box height (must be non-negative)

    Examples
    --------
    >>> boxes1 = np.array([[0, 0, 10, 10], [5, 5, 10, 10]])
    >>> boxes2 = np.array([[0, 0, 5, 5], [10, 10, 5, 5]])
    >>> iobs_a, iobs_b = iobs(boxes1, boxes2)
    >>> iobs_a.shape
    (2, 2)

    """
    if alrbs.ndim != 2 or alrbs.shape[1] != 4:
        raise ValueError("alrbs must have shape (N, 4) with format [left, top, width, height].")
    if blrbs.ndim != 2 or blrbs.shape[1] != 4:
        raise ValueError("blrbs must have shape (M, 4) with format [left, top, width, height].")
    if np.any(alrbs[:, 2:] < 0) or np.any(blrbs[:, 2:] < 0):
        raise ValueError("Box width and height must be non-negative.")

    num_a = alrbs.shape[0]
    num_b = blrbs.shape[0]
    iobs_a = np.zeros((num_a, num_b))
    iobs_b = np.zeros((num_a, num_b))

    for i in range(num_a):
        for j in range(num_b):
            iobs_a[i, j], iobs_b[i, j] = iob(alrbs[i, :], blrbs[j, :])

    return iobs_a, iobs_b