Skip to content

Data

BatchData(datas, device='cpu')

Source code in src/deepvisiontools/data/data.py
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
def __init__(self, datas: Sequence[BaseData], device: DeviceLike = "cpu"):
    self.device = device
    datas = list(datas)

    if len(datas) > 0:
        # 1) type check: all items must be the same concrete data type
        types = [type(d) for d in datas]
        if len(set(types)) != 1:
            raise ValueError(
                f"Can't create BatchData with multiple data types, got {types}"
            )

        # 2) move to device (in-place is fine; BaseData.to returns self)
        datas = [d.to(device) for d in datas]

        # 3) ensure same canvas size by padding to max(H,W) if needed
        canvas = [d.canvas_size for d in datas]
        hs = [c[0] for c in canvas]
        ws = [c[1] for c in canvas]
        need_pad = (len(set(hs)) != 1) or (len(set(ws)) != 1)
        if need_pad:
            h_max = max(hs)
            w_max = max(ws)
            datas = [d.padto((h_max, w_max)) for d in datas]

        # 4) commit
        self.datas = datas
        self.canvas_size = self.datas[0].canvas_size
        self.nb_object = len(self.datas)
        # recover data_type key from registry

        self.data_type = {v: k for k, v in REGISTRY.items()}[type(self.datas[0])]
    else:
        # empty batch
        self.datas = []
        self.canvas_size = None
        self.nb_object = 0
        self.data_type = None

device = device instance-attribute

Batch a list of BaseData of the same concrete type and normalize their shapes through padding.

Note
  • Ensures all elements are of the same BaseData subclass (e.g., all BboxData).
  • Moves items to the requested device.
  • Pads each item to a common canvas_size = (max(H_i), max(W_i)).
  • Exposes .datas, .data_type, .nb_object, .canvas_size, .device.

Parameters:

Name Type Description Default
datas List[deepvisiontools.data.data.BaseData]

Items of the same type (may be empty).

required
device typing.Literal['cpu', 'cuda']

Target device. Defaults to "cpu".

required
Usage
b1 = BboxData(torch.tensor([[1,1,3,3]]), torch.tensor([0]), (8,8), "XYXY")
b2 = BboxData(torch.tensor([[2,2,4,4]]), torch.tensor([1]), (6,6), "XYXY")
batch = BatchData([b1, b2])
len(batch), batch.canvas_size
>>> (2, (8, 8))
sub = batch[1:]
len(sub), sub.canvas_size
>>> (1, (8, 8))

#Concatenation:
merged = sub + sub
len(merged)
>>> 2

empty(device='cpu') classmethod

Construct an empty batch on the given device.

Source code in src/deepvisiontools/data/data.py
1735
1736
1737
1738
@classmethod
def empty(cls, device: DeviceLike = "cpu"):
    """Construct an **empty** batch on the given `device`."""
    return BatchData([], device)

to(device)

Move all elements to device (in-place) and return self.

Source code in src/deepvisiontools/data/data.py
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
def to(self, device: DeviceLike) -> BatchData:
    """
    Move all elements to `device` (in-place) and return `self`.
    """
    if self.nb_object == 0:
        self.device = device
        return self
    self.datas = [d.to(device) for d in self.datas]
    self.device = device
    return self

padto(new_size)

Pad all elements to new_size=(H, W) and return a new BatchData.

Returns:

Name Type Description
BatchData deepvisiontools.data.data.BatchData

with all items padded to the same canvas.

Source code in src/deepvisiontools/data/data.py
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
def padto(self, new_size: tuple[int, int]) -> BatchData:
    """
    Pad all elements to `new_size=(H, W)` and return a **new** `BatchData`.

    Returns:
        BatchData: with all items padded to the same canvas.
    """
    if self.nb_object == 0:
        return BatchData([], self.device)
    datas = [d.padto(new_size) for d in self.datas]
    return BatchData(datas, self.device)

labels()

Return labels tensor for each item as list.

Source code in src/deepvisiontools/data/data.py
1878
1879
1880
1881
1882
def labels(self) -> list[torch.Tensor]:
    """
    Return labels tensor for each item as list.
    """
    return [d.labels for d in self.datas]

values()

Return geometry tensors for each item (boxes/masks etc.) as list.

Source code in src/deepvisiontools/data/data.py
1884
1885
1886
1887
1888
def values(self) -> list[torch.Tensor]:
    """
    Return geometry tensors for each item (boxes/masks etc.) as list.
    """
    return [d.value for d in self.datas]

scores()

Return scores tensors for each item as a list.

Source code in src/deepvisiontools/data/data.py
1890
1891
1892
1893
1894
def scores(self) -> list[torch.Tensor | None | SemanticLogits]:
    """
    Return scores tensors for each item as a list.
    """
    return [d.scores for d in self.datas]

BaseData

Bases: abc.ABC

Minimal, uniform abstract interface for model-ready annotations (boxes, instance masks, semantic masks). Concrete implementations must subclass BaseData.

Key ideas
  • A BaseData wraps the geometry/labels/scores for a single image.
  • It remembers its canvas_size=(H, W) so it can be padded/cropped safely.
  • It exposes common utilities (sanitize, pad, crop, augment, visualize) implemented via operator visitors. Concrete implementation of these utilities is implemented for concrete daughter classes.
  • BaseData must be implemented through concrete classes. See BboxData for examples.
