Skip to content

Drawing#

Lightweight drawing helpers built on top of OpenCV. They take a frame (a numpy.ndarray in BGR order, as returned by Video) and draw detections, tracked objects, paths, or reference grids in place.

The drawing helpers accept both Detections and TrackedObjects, so you can use the same call to visualize raw detector output and the final tracker output on the same frame.

Key functions#

  • draw_points — draw points (keypoints, centroids) with optional labels, ids, and scores. This is the modern replacement for draw_tracked_objects and works with both detections and tracked objects.
  • draw_boxes — draw axis-aligned bounding boxes. Expects each drawable to carry its two corner points (top_left, bottom_right). Replaces draw_tracked_boxes.
  • Paths / AbsolutePaths — trail-style visualization of where each tracked object has been.
  • FixedCamera — compensates for camera motion when visualizing, pairs with MotionEstimator.
  • draw_absolute_grid — draws a reference grid in world coordinates, useful for debugging camera motion estimation.
  • Color / Palette — named colors and palette configuration for color="by_id" / "by_label".

Example#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from norfair import Detection, Tracker, Video, draw_boxes, draw_points

tracker = Tracker(distance_function="euclidean", distance_threshold=50)

with Video(input_path="video.mp4") as video:
    for frame in video:
        raw = my_detector(frame)
        detections = [Detection(points=p) for p in raw]
        tracked_objects = tracker.update(detections=detections)

        # Draw raw detections in a fixed color so they stand out during debugging.
        draw_points(frame, detections, color="red", draw_ids=False)

        # Draw the tracker output with per-id colors and labels.
        draw_points(frame, tracked_objects, color="by_id")

        video.write(frame)

To draw bounding boxes instead, build each Detection with a (2, 2) array of [[x1, y1], [x2, y2]] corners and call draw_boxes instead of draw_points.

API#

draw_points #

Draw detection / tracked-object points onto a video frame.

draw_points(frame, drawables=None, radius=None, thickness=None, color='by_id', color_by_label=None, draw_labels=True, text_size=None, draw_ids=True, draw_points=True, text_thickness=None, text_color=None, hide_dead_points=True, detections=None, label_size=None, draw_scores=False) #

Draw the points of a list of Detection or TrackedObject.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on. Modified in place.

required
drawables Sequence[Detection] or Sequence[TrackedObject]

Objects to draw. Both Detection and TrackedObject are accepted.

None
radius int

Radius of the circles representing each point. By default a sensible value is picked based on the frame size.

None
thickness int

Thickness of the stroke. -1 (the default) produces filled circles.

None
color ColorLike

The color to use. May be:

  1. A BGR int tuple like (0, 0, 255).
  2. A 6-digit hex string such as "#FF0000".
  3. One of the predefined color names (e.g. "red").
  4. A palette strategy — "by_id", "by_label" or "random".

When "by_id" or "by_label" is used but the object lacks that field (detections never have id), every object is drawn in the palette's default color.

'by_id'
color_by_label bool

Deprecated. Set color="by_label" instead.

None
draw_labels bool

If True, the label is drawn above the points. Ignored when the object has no label.

True
draw_scores bool

If True, the detection score is drawn above the points. Ignored when the object has no score.

False
text_size int

Size multiplier for the base font used for the text. By default, the size is scaled automatically based on the frame size.

None
draw_ids bool

If True, the id is drawn above the points. Ignored when the object has no id.

True
draw_points bool

Set to False to hide the points and only draw the text.

True
text_thickness int

Stroke thickness of the text. By default it scales with text_size.

None
text_color ColorLike

Color of the text. Defaults to the object's color.

None
hide_dead_points bool

Set to False to draw all points, including "dead" ones. A point is dead when the corresponding entry in TrackedObject.live_points is False. If every point is dead the whole object is skipped. Detection points are always treated as live.

True
detections Sequence[Detection]

Deprecated. Use drawables.

None
label_size int

Deprecated. Use text_size.

None

Returns:

Type Description
ndarray

The frame passed in (drawn on in place).

Source code in norfair/drawing/draw_points.py
 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
def draw_points(
    frame: np.ndarray,
    drawables: Sequence[Detection] | Sequence[TrackedObject] | None = None,
    radius: int | None = None,
    thickness: int | None = None,
    color: ColorLike = "by_id",
    color_by_label: bool | None = None,  # deprecated
    draw_labels: bool = True,
    text_size: int | None = None,
    draw_ids: bool = True,
    draw_points: bool = True,  # pylint: disable=redefined-outer-name
    text_thickness: int | None = None,
    text_color: ColorLike | None = None,
    hide_dead_points: bool = True,
    detections: Sequence["Detection"] | None = None,  # deprecated
    label_size: int | None = None,  # deprecated
    draw_scores: bool = False,
) -> np.ndarray:
    """Draw the points of a list of ``Detection`` or ``TrackedObject``.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on. Modified in place.
    drawables : Sequence[Detection] or Sequence[TrackedObject], optional
        Objects to draw. Both ``Detection`` and ``TrackedObject`` are
        accepted.
    radius : int, optional
        Radius of the circles representing each point. By default a
        sensible value is picked based on the frame size.
    thickness : int, optional
        Thickness of the stroke. ``-1`` (the default) produces filled
        circles.
    color : ColorLike, optional
        The color to use. May be:

        1. A BGR int tuple like ``(0, 0, 255)``.
        2. A 6-digit hex string such as ``"#FF0000"``.
        3. One of the predefined color names (e.g. ``"red"``).
        4. A palette strategy — ``"by_id"``, ``"by_label"`` or
           ``"random"``.

        When ``"by_id"`` or ``"by_label"`` is used but the object lacks
        that field (detections never have ``id``), every object is drawn
        in the palette's default color.
    color_by_label : bool, optional
        **Deprecated.** Set ``color="by_label"`` instead.
    draw_labels : bool, optional
        If ``True``, the label is drawn above the points. Ignored when
        the object has no label.
    draw_scores : bool, optional
        If ``True``, the detection score is drawn above the points.
        Ignored when the object has no score.
    text_size : int, optional
        Size multiplier for the base font used for the text. By default,
        the size is scaled automatically based on the frame size.
    draw_ids : bool, optional
        If ``True``, the id is drawn above the points. Ignored when the
        object has no id.
    draw_points : bool, optional
        Set to ``False`` to hide the points and only draw the text.
    text_thickness : int, optional
        Stroke thickness of the text. By default it scales with
        ``text_size``.
    text_color : ColorLike, optional
        Color of the text. Defaults to the object's color.
    hide_dead_points : bool, optional
        Set to ``False`` to draw all points, including "dead" ones. A
        point is dead when the corresponding entry in
        ``TrackedObject.live_points`` is ``False``. If every point is
        dead the whole object is skipped. Detection points are always
        treated as live.
    detections : Sequence[Detection], optional
        **Deprecated.** Use ``drawables``.
    label_size : int, optional
        **Deprecated.** Use ``text_size``.

    Returns
    -------
    np.ndarray
        The ``frame`` passed in (drawn on in place).
    """
    #
    # handle deprecated parameters
    #
    if color_by_label is not None:
        warn_once(
            'Parameter "color_by_label" on function draw_points is deprecated, set `color="by_label"` instead'
        )
        color = "by_label"
    if detections is not None:
        warn_once(
            "Parameter 'detections' on function draw_points is deprecated, use 'drawables' instead"
        )
        drawables = detections
    if label_size is not None:
        warn_once(
            "Parameter 'label_size' on function draw_points is deprecated, use 'text_size' instead"
        )
        text_size = label_size
    # end

    if drawables is None:
        return frame

    if text_color is not None:
        text_color = parse_color(text_color)

    if color is None:
        color = "by_id"
    if thickness is None:
        thickness = -1
    if radius is None:
        radius = int(round(max(max(frame.shape) * 0.002, 1)))

    for o in drawables:
        if not isinstance(o, Drawable):
            d = Drawable(o)
        else:
            d = o

        if hide_dead_points and not d.live_points.any():
            continue

        if color == "by_id":
            obj_color = Palette.choose_color(d.id)
        elif color == "by_label":
            obj_color = Palette.choose_color(d.label)
        elif color == "random":
            obj_color = Palette.choose_color(np.random.rand())
        else:
            obj_color = parse_color(color)

        if text_color is None:
            obj_text_color = obj_color
        else:
            obj_text_color = text_color

        if draw_points:
            for point, live in zip(d.points, d.live_points):
                if live or not hide_dead_points:
                    safe_pos = _safe_int_point(point)
                    if safe_pos is None:
                        continue
                    Drawer.circle(
                        frame,
                        safe_pos,
                        radius=radius,
                        color=obj_color,
                        thickness=thickness,
                    )

        if draw_labels or draw_ids or draw_scores:
            live = d.points[d.live_points]
            if len(live) > 0:
                position = live.mean(axis=0)
                position -= radius
                text_pos = _safe_int_point(position)
                if text_pos is None:
                    continue
                text = _build_text(
                    d,
                    draw_labels=draw_labels,
                    draw_ids=draw_ids,
                    draw_scores=draw_scores,
                )

                Drawer.text(
                    frame,
                    text,
                    text_pos,
                    size=text_size,
                    color=obj_text_color,
                    thickness=text_thickness,
                )

    return frame

draw_tracked_objects(frame, objects, radius=None, color=None, id_size=None, id_thickness=None, draw_points=True, color_by_label=False, draw_labels=False, label_size=None) #

Draw tracked objects onto frame.

.. deprecated:: Use :func:draw_points instead. This function is kept for backward compatibility and forwards its arguments to :func:draw_points.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on. Modified in place.

required
objects Sequence[TrackedObject]

The tracked objects to draw.

required
radius int

Radius of the circles representing each point.

None
color ColorLike

Color to use. See :func:draw_points for accepted formats.

None
id_size float

Size multiplier for the id text. Set to 0 to disable id rendering.

None
id_thickness int

Stroke thickness of the id text.

None
draw_points bool

Set to False to hide the point circles and only draw text.

