Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1114 rle support for coco #1163

Merged
merged 60 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
e30d4a9
add test for generating annotation with mask
emSko May 2, 2024
cc5e72d
test for RLE format
emSko May 2, 2024
7f114cb
RLE decoding
emSko May 2, 2024
6913ecf
2 annotations with segmentation, one polygon one RLE
emSko May 2, 2024
4a068ff
binary mask to RL encoding
emSko May 3, 2024
dc1ce02
move rle encode decode functions to dataset/utils.py
emSko May 3, 2024
869204d
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 5, 2024
18a790e
typing chanches and doc strings for rle_to_mask and mask_to_rle funct…
emSko May 6, 2024
3ade7f0
fix order caused error with mask generation in coco_annotations_to_de…
emSko May 6, 2024
2727152
merge with upstream
emSko May 6, 2024
a941583
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 6, 2024
88a43cc
unit tests for rle_to_mask and mask_to_rle functions
emSko May 6, 2024
6b5dacd
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 6, 2024
a9f821c
Assertion error when number of pixel in RLE does not match the nuber …
emSko May 7, 2024
3618ac3
merge
emSko May 7, 2024
d59ec0f
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 7, 2024
aac1d88
assertion for valid mask in mask_to_rle function
emSko May 7, 2024
36a301e
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 7, 2024
649e273
fix the line lengths
emSko May 8, 2024
008711a
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 8, 2024
e4e66f8
faster mask to rle
emSko May 10, 2024
39477fe
Merge branch '1114_RLE_support_for_COCO' of github.com:emSko/supervis…
emSko May 10, 2024
7bd92b0
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 10, 2024
8c857dd
speed up rle to mask
emSko May 12, 2024
d292cbf
Merge branch '1114_RLE_support_for_COCO' of github.com:emSko/supervis…
emSko May 12, 2024
14b5734
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 12, 2024
d21c98a
docs updated; `rle_to_mask` and `mask_to_rle` added to `__init__.py`
SkalskiP May 13, 2024
9a1c112
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 13, 2024
59b273b
small refactor
SkalskiP May 13, 2024
cccacab
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 13, 2024
aab861c
test_coco_annotations_to_detections result matrices defined in place
emSko May 15, 2024
f08404e
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 15, 2024
d8679d9
automatic RLE for masks with holes or in multiple pieces
emSko May 16, 2024
eefa05c
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 16, 2024
1e8ee47
craete copies of mask in _has_holes and _mask_has_multiple_segments
emSko May 16, 2024
ce05a0c
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 16, 2024
3ee72e3
check for empty mask
emSko May 16, 2024
511f0ab
check for empty mask
emSko May 16, 2024
b3b7455
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 16, 2024
879ae94
test polygon mask when mask in single component and no holes
emSko May 17, 2024
ab775d9
Attempt to fix SegFault
LinasKo May 17, 2024
aad5e2a
merge seg fault fix
emSko May 17, 2024
0cd2c9d
documentation change for as_coco
emSko May 17, 2024
8d432c7
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 17, 2024
a40bf78
coco mock method refactoring
emSko May 20, 2024
2626263
move has_holes and mask_has_multiple_segments to detections utils
emSko May 20, 2024
179d00b
merge
emSko May 20, 2024
7ee84c4
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 20, 2024
68f07e1
tests for mask_has_holes
emSko May 20, 2024
f0fc76c
Merge branch '1114_RLE_support_for_COCO' of github.com:emSko/supervis…
emSko May 20, 2024
c26eb1a
unit tests for test_mask_has_multiple_segments
emSko May 20, 2024
dfac30c
change docu for mask_has_multiple_segments and mask_has_holes and add…
emSko May 20, 2024
7b42d83
Merge branch 'develop' into 1114_RLE_support_for_COCO
emSko May 20, 2024
ab55e81
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 20, 2024
6dbf3f6
fix for unit tests for test_mask_has_holes
emSko May 20, 2024
f271625
small refactor + docs improvements
SkalskiP May 21, 2024
996b0a3
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 21, 2024
1d43042
small refactor + docs improvements
SkalskiP May 21, 2024
c3855c9
small refactor + docs improvements
SkalskiP May 21, 2024
5840889
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/datasets.md → docs/datasets/core.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
comments: true
status: new
---

# Datasets
Expand Down
18 changes: 18 additions & 0 deletions docs/datasets/utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
comments: true
status: new
---

# Datasets Utils

<div class="md-typeset">
<h2><a href="#supervision.dataset.utils.rle_to_mask">rle_to_mask</a></h2>
</div>