Basic Usage
# BaseData should not be used directly, use concretes implementations like BboxData.
from deepvisiontools.data import BboxData
b = BboxData(
    value=torch.tensor([[10, 10, 20, 20]]),  # XYXY
    labels=torch.tensor([1]),
    canvas_size=(64, 64),
    boxformat="XYXY",
)
ib = BatchData([b])                     # batch of 1 box annotation
ib.data_type
>>> 'bbox'
ib.canvas_size
>>> (64, 64)

empty(canvas_size, device='cpu') abstractmethod classmethod

Create an empty annotation object for a given canvas size (no objects).

Source code in src/deepvisiontools/data/data.py
59
60
61
62
63
64
65
@classmethod
@abstractmethod
def empty(
    cls, canvas_size: tuple[int, int], device: DeviceLike = "cpu"
) -> BaseData:
    """Create an **empty** annotation object for a given canvas size (no objects)."""
    ...

accept(op, **context)

Visitor pattern entry-point. Operators are registered per-type and dispatched here.

Source code in src/deepvisiontools/data/data.py
105
106
107
108
109
110
111
112
113
def accept(self, op: BaseOperator, **context: Any) -> BaseData:
    """
    Visitor pattern entry-point. Operators are registered per-type and dispatched here.
    """
    from .operators import ensure_registry_loaded

    ensure_registry_loaded()
    visit = cast(Any, op._visit)
    return cast(BaseData, visit(self, **context))

sanitize()

Clean the object (remove tiny boxes/instances, fix labels/scores sizes, etc.).

Source code in src/deepvisiontools/data/data.py
116
117
118
119
120
121
122
def sanitize(self) -> BaseData:
    """
    Clean the object (remove tiny boxes/instances, fix labels/scores sizes, etc.).
    """
    from .operators import Sanitize

    return Sanitize()(self)

crop(t, l, h, w)

Crop the annotation to a (h, w) window with top/left offsets (t, l).

This mirrors the exact crop applied to the image.

Source code in src/deepvisiontools/data/data.py
124
125
126
127
128
129
130
131
132
def crop(self, t: int, l: int, h: int, w: int) -> BaseData:  # noqa: E741
    """
    Crop the annotation to a `(h, w)` window with top/left offsets `(t, l)`.

    This mirrors the exact crop applied to the image.
    """
    from .operators import Crop

    return Crop(t, l, h, w)(self)

pad(l, t, r, b)

Pad annotation with (left, top, right, bottom) pixels.

Source code in src/deepvisiontools/data/data.py
134
135
136
137
138
139
140
def pad(self, l: int, t: int, r: int, b: int) -> BaseData:  # noqa: E741
    """
    Pad annotation with `(left, top, right, bottom)` pixels.
    """
    from .operators import Pad

    return Pad(l, t, r, b)(self)

padto(new_size)

Pad annotation to exactly new_size=(H, W) (no crop).

Source code in src/deepvisiontools/data/data.py
142
143
144
145
146
147
148
def padto(self, new_size: tuple[int, int]) -> BaseData:
    """
    Pad annotation to **exactly** `new_size=(H, W)` (no crop).
    """
    from .operators import Padto

    return Padto(new_size)(self)

augment(image, transform)

Apply a torchvision-compatible transform jointly to (annotation, image).

Source code in src/deepvisiontools/data/data.py
150
151
152
153
154
155
156
157
158
def augment(
    self, image: torch.Tensor, transform: Any
) -> tuple[BaseData, torch.Tensor]:
    """
    Apply a torchvision-compatible `transform` jointly to `(annotation, image)`.
    """
    from .operators import Augment

    return Augment(transform)(self, image=image)

visualize(image=None)

Render a preview of the annotation over an optional image.

Returns:

Type Description
torch.Tensor

torch.Tensor: Visualized RGB image tensor.

Source code in src/deepvisiontools/data/data.py
160
161
162
163
164
165
166
167
168
169
def visualize(self, image: torch.Tensor | None = None) -> torch.Tensor:
    """
    Render a preview of the annotation over an optional `image`.

    Returns:
        torch.Tensor: Visualized RGB image tensor.
    """
    from .operators import Visualize

    return Visualize()(self, image=image)

to(device)

Move all internal tensors to device (in-place) and return self.

Source code in src/deepvisiontools/data/data.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def to(self, device: DeviceLike) -> BaseData:
    """
    Move all internal tensors to `device` (in-place) and return `self`.
    """
    self.value = self.value.to(device)

    if isinstance(getattr(self, "labels", None), torch.Tensor):
        self.labels = self.labels.to(device)

    sc = getattr(self, "scores", None)
    if sc is not None and (isinstance(sc, torch.Tensor) or hasattr(sc, "to")):
        self.scores = sc.to(device)

    self.device = device
    return self

BboxData(value, labels, canvas_size, boxformat='XYXY', scores=None, device='cpu')

Bases: deepvisiontools.data.data.BaseData