True
color_by_label bool

If True, color objects by label instead of id.

False
draw_labels bool

If True, draw the label above the points.

False
label_size int

Size of the label text.

None

Returns:

Type Description
ndarray

The frame passed in (drawn on in place).

Source code in norfair/drawing/draw_points.py
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
275
276
277
278
279
280
281
282
283
284
285
def draw_tracked_objects(
    frame: np.ndarray,
    objects: Sequence["TrackedObject"],
    radius: int | None = None,
    color: ColorLike | None = None,
    id_size: float | None = None,
    id_thickness: int | None = None,
    draw_points: bool = True,  # pylint: disable=redefined-outer-name
    color_by_label: bool = False,
    draw_labels: bool = False,
    label_size: int | None = None,
):
    """Draw tracked objects onto ``frame``.

    .. deprecated::
        Use :func:`draw_points` instead. This function is kept for
        backward compatibility and forwards its arguments to
        :func:`draw_points`.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on. Modified in place.
    objects : Sequence[TrackedObject]
        The tracked objects to draw.
    radius : int, optional
        Radius of the circles representing each point.
    color : ColorLike, optional
        Color to use. See :func:`draw_points` for accepted formats.
    id_size : float, optional
        Size multiplier for the id text. Set to ``0`` to disable id
        rendering.
    id_thickness : int, optional
        Stroke thickness of the id text.
    draw_points : bool, optional
        Set to ``False`` to hide the point circles and only draw text.
    color_by_label : bool, optional
        If ``True``, color objects by label instead of id.
    draw_labels : bool, optional
        If ``True``, draw the label above the points.
    label_size : int, optional
        Size of the label text.

    Returns
    -------
    np.ndarray
        The ``frame`` passed in (drawn on in place).

    """
    warn_once("draw_tracked_objects is deprecated, use draw_points instead")

    frame_scale = frame.shape[0] / 100
    if radius is None:
        radius = int(frame_scale * 0.5)
    if id_size is None:
        id_size = frame_scale / 10
    if id_thickness is None:
        id_thickness = int(frame_scale / 5)
    if label_size is None:
        label_size = int(max(frame_scale / 100, 1))

    # Determine color - default to "by_id" if None
    selected_color: ColorLike = (
        "by_label" if color_by_label else (color if color is not None else "by_id")
    )

    # Convert id_size to int if it's a float
    text_size_value: int | None = None
    if label_size is not None:
        text_size_value = label_size
    elif id_size is not None:
        text_size_value = int(id_size)

    return _draw_points_alias(
        frame=frame,
        drawables=objects,
        color=selected_color,
        radius=radius,
        thickness=None,
        draw_labels=draw_labels,
        draw_ids=id_size is not None and id_size > 0,
        draw_points=draw_points,
        text_size=text_size_value,
        text_thickness=id_thickness,
        text_color=None,
        hide_dead_points=True,
    )

draw_boxes #

Draw detection / tracked-object bounding boxes onto a video frame.

draw_boxes(frame, drawables=None, color='by_id', thickness=None, random_color=None, color_by_label=None, draw_labels=False, text_size=None, draw_ids=True, text_color=None, text_thickness=None, draw_box=True, detections=None, line_color=None, line_width=None, label_size=None, draw_scores=False) #

Draw bounding boxes for Detection or TrackedObject.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on. Modified in place.

required
drawables Sequence[Detection] or Sequence[TrackedObject]

Objects to draw. Each object is assumed to contain two two-dimensional points defining the bounding box as [[x0, y0], [x1, y1]].

None
color ColorLike

The color to use. May be:

  1. A BGR int tuple like (0, 0, 255).
  2. A 6-digit hex string such as "#FF0000".
  3. One of the predefined color names (e.g. "red").
  4. A palette strategy — "by_id", "by_label" or "random".

When "by_id" or "by_label" is used but the object lacks that field (detections never have id), every object is drawn in the palette's default color.

'by_id'
thickness int

Thickness (width) of the box outline.

None
random_color bool

Deprecated. Set color="random" instead.

None
color_by_label bool

Deprecated. Set color="by_label" instead.

None
draw_labels bool

If True, the label is drawn above the box. Ignored when the object has no label.

False
draw_scores bool

If True, the detection score is drawn above the box. Ignored when the object has no score.

False
text_size float

Size multiplier for the base font used for the text. By default, the size is scaled automatically based on the frame size.

None
draw_ids bool

If True, the id is drawn above the box. Ignored when the object has no id.

True
text_color ColorLike

Color of the text. Defaults to the same color as the box.

None
text_thickness int

Stroke thickness of the text. By default it scales with text_size.

None
draw_box bool

Set to False to hide the box and only draw the text.

True
detections Sequence[Detection]

Deprecated. Use drawables.

None
line_color ColorLike

Deprecated. Use color.

None
line_width int

Deprecated. Use thickness.

None
label_size int

Deprecated. Use text_size.

None

Returns:

Type Description
ndarray

The frame passed in (drawn on in place).

Source code in norfair/drawing/draw_boxes.py
 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
def draw_boxes(
    frame: np.ndarray,
    drawables: Sequence[Detection] | Sequence[TrackedObject] | None = None,
    color: ColorLike = "by_id",
    thickness: int | None = None,
    random_color: bool | None = None,  # Deprecated
    color_by_label: bool | None = None,  # Deprecated
    draw_labels: bool = False,
    text_size: float | None = None,
    draw_ids: bool = True,
    text_color: ColorLike | None = None,
    text_thickness: int | None = None,
    draw_box: bool = True,
    detections: Sequence["Detection"] | None = None,  # Deprecated
    line_color: ColorLike | None = None,  # Deprecated
    line_width: int | None = None,  # Deprecated
    label_size: int | None = None,  # Deprecated
    draw_scores: bool = False,
) -> np.ndarray:
    """Draw bounding boxes for ``Detection`` or ``TrackedObject``.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on. Modified in place.
    drawables : Sequence[Detection] or Sequence[TrackedObject], optional
        Objects to draw. Each object is assumed to contain two
        two-dimensional points defining the bounding box as
        ``[[x0, y0], [x1, y1]]``.
    color : ColorLike, optional
        The color to use. May be:

        1. A BGR int tuple like ``(0, 0, 255)``.
        2. A 6-digit hex string such as ``"#FF0000"``.
        3. One of the predefined color names (e.g. ``"red"``).
        4. A palette strategy — ``"by_id"``, ``"by_label"`` or
           ``"random"``.

        When ``"by_id"`` or ``"by_label"`` is used but the object lacks
        that field (detections never have ``id``), every object is drawn
        in the palette's default color.
    thickness : int, optional
        Thickness (width) of the box outline.
    random_color : bool, optional
        **Deprecated.** Set ``color="random"`` instead.
    color_by_label : bool, optional
        **Deprecated.** Set ``color="by_label"`` instead.
    draw_labels : bool, optional
        If ``True``, the label is drawn above the box. Ignored when the
        object has no label.
    draw_scores : bool, optional
        If ``True``, the detection score is drawn above the box. Ignored
        when the object has no score.
    text_size : float, optional
        Size multiplier for the base font used for the text. By default,
        the size is scaled automatically based on the frame size.
    draw_ids : bool, optional
        If ``True``, the id is drawn above the box. Ignored when the
        object has no id.
    text_color : ColorLike, optional
        Color of the text. Defaults to the same color as the box.
    text_thickness : int, optional
        Stroke thickness of the text. By default it scales with
        ``text_size``.
    draw_box : bool, optional
        Set to ``False`` to hide the box and only draw the text.
    detections : Sequence[Detection], optional
        **Deprecated.** Use ``drawables``.
    line_color : ColorLike, optional
        **Deprecated.** Use ``color``.
    line_width : int, optional
        **Deprecated.** Use ``thickness``.
    label_size : int, optional
        **Deprecated.** Use ``text_size``.

    Returns
    -------
    np.ndarray
        The ``frame`` passed in (drawn on in place).

    """
    #
    # handle deprecated parameters
    #
    if random_color is not None:
        warn_once(
            'Parameter "random_color" is deprecated, set `color="random"` instead'
        )
        color = "random"
    if color_by_label is not None:
        warn_once(
            'Parameter "color_by_label" is deprecated, set `color="by_label"` instead'
        )
        color = "by_label"
    if detections is not None:
        warn_once('Parameter "detections" is deprecated, use "drawables" instead')
        drawables = detections
    if line_color is not None:
        warn_once('Parameter "line_color" is deprecated, use "color" instead')
        color = line_color
    if line_width is not None:
        warn_once('Parameter "line_width" is deprecated, use "thickness" instead')
        thickness = line_width
    if label_size is not None:
        warn_once('Parameter "label_size" is deprecated, use "text_size" instead')
        text_size = label_size
    # end

    if color is None:
        color = "by_id"
    if thickness is None:
        thickness = int(max(frame.shape) / 500)

    if drawables is None:
        return frame

    if text_color is not None:
        text_color = parse_color(text_color)

    for obj in drawables:
        if not isinstance(obj, Drawable):
            d = Drawable(obj)
        else:
            d = obj

        if color == "by_id":
            obj_color = Palette.choose_color(d.id)
        elif color == "by_label":
            obj_color = Palette.choose_color(d.label)
        elif color == "random":
            obj_color = Palette.choose_color(np.random.rand())
        else:
            obj_color = parse_color(color)

        # Skip objects with non-finite coordinates (#41)
        if not np.all(np.isfinite(d.points)):
            continue

        points = d.points.astype(int)
        if draw_box:
            Drawer.rectangle(
                frame,
                tuple(points),
                color=obj_color,
                thickness=thickness,
            )

        text = _build_text(
            d, draw_labels=draw_labels, draw_ids=draw_ids, draw_scores=draw_scores
        )
        if text:
            if text_color is None:
                obj_text_color = obj_color
            else:
                obj_text_color = text_color
            # the anchor will become the bottom-left of the text,
            # we select-top left of the bbox compensating for the thickness of the box
            text_anchor = (
                points[0, 0] - thickness // 2,
                points[0, 1] - thickness // 2 - 1,
            )
            frame = Drawer.text(
                frame,
                text,
                position=text_anchor,
                size=text_size,
                color=obj_text_color,
                thickness=text_thickness,
            )

    return frame

