Skip to content

feat: Detections.from_inference supports compressed RLE masks#2178

Merged
Borda merged 27 commits intodevelopfrom
from-inference-rle-mask
Apr 15, 2026
Merged

feat: Detections.from_inference supports compressed RLE masks#2178
Borda merged 27 commits intodevelopfrom
from-inference-rle-mask

Conversation

@leeclemnet
Copy link
Copy Markdown
Contributor

@leeclemnet leeclemnet commented Mar 20, 2026

Before submitting
  • Self-reviewed the code
  • Updated documentation, follow Google-style
  • Added docs entry for autogeneration (if new functions/classes)
  • Added/updated tests
  • All tests pass locally

Description

Add RLE mask support to sv.Detections.from_inference for models that return compressed COCO RLE (e.g. SAM3, semantic segmentation), and extend rle_to_mask/mask_to_rle with compressed string encoding/decoding — implemented as a pure Python COCO RLE codec with no new dependencies.

cc @PawelPeczek-Roboflow

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)

Motivation and Context

Workflows currently supports COCO RLE compressed string serialization for segmentation masks, originally for SAM3 instance segmentation and now also for semantic segmentation models where masks may not be contiguous and therefore can't be represented as polygons. We would like sv.Detections.from_inference() to handle such data and automatically decode it into binary masks as it currently does for polygon mask data.