:::supervision.dataset.utils.rle_to_mask

<div class="md-typeset">
<h2><a href="#supervision.dataset.utils.mask_to_rle">mask_to_rle</a></h2>
</div>

:::supervision.dataset.utils.mask_to_rle
4 changes: 3 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ nav:
- Detection Smoother: detection/tools/smoother.md
- Save Detections: detection/tools/save_detections.md
- Trackers: trackers.md
- Datasets: datasets.md
- Datasets:
- Core: datasets/core.md
- Utils: datasets/utils.md
- Utils:
- Video: utils/video.md
- Image: utils/image.md
Expand Down
1 change: 1 addition & 0 deletions supervision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ClassificationDataset,
DetectionDataset,
)
from supervision.dataset.utils import mask_to_rle, rle_to_mask
from supervision.detection.annotate import BoxAnnotator
from supervision.detection.core import Detections
from supervision.detection.line_zone import LineZone, LineZoneAnnotator
Expand Down
4 changes: 4 additions & 0 deletions supervision/dataset/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ def as_coco(
"""
Exports the dataset to COCO format. This method saves the
images and their corresponding annotations in COCO format.
The format of the mask is determined automatically:
when a mask consists of multiple disconnected elements
or has holes the RLE format is used,
otherwise, the mask is encoded as a polygon.

Args:
images_directory_path (Optional[str]): The path to the directory
Expand Down
84 changes: 62 additions & 22 deletions supervision/dataset/formats/coco.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import cv2
import numpy as np
import numpy.typing as npt

from supervision.dataset.utils import (
approximate_mask_with_polygons,
map_detections_class_id,
mask_to_rle,
rle_to_mask,
)
from supervision.detection.core import Detections
from supervision.detection.utils import polygon_to_mask
Expand Down Expand Up @@ -57,13 +60,24 @@ def group_coco_annotations_by_image_id(
return annotations


def _polygons_to_masks(
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
) -> np.ndarray:
def coco_annotations_to_masks(
image_annotations: List[dict], resolution_wh: Tuple[int, int]
) -> npt.NDArray[np.bool_]:
return np.array(
[
polygon_to_mask(polygon=polygon, resolution_wh=resolution_wh)
for polygon in polygons
rle_to_mask(
rle=np.array(image_annotation["segmentation"]["counts"]),
resolution_wh=resolution_wh,
)
if image_annotation["iscrowd"]
else polygon_to_mask(
polygon=np.reshape(
np.asarray(image_annotation["segmentation"], dtype=np.int32),
(-1, 2),
),
resolution_wh=resolution_wh,
)
for image_annotation in image_annotations
],
dtype=bool,
)
Expand All @@ -83,20 +97,33 @@ def coco_annotations_to_detections(
xyxy[:, 2:4] += xyxy[:, 0:2]

if with_masks:
polygons = [
np.reshape(
np.asarray(image_annotation["segmentation"], dtype=np.int32), (-1, 2)
)
for image_annotation in image_annotations
]
mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
mask = coco_annotations_to_masks(
image_annotations=image_annotations, resolution_wh=resolution_wh
)
return Detections(
class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask
)

return Detections(xyxy=xyxy, class_id=np.asarray(class_ids, dtype=int))


def _mask_has_holes(mask: np.ndarray) -> bool:
emSko marked this conversation as resolved.
Show resolved Hide resolved
mask_uint8 = mask.astype(np.uint8)
_, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
parent_countour_index = 3
for h in hierarchy[0]:
if h[parent_countour_index] != -1:
return True
return False


def _mask_has_multiple_segments(mask: np.ndarray) -> bool:
mask_uint8 = mask.astype(np.uint8)
labels = np.zeros_like(mask_uint8, dtype=np.int32)
number_of_labels, _ = cv2.connectedComponents(mask_uint8, labels, connectivity=4)
return number_of_labels > 2


def detections_to_coco_annotations(
detections: Detections,
image_id: int,
Expand All @@ -108,24 +135,37 @@ def detections_to_coco_annotations(
coco_annotations = []
for xyxy, mask, _, class_id, _, _ in detections:
box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
polygon = []
segmentation = []
iscrowd = 0
if mask is not None:
polygon = list(
approximate_mask_with_polygons(
mask=mask,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)[0].flatten()
iscrowd = _mask_has_holes(mask=mask) or _mask_has_multiple_segments(
mask=mask
)

if iscrowd:
segmentation = {
"counts": mask_to_rle(mask=mask),
"size": list(mask.shape[:2]),
}
else:
segmentation = [
list(
approximate_mask_with_polygons(
mask=mask,
min_image_area_percentage=min_image_area_percentage,
max_image_area_percentage=max_image_area_percentage,
approximation_percentage=approximation_percentage,
)[0].flatten()
)
] # multicomponent masks supported only for rle format
emSko marked this conversation as resolved.
Show resolved Hide resolved
coco_annotation = {
"id": annotation_id,
"image_id": image_id,
"category_id": int(class_id),
"bbox": [xyxy[0], xyxy[1], box_width, box_height],
"area": box_width * box_height,
"segmentation": [polygon] if polygon else [],
"iscrowd": 0,
"segmentation": segmentation,
"iscrowd": iscrowd,
}
coco_annotations.append(coco_annotation)
annotation_id += 1
Expand Down
115 changes: 114 additions & 1 deletion supervision/dataset/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import os
import random
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypeVar
from typing import Dict, List, Optional, Tuple, TypeVar, Union

import cv2
import numpy as np
import numpy.typing as npt

from supervision.detection.core import Detections
from supervision.detection.utils import (
Expand Down Expand Up @@ -129,3 +130,115 @@ def train_test_split(

split_index = int(len(data) * train_ratio)
return data[:split_index], data[split_index:]


def rle_to_mask(
rle: Union[npt.NDArray[np.int_], List[int]], resolution_wh: Tuple[int, int]
) -> npt.NDArray[np.bool_]:
"""
Converts run-length encoding (RLE) to a binary mask.

Args:
rle (Union[npt.NDArray[np.int_], List[int]]): The 1D RLE array, the format
used in the COCO dataset (column-wise encoding, values of an array with
even indices represent the number of pixels assigned as background,
values of an array with odd indices represent the number of pixels
assigned as foreground object).
resolution_wh (Tuple[int, int]): The width (w) and height (h)
of the desired binary mask.

Returns:
The generated 2D Boolean mask of shape `(h, w)`, where the foreground object is
marked with `True`'s and the rest is filled with `False`'s.

Raises:
AssertionError: If the sum of pixels encoded in RLE differs from the
number of pixels in the expected mask (computed based on resolution_wh).

Examples:
```python
import supervision as sv

sv.rle_to_mask([2, 2, 2], (3, 2))
# array([
# [False, True, False],
# [False, True, False]
# ])
```
"""
if isinstance(rle, list):
rle = np.array(rle, dtype=int)

width, height = resolution_wh

assert width * height == np.sum(rle), (
"the sum of the number of pixels in the RLE must be the same "
"as the number of pixels in the expected mask"
)

zero_one_values = np.zeros(shape=(rle.size, 1), dtype=np.uint8)
zero_one_values[1::2] = 1

decoded_rle = np.repeat(zero_one_values, rle, axis=0)
decoded_rle = np.append(
decoded_rle, np.zeros(width * height - len(decoded_rle), dtype=np.uint8)
)
return decoded_rle.reshape((height, width), order="F")


def mask_to_rle(mask: npt.NDArray[np.bool_]) -> List[int]:
"""
Converts a binary mask into a run-length encoding (RLE).

Args:
mask (npt.NDArray[np.bool_]): 2D binary mask where `True` indicates foreground
object and `False` indicates background.

Returns:
The run-length encoded mask. Values of a list with even indices
represent the number of pixels assigned as background (`False`), values
of a list with odd indices represent the number of pixels assigned
as foreground object (`True`).

Raises:
AssertionError: If input mask is not 2D or is empty.

Examples:
```python
import numpy as np
import supervision as sv

mask = np.array([
[False, True, True],
[False, True, True]
])
sv.mask_to_rle(mask)
# [2, 4]

mask = np.array([
[True, True, True],
[True, True, True]
])
sv.mask_to_rle(mask)
# [0, 6]
```
"""
assert mask.ndim == 2, "Input mask must be 2D"
assert mask.size != 0, "Input mask cannot be empty"

on_value_change_indices = np.where(
mask.ravel(order="F") != np.roll(mask.ravel(order="F"), 1)
)[0]

on_value_change_indices = np.append(on_value_change_indices, mask.size)
# need to add 0 at the beginning when the same value is in the first and
# last element of the flattened mask
if on_value_change_indices[0] != 0:
on_value_change_indices = np.insert(on_value_change_indices, 0, 0)

rle = np.diff(on_value_change_indices)

if mask[0][0] == 1:
rle = np.insert(rle, 0, 0)

return list(rle)