draw_tracked_boxes(frame, objects, border_colors=None, border_width=None, id_size=None, id_thickness=None, draw_box=True, color_by_label=False, draw_labels=False, label_size=None, label_width=None) #

Draw tracked-object bounding boxes onto frame.

.. deprecated:: Use :func:draw_boxes instead. This function is kept for backward compatibility and forwards its arguments to :func:draw_boxes.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on. Modified in place.

required
objects Sequence[TrackedObject]

The tracked objects to draw.

required
border_colors tuple of int

BGR border color. Ignored if color_by_label is True.

None
border_width int

Thickness of the border line.

None
id_size int

Size of the id text. Set to 0 to disable id rendering.

None
id_thickness int

Stroke thickness of the id text.

None
draw_box bool

Set to False to hide the box and only draw the text.

True
color_by_label bool

If True, color objects by label instead of by a fixed border_colors.

False
draw_labels bool

If True, draw the label above the box.

False
label_size int

Size of the label text.

None
label_width int

Stroke thickness of the label text.

None

Returns:

Type Description
ndarray

The frame passed in (drawn on in place).

Source code in norfair/drawing/draw_boxes.py
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
def draw_tracked_boxes(
    frame: np.ndarray,
    objects: Sequence["TrackedObject"],
    border_colors: tuple[int, int, int] | None = None,
    border_width: int | None = None,
    id_size: int | None = None,
    id_thickness: int | None = None,
    draw_box: bool = True,
    color_by_label: bool = False,
    draw_labels: bool = False,
    label_size: int | None = None,
    label_width: int | None = None,
) -> np.ndarray:
    """Draw tracked-object bounding boxes onto ``frame``.

    .. deprecated::
        Use :func:`draw_boxes` instead. This function is kept for
        backward compatibility and forwards its arguments to
        :func:`draw_boxes`.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on. Modified in place.
    objects : Sequence[TrackedObject]
        The tracked objects to draw.
    border_colors : tuple of int, optional
        BGR border color. Ignored if ``color_by_label`` is ``True``.
    border_width : int, optional
        Thickness of the border line.
    id_size : int, optional
        Size of the id text. Set to ``0`` to disable id rendering.
    id_thickness : int, optional
        Stroke thickness of the id text.
    draw_box : bool, optional
        Set to ``False`` to hide the box and only draw the text.
    color_by_label : bool, optional
        If ``True``, color objects by label instead of by a fixed
        ``border_colors``.
    draw_labels : bool, optional
        If ``True``, draw the label above the box.
    label_size : int, optional
        Size of the label text.
    label_width : int, optional
        Stroke thickness of the label text.

    Returns
    -------
    np.ndarray
        The ``frame`` passed in (drawn on in place).

    """
    warn_once("draw_tracked_boxes is deprecated, use draw_boxes instead")
    # Determine color - default to "by_id" if border_colors is None
    selected_color: ColorLike = (
        "by_label"
        if color_by_label
        else (border_colors if border_colors is not None else "by_id")
    )
    return draw_boxes(
        frame=frame,
        drawables=objects,
        color=selected_color,
        thickness=border_width,
        text_size=label_size or id_size,
        text_thickness=id_thickness or label_width,
        draw_labels=draw_labels,
        draw_ids=id_size is not None and id_size > 0,
        draw_box=draw_box,
    )

color #

Color palette utilities for Norfair drawing helpers.

Color #

Namespace of predefined BGR color constants.

Colors are stored as (B, G, R) tuples of integers in the range 0-255 — the format that OpenCV consumes. The set includes the CSS/PIL named colors plus the Seaborn tab20 and colorblind palettes (accessible as tab1..tab20 and cb1..cb10).

Source code in norfair/drawing/color.py
 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
232
233
234
235
236
237
238
class Color:
    """Namespace of predefined BGR color constants.

    Colors are stored as ``(B, G, R)`` tuples of integers in the range
    ``0-255`` — the format that OpenCV consumes. The set includes the
    CSS/PIL named colors plus the Seaborn ``tab20`` and ``colorblind``
    palettes (accessible as ``tab1``..``tab20`` and ``cb1``..``cb10``).
    """

    # from PIL.ImageColors.colormap
    aliceblue = hex_to_bgr("#f0f8ff")
    antiquewhite = hex_to_bgr("#faebd7")
    aqua = hex_to_bgr("#00ffff")
    aquamarine = hex_to_bgr("#7fffd4")
    azure = hex_to_bgr("#f0ffff")
    beige = hex_to_bgr("#f5f5dc")
    bisque = hex_to_bgr("#ffe4c4")
    black = hex_to_bgr("#000000")
    blanchedalmond = hex_to_bgr("#ffebcd")
    blue = hex_to_bgr("#0000ff")
    blueviolet = hex_to_bgr("#8a2be2")
    brown = hex_to_bgr("#a52a2a")
    burlywood = hex_to_bgr("#deb887")
    cadetblue = hex_to_bgr("#5f9ea0")
    chartreuse = hex_to_bgr("#7fff00")
    chocolate = hex_to_bgr("#d2691e")
    coral = hex_to_bgr("#ff7f50")
    cornflowerblue = hex_to_bgr("#6495ed")
    cornsilk = hex_to_bgr("#fff8dc")
    crimson = hex_to_bgr("#dc143c")
    cyan = hex_to_bgr("#00ffff")
    darkblue = hex_to_bgr("#00008b")
    darkcyan = hex_to_bgr("#008b8b")
    darkgoldenrod = hex_to_bgr("#b8860b")
    darkgray = hex_to_bgr("#a9a9a9")
    darkgrey = hex_to_bgr("#a9a9a9")
    darkgreen = hex_to_bgr("#006400")
    darkkhaki = hex_to_bgr("#bdb76b")
    darkmagenta = hex_to_bgr("#8b008b")
    darkolivegreen = hex_to_bgr("#556b2f")
    darkorange = hex_to_bgr("#ff8c00")
    darkorchid = hex_to_bgr("#9932cc")
    darkred = hex_to_bgr("#8b0000")
    darksalmon = hex_to_bgr("#e9967a")
    darkseagreen = hex_to_bgr("#8fbc8f")
    darkslateblue = hex_to_bgr("#483d8b")
    darkslategray = hex_to_bgr("#2f4f4f")
    darkslategrey = hex_to_bgr("#2f4f4f")
    darkturquoise = hex_to_bgr("#00ced1")
    darkviolet = hex_to_bgr("#9400d3")
    deeppink = hex_to_bgr("#ff1493")
    deepskyblue = hex_to_bgr("#00bfff")
    dimgray = hex_to_bgr("#696969")
    dimgrey = hex_to_bgr("#696969")
    dodgerblue = hex_to_bgr("#1e90ff")
    firebrick = hex_to_bgr("#b22222")
    floralwhite = hex_to_bgr("#fffaf0")
    forestgreen = hex_to_bgr("#228b22")
    fuchsia = hex_to_bgr("#ff00ff")
    gainsboro = hex_to_bgr("#dcdcdc")
    ghostwhite = hex_to_bgr("#f8f8ff")
    gold = hex_to_bgr("#ffd700")
    goldenrod = hex_to_bgr("#daa520")
    gray = hex_to_bgr("#808080")
    grey = hex_to_bgr("#808080")
    green = (0, 128, 0)
    greenyellow = hex_to_bgr("#adff2f")
    honeydew = hex_to_bgr("#f0fff0")
    hotpink = hex_to_bgr("#ff69b4")
    indianred = hex_to_bgr("#cd5c5c")
    indigo = hex_to_bgr("#4b0082")
    ivory = hex_to_bgr("#fffff0")
    khaki = hex_to_bgr("#f0e68c")
    lavender = hex_to_bgr("#e6e6fa")
    lavenderblush = hex_to_bgr("#fff0f5")
    lawngreen = hex_to_bgr("#7cfc00")
    lemonchiffon = hex_to_bgr("#fffacd")
    lightblue = hex_to_bgr("#add8e6")
    lightcoral = hex_to_bgr("#f08080")
    lightcyan = hex_to_bgr("#e0ffff")
    lightgoldenrodyellow = hex_to_bgr("#fafad2")
    lightgreen = hex_to_bgr("#90ee90")
    lightgray = hex_to_bgr("#d3d3d3")
    lightgrey = hex_to_bgr("#d3d3d3")
    lightpink = hex_to_bgr("#ffb6c1")
    lightsalmon = hex_to_bgr("#ffa07a")
    lightseagreen = hex_to_bgr("#20b2aa")
    lightskyblue = hex_to_bgr("#87cefa")
    lightslategray = hex_to_bgr("#778899")
    lightslategrey = hex_to_bgr("#778899")
    lightsteelblue = hex_to_bgr("#b0c4de")
    lightyellow = hex_to_bgr("#ffffe0")
    lime = hex_to_bgr("#00ff00")
    limegreen = hex_to_bgr("#32cd32")
    linen = hex_to_bgr("#faf0e6")
    magenta = hex_to_bgr("#ff00ff")
    maroon = hex_to_bgr("#800000")
    mediumaquamarine = hex_to_bgr("#66cdaa")
    mediumblue = hex_to_bgr("#0000cd")
    mediumorchid = hex_to_bgr("#ba55d3")
    mediumpurple = hex_to_bgr("#9370db")
    mediumseagreen = hex_to_bgr("#3cb371")
    mediumslateblue = hex_to_bgr("#7b68ee")
    mediumspringgreen = hex_to_bgr("#00fa9a")
    mediumturquoise = hex_to_bgr("#48d1cc")
    mediumvioletred = hex_to_bgr("#c71585")
    midnightblue = hex_to_bgr("#191970")
    mintcream = hex_to_bgr("#f5fffa")
    mistyrose = hex_to_bgr("#ffe4e1")
    moccasin = hex_to_bgr("#ffe4b5")
    navajowhite = hex_to_bgr("#ffdead")
    navy = hex_to_bgr("#000080")
    oldlace = hex_to_bgr("#fdf5e6")
    olive = hex_to_bgr("#808000")
    olivedrab = hex_to_bgr("#6b8e23")
    orange = hex_to_bgr("#ffa500")
    orangered = hex_to_bgr("#ff4500")
    orchid = hex_to_bgr("#da70d6")
    palegoldenrod = hex_to_bgr("#eee8aa")
    palegreen = hex_to_bgr("#98fb98")
    paleturquoise = hex_to_bgr("#afeeee")
    palevioletred = hex_to_bgr("#db7093")
    papayawhip = hex_to_bgr("#ffefd5")
    peachpuff = hex_to_bgr("#ffdab9")
    peru = hex_to_bgr("#cd853f")
    pink = hex_to_bgr("#ffc0cb")
    plum = hex_to_bgr("#dda0dd")
    powderblue = hex_to_bgr("#b0e0e6")
    purple = hex_to_bgr("#800080")
    rebeccapurple = hex_to_bgr("#663399")
    red = hex_to_bgr("#ff0000")
    rosybrown = hex_to_bgr("#bc8f8f")
    royalblue = hex_to_bgr("#4169e1")
    saddlebrown = hex_to_bgr("#8b4513")
    salmon = hex_to_bgr("#fa8072")
    sandybrown = hex_to_bgr("#f4a460")
    seagreen = hex_to_bgr("#2e8b57")
    seashell = hex_to_bgr("#fff5ee")
    sienna = hex_to_bgr("#a0522d")
    silver = hex_to_bgr("#c0c0c0")
    skyblue = hex_to_bgr("#87ceeb")
    slateblue = hex_to_bgr("#6a5acd")
    slategray = hex_to_bgr("#708090")
    slategrey = hex_to_bgr("#708090")
    snow = hex_to_bgr("#fffafa")
    springgreen = hex_to_bgr("#00ff7f")
    steelblue = hex_to_bgr("#4682b4")
    tan = hex_to_bgr("#d2b48c")
    teal = hex_to_bgr("#008080")
    thistle = hex_to_bgr("#d8bfd8")
    tomato = hex_to_bgr("#ff6347")
    turquoise = hex_to_bgr("#40e0d0")
    violet = hex_to_bgr("#ee82ee")
    wheat = hex_to_bgr("#f5deb3")
    white = hex_to_bgr("#ffffff")
    whitesmoke = hex_to_bgr("#f5f5f5")
    yellow = hex_to_bgr("#ffff00")
    yellowgreen = hex_to_bgr("#9acd32")

    # seaborn tab20 colors
    tab1 = hex_to_bgr("#1f77b4")
    tab2 = hex_to_bgr("#aec7e8")
    tab3 = hex_to_bgr("#ff7f0e")
    tab4 = hex_to_bgr("#ffbb78")
    tab5 = hex_to_bgr("#2ca02c")
    tab6 = hex_to_bgr("#98df8a")
    tab7 = hex_to_bgr("#d62728")
    tab8 = hex_to_bgr("#ff9896")
    tab9 = hex_to_bgr("#9467bd")
    tab10 = hex_to_bgr("#c5b0d5")
    tab11 = hex_to_bgr("#8c564b")
    tab12 = hex_to_bgr("#c49c94")
    tab13 = hex_to_bgr("#e377c2")
    tab14 = hex_to_bgr("#f7b6d2")
    tab15 = hex_to_bgr("#7f7f7f")
    tab16 = hex_to_bgr("#c7c7c7")
    tab17 = hex_to_bgr("#bcbd22")
    tab18 = hex_to_bgr("#dbdb8d")
    tab19 = hex_to_bgr("#17becf")
    tab20 = hex_to_bgr("#9edae5")
    # seaborn colorblind
    cb1 = hex_to_bgr("#0173b2")
    cb2 = hex_to_bgr("#de8f05")
    cb3 = hex_to_bgr("#029e73")
    cb4 = hex_to_bgr("#d55e00")
    cb5 = hex_to_bgr("#cc78bc")
    cb6 = hex_to_bgr("#ca9161")
    cb7 = hex_to_bgr("#fbafe4")
    cb8 = hex_to_bgr("#949494")
    cb9 = hex_to_bgr("#ece133")
    cb10 = hex_to_bgr("#56b4e9")