Source code in src/deepvisiontools/data/data.py
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
def __init__(
    self,
    value: torch.Tensor | BoundingBoxes,
    labels: torch.Tensor,
    canvas_size: tuple[int, int],
    boxformat: (
        Literal["XYXY", "XYWH", "CXCYWH"] | BoundingBoxFormat | None
    ) = "XYXY",
    scores: torch.Tensor | None = None,
    device: DeviceLike = "cpu",
) -> None:

    if not (value.dim() == 2 and value.shape[-1] == 4):
        raise ValueError(
            f"value does not seem to be a bounding box type tensor (N, 4). Got shape {value.shape}"
        )

    if labels.shape[0] != value.shape[0]:
        raise ValueError(
            f"labels of shape {labels.shape} don't match number of boxes of shape {value.shape}"
        )

    if scores is not None and scores.shape[0] != value.shape[0]:
        raise ValueError(
            f"scores of shape {scores.shape} don't match number of boxes of shape {value.shape}"
        )
    if boxformat is None:
        warn("boxformat not provided in BboxData __init__, use default XYXY")
    desired_fmt: BoundingBoxFormat | None
    if boxformat is None:
        desired_fmt = None
    else:
        desired_fmt = (
            BoundingBoxFormat(boxformat)
            if isinstance(boxformat, str)
            else boxformat
        )

    if isinstance(value, BoundingBoxes):
        # If no desired format provided, inherit from the input
        if desired_fmt is None:
            desired_fmt = value.format
        # If mismatch, convert the tv-tensor to desired format
        elif value.format != desired_fmt:
            conv = ConvertBoundingBoxFormat(desired_fmt)
            value = conv(value)

        # If caller passed a canvas_size, keep it; otherwise inherit from value
        if canvas_size is None:
            canvas_size = value.canvas_size
    else:
        # Wrap plain tensor in BoundingBoxes with a concrete desired format
        if desired_fmt is None:
            desired_fmt = BoundingBoxFormat("XYXY")
        value = BoundingBoxes(value, format=desired_fmt, canvas_size=canvas_size)

    # 3) Assign fields
    self.value: BoundingBoxes = value.to(device)
    self._boxformat: BoundingBoxFormat = (
        desired_fmt  # <- always a BoundingBoxFormat now
    )
    self.labels = labels.to(device)
    self.scores = scores.to(device) if isinstance(scores, torch.Tensor) else None
    self.nb_object = self.value.shape[0]
    self.canvas_size = canvas_size
    self.device = device

device = device instance-attribute

Concrete BaseData for bounding boxes annotations.

Parameters:

Name Type Description Default
value torch.Tensor | torchvision.tv_tensors.BoundingBoxes

(N, 4) coordinates.

required
labels torch.Tensor

(N,) class ids aligned with boxes.

required
canvas_size Tuple[int, int]

Image size (H, W).

required
boxformat Optional[typing.Literal['XYXY', 'XYWH', 'CXCYWH'] | torchvision.tv_tensors.BoundingBoxFormat]

Format for input boxes; the .boxformat property can be changed later. Defaults to "XYXY".

required
scores torch.Tensor | None

(N,) confidence scores. Defaults to None.

required
device typing.Literal['cpu', 'cuda']

Device for tensors. Defaults to "cpu".

required
Tips
  • You can construct from InstanceMaskData with BboxData.from_instance_mask(...).
  • You can convert formats later by modifying attribute (property) .boxformat = "CXCYWH" etc.
Basic Usage
boxes = torch.tensor([[10, 10, 20, 20], [5, 5, 7, 9]])
labels = torch.tensor([1, 2])
b = BboxData(boxes, labels, canvas_size=(64, 64), boxformat="XYXY")
b.nb_object
>>> 2
b.boxformat
>>> BoundingBoxFormat.XYXY
b.boxformat = "CXCYWH"  # modify box format
b.value.shape
>>> torch.Size([2, 4])
sample = boxes[0]
sample.nb_object
>>> 1
sample.labels
>>> torch.Tensor([1])

boxformat property writable

Convert boxes in-place to the requested format.

Usage
b = BboxData(
    value=torch.tensor([[10,10,20,20]]), labels=torch.tensor([1]),
    canvas_size=(32,32), boxformat="XYXY"
)
b.boxformat = "CXCYWH"  # internally change box coordinates
b.boxformat
>>> BoundingBoxFormat.CXCYWH

from_instance_mask(instance_mask, boxformat=None) classmethod

Build boxes by boxing each instance in an InstanceMaskData.

Parameters:

Name Type Description Default
instance_mask deepvisiontools.data.data.InstanceMaskData

Source instances.

required
boxformat typing.Literal['XYXY', 'XYWH', 'CXCYWH'] | None

Optional output format.

None

Returns:

Name Type Description
BboxData deepvisiontools.data.data.BboxData

Boxes + labels (+ scores if present on the instance mask).

