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

feat: Merge Multiple Polygons in a COCO Dataset Annotation #1229

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/datasets/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ status: new
</div>

:::supervision.dataset.utils.mask_to_rle

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

:::supervision.dataset.utils.merge_polygons
3 changes: 3 additions & 0 deletions supervision/dataset/formats/coco.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
approximate_mask_with_polygons,
map_detections_class_id,
mask_to_rle,
merge_polygons,
rle_to_mask,
)
from supervision.detection.core import Detections
Expand Down Expand Up @@ -74,6 +75,8 @@ def coco_annotations_to_masks(
resolution_wh=resolution_wh,
)
if image_annotation["iscrowd"]
else merge_polygons(image_annotation["segmentation"], resolution_wh)
if len(image_annotation["segmentation"]) > 1
else polygon_to_mask(
polygon=np.reshape(
np.asarray(image_annotation["segmentation"], dtype=np.int32),
Expand Down
54 changes: 54 additions & 0 deletions supervision/dataset/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
approximate_polygon,
filter_polygons_by_area,
mask_to_polygons,
polygon_to_mask,
)

T = TypeVar("T")
Expand Down Expand Up @@ -46,6 +47,59 @@ def approximate_mask_with_polygons(
]


def merge_polygons(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the merge_polygons name is a bit misleading and not conststant with outhe util names in supervision. I think we should call it polygons_to_mask.

And of course we already have a function called polygon_to_mask that does the same but only take one mask.

It seems a bit silly that we would have 2 functions doeng almost the same. I thin kwe should:

  • rename merge_polygons to polygons_to_mask.
  • make sure that polygons_to_mask is not using polygon_to_mask internally.
  • modify places where polygon_to_mask was used and use polygons_to_mask instead.
  • mask polygon_to_mask as deprecated and recommend using polygons_to_mask instead.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@DancinParrot, here is an example of how we mark functions as deprecated. We use deprecated decorator to do it.

@deprecated(
    "`Detections.from_roboflow` is deprecated and will be removed in "
    "`supervision-0.22.0`. Use `Detections.from_inference` instead."
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use supervision-0.27.0 as a deprecation version.

polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
) -> np.ndarray:
"""
Merge polygons (in the form of vertices) within the segmentation list
of a COCO annotation.

Args:
polygons (List[np.ndarray]): A nested list of vertices, with each
nested list representing one polygon
resolution_wh (Tuple[int, int]): The width (w) and height (h)
of the desired binary mask.

Returns:
np.ndarray: The generated 2D mask, where the polygon is marked with
`1`'s and the rest is filled with `0`'s.

Examples:
```python
import supervision as sv

sv.merge_polygons([[1.0, 1.0, 2.0, 3.0], [1.0, 1.0, 3.0, 2.0]], (8, 4))
# array([
# [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
# [0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
# [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0],
# [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
# ])
```
"""
parent_polygon = polygons[0]
parent_mask = polygon_to_mask(
polygon=np.reshape(
np.asarray(parent_polygon, dtype=np.int32),
(-1, 2),
),
resolution_wh=resolution_wh,
)

for p in polygons[1:]:
child_mask = polygon_to_mask(
polygon=np.reshape(
np.asarray(p, dtype=np.int32),
(-1, 2),
),
resolution_wh=resolution_wh,
)

parent_mask = np.logical_or(parent_mask, child_mask)

return parent_mask.astype(float)


def merge_class_lists(class_lists: List[List[str]]) -> List[str]:
unique_classes = set()

Expand Down
58 changes: 58 additions & 0 deletions test/dataset/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
map_detections_class_id,
mask_to_rle,
merge_class_lists,
merge_polygons,
rle_to_mask,
train_test_split,
)
Expand Down Expand Up @@ -125,6 +126,63 @@ def test_merge_class_maps(
assert result == expected_result


@pytest.mark.parametrize(
"polygons, resolution_wh, expected_result, exception",
[
(
np.array([[1.0, 1.0, 2.0, 3.0]]),
(8, 4),
np.array(
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]
),
DoesNotRaise(),
), # single polygon
(
np.array([[1.0, 1.0, 2.0, 3.0], [1.0, 1.0, 3.0, 2.0]]),
(8, 4),
np.array(
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]
),
DoesNotRaise(),
), # two polygons
(
np.array(
[[1.0, 0.0, 2.0, 3.0], [1.0, 1.0, 3.0, 2.0], [1.0, 2.0, 1.0, 2.0]]
),
(8, 4),
np.array(
[
[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]
),
DoesNotRaise(),
), # multiple polygons
],
)
def test_merge_polygons(
polygons: List[np.ndarray],
resolution_wh: Tuple[int, int],
expected_result: np.ndarray,
exception: Exception,
) -> None:
with exception:
result = merge_polygons(polygons=polygons, resolution_wh=resolution_wh)
assert np.array_equal(result, expected_result)


@pytest.mark.parametrize(
"source_classes, target_classes, expected_result, exception",
[
Expand Down
Loading