Palette #

Process-wide color palette used by the drawing helpers.

The palette powers the "by_id" and "by_label" color strategies in functions like draw_points and draw_boxes.

Examples:

Change the active palette by name::

1
2
>>> from norfair import Palette
>>> Palette.set("colorblind")

Or supply a custom list of colors::

1
2
>>> from norfair import Color, Palette
>>> Palette.set([Color.red, Color.blue, "#ffeeff"])
Source code in norfair/drawing/color.py
333
334
335
336
337
338
339
340
341
342
343
344
345
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
388
389
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
class Palette:
    """Process-wide color palette used by the drawing helpers.

    The palette powers the ``"by_id"`` and ``"by_label"`` color
    strategies in functions like
    [`draw_points`][norfair.drawing.draw_points.draw_points] and
    [`draw_boxes`][norfair.drawing.draw_boxes.draw_boxes].

    Examples
    --------
    Change the active palette by name::

        >>> from norfair import Palette
        >>> Palette.set("colorblind")

    Or supply a custom list of colors::

        >>> from norfair import Color, Palette
        >>> Palette.set([Color.red, Color.blue, "#ffeeff"])

    """

    _colors = PALETTES["tab10"]
    _default_color = Color.black

    @classmethod
    def set(cls, palette: str | Iterable[ColorLike]):
        """Select the active color palette.

        Parameters
        ----------
        palette : str or iterable of ColorLike
            Either:

            - The name of one of the predefined palettes: ``"tab10"``,
              ``"tab20"`` or ``"colorblind"``.
            - An iterable of ``ColorLike`` values that can be parsed by
              [`parse_color`][norfair.drawing.color.parse_color].

        Raises
        ------
        ValueError
            If ``palette`` is an unknown palette name.

        """
        if isinstance(palette, str):
            try:
                cls._colors = PALETTES[palette]
            except KeyError as e:
                raise ValueError(
                    f"Invalid palette name '{palette}', valid values are {PALETTES.keys()}"
                ) from e
        else:
            colors = []
            for c in palette:
                colors.append(parse_color(c))

            cls._colors = colors

    @classmethod
    def set_default_color(cls, color: ColorLike):
        """Set the fallback color used when ``choose_color`` is called with ``None``.

        Parameters
        ----------
        color : ColorLike
            The new default color.

        """
        cls._default_color = parse_color(color)

    @classmethod
    def choose_color(cls, hashable: Hashable) -> ColorType:
        """Deterministically pick a color for ``hashable`` from the palette.

        Parameters
        ----------
        hashable : Hashable or None
            Any hashable value (typically a tracked-object id or label).
            When ``None``, the palette's default color is returned.

        Returns
        -------
        tuple of int
            A BGR triple from the active palette.

        """
        if hashable is None:
            return cls._default_color
        return cls._colors[abs(hash(hashable)) % len(cls._colors)]

set(palette) classmethod #

Select the active color palette.

Parameters:

Name Type Description Default
palette str or iterable of ColorLike

Either:

  • The name of one of the predefined palettes: "tab10", "tab20" or "colorblind".
  • An iterable of ColorLike values that can be parsed by parse_color.
required

Raises:

Type Description
ValueError

If palette is an unknown palette name.

Source code in norfair/drawing/color.py
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
388
389
390
@classmethod
def set(cls, palette: str | Iterable[ColorLike]):
    """Select the active color palette.

    Parameters
    ----------
    palette : str or iterable of ColorLike
        Either:

        - The name of one of the predefined palettes: ``"tab10"``,
          ``"tab20"`` or ``"colorblind"``.
        - An iterable of ``ColorLike`` values that can be parsed by
          [`parse_color`][norfair.drawing.color.parse_color].

    Raises
    ------
    ValueError
        If ``palette`` is an unknown palette name.

    """
    if isinstance(palette, str):
        try:
            cls._colors = PALETTES[palette]
        except KeyError as e:
            raise ValueError(
                f"Invalid palette name '{palette}', valid values are {PALETTES.keys()}"
            ) from e
    else:
        colors = []
        for c in palette:
            colors.append(parse_color(c))

        cls._colors = colors

set_default_color(color) classmethod #

Set the fallback color used when choose_color is called with None.

Parameters:

Name Type Description Default
color ColorLike

The new default color.

required
Source code in norfair/drawing/color.py
392
393
394
395
396
397
398
399
400
401
402
@classmethod
def set_default_color(cls, color: ColorLike):
    """Set the fallback color used when ``choose_color`` is called with ``None``.

    Parameters
    ----------
    color : ColorLike
        The new default color.

    """
    cls._default_color = parse_color(color)

choose_color(hashable) classmethod #

Deterministically pick a color for hashable from the palette.

Parameters:

Name Type Description Default
hashable Hashable or None

Any hashable value (typically a tracked-object id or label). When None, the palette's default color is returned.

required

Returns:

Type Description
tuple of int

A BGR triple from the active palette.

Source code in norfair/drawing/color.py
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
@classmethod
def choose_color(cls, hashable: Hashable) -> ColorType:
    """Deterministically pick a color for ``hashable`` from the palette.

    Parameters
    ----------
    hashable : Hashable or None
        Any hashable value (typically a tracked-object id or label).
        When ``None``, the palette's default color is returned.

    Returns
    -------
    tuple of int
        A BGR triple from the active palette.

    """
    if hashable is None:
        return cls._default_color
    return cls._colors[abs(hash(hashable)) % len(cls._colors)]

hex_to_bgr(hex_value) #

Convert a hex color string to a BGR tuple.

Parameters:

Name Type Description Default
hex_value str

Hex value with a leading #. Both 6-digit ("#ff0000") and 3-digit ("#f00") shorthand are accepted.