Source code in src/deepvisiontools/data/data.py
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
@classmethod
def from_instance_mask(
    cls,
    instance_mask: InstanceMaskData,
    boxformat: Literal["XYXY", "XYWH", "CXCYWH"] | None = None,
) -> BboxData:
    """
    Build boxes by **boxing** each instance in an `InstanceMaskData`.

    Args:
        instance_mask (InstanceMaskData): Source instances.
        boxformat (Literal["XYXY","XYWH","CXCYWH"] | None): Optional output format.

    Returns:
        BboxData: Boxes + labels (+ scores if present on the instance mask).
    """
    if instance_mask.nb_object == 0:
        return cls.empty(instance_mask.canvas_size, device=instance_mask.device)

    masks = instance_mask.separate_masks
    masks = torch.as_tensor(masks, device=instance_mask.device)

    if masks.ndim != 3:
        raise ValueError(
            "BboxData.from_instance_mask expects separate masks with shape "
            f"(N, H, W), got {tuple(masks.shape)}."
        )

    areas = masks.flatten(1).sum(dim=1)
    keep = areas > 0

    if not bool(keep.any().item()):
        return cls.empty(instance_mask.canvas_size, device=instance_mask.device)

    masks = masks[keep].to(dtype=torch.float32)
    labels = (
        instance_mask.labels[keep]
        if instance_mask.labels.numel()
        else instance_mask.labels
    )

    if isinstance(instance_mask.scores, torch.Tensor):
        scores = instance_mask.scores[keep]
    else:
        scores = None

    boxes = masks_to_boxes(masks)

    boxdata = BboxData(
        value=boxes,
        labels=labels,
        canvas_size=instance_mask.canvas_size,
        scores=scores,
        device=instance_mask.device,
    )
    if boxformat is not None:
        boxdata.boxformat = boxformat
    return boxdata

empty(canvas_size, boxformat='XYXY', device='cpu') classmethod

Create an empty BboxData for the given canvas.

Source code in src/deepvisiontools/data/data.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
@classmethod
def empty(  # type: ignore[override]
    cls,
    canvas_size: tuple[int, int],
    boxformat: (
        Literal["XYXY", "XYWH", "CXCYWH"] | BoundingBoxFormat | None
    ) = "XYXY",
    device: DeviceLike = "cpu",
) -> BboxData:
    """
    Create an empty `BboxData` for the given canvas.
    """
    val = torch.empty((0, 4), device=device)
    labels = torch.empty((0,), dtype=torch.long, device=device)
    return BboxData(
        value=val,
        labels=labels,
        canvas_size=canvas_size,
        boxformat=boxformat,
        device=device,
    )

InstanceMaskData(value=None, labels=None, canvas_size=None, scores=None, separate_masks=None, separate_mask_scores=None, device=None)

Bases: deepvisiontools.data.data.BaseData

