# utils

Python API: `luxonis_ml.data.augmentations.utils`

Utilities for adapting LDF annotations to Albumentations.

The augmentation engine receives annotations in Luxonis Data Format (LDF), converts them to Albumentations-compatible arrays
before spatial transforms, and converts them back afterward. This module keeps those boundary conversions centralized.

Layout conversions

| Target | LDF layout | Albumentations layout |
| --- | --- | --- |
| Masks | (C, H, W) or (N, H, W) | (H, W, C) or (H, W, N) |
| Bounding boxes | [c, x, y, w, h] | [xmin, ymin, xmax, ymax, c, i] |
| Keypoints | (N, 3K) normalized rows | (NK, 3) pixel-space rows |

The appended bbox index i is a position into the bbox-associated labels, such as instance masks and keypoints, and is used to keep
them aligned with the bounding boxes that survive augmentation and filtering. It is local to whichever stage last wrote it:
[BatchCompose](https://docs.luxonis.com/software-v3/ai-inference/model-source/training/luxonis-ml/luxonis-ml-api-reference/data/augmentations/batch_compose.md)
restamps it after every batch transform, so it is not stable across the whole pipeline.

## Functions

### instance_count

```python
def instance_count(array: np.ndarray, target_type: str, n_keypoints: int = 0) -> int:
```

Count the instances an Albumentations-layout target describes.

Each per-instance target type stores its instances on a different axis, so the count cannot be read off `len(array)` alone.

> **Examples**
> ```pycon
>>> instance_count(np.zeros((8, 8, 3)), "instance_mask")
3
>>> instance_count(np.zeros((6, 3)), "keypoints", n_keypoints=2)
3
>>> instance_count(np.zeros((4, 5)), "metadata")
4
```

Parameters

 * `array` (`np.ndarray`): Target in the Albumentations layout.
 * `target_type` (`str`): Target type the array holds, such as `"instance_mask"` or `"keypoints"`.
 * `n_keypoints` (`int`): Number of keypoints K per instance. Only used for keypoint targets, which store K rows per instance.

Returns

 * `int`: Number of instances in `array`.

Raises

 * `ValueError`: If `target_type` is `"keypoints"` and `n_keypoints` is not positive.

### pad_empty_entries

```python
def pad_empty_entries(batch: list[np.ndarray], n_columns: int = 6) -> list[np.ndarray]:
```

Shape every empty entry so the batch can be concatenated.

A sample carrying no boxes or keypoints comes back in whatever shape the transform left it in, including a one-dimensional `np.array([])` that will not concatenate with the populated samples. Those entries are replaced by correctly shaped empty rows.

The width is read off a populated entry, since a pipeline's optional fields decide how wide these rows are. An entry that is empty but still two-dimensional carries the same width and is used when no entry is populated.

> **Examples**
> ```pycon
>>> batch = [np.array([]), np.zeros((2, 7))]
>>> [array.shape for array in pad_empty_entries(batch)]
[(0, 7), (2, 7)]
>>> [array.shape for array in pad_empty_entries([np.array([])])]
[(0, 6)]
```

Parameters

 * `batch` (`list[np.ndarray]`): Per-sample arrays, some of which may be empty.
 * `n_columns` (`int`): Width to fall back on when no entry carries one. Both bbox rows [xmin, ymin, xmax, ymax, c, i] and
   keypoint rows are six columns wide.

Returns

 * `list[np.ndarray]`: The batch, with every empty entry shaped (0, n).

### postprocess_bboxes

```python
def postprocess_bboxes(bboxes: np.ndarray, area_threshold: float = 0.0004) -> tuple[np.ndarray, np.ndarray]:
```

Convert augmented bounding boxes back to LDF format.

Bounding boxes smaller than `area_threshold` are discarded. The appended bbox indices are returned alongside the boxes so labels
tied to boxes, such as instance masks, keypoints, arrays, and metadata, can be filtered in the same way.

> **Examples**
> ```pycon
>>> bboxes = np.array([[0.1, 0.2, 0.4, 0.6, 2, 5]], dtype=float)
>>> out_bboxes, ordering = postprocess_bboxes(bboxes)
>>> np.round(out_bboxes, 2).tolist()
[[2.0, 0.1, 0.2, 0.3, 0.4]]
>>> ordering.tolist()
[5]
>>> tiny = np.array([[0.0, 0.0, 0.01, 0.01, 1, 3]], dtype=float)
>>> postprocess_bboxes(tiny, area_threshold=0.1)[0].shape
(0, 5)
>>> postprocess_bboxes(np.empty((0, 6)), area_threshold=0.1)[0].shape
(0, 5)
```

Parameters

 * `bboxes` (`np.ndarray`): Augmented bounding boxes of shape (N, 6) in [xmin, ymin, xmax, ymax, c, i] format, where c is the class ID and i is the bbox index.
 * `area_threshold` (`float`): Minimum normalized box area w⋅h required for a box to remain valid.

Returns

 * `A tuple containing`: * Bounding boxes of shape (M, 5) in [c, x, y, w, h] format, where M ≤ N and c is the class ID.
    * Integer indices of shape (M) identifying which original bbox-associated labels survived filtering.

### postprocess_keypoints

```python
def postprocess_keypoints(keypoints: np.ndarray, bboxes_ordering: np.ndarray, image_height: int, image_width: int, n_keypoints:
int) -> np.ndarray:
```

Convert augmented keypoints back to normalized LDF rows.

The input contains pixel-space keypoints after Albumentations has applied spatial transforms. This function restores the original annotation grouping, keeps only annotations associated with surviving bounding boxes, marks out-of-image keypoints as invisible, clips coordinates to the image extent, and normalizes coordinates back to [0, 1].

> **Examples**
> ```pycon
>>> keypoints = np.array([[5, 5, 2], [12, -1, 2]], dtype=float)
>>> out = postprocess_keypoints(keypoints, np.array([0]), 10, 10, 2)
>>> np.round(out, 2).tolist()
[[0.5, 0.5, 2.0, 1.0, 0.0, 0.0]]
>>> keypoints = np.array([[1, 1, 2], [9, 9, 1]], dtype=float)
>>> postprocess_keypoints(
...     keypoints, np.array([1, 0]), 10, 10, 1
... ).tolist()
[[0.9, 0.9, 1.0], [0.1, 0.1, 2.0]]
```

Parameters

 * `keypoints` (`np.ndarray`): Augmented keypoints of shape (NK, D), where the first 3K values per annotation are (x, y, v)
   triplets and any extra columns are discarded.
 * `bboxes_ordering` (`np.ndarray`): Indices of bbox-associated annotations that survived bbox postprocessing.
 * `image_height` (`int`): Height of the augmented image used to normalize y coordinates.
 * `image_width` (`int`): Width of the augmented image used to normalize x coordinates.
 * `n_keypoints` (`int`): Number of keypoints K in each annotation.

Returns

 * `np.ndarray`: Keypoints of shape (M, 3K), where M is the number of retained annotations. Each row is in normalized LDF format
   [x1, y1, v1, x2, y2, v2, …].

### postprocess_mask

```python
def postprocess_mask(mask: np.ndarray) -> np.ndarray:
```

Convert an augmented mask back to the LDF layout.

> **Examples**
> ```pycon
>>> postprocess_mask(np.array([[1, 0], [0, 1]])).shape
(1, 2, 2)
>>> mask = np.array([[[1, 5], [2, 6]], [[3, 7], [4, 8]]])
>>> postprocess_mask(mask).tolist()
[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
```

Parameters

 * `mask` (`np.ndarray`): Augmented mask. Two-dimensional inputs are interpreted as a single mask of shape (H, W). Three-dimensional inputs are expected to have shape (H, W, C) or (H, W, N).

Returns

 * `np.ndarray`: Mask array in LDF channels-first layout. Two-dimensional inputs become (1, H, W), and three-dimensional inputs become (C, H, W) or (N, H, W).

### preprocess_bboxes

```python
def preprocess_bboxes(bboxes: np.ndarray) -> np.ndarray:
```

Convert LDF bounding boxes to Albumentations format.

LDF stores bounding boxes as normalized [c, x, y, w, h] rows, where c is the class ID and x and y are the top-left corner. Albumentations expects normalized [xmin, ymin, xmax, ymax, c] rows. This function also appends a per-box index used after augmentation to keep bbox-associated labels aligned with boxes that survived filtering.

> **Examples**
> ```pycon
>>> bboxes = np.array([[2, 0.1, 0.2, 0.3, 0.4]])
>>> out = preprocess_bboxes(bboxes)
>>> np.round(out[:, :4], 4).tolist()
[[0.1, 0.2, 0.4, 0.6]]
>>> out[:, 4:].astype(int).tolist()
[[2, 0]]
```

Parameters

 * `bboxes` (`np.ndarray`): Bounding boxes of shape (N, 5) in [c, x, y, w, h] format.

Returns

 * `np.ndarray`: Bounding boxes of shape (N, 6) in [xmin, ymin, xmax, ymax, c, i] format, where i is the bbox index.

### preprocess_keypoints

```python
def preprocess_keypoints(keypoints: np.ndarray, height: int, width: int) -> np.ndarray:
```

Convert normalized LDF keypoints to absolute coordinates.

LDF stores keypoints as flattened rows of normalized (x, y, v) triplets. Albumentations expects one keypoint per row and uses
pixel-space coordinates for spatial transforms.

> **Examples**
> ```pycon
>>> keypoints = np.array([[0.5, 0.25, 2, 1.0, 0.0, 1]])
>>> preprocess_keypoints(keypoints, height=8, width=4).tolist()
[[2.0, 2.0, 2.0], [4.0, 0.0, 1.0]]
>>> preprocess_keypoints(np.empty((0, 3)), 8, 4).shape
(0, 3)
```

Parameters

 * `keypoints` (`np.ndarray`): Keypoints of shape (N, 3K), where N is the number of annotations and K is the number of keypoints per annotation. Each row has format [x1, y1, v1, x2, y2, v2, …].
 * `height` (`int`): Image height used to scale normalized y coordinates.
 * `width` (`int`): Image width used to scale normalized x coordinates.

Returns

 * `np.ndarray`: Keypoints of shape (NK, 3) in pixel-space [x, y, v] format.

### preprocess_mask

```python
def preprocess_mask(seg: np.ndarray) -> np.ndarray:
```

Convert an LDF mask to the Albumentations mask layout.

Luxonis Data Format stores semantic masks as (C, H, W) and instance masks as (N, H, W). Albumentations expects the spatial dimensions first, so this function moves the channel or instance dimension to the end.

> **Examples**
> ```pycon
>>> seg = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
>>> preprocess_mask(seg).shape
(2, 2, 2)
>>> preprocess_mask(seg).tolist()
[[[1, 5], [2, 6]], [[3, 7], [4, 8]]]
```

Parameters

 * `seg` (`np.ndarray`): Mask array of shape (C, H, W) or (N, H, W).

Returns

 * `np.ndarray`: Mask array of shape (H, W, C) or (H, W, N).

### yield_batches

```python
def yield_batches(data_batch: list[dict[str, T]], batch_size: int) -> Iterator[dict[str, list[T]]]:
```

Yield fixed-size chunks of sample dictionaries.

> **Examples**
> ```pycon
>>> samples = [
...     {"image": "a", "label": 1},
...     {"image": "b", "label": 2},
...     {"image": "c", "label": 3},
... ]
>>> list(yield_batches(samples, 2))
[{'image': ['a', 'b'], 'label': [1, 2]}, {'image': ['c'], 'label': [3]}]
>>> next(yield_batches(samples, 10))["label"]
[1, 2, 3]
```

Parameters

 * `data_batch` (`list[dict[str, T]]`): Dictionaries containing data for each sample. All dictionaries are expected to expose the same keys.
 * `batch_size` (`int`): Maximum number of samples in each yielded batch.

Returns

 * `Iterator[dict[str, list[T]]]`: Iterator over dictionaries grouped by key. For an input chunk of B samples, each yielded value maps every key to a list of B values.

## Attributes

### T