required

Returns:

Type Description
tuple of int

The (B, G, R) triple corresponding to hex_value.

Raises:

Type Description
ValueError

If hex_value does not match a supported hex color format.

Source code in norfair/drawing/color.py
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
def hex_to_bgr(hex_value: str) -> ColorType:
    """Convert a hex color string to a BGR tuple.

    Parameters
    ----------
    hex_value : str
        Hex value with a leading ``#``. Both 6-digit (``"#ff0000"``)
        and 3-digit (``"#f00"``) shorthand are accepted.

    Returns
    -------
    tuple of int
        The ``(B, G, R)`` triple corresponding to ``hex_value``.

    Raises
    ------
    ValueError
        If ``hex_value`` does not match a supported hex color format.

    """
    if re.match("#[a-fA-F0-9]{6}$", hex_value):
        return (
            int(hex_value[5:7], 16),
            int(hex_value[3:5], 16),
            int(hex_value[1:3], 16),
        )

    if re.match("#[a-fA-F0-9]{3}$", hex_value):
        return (
            int(hex_value[3] * 2, 16),
            int(hex_value[2] * 2, 16),
            int(hex_value[1] * 2, 16),
        )
    raise ValueError(f"'{hex_value}' is not a valid color")

parse_color(color_like) #

Best-effort parse of a ColorLike value to a ColorType.

Parameters:

Name Type Description Default
color_like ColorLike

One of:

  1. A 6-digit hex string (e.g. "#ff0000").
  2. The name of one of the predefined colors on :class:Color (e.g. "red").
  3. A BGR tuple (e.g. (0, 0, 255)).
required

Returns:

Type Description
tuple of int

The resolved (B, G, R) triple.

Raises:

Type Description
ValueError

If color_like is a malformed hex string.

AttributeError

If color_like is an unknown color name.

Source code in norfair/drawing/color.py
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
275
276
277
278
279
280
def parse_color(color_like: ColorLike) -> ColorType:
    """Best-effort parse of a ``ColorLike`` value to a ``ColorType``.

    Parameters
    ----------
    color_like : ColorLike
        One of:

        1. A 6-digit hex string (e.g. ``"#ff0000"``).
        2. The name of one of the predefined colors on :class:`Color`
           (e.g. ``"red"``).
        3. A BGR tuple (e.g. ``(0, 0, 255)``).

    Returns
    -------
    tuple of int
        The resolved ``(B, G, R)`` triple.

    Raises
    ------
    ValueError
        If ``color_like`` is a malformed hex string.
    AttributeError
        If ``color_like`` is an unknown color name.

    """
    if isinstance(color_like, str):
        if color_like.startswith("#"):
            return hex_to_bgr(color_like)
        try:
            return getattr(Color, color_like)
        except AttributeError as exc:
            raise ValueError(
                f"Unknown color name {color_like!r}. Pass a 6-digit hex "
                f"string like '#ff0000', a BGR tuple, or one of the names "
                f"defined in norfair.drawing.Color."
            ) from exc
    # color_like is already a ColorType tuple at this point
    # Ensure it's properly typed as tuple[int, int, int]
    return (int(color_like[0]), int(color_like[1]), int(color_like[2]))

path #

Drawers that trace the trajectories of tracked points across frames.

Paths #

Draw the trajectories of points of interest on each tracked object.

Parameters:

Name Type Description Default
get_points_to_draw callable

Callable taking the .estimate of a TrackedObject and returning a sequence of points whose paths should be drawn. By default the mean of all points in the tracker is used.

None
thickness int

Thickness of the circles representing the path.

None
color tuple of int

BGR Color of the path circles. By default the color is selected from the active Palette based on the object's id.

None
radius int

Radius of the circles representing the path.

None
attenuation float

Value in [0, 1] controlling how fast existing path pixels fade between frames. Use 0 to keep the path forever.

0.01

Examples:

Overlay trajectories on top of tracked objects::

1
2
3
4
5
6
7
8
9
>>> from norfair import Paths, Tracker, Video
>>> tracker = Tracker(...)
>>> path_drawer = Paths()
>>> with Video(input_path="video.mp4") as video:
...     for frame in video:
...         detections = get_detections(frame)
...         tracked_objects = tracker.update(detections)
...         frame = path_drawer.draw(frame, tracked_objects)
...         video.write(frame)
Source code in norfair/drawing/path.py
 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
class Paths:
    """Draw the trajectories of points of interest on each tracked object.

    Parameters
    ----------
    get_points_to_draw : callable, optional
        Callable taking the ``.estimate`` of a
        [`TrackedObject`][norfair.tracker.TrackedObject] and returning
        a sequence of points whose paths should be drawn. By default
        the mean of all points in the tracker is used.
    thickness : int, optional
        Thickness of the circles representing the path.
    color : tuple of int, optional
        BGR [Color][norfair.drawing.Color] of the path circles. By
        default the color is selected from the active
        [`Palette`][norfair.drawing.Palette] based on the object's id.
    radius : int, optional
        Radius of the circles representing the path.
    attenuation : float, optional
        Value in ``[0, 1]`` controlling how fast existing path pixels
        fade between frames. Use ``0`` to keep the path forever.

    Examples
    --------
    Overlay trajectories on top of tracked objects::

        >>> from norfair import Paths, Tracker, Video
        >>> tracker = Tracker(...)
        >>> path_drawer = Paths()
        >>> with Video(input_path="video.mp4") as video:
        ...     for frame in video:
        ...         detections = get_detections(frame)
        ...         tracked_objects = tracker.update(detections)
        ...         frame = path_drawer.draw(frame, tracked_objects)
        ...         video.write(frame)

    """

    def __init__(
        self,
        get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
        | None = None,
        thickness: int | None = None,
        color: tuple[int, int, int] | None = None,
        radius: int | None = None,
        attenuation: float = 0.01,
    ):
        """Configure the path drawer with its rendering knobs."""
        self.get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
        if get_points_to_draw is None:

            def default_get_points(
                points: NDArray[np.float64],
            ) -> NDArray[np.float64]:
                """Return the centroid of *points* as a single-element array.

                Parameters
                ----------
                points : NDArray[np.float64]
                    Array of shape ``(N, D)`` representing tracked point
                    coordinates.

                Returns
                -------
                NDArray[np.float64]
                    Array of shape ``(1, D)`` containing the centroid.
                """
                return np.array([np.mean(np.array(points), axis=0)])

            self.get_points_to_draw = default_get_points
        else:
            self.get_points_to_draw = get_points_to_draw

        self.radius = radius
        self.thickness = thickness
        self.color = color
        self.mask: NDArray[np.uint8] | None = None
        self.attenuation_factor = 1 - attenuation

    def draw(
        self, frame: np.ndarray, tracked_objects: Sequence[TrackedObject]
    ) -> np.ndarray:
        """Update and render the accumulated path mask onto ``frame``.

        !!! warning
            Unlike most other drawers, this method does **not** mutate
            ``frame`` in place — the blended result is returned.

        Parameters
        ----------
        frame : np.ndarray
            The OpenCV frame to draw on.
        tracked_objects : Sequence[TrackedObject]
            The [`TrackedObject`][norfair.tracker.TrackedObject] list
            whose points-of-interest are appended to the running path
            mask for this frame.

        Returns
        -------
        np.ndarray
            A new frame with the current path mask blended on top.

        """
        if self.mask is None:
            frame_scale = frame.shape[0] / 100

            if self.radius is None:
                self.radius = int(max(frame_scale * 0.7, 1))
            if self.thickness is None:
                self.thickness = int(max(frame_scale / 7, 1))

            self.mask = np.zeros(frame.shape, np.uint8)

        # Saturate values before the uint8 cast: ``attenuation_factor`` can be
        # >= 1 (e.g. ``attenuation == 0``), and an unclipped cast wraps.
        mask = np.clip(self.mask * self.attenuation_factor, 0, 255).astype("uint8")

        for obj in tracked_objects:
            if obj.abs_to_rel is not None:
                warn_once(
                    "It seems that you're using the Path drawer together with MotionEstimator. This is not fully supported and the results will not be what's expected"
                )

            if self.color is None:
                color = Palette.choose_color(obj.id)
            else:
                color = self.color

            points_to_draw = self.get_points_to_draw(obj.estimate)

            for point in points_to_draw:
                safe_pos = _safe_int_point(point)
                if safe_pos is None:
                    continue
                mask = Drawer.circle(
                    mask,
                    position=safe_pos,
                    radius=self.radius,
                    color=color,
                    thickness=self.thickness,
                )

        self.mask = mask
        return Drawer.alpha_blend(mask, frame, alpha=1, beta=1)

__init__(get_points_to_draw=None, thickness=None, color=None, radius=None, attenuation=0.01) #

Configure the path drawer with its rendering knobs.

Source code in norfair/drawing/path.py
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
def __init__(
    self,
    get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
    | None = None,
    thickness: int | None = None,
    color: tuple[int, int, int] | None = None,
    radius: int | None = None,
    attenuation: float = 0.01,
):
    """Configure the path drawer with its rendering knobs."""
    self.get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
    if get_points_to_draw is None:

        def default_get_points(
            points: NDArray[np.float64],
        ) -> NDArray[np.float64]:
            """Return the centroid of *points* as a single-element array.

            Parameters
            ----------
            points : NDArray[np.float64]
                Array of shape ``(N, D)`` representing tracked point
                coordinates.

            Returns
            -------
            NDArray[np.float64]
                Array of shape ``(1, D)`` containing the centroid.
            """
            return np.array([np.mean(np.array(points), axis=0)])

        self.get_points_to_draw = default_get_points
    else:
        self.get_points_to_draw = get_points_to_draw

    self.radius = radius
    self.thickness = thickness
    self.color = color
    self.mask: NDArray[np.uint8] | None = None
    self.attenuation_factor = 1 - attenuation

draw(frame, tracked_objects) #

Update and render the accumulated path mask onto frame.

Warning