Source code in src/deepvisiontools/data/data.py
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
def __init__(
    self,
    value: Mask | torch.Tensor | None = None,
    labels: torch.Tensor | None = None,
    canvas_size: tuple[int, int] | None = None,
    scores: torch.Tensor | None = None,
    separate_masks: torch.Tensor | None = None,
    separate_mask_scores: torch.Tensor | None = None,
    device: DeviceLike | None = None,
) -> None:

    if separate_masks is not None and value is not None:
        value = None

    # Backward-compat: if user passed (N,H,W) as `value`, treat it as separate_masks
    if (
        separate_masks is None
        and isinstance(value, torch.Tensor)
        and value.ndim == 3
    ):
        separate_masks = value
        value = None

    if value is None and separate_masks is None:
        raise ValueError(
            "InstanceMaskData requires either `value` (H,W) stacked id-map or `separate_masks` (N,H,W)."
        )

    # infer device
    dev: DeviceLike
    if device is None:
        dev = (
            str(torch.as_tensor(separate_masks).device)
            if separate_masks is not None
            else str(torch.as_tensor(value).device)
        )
    else:
        dev = device
    self.device = dev

    # normalize labels/scores
    labels_t = (
        torch.as_tensor(labels)
        if labels is not None
        else torch.empty((0,), dtype=torch.long)
    )
    if labels_t.dtype != torch.long:
        labels_t = labels_t.long()
    labels_t = labels_t.to(self.device)

    scores_t = None if scores is None else torch.as_tensor(scores).to(self.device)

    # -------------------------
    # Path A: separate masks explicitly provided -> store packed + build value
    # -------------------------
    if separate_masks is not None:
        sep = torch.as_tensor(separate_masks)
        if sep.ndim != 3:
            raise ValueError(
                f"`separate_masks` must be (N,H,W). Got {tuple(sep.shape)}"
            )

        sep_bool = self._ensure_bool_stack(sep)
        N, H, W = (
            int(sep_bool.shape[0]),
            int(sep_bool.shape[1]),
            int(sep_bool.shape[2]),
        )

        if canvas_size is None:
            canvas_size = (H, W)
        elif canvas_size != (H, W):
            raise ValueError(
                f"canvas_size {canvas_size} does not match separate_masks spatial {(H, W)}"
            )

        if labels_t.numel() != 0 and labels_t.shape[0] != N:
            raise ValueError(
                f"labels shape {tuple(labels_t.shape)} does not match N={N} from separate_masks."
            )
        if labels_t.numel() == 0 and N != 0:
            raise ValueError(
                "labels must be provided with shape (N,) when separate_masks implies N>0."
            )
        if scores_t is not None and scores_t.shape[0] != N:
            raise ValueError(
                f"scores shape {tuple(scores_t.shape)} does not match N={N} from separate_masks."
            )

        # soft masks
        if separate_mask_scores is not None:
            sms = torch.as_tensor(separate_mask_scores)
            if sms.ndim != 3 or int(sms.shape[0]) != N:
                raise ValueError(
                    f"separate_mask_scores must be (N,H,W) with N={N}. Got {tuple(sms.shape)}"
                )
            if (int(sms.shape[1]), int(sms.shape[2])) != (H, W):
                raise ValueError(
                    f"separate_mask_scores spatial must match {(H, W)}. Got {tuple(sms.shape[-2:])}"
                )
            sms = sms.to(self.device)
            if not sms.is_floating_point():
                sms = sms.float()
            self.separate_mask_scores = sms.to(torch.float16)
        else:
            self.separate_mask_scores = None

        value_hw = self._stacked_from_separate(sep_bool.to(self.device), scores_t)

        # store packbits on CPU
        self._separate_masks_shape = (H, W)
        self.separate_masks = sep_bool  # packs to CPU

        self.value = Mask(value_hw) if not isinstance(value_hw, Mask) else value_hw
        self.labels = labels_t
        self.scores = scores_t
        self.canvas_size = canvas_size
        self.nb_object = N
        return

    # -------------------------
    # Path B: stacked id-map provided -> FAST PATH (do NOT compute/pack separate)
    # -------------------------
    value_hw = torch.as_tensor(value).to(self.device).long()
    if value_hw.ndim != 2:
        raise ValueError(f"`value` must be (H,W). Got {tuple(value_hw.shape)}")

    H, W = int(value_hw.shape[-2]), int(value_hw.shape[-1])
    if canvas_size is None:
        canvas_size = (H, W)
    elif canvas_size != (H, W):
        raise ValueError(
            f"canvas_size {canvas_size} does not match value spatial {(H, W)}"
        )

    # infer N from max id (assumes sanitize keeps 1..N contiguous, like before)
    max_id = int(value_hw.max().item()) if value_hw.numel() else 0
    N = max_id

    if labels_t.numel() != 0 and labels_t.shape[0] != N:
        raise ValueError(
            f"labels shape {tuple(labels_t.shape)} does not match N={N} inferred from value."
        )
    if labels_t.numel() == 0 and N != 0:
        raise ValueError(
            f"labels must be provided with shape (N,) when value implies N={N}."
        )
    if scores_t is not None and scores_t.shape[0] != N:
        raise ValueError(
            f"scores shape {tuple(scores_t.shape)} does not match N={N} inferred from value."
        )

    # soft mask scores without explicit separate masks: we must treat as "stored separate" case
    if separate_mask_scores is not None:
        # we need separate masks to align spatially -> compute once + store packed
        sep_bool = self._separate_from_stacked(value_hw)
        if int(sep_bool.shape[0]) != N:
            # very rare case if ids aren't contiguous; sanitize should fix upstream
            N = int(sep_bool.shape[0])

        sms = torch.as_tensor(separate_mask_scores).to(self.device)
        if sms.ndim != 3 or int(sms.shape[0]) != N:
            raise ValueError(
                f"separate_mask_scores must be (N,H,W) with N={N}. Got {tuple(sms.shape)}"
            )
        if (int(sms.shape[1]), int(sms.shape[2])) != (H, W):
            raise ValueError(
                f"separate_mask_scores spatial must match {(H, W)}. Got {tuple(sms.shape[-2:])}"
            )
        if not sms.is_floating_point():
            sms = sms.float()
        self.separate_mask_scores = sms.to(torch.float16)

        self._separate_masks_shape = (H, W)
        self.separate_masks = sep_bool  # packs CPU
    else:
        self.separate_mask_scores = None
        self._separate_masks_shape = (H, W)
        self._separate_masks_packed = None

    self.value = Mask(value_hw) if not isinstance(value_hw, Mask) else value_hw
    self.labels = labels_t
    self.scores = scores_t
    self.canvas_size = canvas_size
    self.nb_object = N

separate_masks property writable

Returns (N,H,W) bool on self.device.

  • If packed separate masks exist: decode from CPU packbits.
  • Else: compute from stacked id-map on demand (fast enough for metrics).

empty(canvas_size, device='cpu') classmethod

Return empty InstanceMask data object (0 objects)

Source code in src/deepvisiontools/data/data.py
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
@classmethod
def empty(
    cls, canvas_size: tuple[int, int], device: DeviceLike = "cpu"
) -> InstanceMaskData:
    """Return empty `InstanceMask` data object (0 objects)"""
    H, W = int(canvas_size[0]), int(canvas_size[1])
    val = torch.zeros((H, W), dtype=torch.long, device=device)
    labels = torch.empty((0,), dtype=torch.long, device=device)
    out = InstanceMaskData(
        value=val, labels=labels, canvas_size=(H, W), device=device
    )
    out.nb_object = 0
    # do NOT allocate packed storage by default
    out._separate_masks_shape = (H, W)
    out._separate_masks_packed = None
    out.separate_mask_scores = None
    return out

SemanticLogits(logits, num_classes=_default_num_classes(), binary_threshold=_default_binary_threshold(), mode=None) dataclass

Container for semantic logits with a stable API and shape checks.

Parameters:

Name Type Description Default
logits torchvision.tv_tensors.Mask | torch.Tensor

