9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
194 lines
8.0 KiB
Markdown
194 lines
8.0 KiB
Markdown
---
|
||
comments: true
|
||
description: Use CompactMask for memory-efficient instance segmentation in supervision — ingest COCO RLE payloads, skip mask materialisation, and merge mixed dense and compact detections without allocating a full pixel stack.
|
||
authors:
|
||
- name: Borda
|
||
role: Open Source Engineer, Roboflow
|
||
github: https://github.com/borda
|
||
date_modified: 2026-07-01
|
||
---
|
||
|
||
# Use Compact Masks for Memory-Efficient Segmentation
|
||
|
||
[CompactMask][supervision.detection.compact_mask.CompactMask] stores each instance mask as a run-length encoding of its bounding-box **crop** rather than a full `(H, W)` boolean frame. For high-resolution images with many sparse masks this can reduce memory from tens of gigabytes to tens of megabytes, and eliminates full-frame decode work in annotators that only need the cropped region.
|
||
|
||
!!! Note
|
||
|
||
`sv.mask_to_xyxy` keeps supervision's inclusive max-coordinate convention for compatibility with `CompactMask` and current box-based adapters. Use `sv.mask_to_roi` when you need exclusive slice bounds for NumPy indexing or crop extraction.
|
||
|
||
This guide covers the four main integration points:
|
||
|
||
1. [Ingesting COCO RLE payloads directly as CompactMask](#ingest-coco-rle-payloads)
|
||
2. [Parsing Roboflow Inference results without a dense stack](#parse-inference-results)
|
||
3. [Skipping mask materialisation for box/label annotators](#skip-unnecessary-materialisation)
|
||
4. [Merging mixed dense and compact detections](#merge-mixed-detections)
|
||
|
||
---
|
||
|
||
## Ingest COCO RLE Payloads
|
||
|
||
If your model or API returns masks in the COCO RLE format (`{"size": [H, W], "counts": "..."}`) you can convert them directly to `CompactMask` without allocating an `(N, H, W)` boolean array:
|
||
|
||
```python
|
||
import numpy as np
|
||
import supervision as sv
|
||
from supervision.detection.compact_mask import CompactMask
|
||
|
||
# Example: two COCO RLE masks for a 720×1280 frame.
|
||
# Replace the counts strings with actual compressed RLE payloads from your
|
||
# model or API — e.g., from pycocotools mask.encode() or an Inference response.
|
||
rles = [
|
||
{"size": [720, 1280], "counts": "YOUR_RLE_COUNTS_STRING_HERE"},
|
||
{"size": [720, 1280], "counts": "YOUR_RLE_COUNTS_STRING_HERE"},
|
||
]
|
||
xyxy = np.array(
|
||
[
|
||
[100.0, 50.0, 400.0, 300.0],
|
||
[500.0, 200.0, 900.0, 600.0],
|
||
]
|
||
)
|
||
|
||
compact = CompactMask.from_coco_rle(rles, xyxy, image_shape=(720, 1280))
|
||
|
||
detections = sv.Detections(
|
||
xyxy=xyxy,
|
||
mask=compact,
|
||
class_id=np.array([0, 1]),
|
||
)
|
||
```
|
||
|
||
`from_coco_rle` uses run-length arithmetic scoped to each bounding box so no dense pixel array is ever created. Uncompressed integer count lists are also accepted in place of compressed strings.
|
||
|
||
---
|
||
|
||
## Parse Inference Results
|
||
|
||
`Detections.from_inference` accepts a `compact_masks=True` flag that routes the Roboflow RLE payload through `CompactMask.from_coco_rle` instead of decoding to a dense stack:
|
||
|
||
```python
|
||
import supervision as sv
|
||
|
||
# result: a Roboflow Inference v2 response dict with instance masks.
|
||
detections = sv.Detections.from_inference(result, compact_masks=True)
|
||
|
||
from supervision.detection.compact_mask import CompactMask
|
||
|
||
assert isinstance(detections.mask, CompactMask)
|
||
```
|
||
|
||
!!! Warning
|
||
|
||
`compact_masks=True` crops each mask to its detector bounding box. Pixels outside the box are silently dropped. For masks that extend meaningfully beyond the reported bounding box, use the default `compact_masks=False` (dense decode) to preserve all pixels.
|
||
|
||
To convert an existing dense-mask `Detections` to compact at any point:
|
||
|
||
```python
|
||
detections_compact = detections.to_compact_masks()
|
||
```
|
||
|
||
---
|
||
|
||
## Skip Unnecessary Materialisation
|
||
|
||
Annotators that do not draw masks (box, label, circle, ellipse, trace, keypoint) expose `requires_mask = False`. Integrations can branch on this flag to avoid decoding compact or RLE masks before annotation:
|
||
|
||
```python
|
||
import supervision as sv
|
||
|
||
annotators = [
|
||
sv.BoxAnnotator(),
|
||
sv.LabelAnnotator(),
|
||
sv.MaskAnnotator(), # requires_mask = True
|
||
]
|
||
|
||
for ann in annotators:
|
||
if ann.requires_mask:
|
||
# Annotator reads mask pixels — CompactMask decodes lazily per crop.
|
||
scene = ann.annotate(scene, detections)
|
||
else:
|
||
# Annotator ignores masks — strip mask field to eliminate any decode cost.
|
||
det_no_mask = sv.Detections(
|
||
xyxy=detections.xyxy,
|
||
confidence=detections.confidence,
|
||
class_id=detections.class_id,
|
||
)
|
||
scene = ann.annotate(scene, det_no_mask)
|
||
```
|
||
|
||
Annotators that set `requires_mask = True`: [MaskAnnotator][supervision.annotators.core.MaskAnnotator], [PolygonAnnotator][supervision.annotators.core.PolygonAnnotator], [HaloAnnotator][supervision.annotators.core.HaloAnnotator].
|
||
|
||
All others default to `requires_mask = False`.
|
||
|
||
!!! Note
|
||
|
||
`PolygonAnnotator` and `MaskAnnotator` both operate directly on `CompactMask` without materialising the full `(N, H, W)` frame — passing compact detections to them is already efficient.
|
||
|
||
---
|
||
|
||
## Merge Mixed Detections
|
||
|
||
When merging `Detections` objects that mix dense `ndarray` masks and `CompactMask` instances, `Detections.merge` converts dense inputs to `CompactMask` automatically. No full `(N, H, W)` stack is allocated:
|
||
|
||
```python
|
||
import numpy as np
|
||
import supervision as sv
|
||
from supervision.detection.compact_mask import CompactMask
|
||
|
||
H, W = 720, 1280
|
||
|
||
# Compact detections from an RLE-based source.
|
||
# Replace the counts string with a real compressed RLE payload from your model or API.
|
||
rles = [{"size": [H, W], "counts": "YOUR_RLE_COUNTS_STRING_HERE"}]
|
||
xyxy_a = np.array([[100.0, 50.0, 400.0, 300.0]])
|
||
cm = CompactMask.from_coco_rle(rles, xyxy_a, image_shape=(H, W))
|
||
det_a = sv.Detections(xyxy=xyxy_a, mask=cm, class_id=np.array([0]))
|
||
|
||
# Dense detections from a different source.
|
||
masks_b = np.zeros((1, H, W), dtype=bool)
|
||
masks_b[0, 200:400, 500:800] = True
|
||
xyxy_b = np.array([[500.0, 200.0, 799.0, 399.0]])
|
||
det_b = sv.Detections(xyxy=xyxy_b, mask=masks_b, class_id=np.array([1]))
|
||
|
||
# Output is CompactMask regardless of input order.
|
||
merged = sv.Detections.merge([det_a, det_b])
|
||
assert isinstance(merged.mask, CompactMask)
|
||
assert len(merged) == 2
|
||
```
|
||
|
||
Merge rules:
|
||
|
||
| Inputs | Output mask type |
|
||
| ------------------------------------- | ------------------------------- |
|
||
| All `CompactMask` | `CompactMask` |
|
||
| Mixed `CompactMask` + dense `ndarray` | `CompactMask` |
|
||
| All dense `ndarray` | `ndarray` (backward compatible) |
|
||
|
||
All `CompactMask` inputs must share the same `image_shape`; mismatches raise `ValueError`.
|
||
|
||
---
|
||
|
||
## Performance Notes
|
||
|
||
These estimates apply to the **parsing and annotation stage**, not end-to-end pipeline FPS. Model inference typically dominates total runtime.
|
||
|
||
| Optimisation | Realistic gain | Applies when |
|
||
| ---------------------------- | -------------------------- | ------------------------------------------------------------- |
|
||
| `from_coco_rle` ingestion | 25–60% faster parse | Full-frame COCO RLE payload; current dense decode path |
|
||
| `MaskAnnotator` ROI blending | 10–35% faster annotation | Many small, sparse masks on high-res frames |
|
||
| `PolygonAnnotator` crop path | 15–45% faster polygon draw | Many compact masks; full-frame materialise was the bottleneck |
|
||
| Mixed-mask merge | 5–20% faster merge | Mix of compact and dense sources (e.g. multi-camera stitch) |
|
||
|
||
Upper-end gains assume: ≥1080p frames, tens to hundreds of instances, masks covering less than ~20% of total pixels.
|
||
|
||
---
|
||
|
||
## API Reference
|
||
|
||
- [CompactMask][supervision.detection.compact_mask.CompactMask]
|
||
- [CompactMask.from_coco_rle][supervision.detection.compact_mask.CompactMask.from_coco_rle]
|
||
- [CompactMask.from_dense][supervision.detection.compact_mask.CompactMask.from_dense]
|
||
- [Detections.from_inference][supervision.detection.core.Detections.from_inference]
|
||
- [Detections.to_compact_masks][supervision.detection.core.Detections.to_compact_masks]
|
||
- [Detections.merge][supervision.detection.core.Detections.merge]
|
||
- [BaseAnnotator.requires_mask][supervision.annotators.base.BaseAnnotator]
|