Unlike most other drawers, this method does not mutate frame in place — the blended result is returned.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on.

required
tracked_objects Sequence[TrackedObject]

The TrackedObject list whose points-of-interest are appended to the running path mask for this frame.

required

Returns:

Type Description
ndarray

A new frame with the current path mask blended on top.

Source code in norfair/drawing/path.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
149
150
151
152
153
154
155
156
157
158
def draw(
    self, frame: np.ndarray, tracked_objects: Sequence[TrackedObject]
) -> np.ndarray:
    """Update and render the accumulated path mask onto ``frame``.

    !!! warning
        Unlike most other drawers, this method does **not** mutate
        ``frame`` in place — the blended result is returned.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on.
    tracked_objects : Sequence[TrackedObject]
        The [`TrackedObject`][norfair.tracker.TrackedObject] list
        whose points-of-interest are appended to the running path
        mask for this frame.

    Returns
    -------
    np.ndarray
        A new frame with the current path mask blended on top.

    """
    if self.mask is None:
        frame_scale = frame.shape[0] / 100

        if self.radius is None:
            self.radius = int(max(frame_scale * 0.7, 1))
        if self.thickness is None:
            self.thickness = int(max(frame_scale / 7, 1))

        self.mask = np.zeros(frame.shape, np.uint8)

    # Saturate values before the uint8 cast: ``attenuation_factor`` can be
    # >= 1 (e.g. ``attenuation == 0``), and an unclipped cast wraps.
    mask = np.clip(self.mask * self.attenuation_factor, 0, 255).astype("uint8")

    for obj in tracked_objects:
        if obj.abs_to_rel is not None:
            warn_once(
                "It seems that you're using the Path drawer together with MotionEstimator. This is not fully supported and the results will not be what's expected"
            )

        if self.color is None:
            color = Palette.choose_color(obj.id)
        else:
            color = self.color

        points_to_draw = self.get_points_to_draw(obj.estimate)

        for point in points_to_draw:
            safe_pos = _safe_int_point(point)
            if safe_pos is None:
                continue
            mask = Drawer.circle(
                mask,
                position=safe_pos,
                radius=self.radius,
                color=color,
                thickness=self.thickness,
            )

    self.mask = mask
    return Drawer.alpha_blend(mask, frame, alpha=1, beta=1)

AbsolutePaths #

Draw tracked-object trajectories in absolute (world) coordinates.

Behaves like Paths, but takes camera motion into account so trajectories stay pinned to the world frame.

Warning

This drawer is not optimized and can be extremely slow: rendering cost grows linearly with max_history * number_of_tracked_objects.

Parameters:

Name Type Description Default
get_points_to_draw callable

Callable taking the .estimate of a TrackedObject and returning a sequence of points whose paths should be drawn. By default the mean of all points in the tracker is used.

None
thickness int

Thickness of the circles / connecting lines.

None
color tuple of int

BGR Color used for every object. By default the color is selected from the active palette based on the object's id.

None
radius int

Radius of the circles representing the latest point.

None
max_history int

Number of past samples to include in the path. Higher values make the drawing slower.

20

Examples:

Overlay trajectories on top of tracked objects while accounting for camera motion::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> from norfair import AbsolutePaths, MotionEstimator, Tracker, Video
>>> tracker = Tracker(...)
>>> motion_estimator = MotionEstimator()
>>> path_drawer = AbsolutePaths()
>>> with Video(input_path="video.mp4") as video:
...     for frame in video:
...         coord_transform = motion_estimator.update(frame)
...         detections = get_detections(frame)
...         tracked_objects = tracker.update(
...             detections, coord_transformations=coord_transform
...         )
...         frame = path_drawer.draw(frame, tracked_objects, coord_transform)
...         video.write(frame)
Source code in norfair/drawing/path.py
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
275
276
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
311
312
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
class AbsolutePaths:
    """Draw tracked-object trajectories in absolute (world) coordinates.

    Behaves like [`Paths`][norfair.drawing.Paths], but takes camera
    motion into account so trajectories stay pinned to the world
    frame.

    !!! warning
        This drawer is not optimized and can be extremely slow:
        rendering cost grows linearly with
        ``max_history * number_of_tracked_objects``.

    Parameters
    ----------
    get_points_to_draw : callable, optional
        Callable taking the ``.estimate`` of a
        [`TrackedObject`][norfair.tracker.TrackedObject] and returning
        a sequence of points whose paths should be drawn. By default
        the mean of all points in the tracker is used.
    thickness : int, optional
        Thickness of the circles / connecting lines.
    color : tuple of int, optional
        BGR [Color][norfair.drawing.Color] used for every object. By
        default the color is selected from the active palette based on
        the object's id.
    radius : int, optional
        Radius of the circles representing the latest point.
    max_history : int, optional
        Number of past samples to include in the path. Higher values
        make the drawing slower.

    Examples
    --------
    Overlay trajectories on top of tracked objects while accounting
    for camera motion::

        >>> from norfair import AbsolutePaths, MotionEstimator, Tracker, Video
        >>> tracker = Tracker(...)
        >>> motion_estimator = MotionEstimator()
        >>> path_drawer = AbsolutePaths()
        >>> with Video(input_path="video.mp4") as video:
        ...     for frame in video:
        ...         coord_transform = motion_estimator.update(frame)
        ...         detections = get_detections(frame)
        ...         tracked_objects = tracker.update(
        ...             detections, coord_transformations=coord_transform
        ...         )
        ...         frame = path_drawer.draw(frame, tracked_objects, coord_transform)
        ...         video.write(frame)

    """

    def __init__(
        self,
        get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
        | None = None,
        thickness: int | None = None,
        color: tuple[int, int, int] | None = None,
        radius: int | None = None,
        max_history=20,
    ):
        """Configure the absolute-coordinates path drawer."""
        self.get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
        if get_points_to_draw is None:

            def default_get_points(
                points: NDArray[np.float64],
            ) -> NDArray[np.float64]:
                """Return the centroid of *points* as a single-element array.

                Parameters
                ----------
                points : NDArray[np.float64]
                    Array of shape ``(N, D)`` representing tracked point
                    coordinates.

                Returns
                -------
                NDArray[np.float64]
                    Array of shape ``(1, D)`` containing the centroid.
                """
                return np.array([np.mean(np.array(points), axis=0)])

            self.get_points_to_draw = default_get_points
        else:
            self.get_points_to_draw = get_points_to_draw

        self.radius = radius
        self.thickness = thickness
        self.color = color
        self.max_history = max_history
        # Bounded history per object; most recent sample lives at index 0.
        self.past_points: defaultdict[int | None, deque[NDArray[np.float64]]] = (
            defaultdict(lambda: deque(maxlen=self.max_history))
        )
        self.alphas = np.linspace(0.99, 0.01, max_history)
        # Scratch buffer reused across draw iterations. Lazy-allocated in
        # ``draw`` to match the incoming frame's shape/dtype.
        self._overlay_scratch: np.ndarray | None = None

    def draw(self, frame, tracked_objects, coord_transform=None):
        """Render accumulated absolute paths onto ``frame``.

        Parameters
        ----------
        frame : np.ndarray
            The OpenCV frame to draw on. Modified in place.
        tracked_objects : Sequence[TrackedObject]
            Tracked objects whose absolute trajectories are updated
            and rendered for this frame.
        coord_transform : CoordinatesTransformation, optional
            Transformation between absolute and relative coordinates
            for the current frame. When ``None``, points are assumed
            to already be in the frame's pixel coordinates.

        Returns
        -------
        np.ndarray
            The ``frame`` passed in, with the paths blended on top.

        """
        frame_scale = frame.shape[0] / 100

        if self.radius is None:
            self.radius = int(max(frame_scale * 0.7, 1))
        if self.thickness is None:
            self.thickness = int(max(frame_scale / 7, 1))

        # (Re-)allocate the scratch buffer when the frame's shape or dtype
        # changes; otherwise the existing buffer is reused as-is.
        if (
            self._overlay_scratch is None
            or self._overlay_scratch.shape != frame.shape
            or self._overlay_scratch.dtype != frame.dtype
        ):
            self._overlay_scratch = np.empty_like(frame)

        for obj in tracked_objects:
            if not obj.live_points.any():
                continue

            if self.color is None:
                color = Palette.choose_color(obj.id)
            else:
                color = self.color

            points_to_draw = self.get_points_to_draw(obj.get_estimate(absolute=True))

            # Convert from absolute to relative coordinates if transform is provided
            points_rel = (
                coord_transform.abs_to_rel(points_to_draw)
                if coord_transform is not None
                else points_to_draw
            )

            for point in points_rel:
                safe_pos = _safe_int_point(point)
                if safe_pos is None:
                    continue
                Drawer.circle(
                    frame,
                    position=safe_pos,
                    radius=self.radius,
                    color=color,
                    thickness=self.thickness,
                )

            last = points_to_draw
            assert self._overlay_scratch is not None
            overlay = self._overlay_scratch
            for i, past_points in enumerate(self.past_points[obj.id]):
                # Refill the scratch buffer with the current frame before
                # drawing the segment that will be blended back onto it.
                np.copyto(overlay, frame)
                last_rel = (
                    coord_transform.abs_to_rel(last)
                    if coord_transform is not None
                    else last
                )
                past_points_rel = (
                    coord_transform.abs_to_rel(past_points)
                    if coord_transform is not None
                    else past_points
                )
                for j, point in enumerate(past_points_rel):
                    start = _safe_int_point(last_rel[j])
                    end = _safe_int_point(point)
                    if start is None or end is None:
                        continue
                    Drawer.line(
                        overlay,
                        start,
                        end,
                        color=color,
                        thickness=self.thickness,
                    )
                last = past_points

                alpha = self.alphas[i]
                frame = Drawer.alpha_blend(overlay, frame, alpha=alpha)
            self.past_points[obj.id].appendleft(points_to_draw)

        # Clean up dead objects to prevent memory leak
        active_ids = {obj.id for obj in tracked_objects}
        dead_ids = [k for k in self.past_points if k not in active_ids]
        for k in dead_ids:
            del self.past_points[k]

        return frame