Raw (pre-softmax) logits of shape (C, H, W).

required
num_classes int

Number of classes C. Defaults to current().data_semantic_mask.num_classes.

deepvisiontools.data.data._default_num_classes()
Note
  • Validates the (C, H, W) shape and rejects already-softmaxed probabilities.
  • Keeps logits as a Mask (Tensor subclass) for dtype/device convenience.
  • Provides to(...), cpu(), cuda(), clone(), detach() that preserve num_classes.
Tips
  • .softmax() → (C,H,W) probabilities (as Mask).
  • .toSemanticMaskData() → SemanticMaskData by argmax.
Basic Usage
C, H, W = 3, 8, 8
logits = torch.randn(C, H, W)
sl = SemanticLogits(logits, num_classes=C)  # if C=None value infered from current() config.
sm = sl.toSemanticMaskData()
sm.value.shape
>>> torch.Size([8, 8])

toSemanticMaskData()

Convert logits to SemanticMaskData by: 1) softmax over classes, 2) argmax over channels.

Returns:

Name Type Description
SemanticMaskData deepvisiontools.data.data.SemanticMaskData

Semantic map with .scores = self.

Source code in src/deepvisiontools/data/data.py
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
def toSemanticMaskData(self) -> SemanticMaskData:
    """
    Convert logits to `SemanticMaskData` by:
        1) softmax over classes,
        2) `argmax` over channels.

    Returns:
        SemanticMaskData: Semantic map with `.scores = self`.
    """
    # use normalized probabilities for the argmax / threshold
    probs = self.probas()  # Mask/Tensor, (C,H,W)
    if self.mode == "multiclass":
        val = torch.argmax(probs, dim=0).long()
    else:
        val = (probs > self.binary_threshold).long().squeeze()
    return SemanticMaskData(val, scores=self, device=self.logits.device)

to(*args, **kwargs)

Return a copy with tensors moved/cast like Tensor.to.

Source code in src/deepvisiontools/data/data.py
1656
1657
1658
def to(self, *args, **kwargs) -> SemanticLogits:
    """Return a copy with tensors moved/cast like `Tensor.to`."""
    return SemanticLogits(self.logits.to(*args, **kwargs), self.num_classes)

cpu()

Return a CPU copy.

Source code in src/deepvisiontools/data/data.py
1660
1661
1662
def cpu(self) -> SemanticLogits:
    """Return a CPU copy."""
    return SemanticLogits(self.logits.cpu(), self.num_classes)

cuda(device=None, non_blocking=False)

Return a CUDA copy.

Source code in src/deepvisiontools/data/data.py
1664
1665
1666
1667
1668
def cuda(self, device=None, non_blocking=False) -> SemanticLogits:
    """Return a CUDA copy."""
    return SemanticLogits(
        self.logits.cuda(device=device, non_blocking=non_blocking), self.num_classes
    )

clone()

Return a clone that preserves num_classes.

Source code in src/deepvisiontools/data/data.py
1670
1671
1672
def clone(self) -> SemanticLogits:
    """Return a clone that preserves `num_classes`."""
    return SemanticLogits(self.logits.clone(), self.num_classes)

detach()

Return a detached copy that preserves num_classes.

Source code in src/deepvisiontools/data/data.py
1674
1675
1676
def detach(self) -> SemanticLogits:
    """Return a detached copy that preserves `num_classes`."""
    return SemanticLogits(self.logits.detach(), self.num_classes)

probas()

Convert logits to normalized probabilities across classes.

Returns:

Name Type Description
Mask torchvision.tv_tensors.Mask

(C,H,W) probabilities in a Mask tensor.

Source code in src/deepvisiontools/data/data.py
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
def probas(self) -> Mask:
    """
    Convert logits to normalized probabilities across classes.

    Returns:
        Mask: `(C,H,W)` probabilities in a `Mask` tensor.
    """
    t = (
        self.logits
        if isinstance(self.logits, torch.Tensor)
        else torch.as_tensor(self.logits)
    )
    probs = F.softmax(t, dim=0) if self.mode == "multiclass" else F.sigmoid(t)
    return Mask(probs)

SemanticMaskData(value, labels=None, canvas_size=None, scores=None, device='cpu')

Bases: deepvisiontools.data.data.BaseData