The existing rle_to_mask util supports this conversion but not from compressed strings (see #1952).

Closes #1952

Changes Made

  • Add RLE mask support to sv.Detections.from_inference for models like SAM3 and semantic segmentation that return compressed COCO RLE instead of polygons
  • Extend rle_to_mask to accept compressed COCO RLE strings (no new dependencies — pure Python decoder)
  • Add compressed flag to mask_to_rle for producing COCO RLE strings
  • Move rle_to_mask and mask_to_rle from dataset/utils to detection/utils/converters with the other converters - necessary to avoid circular dependency, and dataset.utils already imports from detection.utils.converters - backwards compatibility maintained through re-export
  • Bugfix: rle_to_mask return type was uint8, now bool as per function signature

Testing

  • I have tested this code locally
  • I have added unit tests that prove my fix is effective or that my feature works
  • All new and existing tests pass

Google Colab (optional)

Colab link:

Screenshots/Videos (optional)

Additional Notes

@leeclemnet leeclemnet changed the title feat: detections.from_inference supports compressed RLE masks feat: Detections.from_inference supports compressed RLE masks Mar 20, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 97.14286% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 77%. Comparing base (f28874c) to head (1b9a1d4).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff           @@
##           develop   #2178   +/-   ##
=======================================
  Coverage       77%     77%           
=======================================
  Files           66      66           
  Lines         8115    8189   +74     
=======================================
+ Hits          6232    6303   +71     
- Misses        1883    1886    +3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leeclemnet leeclemnet marked this pull request as ready for review March 20, 2026 21:04
@leeclemnet leeclemnet requested a review from SkalskiP as a code owner March 20, 2026 21:04
Borda
Borda previously approved these changes Mar 21, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds compressed COCO RLE mask support across Supervision’s detection inference pipeline and converters, enabling sv.Detections.from_inference() to decode masks provided as compressed RLE strings/bytes (e.g., SAM3 / semantic segmentation outputs) without adding dependencies.

Changes:

  • Implement pure-Python COCO compressed RLE string encode/decode and extend rle_to_mask / mask_to_rle to support compressed formats.
  • Update inference result processing to accept RLE mask payloads (rle / rle_mask) and build Detections.mask accordingly.
  • Move RLE utilities into detection.utils.converters, update exports/docs, and relocate tests accordingly.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/detection/utils/test_internal.py Adds inference parsing test cases for compressed RLE masks (incl. non-contiguous + tracker_id).
tests/detection/utils/test_converters.py Adds unit tests for compressed RLE codec and mask↔RLE conversions.
tests/dataset/test_utils.py Removes RLE tests from dataset utils (now covered under converters).
src/supervision/detection/utils/internal.py Extends Roboflow/inference result parsing to decode RLE masks.
src/supervision/detection/utils/converters.py Adds COCO compressed RLE codec + compressed support in converters.
src/supervision/dataset/utils.py Removes old implementations and re-exports converters versions for compatibility.
src/supervision/dataset/formats/coco.py Updates imports to use the new converter locations.
src/supervision/init.py Exposes mask_to_rle / rle_to_mask from converters at top-level API.
mkdocs.yml Removes datasets utils docs page from nav.
docs/detection/utils/converters.md Documents rle_to_mask / mask_to_rle under converters.
docs/datasets/utils.md Removes the old dataset utils docs page.

Comment thread src/supervision/detection/utils/internal.py
Comment thread src/supervision/dataset/utils.py Outdated
Comment thread src/supervision/detection/utils/internal.py Outdated
Borda and others added 2 commits March 21, 2026 22:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Add bounds check in _decode_coco_rle_string() so malformed/truncated
  compressed RLE strings raise ValueError instead of IndexError
- Guard process_roboflow_result against non-dict, missing-key, and
  malformed-counts RLE payloads — all fall through to box-only detection
- Fix stale masks type annotation (uint8 → bool_) in internal.py
- Add warn_deprecated() wrappers on rle_to_mask/mask_to_rle re-exports
  in dataset/utils for consistency with project deprecation pattern
- Add negative-path tests: malformed counts, missing size/counts keys,
  non-dict rle, rle_mask fallback key, truncated compressed strings
- Add CHANGELOG entries under develop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Borda Borda added the enhancement New feature or request label Mar 21, 2026
@leeclemnet leeclemnet enabled auto-merge (squash) March 23, 2026 15:11
@Borda Borda self-assigned this Mar 26, 2026
@Borda Borda disabled auto-merge April 14, 2026 09:15
Borda and others added 3 commits April 14, 2026 11:20
- Change color=1 to color=(1,) for mypy type compatibility with cv2 stubs

---
Co-authored-by: Claude Code <noreply@anthropic.com>
When a roboflow_result mixes RLE-encoded predictions (decoded to
rle_data["size"]) and polygon predictions (decoded to image dims),
np.array(masks) raises ValueError on inhomogeneous shapes. Resize
RLE masks with INTER_NEAREST to (image_height, image_width) when
the decoded RLE dimensions differ from the reported image size.

---
Co-authored-by: Claude Code <noreply@anthropic.com>
Comment thread src/supervision/detection/utils/converters.py Outdated
Comment thread src/supervision/detection/utils/converters.py
Borda and others added 6 commits April 14, 2026 11:52
[resolve #5] /review finding by sw-engineer (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"coco.py:88-89 wraps segmentation["counts"] in np.array() before passing to rle_to_mask. For compressed string counts, np.array("52203") produces a 0-d string ndarray that bypasses isinstance(rle, str)..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve #8] /review finding by qa-specialist (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"No test covers mixed RLE + box-only predictions in the same batch. Produces misaligned masks (1 mask for 2 detections)..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve #10] /review finding by qa-specialist (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"No test covers DetectionDataset.from_coco with compressed RLE iscrowd annotations..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve #1] Review comment by @Copilot (gh):
"mask from rle_to_mask is a boolean array, but masks is annotated as list[npt.NDArray[np.uint8]]..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve #6] /review finding by sw-engineer (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"Broad exception swallowing at internal.py:107 silently drops RLE decode failures with no warning..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve #7] /review finding by sw-engineer (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"rle_to_mask uses assert for input validation which can be disabled with python -O; raise ValueError instead..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
Borda and others added 4 commits April 14, 2026 11:58
[resolve #9] /review finding by qa-specialist (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"Consider adding a test with bytes that would fail UTF-8 decode to verify error handling..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
[resolve #11] /review finding by doc-scribe (report: .temp/output-review-from-inference-rle-mask-2026-04-14.md):
"Changelog entry for rle_to_mask / mask_to_rle relocation doesn't mention the dtype change from uint8 to bool..."

---
Co-authored-by: Claude Code <noreply@anthropic.com>
np.int_ accepts only the platform-default int dtype; np.integer[Any]
accepts any integer-typed array (int32, int64, uint16, etc.), which
matches what the implementation actually handles.

---
Co-authored-by: Claude Code <noreply@anthropic.com>
No call site needs Literal-based return-type narrowing, so the
@overload pair (and the Literal/overload imports) are dead weight.
The single signature returns list[int] | str; the one internal call
site in coco.py uses cast() to satisfy mypy.

---
Co-authored-by: Claude Code <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/supervision/detection/utils/internal.py:132

  • process_roboflow_result builds masks only for predictions that include RLE/polygon data. If a batch mixes masked and box-only predictions, masks_arr ends up shorter than xyxy_arr, and Detections.from_inference() will raise during validation because mask.shape[0] != len(xyxy). Consider keeping masks aligned by appending an empty mask for box-only predictions whenever any mask is present in the batch (or, alternatively, dropping masks entirely when they can’t be produced for all predictions).
        if rle_data is not None:
            xyxy.append([x_min, y_min, x_max, y_max])
            class_id.append(prediction["class_id"])
            class_name.append(prediction["class"])
            confidence.append(prediction["confidence"])
            masks.append(mask)
            if "tracker_id" in prediction:
                tracker_ids.append(prediction["tracker_id"])
        elif "points" not in prediction:
            xyxy.append([x_min, y_min, x_max, y_max])
            class_id.append(prediction["class_id"])
            class_name.append(prediction["class"])
            confidence.append(prediction["confidence"])
            if "tracker_id" in prediction:
                tracker_ids.append(prediction["tracker_id"])
        elif len(prediction["points"]) >= 3:

Comment thread src/supervision/detection/utils/converters.py Outdated
Borda and others added 4 commits April 14, 2026 12:30
Both functions implement the COCO pycocotools RLE format which uses
column-major (Fortran) pixel ordering — the opposite of NumPy's default
row-major layout. Make this explicit in the docstrings so callers
understand the format and cannot confuse it with row-major RLE variants.

---
Co-authored-by: Claude Code <noreply@anthropic.com>
…le/rle_to_mask; add pydeprecate dependency
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Borda
Copy link
Copy Markdown
Member

Borda commented Apr 14, 2026

@leeclemnet could you pls check that updated version?

Comment thread src/supervision/dataset/utils.py
Borda added 4 commits April 14, 2026 21:18
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
@Borda Borda merged commit 62efdee into develop Apr 15, 2026
27 checks passed
@Borda Borda deleted the from-inference-rle-mask branch April 15, 2026 20:35
@Borda Borda mentioned this pull request Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

does rle_to_mask to mask support UTF-8 encoded string?

3 participants