__init__(get_points_to_draw=None, thickness=None, color=None, radius=None, max_history=20) #

Configure the absolute-coordinates path drawer.

Source code in norfair/drawing/path.py
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
def __init__(
    self,
    get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
    | None = None,
    thickness: int | None = None,
    color: tuple[int, int, int] | None = None,
    radius: int | None = None,
    max_history=20,
):
    """Configure the absolute-coordinates path drawer."""
    self.get_points_to_draw: Callable[[NDArray[np.float64]], NDArray[np.float64]]
    if get_points_to_draw is None:

        def default_get_points(
            points: NDArray[np.float64],
        ) -> NDArray[np.float64]:
            """Return the centroid of *points* as a single-element array.

            Parameters
            ----------
            points : NDArray[np.float64]
                Array of shape ``(N, D)`` representing tracked point
                coordinates.

            Returns
            -------
            NDArray[np.float64]
                Array of shape ``(1, D)`` containing the centroid.
            """
            return np.array([np.mean(np.array(points), axis=0)])

        self.get_points_to_draw = default_get_points
    else:
        self.get_points_to_draw = get_points_to_draw

    self.radius = radius
    self.thickness = thickness
    self.color = color
    self.max_history = max_history
    # Bounded history per object; most recent sample lives at index 0.
    self.past_points: defaultdict[int | None, deque[NDArray[np.float64]]] = (
        defaultdict(lambda: deque(maxlen=self.max_history))
    )
    self.alphas = np.linspace(0.99, 0.01, max_history)
    # Scratch buffer reused across draw iterations. Lazy-allocated in
    # ``draw`` to match the incoming frame's shape/dtype.
    self._overlay_scratch: np.ndarray | None = None

draw(frame, tracked_objects, coord_transform=None) #

Render accumulated absolute paths onto frame.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on. Modified in place.

required
tracked_objects Sequence[TrackedObject]

Tracked objects whose absolute trajectories are updated and rendered for this frame.

required
coord_transform CoordinatesTransformation

Transformation between absolute and relative coordinates for the current frame. When None, points are assumed to already be in the frame's pixel coordinates.

None

Returns:

Type Description
ndarray

The frame passed in, with the paths blended on top.

Source code in norfair/drawing/path.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
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
311
312
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def draw(self, frame, tracked_objects, coord_transform=None):
    """Render accumulated absolute paths onto ``frame``.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on. Modified in place.
    tracked_objects : Sequence[TrackedObject]
        Tracked objects whose absolute trajectories are updated
        and rendered for this frame.
    coord_transform : CoordinatesTransformation, optional
        Transformation between absolute and relative coordinates
        for the current frame. When ``None``, points are assumed
        to already be in the frame's pixel coordinates.

    Returns
    -------
    np.ndarray
        The ``frame`` passed in, with the paths blended on top.

    """
    frame_scale = frame.shape[0] / 100

    if self.radius is None:
        self.radius = int(max(frame_scale * 0.7, 1))
    if self.thickness is None:
        self.thickness = int(max(frame_scale / 7, 1))

    # (Re-)allocate the scratch buffer when the frame's shape or dtype
    # changes; otherwise the existing buffer is reused as-is.
    if (
        self._overlay_scratch is None
        or self._overlay_scratch.shape != frame.shape
        or self._overlay_scratch.dtype != frame.dtype
    ):
        self._overlay_scratch = np.empty_like(frame)

    for obj in tracked_objects:
        if not obj.live_points.any():
            continue

        if self.color is None:
            color = Palette.choose_color(obj.id)
        else:
            color = self.color

        points_to_draw = self.get_points_to_draw(obj.get_estimate(absolute=True))

        # Convert from absolute to relative coordinates if transform is provided
        points_rel = (
            coord_transform.abs_to_rel(points_to_draw)
            if coord_transform is not None
            else points_to_draw
        )

        for point in points_rel:
            safe_pos = _safe_int_point(point)
            if safe_pos is None:
                continue
            Drawer.circle(
                frame,
                position=safe_pos,
                radius=self.radius,
                color=color,
                thickness=self.thickness,
            )

        last = points_to_draw
        assert self._overlay_scratch is not None
        overlay = self._overlay_scratch
        for i, past_points in enumerate(self.past_points[obj.id]):
            # Refill the scratch buffer with the current frame before
            # drawing the segment that will be blended back onto it.
            np.copyto(overlay, frame)
            last_rel = (
                coord_transform.abs_to_rel(last)
                if coord_transform is not None
                else last
            )
            past_points_rel = (
                coord_transform.abs_to_rel(past_points)
                if coord_transform is not None
                else past_points
            )
            for j, point in enumerate(past_points_rel):
                start = _safe_int_point(last_rel[j])
                end = _safe_int_point(point)
                if start is None or end is None:
                    continue
                Drawer.line(
                    overlay,
                    start,
                    end,
                    color=color,
                    thickness=self.thickness,
                )
            last = past_points

            alpha = self.alphas[i]
            frame = Drawer.alpha_blend(overlay, frame, alpha=alpha)
        self.past_points[obj.id].appendleft(points_to_draw)

    # Clean up dead objects to prevent memory leak
    active_ids = {obj.id for obj in tracked_objects}
    dead_ids = [k for k in self.past_points if k not in active_ids]
    for k in dead_ids:
        del self.past_points[k]

    return frame

fixed_camera #

Video stabilization drawer built on camera-motion estimates.

FixedCamera #

Stabilize the video by compensating for estimated camera motion.

The drawer renders on a larger canvas and shifts the original frame in the opposite direction of the camera motion, so stationary objects in the world stay pinned in the output. Useful for debugging or showcasing camera-motion estimation.

Warning

Only supports TranslationTransformation. Passing a HomographyTransformation yields undefined behavior.

Warning

If combined with other drawers, always apply FixedCamera last. Drawing on the scaled-up frame produced by this class will not give the expected result.

Note

Sometimes the camera moves so far from the starting point that the shifted frame no longer fits inside the scaled-up canvas. In that case, a warning is logged and the frame is cropped.

Parameters:

Name Type Description Default
scale float

The output resolution is scale * (H, W) where H, W is the resolution of the input frame. Increase this when the camera moves a lot.

2
attenuation float

Controls how quickly older content fades toward black.

0.05

Examples:

Stabilize a video using FixedCamera alongside a tracker::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> tracker = Tracker("frobenius", 100)
>>> motion_estimator = MotionEstimator()
>>> fixed_camera = FixedCamera()
>>> with Video(input_path="video.mp4") as video:
...     for frame in video:
...         coord_transformations = motion_estimator.update(frame)
...         detections = get_detections(frame)
...         tracked_objects = tracker.update(
...             detections, coord_transformations=coord_transformations
...         )
...         # apply fixed_camera last
...         draw_points(frame, tracked_objects)
...         bigger_frame = fixed_camera.adjust_frame(
...             frame, coord_transformations
...         )
...         video.write(bigger_frame)
Source code in norfair/drawing/fixed_camera.py
  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
class FixedCamera:
    """Stabilize the video by compensating for estimated camera motion.

    The drawer renders on a larger canvas and shifts the original
    frame in the opposite direction of the camera motion, so stationary
    objects in the world stay pinned in the output. Useful for
    debugging or showcasing camera-motion estimation.

    !!! Warning
        Only supports
        [`TranslationTransformation`][norfair.camera_motion.TranslationTransformation].
        Passing a
        [`HomographyTransformation`][norfair.camera_motion.HomographyTransformation]
        yields undefined behavior.

    !!! Warning
        If combined with other drawers, always apply ``FixedCamera``
        last. Drawing on the scaled-up frame produced by this class
        will not give the expected result.

    !!! Note
        Sometimes the camera moves so far from the starting point that
        the shifted frame no longer fits inside the scaled-up canvas.
        In that case, a warning is logged and the frame is cropped.

    Parameters
    ----------
    scale : float, optional
        The output resolution is ``scale * (H, W)`` where ``H, W`` is
        the resolution of the input frame. Increase this when the
        camera moves a lot.
    attenuation : float, optional
        Controls how quickly older content fades toward black.

    Examples
    --------
    Stabilize a video using ``FixedCamera`` alongside a tracker::

        >>> tracker = Tracker("frobenius", 100)
        >>> motion_estimator = MotionEstimator()
        >>> fixed_camera = FixedCamera()
        >>> with Video(input_path="video.mp4") as video:
        ...     for frame in video:
        ...         coord_transformations = motion_estimator.update(frame)
        ...         detections = get_detections(frame)
        ...         tracked_objects = tracker.update(
        ...             detections, coord_transformations=coord_transformations
        ...         )
        ...         # apply fixed_camera last
        ...         draw_points(frame, tracked_objects)
        ...         bigger_frame = fixed_camera.adjust_frame(
        ...             frame, coord_transformations
        ...         )
        ...         video.write(bigger_frame)

    """

    def __init__(self, scale: float = 2, attenuation: float = 0.05):
        """Initialize the background canvas parameters."""
        self.scale = scale
        self._background: np.ndarray | None = None
        self._attenuation_factor = 1 - attenuation

    def adjust_frame(
        self, frame: np.ndarray, coord_transformation: TranslationTransformation
    ) -> np.ndarray:
        """Render the next frame onto the stabilized background canvas.

        Parameters
        ----------
        frame : np.ndarray
            The OpenCV frame for this time step.
        coord_transformation : TranslationTransformation
            The coordinate transformation as returned by the
            [`MotionEstimator`][norfair.camera_motion.MotionEstimator]
            for this frame.

        Returns
        -------
        np.ndarray
            The scaled-up background canvas with ``frame`` drawn onto
            it at the position that compensates for the estimated
            camera motion.

        """
        # initialize background if necessary
        background: np.ndarray
        if self._background is None:
            original_size = (
                frame.shape[1],
                frame.shape[0],
            )  # OpenCV format is (width, height)

            scaled_size = tuple(
                (np.array(original_size) * np.array(self.scale)).round().astype(int)
            )
            background = np.zeros(
                [scaled_size[1], scaled_size[0], frame.shape[-1]],
                frame.dtype,
            )
        else:
            # Saturate values to the destination integer range before casting
            # to prevent uint/int overflow.
            info = (
                np.iinfo(frame.dtype)
                if np.issubdtype(frame.dtype, np.integer)
                else None
            )
            scaled = self._background * self._attenuation_factor
            if info is not None:
                scaled = np.clip(scaled, info.min, info.max)
            background = scaled.astype(frame.dtype)

        # top_left is the anchor coordinate from where we start drawing the fame on top of the background
        # aim to draw it in the center of the background but transformations will move this point
        top_left = np.array(background.shape[:2]) // 2 - np.array(frame.shape[:2]) // 2
        top_left = (
            coord_transformation.rel_to_abs(top_left[::-1]).round().astype(int)[::-1]
        )
        # box of the background that will be updated and the limits of it
        background_y0, background_y1 = (top_left[0], top_left[0] + frame.shape[0])
        background_x0, background_x1 = (top_left[1], top_left[1] + frame.shape[1])
        background_size_y, background_size_x = background.shape[:2]

        # define box of the frame that will be used
        # if the scale is not enough to support the movement, warn the user but keep drawing
        # cropping the frame so that the operation doesn't fail
        frame_y0, frame_y1, frame_x0, frame_x1 = (0, frame.shape[0], 0, frame.shape[1])
        if (
            background_y0 < 0
            or background_x0 < 0
            or background_y1 > background_size_y
            or background_x1 > background_size_x
        ):
            warn_once(
                "moving_camera_scale is not enough to cover the range of camera movement, frame will be cropped"
            )
            # crop left or top of the frame if necessary
            frame_y0 = max(-background_y0, 0)
            frame_x0 = max(-background_x0, 0)
            # crop right or bottom of the frame if necessary
            frame_y1 = max(
                min(background_size_y - background_y0, background_y1 - background_y0), 0
            )
            frame_x1 = max(
                min(background_size_x - background_x0, background_x1 - background_x0), 0
            )
            # handle cases where the limits of the background become negative which numpy will interpret incorrectly
            background_y0 = max(background_y0, 0)
            background_x0 = max(background_x0, 0)
            background_y1 = max(background_y1, 0)
            background_x1 = max(background_x1, 0)
        background[background_y0:background_y1, background_x0:background_x1, :] = frame[
            frame_y0:frame_y1, frame_x0:frame_x1, :
        ]
        self._background = background
        return background