Source code in src/deepvisiontools/data/data.py
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
def __init__(
    self,
    value: Mask | torch.Tensor,
    labels: torch.Tensor | None = None,
    canvas_size: tuple[int, int] | None = None,
    scores: SemanticLogits | torch.Tensor | None = None,
    device: DeviceLike = "cpu",
) -> None:
    # coerce to 2D long Mask on the right device
    if not (value.dim() == 2):
        raise ValueError(
            f"value does not seem to be a stacked mask type tensor (H, W) with val (0, N_class). Got shape {tuple(value.shape)}"
        )

    value = value.long().to(device)

    # canvas size checks / inference
    if canvas_size is None:
        canvas_size = (value.shape[-2], value.shape[-1])
    else:
        if canvas_size != (value.shape[-2], value.shape[-1]):
            raise ValueError(
                f"Input canvas_size has different height/width than value. Got canvas_size={canvas_size} and shape {tuple(value.shape)}. "
                f"Consider setting canvas_size to None for automatic canvas_size computation"
            )

    # labels sanity if provided
    if labels is not None:
        if labels.shape[0] != value.unique().numel() - 1:
            raise ValueError(
                f"Number of objects mismatch: labels of shape {tuple(labels.shape)} "
                f"don't match number of classes present in mask. Consider setting labels to None for automatic derivation."
            )
        labels = labels.to(device)

    # scores normalization (optional)
    if scores is not None and not isinstance(scores, SemanticLogits):
        scores = SemanticLogits(scores)

    normalized_scores: SemanticLogits | None
    normalized_scores = scores.to(device) if scores is not None else None

    # assign
    self.value: Mask = (
        Mask(value) if not isinstance(value, Mask) else value
    )  # always a Mask
    self.labels = labels if isinstance(labels, torch.Tensor) else value.unique()
    self.scores = normalized_scores
    self.canvas_size = canvas_size
    self.device = device
    self.nb_object = int(self.value.unique().numel() - 1)

device = device instance-attribute

Concrete BaseData for semantic segmentation (single class per pixel).

Parameters:

Name Type Description Default
value torchvision.tv_tensors.Mask | torch.Tensor

Semantic class map (H, W).

required
labels torch.Tensor | None

Classes present; if None, inferred from value.

required
canvas_size Tuple[int, int]

size of mask/corresponding image. If None, inferred from value. Defaults to None.

required
scores deepvisiontools.data.data.SemanticLogits | torch.Tensor | None

Semantic segmentation scores as logits. Can be tensor (C,H,W) or SemanticLogits class. Attribute is fixed to SemanticLogits

required
device typing.Literal['cpu', 'cuda']

Defaults to "cpu".

required
Tips
  • Can be built from IntanceMaskData: SemanticMaskData.from_instance_mask(instance_mask).
  • Can be built from SemanticLogits: SemanticLogits.toSemanticMaskData()
Usage
sem = SemanticMaskData(torch.zeros(4,4, dtype=torch.long), device="cpu")
sem.nb_object
>>> 0
# build from instances
ids = torch.tensor([[0,1,1],[0,0,2]])
lbl = torch.tensor([4, 7])  # two instances with semantic labels 4 and 7
im = InstanceMaskData(ids, lbl, canvas_size=(2,3))
sm = SemanticMaskData.from_instance_mask(im)
set(torch.unique(sm.value).tolist()) == {0, 4, 7}
>>> True

from_instance_mask(instance_mask) classmethod

Convert an instance mask to semantic by assigning each instance its class label and keeping 0 as background.

Source code in src/deepvisiontools/data/data.py
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
@classmethod
def from_instance_mask(cls, instance_mask: InstanceMaskData) -> SemanticMaskData:
    """
    Convert an **instance** mask to **semantic** by assigning each instance its
    class label and keeping 0 as background.
    """
    # Fast path for empty input
    if instance_mask.nb_object == 0:
        return cls.empty(instance_mask.canvas_size, device=instance_mask.device)

    val = instance_mask.value  # (H, W), ids 0..K (contiguous)
    device = val.device
    dtype = torch.long

    # Build LUT: 0 -> 0 (background), i -> labels[i-1] for i in 1..K
    max_id = int(
        val.max().item()
    )  # == instance_mask.nb_object (contiguous by design)
    lut = torch.zeros(max_id + 1, device=device, dtype=dtype)
    # remap instance labels [0 ... Ncls -1] to [1 ... Ncls] for semantic and ignore background
    lut[1:] = instance_mask.labels.to(device=device, dtype=dtype) + 1

    # Vectorized remap from instance ids -> semantic class ids
    sem_val = lut[val]  # (H, W), long

    # Ensure tv_tensors.Mask type (your SemanticMaskData expects Mask internally)
    sem_mask = Mask(sem_val.long())

    return cls(
        value=sem_mask,
        labels=None,  # let constructor infer from value.unique()
        canvas_size=instance_mask.canvas_size,
        scores=None,
        device=instance_mask.device,
    )

empty(canvas_size, device='cpu') classmethod

Create an empty semantic mask (all background).

Source code in src/deepvisiontools/data/data.py
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
@classmethod
def empty(
    cls,
    canvas_size: tuple[int, int],
    device: DeviceLike = "cpu",
) -> SemanticMaskData:
    """Create an empty semantic mask (all background)."""
    # background everywhere (0), ensure Mask type
    val = Mask(torch.zeros(canvas_size, dtype=torch.long, device=device))
    return SemanticMaskData(val, device=device)

confidence()

Convert logits to per-class probabilities (C, H, W).

Returns:

Type Description
torchvision.tv_tensors.Mask | None

Mask | None: Probabilities as a tensor-like Mask, or None if .scores is absent.

Source code in src/deepvisiontools/data/data.py
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
def confidence(self) -> Mask | None:
    """
    Convert logits to per-class probabilities `(C, H, W)`.

    Returns:
        Mask | None: Probabilities as a tensor-like `Mask`, or `None` if `.scores` is absent.
    """
    if self.scores is not None:
        return self.scores.probas()
    return None

Augment(augment)

Bases: deepvisiontools.data.operators.BaseOperator

