Skip to content

Data

Everything moves through one layout, a float32 (N, 7) array of [x1, y1, x2, y2, conf, cls, id]. Tracks wraps it, Track is one row of it, and Preds is the id-less version used for secondary output.

Tracks

Tracks(data=None, names=None, img=None)

A sliceable view over an (N, 7) box array.

names maps class ids to strings (list or dict, either works) and img is the frame the boxes came from, so draw() needs no argument.

Source code in vizor/boxes.py
def __init__(self, data=None, names=None, img=None):
    self.data = _as_data(data)
    self.names = names
    self.img = img

boxes property

boxes

View of the (N, 4) xyxy columns. Writes go through to data.

conf property

conf

View of the (N,) confidence column.

cls property

cls

Class ids as (N,) int. This is a copy, so writes do not go through.

ids property

ids

Track ids as (N,) int, -1 where untracked. A copy, like cls.

name

name(cls)

Class id to string, falling back to the id itself.

Source code in vizor/boxes.py
def name(self, cls):
    """Class id to string, falling back to the id itself."""
    return label(self.names, cls)

copy

copy()

A deep copy of the rows, sharing the names mapping and the source frame.

Source code in vizor/boxes.py
def copy(self):
    """A deep copy of the rows, sharing the names mapping and the source frame."""
    return type(self)(self.data.copy(), self.names, self.img)

draw

draw(img=None, scale=None, thick=None)

Draw the boxes on img (defaults to the source frame) and return it.

Source code in vizor/boxes.py
def draw(self, img=None, scale=None, thick=None):
    """Draw the boxes on ``img`` (defaults to the source frame) and return it."""
    img = self.img if img is None else img
    if img is None:
        raise ValueError("no image to draw on")
    scale = max(0.4, min(1.0, img.shape[0] / 900)) if scale is None else scale
    thick = max(1, round(scale * 2)) if thick is None else thick
    font = cv2.FONT_HERSHEY_SIMPLEX
    for t in self:
        x1, y1, x2, y2 = (int(round(v)) for v in t.box)
        color = COLORS[t.cls % len(COLORS)]
        cv2.rectangle(img, (x1, y1), (x2, y2), color, thick)
        text = self.name(t.cls)
        if t.id >= 0:
            text = f"{t.id}:{text}"
        text = f"{text} {t.conf:.2f}"
        (tw, th), base = cv2.getTextSize(text, font, scale, thick)
        top = max(0, y1 - th - base)
        cv2.rectangle(img, (x1, top), (x1 + tw, top + th + base), color, -1)
        cv2.putText(img, text, (x1, top + th), font, scale, (255, 255, 255),
                    thick, lineType=cv2.LINE_AA)
    return img

Track dataclass

Track(box, conf, cls, id)

One box. box is xyxy, id is -1 when the box is untracked.

wh property

wh

Width and height of the box.

area property

area

Box area in pixels, clamped at zero for an inverted box.

Preds

Preds(data=None, names=None, img=None)

Bases: Tracks

Detections without track ids. Six-column input is padded with id = -1.

Source code in vizor/boxes.py
def __init__(self, data=None, names=None, img=None):
    self.data = _as_data(data)
    self.names = names
    self.img = img

iou

iou(a, b)

Pairwise IoU between two sets of xyxy boxes, shape (len(a), len(b)).

Source code in vizor/boxes.py
def iou(a, b):
    """Pairwise IoU between two sets of xyxy boxes, shape (len(a), len(b))."""
    a, b = np.asarray(a, np.float32), np.asarray(b, np.float32)
    if not len(a) or not len(b):
        return np.zeros((len(a), len(b)), np.float32)
    lt = np.maximum(a[:, None, :2], b[None, :, :2])
    rb = np.minimum(a[:, None, 2:4], b[None, :, 2:4])
    wh = np.clip(rb - lt, 0, None)
    inter = wh[..., 0] * wh[..., 1]
    area_a = ((a[:, 2] - a[:, 0]) * (a[:, 3] - a[:, 1]))[:, None]
    area_b = ((b[:, 2] - b[:, 0]) * (b[:, 3] - b[:, 1]))[None, :]
    return inter / np.clip(area_a + area_b - inter, 1e-9, None)