__init__(scale=2, attenuation=0.05) #

Initialize the background canvas parameters.

Source code in norfair/drawing/fixed_camera.py
66
67
68
69
70
def __init__(self, scale: float = 2, attenuation: float = 0.05):
    """Initialize the background canvas parameters."""
    self.scale = scale
    self._background: np.ndarray | None = None
    self._attenuation_factor = 1 - attenuation

adjust_frame(frame, coord_transformation) #

Render the next frame onto the stabilized background canvas.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame for this time step.

required
coord_transformation TranslationTransformation

The coordinate transformation as returned by the MotionEstimator for this frame.

required

Returns:

Type Description
ndarray

The scaled-up background canvas with frame drawn onto it at the position that compensates for the estimated camera motion.

Source code in norfair/drawing/fixed_camera.py
 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
def adjust_frame(
    self, frame: np.ndarray, coord_transformation: TranslationTransformation
) -> np.ndarray:
    """Render the next frame onto the stabilized background canvas.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame for this time step.
    coord_transformation : TranslationTransformation
        The coordinate transformation as returned by the
        [`MotionEstimator`][norfair.camera_motion.MotionEstimator]
        for this frame.

    Returns
    -------
    np.ndarray
        The scaled-up background canvas with ``frame`` drawn onto
        it at the position that compensates for the estimated
        camera motion.

    """
    # initialize background if necessary
    background: np.ndarray
    if self._background is None:
        original_size = (
            frame.shape[1],
            frame.shape[0],
        )  # OpenCV format is (width, height)

        scaled_size = tuple(
            (np.array(original_size) * np.array(self.scale)).round().astype(int)
        )
        background = np.zeros(
            [scaled_size[1], scaled_size[0], frame.shape[-1]],
            frame.dtype,
        )
    else:
        # Saturate values to the destination integer range before casting
        # to prevent uint/int overflow.
        info = (
            np.iinfo(frame.dtype)
            if np.issubdtype(frame.dtype, np.integer)
            else None
        )
        scaled = self._background * self._attenuation_factor
        if info is not None:
            scaled = np.clip(scaled, info.min, info.max)
        background = scaled.astype(frame.dtype)

    # top_left is the anchor coordinate from where we start drawing the fame on top of the background
    # aim to draw it in the center of the background but transformations will move this point
    top_left = np.array(background.shape[:2]) // 2 - np.array(frame.shape[:2]) // 2
    top_left = (
        coord_transformation.rel_to_abs(top_left[::-1]).round().astype(int)[::-1]
    )
    # box of the background that will be updated and the limits of it
    background_y0, background_y1 = (top_left[0], top_left[0] + frame.shape[0])
    background_x0, background_x1 = (top_left[1], top_left[1] + frame.shape[1])
    background_size_y, background_size_x = background.shape[:2]

    # define box of the frame that will be used
    # if the scale is not enough to support the movement, warn the user but keep drawing
    # cropping the frame so that the operation doesn't fail
    frame_y0, frame_y1, frame_x0, frame_x1 = (0, frame.shape[0], 0, frame.shape[1])
    if (
        background_y0 < 0
        or background_x0 < 0
        or background_y1 > background_size_y
        or background_x1 > background_size_x
    ):
        warn_once(
            "moving_camera_scale is not enough to cover the range of camera movement, frame will be cropped"
        )
        # crop left or top of the frame if necessary
        frame_y0 = max(-background_y0, 0)
        frame_x0 = max(-background_x0, 0)
        # crop right or bottom of the frame if necessary
        frame_y1 = max(
            min(background_size_y - background_y0, background_y1 - background_y0), 0
        )
        frame_x1 = max(
            min(background_size_x - background_x0, background_x1 - background_x0), 0
        )
        # handle cases where the limits of the background become negative which numpy will interpret incorrectly
        background_y0 = max(background_y0, 0)
        background_x0 = max(background_x0, 0)
        background_y1 = max(background_y1, 0)
        background_x1 = max(background_x1, 0)
    background[background_y0:background_y1, background_x0:background_x1, :] = frame[
        frame_y0:frame_y1, frame_x0:frame_x1, :
    ]
    self._background = background
    return background

absolute_grid #

Draw a debugging grid in absolute coordinates over a video frame.

draw_absolute_grid(frame, coord_transformations, grid_size=20, radius=2, thickness=1, color=Color.black, polar=False) #

Draw a grid of points in absolute coordinates onto frame.

Useful for debugging camera-motion estimation: the grid stays put in world space, so any apparent movement of the points reflects the residual error of the estimated transformation.

The points are drawn as if the camera sat at the center of a unit sphere, at the intersection of latitude and longitude lines on that sphere's surface.

Parameters:

Name Type Description Default
frame ndarray

The OpenCV frame to draw on. Modified in place.

required
coord_transformations CoordinatesTransformation or None

The coordinate transformation as returned by a MotionEstimator. If None, no coordinate transformation is applied and the grid points are drawn in their original absolute positions.

required
grid_size int

Number of grid subdivisions per axis.

20
radius int

Radius (in pixels) of each grid cross.

2
thickness int

Stroke thickness of each grid cross.

1
color ColorType

BGR color of the grid crosses.

black
polar bool

If True, on the first frame the points are drawn as if the camera were pointing at a pole. When False (the default), the camera is pointing at the equator.

False
Source code in norfair/drawing/absolute_grid.py
 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
def draw_absolute_grid(
    frame: np.ndarray,
    coord_transformations: CoordinatesTransformation | None,
    grid_size: int = 20,
    radius: int = 2,
    thickness: int = 1,
    color: ColorType = Color.black,
    polar: bool = False,
):
    """Draw a grid of points in absolute coordinates onto ``frame``.

    Useful for debugging camera-motion estimation: the grid stays put
    in world space, so any apparent movement of the points reflects the
    residual error of the estimated transformation.

    The points are drawn as if the camera sat at the center of a unit
    sphere, at the intersection of latitude and longitude lines on that
    sphere's surface.

    Parameters
    ----------
    frame : np.ndarray
        The OpenCV frame to draw on. Modified in place.
    coord_transformations : CoordinatesTransformation or None
        The coordinate transformation as returned by a
        [`MotionEstimator`][norfair.camera_motion.MotionEstimator].
        If ``None``, no coordinate transformation is applied and the grid
        points are drawn in their original absolute positions.
    grid_size : int, optional
        Number of grid subdivisions per axis.
    radius : int, optional
        Radius (in pixels) of each grid cross.
    thickness : int, optional
        Stroke thickness of each grid cross.
    color : ColorType, optional
        BGR color of the grid crosses.
    polar : bool, optional
        If ``True``, on the first frame the points are drawn as if the
        camera were pointing at a pole. When ``False`` (the default),
        the camera is pointing at the equator.

    """
    h, w, _ = frame.shape

    # get absolute points grid
    points = _get_grid(grid_size, w, h, polar=polar).copy()

    # transform the points to relative coordinates
    if coord_transformations is None:
        points_transformed = points
    else:
        points_transformed = coord_transformations.abs_to_rel(points)

    # filter points that are not visible or contain non-finite values (#41)
    finite_mask = np.all(np.isfinite(points_transformed), axis=1)
    visible_points = points_transformed[
        finite_mask
        & (points_transformed <= np.array([w, h])).all(axis=1)
        & (points_transformed >= 0).all(axis=1)
    ]
    for point in visible_points:
        Drawer.cross(
            frame, point.astype(int), radius=radius, thickness=thickness, color=color
        )

See also#