Apply a torchvision v2 transform to a (annotation, image) pair. The paired image tensor must be supplied via the context dict: Augment(transform)(data, image=image_tensor)

Note
  • The concrete logic to align boxes/masks is registered per data type in ops_registry.
  • If you only need to modify the annotation (no image), use Crop/Pad/Padto.
Source code in src/deepvisiontools/data/operators.py
160
161
162
def __init__(self, augment):
    super().__init__()
    self.augment = augment

Crop(top, left, height, width)

Bases: deepvisiontools.data.operators.BaseOperator

Crop annotations to a fixed image window.

Source code in src/deepvisiontools/data/operators.py
97
98
99
def __init__(self, top: int, left: int, height: int, width: int):
    super().__init__()
    self.top, self.left, self.height, self.width = top, left, height, width

Pad(left, top, right, bottom)

Bases: deepvisiontools.data.operators.BaseOperator

Spatial crop by (top, left, height, width).

Pad value tensor and adapt canvas_size, num object (via sanitize) etc.

Source code in src/deepvisiontools/data/operators.py
116
117
118
def __init__(self, left: int, top: int, right: int, bottom: int):
    super().__init__()
    self.left, self.top, self.right, self.bottom = left, top, right, bottom

Padto(new_size)

Bases: deepvisiontools.data.operators.BaseOperator

Pad annotations to exactly a target image size without cropping.

Parameters
  • new_size: Target size as (height, width).
Source code in src/deepvisiontools/data/operators.py
136
137
138
def __init__(self, new_size: tuple[int, int]):
    super().__init__()
    self.new_size = new_size

Sanitize(*args, **kwargs)

Bases: deepvisiontools.data.operators.BaseOperator

Clean an annotation: enforce minimal sizes, fix ids, align labels & scores. Congigs (min_size etc.) are accessed through current() global variable

Note
  • Remove degenerate boxes or tiny instances per the current config (e.g., current().data_box.min_size, etc.).
  • Enforce contiguity of instance ids for InstanceMaskData (1..K).
  • Align lengths of labels/scores with the number of objects.
  • etc.

Returns:

Type Description

Cleaned BaseData of same type.

Source code in src/deepvisiontools/data/operators.py
48
49
def __init__(self, *args, **kwargs):
    ensure_registry_loaded()

Visualize(*args, **kwargs)

Bases: deepvisiontools.data.operators.BaseOperator

Render a visualization tensor (uint8 CHW) of an annotation on an image. If Image is None use a zeroed tensor.

Note
  • Draws boxes, masks, class labels and (optionally) scores onto a blank canvas or over a provided image (if supplied via ctx['image']).
  • Uses styling options from current().viz (colors, thickness, alpha…).
  • Concrete implementation logic is concrete BaseData type dependant
Source code in src/deepvisiontools/data/operators.py
48
49
def __init__(self, *args, **kwargs):
    ensure_registry_loaded()

current()

Get the current global DVConfig (context-local) object. You can configure a lot of options from the usage of current together with set_current

Returns:

Name Type Description
DVConfig deepvisiontools.config.config.DVConfig

The active configuration object.

Tips
  • Using ContextVar makes the config safe for async/task contexts
  • override it in a context manager with use_config(...) block without affecting other concurrent tasks.
Usage
from deepvisiontools.config import current, set_current, use_config
# Access global configs values with current()
cfg = current()
isinstance(cfg, DVConfig)
>>> True
cfg.log.level
>>> 30  # warning level from logging library
cfg.device
>>> "cpu"
cfg.data_box.max_det = 500
# Set a config as a global config
cfg1 = DVConfig(device="cuda")
cfg2 = DVConfig(device="cpu")
set_current(cfg1)
current().device
>>> "cuda"
set_current(cfg2)
>>> "cpu"
# Use a context manager for temporary config
set_current(cfg1)
with cfg2:
    print(current().device)
>>> "cpu"
Source code in src/deepvisiontools/config/config.py
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
def current() -> DVConfig:
    """
    Get the current global `DVConfig` (context-local) object. You can configure a lot of options from the usage of `current` together with `set_current`

    Returns:
        DVConfig: The active configuration object.

    ???+ tip "Tips"
        - Using `ContextVar` makes the config safe for async/task contexts
        - override it in a context manager `with use_config(...)` block without affecting other concurrent tasks.

    ???+ example "Usage"
        ```Python
        from deepvisiontools.config import current, set_current, use_config
        # Access global configs values with current()
        cfg = current()
        isinstance(cfg, DVConfig)
        >>> True
        cfg.log.level
        >>> 30  # warning level from logging library
        cfg.device
        >>> "cpu"
        cfg.data_box.max_det = 500
        # Set a config as a global config
        cfg1 = DVConfig(device="cuda")
        cfg2 = DVConfig(device="cpu")
        set_current(cfg1)
        current().device
        >>> "cuda"
        set_current(cfg2)
        >>> "cpu"
        # Use a context manager for temporary config
        set_current(cfg1)
        with cfg2:
            print(current().device)
        >>> "cpu"
        ```
    """
    cfg = _CURRENT.get()
    if cfg is None:
        cfg = DVConfig()
        _CURRENT.set(cfg)
    return cfg