From 473bb9c7e596ba37605a90d3591a4c70211401b4 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Tue, 13 Jan 2026 16:23:01 +0100 Subject: [PATCH 01/20] Adding models: video and object detection track --- .../model/object_detection_track.py | 69 +++++++++++++++++++ src/labelformat/model/video.py | 11 +++ .../unit/model/test_object_detection_track.py | 55 +++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 src/labelformat/model/object_detection_track.py create mode 100644 src/labelformat/model/video.py create mode 100644 tests/unit/model/test_object_detection_track.py diff --git a/src/labelformat/model/object_detection_track.py b/src/labelformat/model/object_detection_track.py new file mode 100644 index 0000000..ed1fff5 --- /dev/null +++ b/src/labelformat/model/object_detection_track.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from argparse import ArgumentParser +from dataclasses import dataclass +from typing import Iterable, List + +from labelformat.model.bounding_box import BoundingBox +from labelformat.model.category import Category +from labelformat.model.video import Video + + +@dataclass(frozen=True) +class SingleObjectDetectionTrack: + category: Category + boxes: list[BoundingBox | None] + # TODO (Jonas, 01/2026): Add confidence + + +@dataclass(frozen=True) +class VideoObjectDetectionTrack: + """ + The base class for a video alongside with its object detection track annotations. + A a video contains of N frames and of M objects. Each object contains N boxes. + The number of frames and the number of annotations for each object must match + --> one annotation per frame. + If a object is not present on a frame, the corresponding entry has to be None. + """ + + video: Video + objects: List[SingleObjectDetectionTrack] + + def __post_init__(self) -> None: + number_of_frames = self.video.number_of_frames + + for object in self.objects: + if len(object.boxes) != number_of_frames: + raise ValueError( + "Length of object detection track does not match the number of frames in the video." + ) + + +class ObjectDetectionTrackInput(ABC): + @staticmethod + @abstractmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + raise NotImplementedError() + + @abstractmethod + def get_categories(self) -> Iterable[Category]: + raise NotImplementedError() + + @abstractmethod + def get_videos(self) -> Iterable[Video]: + raise NotImplementedError() + + @abstractmethod + def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: + raise NotImplementedError() + + +class ObjectDetectionTrackOutput(ABC): + @staticmethod + @abstractmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + raise NotImplementedError() + + def save(self, label_input: ObjectDetectionTrackInput) -> None: + raise NotImplementedError() diff --git a/src/labelformat/model/video.py b/src/labelformat/model/video.py new file mode 100644 index 0000000..d6f272c --- /dev/null +++ b/src/labelformat/model/video.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Video: + id: int + filename: str + width: int + height: int + number_of_frames: int + # TODO (Jonas, 01/2026): Add list of frames diff --git a/tests/unit/model/test_object_detection_track.py b/tests/unit/model/test_object_detection_track.py new file mode 100644 index 0000000..ea7c52e --- /dev/null +++ b/tests/unit/model/test_object_detection_track.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import pytest + +from labelformat.model.bounding_box import BoundingBox +from labelformat.model.category import Category +from labelformat.model.object_detection_track import ( + SingleObjectDetectionTrack, + VideoObjectDetectionTrack, +) +from labelformat.model.video import Video + + +class TestVideoObjectDetectionTrack: + def test_frames_equal_boxes_length__valid(self) -> None: + track_a = SingleObjectDetectionTrack( + category=Category(id=0, name="cat"), + boxes=[BoundingBox(xmin=0, ymin=0, xmax=1, ymax=1) for _ in range(2)], + ) + + track_b = SingleObjectDetectionTrack( + category=Category(id=1, name="dog"), + boxes=[BoundingBox(xmin=0, ymin=0, xmax=1, ymax=1) for _ in range(2)], + ) + + video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) + + detections = VideoObjectDetectionTrack( + video=video, + objects=[track_a, track_b], + ) + assert len(detections.objects) == 2 + assert len(detections.objects[0].boxes) == 2 + + def test_frames_equal_boxes_length___invalid(self) -> None: + track_a = SingleObjectDetectionTrack( + category=Category(id=0, name="cat"), + boxes=[BoundingBox(xmin=0, ymin=0, xmax=1, ymax=1) for _ in range(2)], + ) + + track_b = SingleObjectDetectionTrack( + category=Category(id=1, name="dog"), + boxes=[BoundingBox(xmin=0, ymin=0, xmax=1, ymax=1) for _ in range(3)], + ) + + video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) + + with pytest.raises( + ValueError, + match="Length of object detection track does not match the number of frames in the video.", + ): + VideoObjectDetectionTrack( + video=video, + objects=[track_a, track_b], + ) From 6bd5ed3dcd6d48e386dd7286cf309571cb11c5c0 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Wed, 14 Jan 2026 10:28:25 +0100 Subject: [PATCH 02/20] Add youtubevis input format --- src/labelformat/formats/__init__.py | 2 + src/labelformat/formats/youtubevis.py | 93 +++++++++++++++++++++++++++ tests/unit/formats/test_youtubevis.py | 92 ++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 src/labelformat/formats/youtubevis.py create mode 100644 tests/unit/formats/test_youtubevis.py diff --git a/src/labelformat/formats/__init__.py b/src/labelformat/formats/__init__.py index 47bbd18..76a513c 100644 --- a/src/labelformat/formats/__init__.py +++ b/src/labelformat/formats/__init__.py @@ -65,6 +65,7 @@ YOLOv26ObjectDetectionInput, YOLOv26ObjectDetectionOutput, ) +from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput __all__ = [ "COCOInstanceSegmentationInput", @@ -105,4 +106,5 @@ "YOLOv26ObjectDetectionInput", "YOLOv26ObjectDetectionOutput", "MaskPairInstanceSegmentationInput", + "YouTubeVISObjectDetectionTrackInput", ] diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py new file mode 100644 index 0000000..60108c6 --- /dev/null +++ b/src/labelformat/formats/youtubevis.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import json +from argparse import ArgumentParser +from pathlib import Path +from typing import Dict, Iterable, List + +from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat +from labelformat.model.category import Category +from labelformat.model.object_detection_track import ( + ObjectDetectionTrackInput, + SingleObjectDetectionTrack, + VideoObjectDetectionTrack, +) +from labelformat.model.video import Video +from labelformat.types import JsonDict + + +class YouTubeVISObjectDetectionTrackInput(ObjectDetectionTrackInput): + @staticmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + parser.add_argument( + "--input-file", + type=Path, + required=True, + help="Path to input YouTube-VIS JSON file", + ) + + def __init__(self, input_file: Path) -> None: + with input_file.open() as file: + self._data = json.load(file) + + def get_categories(self) -> Iterable[Category]: + for category in self._data["categories"]: + yield Category( + id=category["id"], + name=category["name"], + ) + + def get_videos(self) -> Iterable[Video]: + for video in self._data["videos"]: + yield Video( + id=video["id"], + # TODO (Jonas, 1/2026): The file_names do not hold the video file extension. Solution required. + filename=Path(video["file_names"][0]).parent.name, + width=int(video["width"]), + height=int(video["height"]), + number_of_frames=int(video["length"]), + ) + + def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: + video_id_to_video = {video.id: video for video in self.get_videos()} + category_id_to_category = { + category.id: category for category in self.get_categories() + } + video_id_to_tracks: Dict[int, List[JsonDict]] = { + video_id: [] for video_id in video_id_to_video.keys() + } + for ann in self._data["annotations"]: + video_id_to_tracks[ann["video_id"]].append(ann) + + for video_id, tracks in video_id_to_tracks.items(): + video = video_id_to_video[video_id] + objects = [] + for track in tracks: + boxes = _get_object_track_boxes(ann=track) + objects.append( + SingleObjectDetectionTrack( + category=category_id_to_category[ann["category_id"]], + boxes=boxes, + ) + ) + yield VideoObjectDetectionTrack( + video=video, + objects=objects, + ) + + +def _get_object_track_boxes( + ann: JsonDict, +) -> list[BoundingBox | None]: + boxes: list[BoundingBox | None] = [] + for bbox in ann["bboxes"]: + if bbox is None or len(bbox) == 0: + boxes.append(None) + continue + boxes.append( + BoundingBox.from_format( + bbox=[float(x) for x in bbox], + format=BoundingBoxFormat.XYWH, + ) + ) + return boxes diff --git a/tests/unit/formats/test_youtubevis.py b/tests/unit/formats/test_youtubevis.py new file mode 100644 index 0000000..b5218b7 --- /dev/null +++ b/tests/unit/formats/test_youtubevis.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path + +from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput +from labelformat.model.bounding_box import BoundingBox +from labelformat.model.category import Category +from labelformat.model.object_detection_track import ( + SingleObjectDetectionTrack, + VideoObjectDetectionTrack, +) +from labelformat.model.video import Video + + +class TestYouTubeVISObjectDetectionTrackInput: + def test_get_categories(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_json(tmp_path) + label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) + + assert list(label_input.get_categories()) == [Category(id=1, name="cat")] + + def test_get_videos(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_json(tmp_path) + label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) + + assert list(label_input.get_videos()) == [ + Video( + id=5, + filename="video1", + width=640, + height=480, + number_of_frames=2, + ) + ] + + def test_get_labels(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_json(tmp_path) + label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) + + assert list(label_input.get_labels()) == [ + VideoObjectDetectionTrack( + video=Video( + id=5, + filename="video1", + width=640, + height=480, + number_of_frames=2, + ), + objects=[ + SingleObjectDetectionTrack( + category=Category(id=1, name="cat"), + boxes=[ + BoundingBox( + xmin=10.0, + ymin=20.0, + xmax=40.0, + ymax=60.0, + ), + None, + ], + ) + ], + ) + ] + +def _write_youtube_vis_json(tmp_path: Path) -> Path: + data = { + "categories": [ + {"id": 1, "name": "cat"}, + ], + "videos": [ + { + "id": 5, + "file_names": ["video1/00000.jpg", "video1/00001.jpg"], + "width": 640, + "height": 480, + "length": 2, + } + ], + "annotations": [ + { + "video_id": 5, + "category_id": 1, + "bboxes": [ + [10.0, 20.0, 30.0, 40.0], + None, + ], + } + ], + } + input_file = tmp_path / "instances.json" + input_file.write_text(json.dumps(data)) + return input_file \ No newline at end of file From 1b4afd5527290b75751e6b57918ea5d7885b1edf Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Wed, 14 Jan 2026 15:13:54 +0100 Subject: [PATCH 03/20] Video support: Add youtubevis input format Adding the input format for youtube-vis. Note: This PR was inspired by #27 by @fardinayar --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index 36f601f..cc1923a 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.7.16 +3.8 From 198296851bc58d0678a48af6fbcb1f5bfefe6d86 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Wed, 14 Jan 2026 15:14:38 +0100 Subject: [PATCH 04/20] revert python version --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index cc1923a..36f601f 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.8 +3.7.16 From 3fc1180de5aa22ecf978253472220a7b8e2f3f51 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Wed, 14 Jan 2026 16:44:22 +0100 Subject: [PATCH 05/20] Instance Segmentation model --- .../model/instance_segmentation_track.py | 66 ++++++++++++++++++ .../model/test_instance_segmentation_track.py | 68 +++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/labelformat/model/instance_segmentation_track.py create mode 100644 tests/unit/model/test_instance_segmentation_track.py diff --git a/src/labelformat/model/instance_segmentation_track.py b/src/labelformat/model/instance_segmentation_track.py new file mode 100644 index 0000000..cb975e0 --- /dev/null +++ b/src/labelformat/model/instance_segmentation_track.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from argparse import ArgumentParser +from dataclasses import dataclass +from typing import Iterable + +from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation +from labelformat.model.category import Category +from labelformat.model.video import Video +from labelformat.model.multipolygon import MultiPolygon + + +@dataclass(frozen=True) +class SingleInstanceSegmentationTrack: + category: Category + segmentations: list[MultiPolygon | BinaryMaskSegmentation | None] + + +@dataclass(frozen=True) +class VideoInstanceSegmentationTrack: + """ + The base class for a video alongside with its object detection track annotations. + A video consists of N frames and M objects. Each object is defined by N boxes - one for each frame. + If an object is not present on a frame, the corresponding entry is set to None. + """ + + video: Video + objects: list[SingleInstanceSegmentationTrack] + + def __post_init__(self) -> None: + number_of_frames = self.video.number_of_frames + + for obj in self.objects: + if len(obj.segmentations) != number_of_frames: + raise ValueError( + "Length of instance segmentation track does not match the number of frames in the video." + ) + +class InstanceSegmentationTrackInput(ABC): + @staticmethod + @abstractmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + raise NotImplementedError() + + @abstractmethod + def get_categories(self) -> Iterable[Category]: + raise NotImplementedError() + + @abstractmethod + def get_videos(self) -> Iterable[Video]: + raise NotImplementedError() + + @abstractmethod + def get_labels(self) -> Iterable[VideoInstanceSegmentationTrack]: + raise NotImplementedError() + + +class InstanceSegmentationTrackOutput(ABC): + @staticmethod + @abstractmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + raise NotImplementedError() + + def save(self, label_input: InstanceSegmentationTrackInput) -> None: + raise NotImplementedError() diff --git a/tests/unit/model/test_instance_segmentation_track.py b/tests/unit/model/test_instance_segmentation_track.py new file mode 100644 index 0000000..dac742d --- /dev/null +++ b/tests/unit/model/test_instance_segmentation_track.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import pytest + +from labelformat.model.category import Category +from labelformat.model.instance_segmentation_track import ( + SingleInstanceSegmentationTrack, + VideoInstanceSegmentationTrack, +) +from labelformat.model.multipolygon import MultiPolygon +from labelformat.model.video import Video + + +class TestVideoInstanceSegmentationTrack: + def test_post_init__frames_equal_segmentations_length__valid(self) -> None: + track_a = SingleInstanceSegmentationTrack( + category=Category(id=0, name="cat"), + segmentations=[ + MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), + None, + ], + ) + + track_b = SingleInstanceSegmentationTrack( + category=Category(id=1, name="dog"), + segmentations=[ + MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), + MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), + ], + ) + + video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) + + instance_seg = VideoInstanceSegmentationTrack( + video=video, + objects=[track_a, track_b], + ) + assert len(instance_seg.objects) == 2 + assert len(instance_seg.objects[0].segmentations) == 2 + + def test_post_init__frames_equal_segmentations_length___invalid(self) -> None: + track_a = SingleInstanceSegmentationTrack( + category=Category(id=0, name="cat"), + segmentations=[ + MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), + None, + ], + ) + + track_b = SingleInstanceSegmentationTrack( + category=Category(id=1, name="dog"), + segmentations=[ + MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), + MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), + None, + ], + ) + + video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) + + with pytest.raises( + ValueError, + match="Length of instance segmentation track does not match the number of frames in the video.", + ): + VideoInstanceSegmentationTrack( + video=video, + objects=[track_a, track_b], + ) From 6fcc25fb025eac9a9c5df175b34962bc3e7283fa Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 13:44:37 +0100 Subject: [PATCH 06/20] format --- src/labelformat/model/instance_segmentation_track.py | 3 ++- tests/unit/formats/test_youtubevis.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/labelformat/model/instance_segmentation_track.py b/src/labelformat/model/instance_segmentation_track.py index cb975e0..4024b6b 100644 --- a/src/labelformat/model/instance_segmentation_track.py +++ b/src/labelformat/model/instance_segmentation_track.py @@ -7,8 +7,8 @@ from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation from labelformat.model.category import Category -from labelformat.model.video import Video from labelformat.model.multipolygon import MultiPolygon +from labelformat.model.video import Video @dataclass(frozen=True) @@ -37,6 +37,7 @@ def __post_init__(self) -> None: "Length of instance segmentation track does not match the number of frames in the video." ) + class InstanceSegmentationTrackInput(ABC): @staticmethod @abstractmethod diff --git a/tests/unit/formats/test_youtubevis.py b/tests/unit/formats/test_youtubevis.py index 40ac9cf..e176720 100644 --- a/tests/unit/formats/test_youtubevis.py +++ b/tests/unit/formats/test_youtubevis.py @@ -62,6 +62,7 @@ def test_get_labels(self, tmp_path: Path) -> None: ) ] + def _write_youtube_vis_json(input_file: Path) -> Path: data = { "categories": [ @@ -88,4 +89,4 @@ def _write_youtube_vis_json(input_file: Path) -> Path: ], } input_file.write_text(json.dumps(data)) - return input_file \ No newline at end of file + return input_file From d676ad94d1d3dce3638b9cf013c7a8a0b26963c8 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:27:39 +0100 Subject: [PATCH 07/20] Move required methods to helper --- src/labelformat/formats/coco.py | 57 ++++--------------- .../formats/coco_segmentation_helpers.py | 48 ++++++++++++++++ 2 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 src/labelformat/formats/coco_segmentation_helpers.py diff --git a/src/labelformat/formats/coco.py b/src/labelformat/formats/coco.py index cdbfd1e..07557b5 100644 --- a/src/labelformat/formats/coco.py +++ b/src/labelformat/formats/coco.py @@ -3,7 +3,7 @@ import json from argparse import ArgumentParser from pathlib import Path -from typing import Dict, Iterable, List, TypedDict +from typing import Dict, Iterable, List from labelformat.cli.registry import Task, cli_register from labelformat.model.binary_mask_segmentation import ( @@ -26,6 +26,12 @@ ObjectDetectionOutput, SingleObjectDetection, ) +from labelformat.formats.coco_segmentation_helpers import ( + coco_segmentation_to_binary_mask_rle, + coco_segmentation_to_multipolygon, + COCOInstanceSegmentationMultiPolygon, + COCOInstanceSegmentationRLE, +) from labelformat.types import JsonDict, ParseError @@ -60,14 +66,6 @@ def get_images(self) -> Iterable[Image]: ) -class _COCOInstanceSegmentationRLE(TypedDict): - counts: list[int] - size: list[int] - - -_COCOInstanceSegmentationMultiPolygon = List[List[float]] - - @cli_register(format="coco", task=Task.OBJECT_DETECTION) class COCOObjectDetectionInput(_COCOBaseInput, ObjectDetectionInput): def get_labels(self) -> Iterable[ImageObjectDetection]: @@ -119,11 +117,11 @@ def get_labels(self) -> Iterable[ImageInstanceSegmentation]: raise ParseError(f"Segmentation missing for image id {image_id}") segmentation: MultiPolygon | BinaryMaskSegmentation if ann["iscrowd"] == 1: - segmentation = _coco_segmentation_to_binary_mask_rle( + segmentation = coco_segmentation_to_binary_mask_rle( segmentation=ann["segmentation"], bbox=ann["bbox"] ) else: - segmentation = _coco_segmentation_to_multipolygon( + segmentation = coco_segmentation_to_multipolygon( coco_segmentation=ann["segmentation"] ) objects.append( @@ -189,7 +187,7 @@ def save(self, label_input: InstanceSegmentationInput) -> None: for label in label_input.get_labels(): for obj in label.objects: segmentation: ( - _COCOInstanceSegmentationMultiPolygon | _COCOInstanceSegmentationRLE + COCOInstanceSegmentationMultiPolygon | COCOInstanceSegmentationRLE ) if isinstance(obj.segmentation, BinaryMaskSegmentation): segmentation = _binary_mask_rle_to_coco_segmentation( @@ -231,28 +229,9 @@ def save(self, label_input: InstanceSegmentationInput) -> None: json.dump(data, file, indent=2) -def _coco_segmentation_to_multipolygon( - coco_segmentation: _COCOInstanceSegmentationMultiPolygon, -) -> MultiPolygon: - """Convert COCO segmentation to MultiPolygon.""" - polygons = [] - for polygon in coco_segmentation: - if len(polygon) % 2 != 0: - raise ParseError(f"Invalid polygon with {len(polygon)} points: {polygon}") - polygons.append( - list( - zip( - [float(x) for x in polygon[0::2]], - [float(x) for x in polygon[1::2]], - ) - ) - ) - return MultiPolygon(polygons=polygons) - - def _multipolygon_to_coco_segmentation( multipolygon: MultiPolygon, -) -> _COCOInstanceSegmentationMultiPolygon: +) -> COCOInstanceSegmentationMultiPolygon: """Convert MultiPolygon to COCO segmentation.""" coco_segmentation = [] for polygon in multipolygon.polygons: @@ -260,21 +239,9 @@ def _multipolygon_to_coco_segmentation( return coco_segmentation -def _coco_segmentation_to_binary_mask_rle( - segmentation: _COCOInstanceSegmentationRLE, bbox: list[float] -) -> BinaryMaskSegmentation: - counts = segmentation["counts"] - height, width = segmentation["size"] - binary_mask = RLEDecoderEncoder.decode_column_wise_rle(counts, height, width) - bounding_box = BoundingBox.from_format(bbox=bbox, format=BoundingBoxFormat.XYWH) - return BinaryMaskSegmentation.from_binary_mask( - binary_mask, bounding_box=bounding_box - ) - - def _binary_mask_rle_to_coco_segmentation( binary_mask_rle: BinaryMaskSegmentation, -) -> _COCOInstanceSegmentationRLE: +) -> COCOInstanceSegmentationRLE: binary_mask = binary_mask_rle.get_binary_mask() counts = RLEDecoderEncoder.encode_column_wise_rle(binary_mask) return {"counts": counts, "size": [binary_mask_rle.height, binary_mask_rle.width]} diff --git a/src/labelformat/formats/coco_segmentation_helpers.py b/src/labelformat/formats/coco_segmentation_helpers.py new file mode 100644 index 0000000..c7144ac --- /dev/null +++ b/src/labelformat/formats/coco_segmentation_helpers.py @@ -0,0 +1,48 @@ +from __future__ import annotations +from typing import List, TypedDict + +from labelformat.model.binary_mask_segmentation import ( + BinaryMaskSegmentation, + RLEDecoderEncoder, +) +from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat +from labelformat.model.multipolygon import MultiPolygon +from labelformat.types import ParseError + + +class COCOInstanceSegmentationRLE(TypedDict): + counts: list[int] + size: list[int] + + +COCOInstanceSegmentationMultiPolygon = List[List[float]] + +def coco_segmentation_to_binary_mask_rle( + segmentation: COCOInstanceSegmentationRLE, bbox: list[float] +) -> BinaryMaskSegmentation: + counts = segmentation["counts"] + height, width = segmentation["size"] + binary_mask = RLEDecoderEncoder.decode_column_wise_rle(counts, height, width) + bounding_box = BoundingBox.from_format(bbox=bbox, format=BoundingBoxFormat.XYWH) + return BinaryMaskSegmentation.from_binary_mask( + binary_mask, bounding_box=bounding_box + ) + + +def coco_segmentation_to_multipolygon( + coco_segmentation: COCOInstanceSegmentationMultiPolygon, +) -> MultiPolygon: + """Convert COCO segmentation to MultiPolygon.""" + polygons = [] + for polygon in coco_segmentation: + if len(polygon) % 2 != 0: + raise ParseError(f"Invalid polygon with {len(polygon)} points: {polygon}") + polygons.append( + list( + zip( + [float(x) for x in polygon[0::2]], + [float(x) for x in polygon[1::2]], + ) + ) + ) + return MultiPolygon(polygons=polygons) From 3054c0365771f5ba31b4ea5f127c4a3384d0f15a Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:30:40 +0100 Subject: [PATCH 08/20] remove unnecessary changes --- src/labelformat/formats/youtubevis.py | 93 --------------------------- tests/unit/formats/test_youtubevis.py | 92 -------------------------- 2 files changed, 185 deletions(-) delete mode 100644 src/labelformat/formats/youtubevis.py delete mode 100644 tests/unit/formats/test_youtubevis.py diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py deleted file mode 100644 index be8c273..0000000 --- a/src/labelformat/formats/youtubevis.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import json -from argparse import ArgumentParser -from pathlib import Path -from typing import Dict, Iterable, List - -from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat -from labelformat.model.category import Category -from labelformat.model.object_detection_track import ( - ObjectDetectionTrackInput, - SingleObjectDetectionTrack, - VideoObjectDetectionTrack, -) -from labelformat.model.video import Video -from labelformat.types import JsonDict - - -class YouTubeVISObjectDetectionTrackInput(ObjectDetectionTrackInput): - @staticmethod - def add_cli_arguments(parser: ArgumentParser) -> None: - parser.add_argument( - "--input-file", - type=Path, - required=True, - help="Path to input YouTube-VIS JSON file", - ) - - def __init__(self, input_file: Path) -> None: - with input_file.open() as file: - self._data = json.load(file) - - def get_categories(self) -> Iterable[Category]: - for category in self._data["categories"]: - yield Category( - id=category["id"], - name=category["name"], - ) - - def get_videos(self) -> Iterable[Video]: - for video in self._data["videos"]: - yield Video( - id=video["id"], - # TODO (Jonas, 1/2026): The file_names do not hold the video file extension. Solution required. - filename=Path(video["file_names"][0]).parent.name, - width=int(video["width"]), - height=int(video["height"]), - number_of_frames=int(video["length"]), - ) - - def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: - video_id_to_video = {video.id: video for video in self.get_videos()} - category_id_to_category = { - category.id: category for category in self.get_categories() - } - video_id_to_tracks: Dict[int, List[JsonDict]] = { - video_id: [] for video_id in video_id_to_video.keys() - } - for ann in self._data["annotations"]: - video_id_to_tracks[ann["video_id"]].append(ann) - - for video_id, tracks in video_id_to_tracks.items(): - video = video_id_to_video[video_id] - objects = [] - for track in tracks: - boxes = _get_object_track_boxes(ann=track) - objects.append( - SingleObjectDetectionTrack( - category=category_id_to_category[ann["category_id"]], - boxes=boxes, - ) - ) - yield VideoObjectDetectionTrack( - video=video, - objects=objects, - ) - - -def _get_object_track_boxes( - ann: JsonDict, -) -> list[BoundingBox | None]: - boxes: list[BoundingBox | None] = [] - for bbox in ann["bboxes"]: - if bbox is None or len(bbox) == 0: - boxes.append(None) - else: - boxes.append( - BoundingBox.from_format( - bbox=[float(x) for x in bbox], - format=BoundingBoxFormat.XYWH, - ) - ) - return boxes diff --git a/tests/unit/formats/test_youtubevis.py b/tests/unit/formats/test_youtubevis.py deleted file mode 100644 index e176720..0000000 --- a/tests/unit/formats/test_youtubevis.py +++ /dev/null @@ -1,92 +0,0 @@ -import json -from pathlib import Path - -from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput -from labelformat.model.bounding_box import BoundingBox -from labelformat.model.category import Category -from labelformat.model.object_detection_track import ( - SingleObjectDetectionTrack, - VideoObjectDetectionTrack, -) -from labelformat.model.video import Video - - -class TestYouTubeVISObjectDetectionTrackInput: - def test_get_categories(self, tmp_path: Path) -> None: - input_file = _write_youtube_vis_json(tmp_path / "instances.json") - label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) - - assert list(label_input.get_categories()) == [Category(id=1, name="cat")] - - def test_get_videos(self, tmp_path: Path) -> None: - input_file = _write_youtube_vis_json(tmp_path / "instances.json") - label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) - - assert list(label_input.get_videos()) == [ - Video( - id=5, - filename="video1", - width=640, - height=480, - number_of_frames=2, - ) - ] - - def test_get_labels(self, tmp_path: Path) -> None: - input_file = _write_youtube_vis_json(tmp_path / "instances.json") - label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) - - assert list(label_input.get_labels()) == [ - VideoObjectDetectionTrack( - video=Video( - id=5, - filename="video1", - width=640, - height=480, - number_of_frames=2, - ), - objects=[ - SingleObjectDetectionTrack( - category=Category(id=1, name="cat"), - boxes=[ - BoundingBox( - xmin=10.0, - ymin=20.0, - xmax=40.0, - ymax=60.0, - ), - None, - ], - ) - ], - ) - ] - - -def _write_youtube_vis_json(input_file: Path) -> Path: - data = { - "categories": [ - {"id": 1, "name": "cat"}, - ], - "videos": [ - { - "id": 5, - "file_names": ["video1/00000.jpg", "video1/00001.jpg"], - "width": 640, - "height": 480, - "length": 2, - } - ], - "annotations": [ - { - "video_id": 5, - "category_id": 1, - "bboxes": [ - [10.0, 20.0, 30.0, 40.0], - None, - ], - } - ], - } - input_file.write_text(json.dumps(data)) - return input_file From 148f769caf30fb521202972452b5c42497f2c506 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:33:24 +0100 Subject: [PATCH 09/20] update --- src/labelformat/formats/youtubevis.py | 93 +++++++++++++++++++ .../model/instance_segmentation_track.py | 67 ------------- tests/unit/formats/test_youtubevis.py | 92 ++++++++++++++++++ .../model/test_instance_segmentation_track.py | 68 -------------- 4 files changed, 185 insertions(+), 135 deletions(-) create mode 100644 src/labelformat/formats/youtubevis.py delete mode 100644 src/labelformat/model/instance_segmentation_track.py create mode 100644 tests/unit/formats/test_youtubevis.py delete mode 100644 tests/unit/model/test_instance_segmentation_track.py diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py new file mode 100644 index 0000000..be8c273 --- /dev/null +++ b/src/labelformat/formats/youtubevis.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import json +from argparse import ArgumentParser +from pathlib import Path +from typing import Dict, Iterable, List + +from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat +from labelformat.model.category import Category +from labelformat.model.object_detection_track import ( + ObjectDetectionTrackInput, + SingleObjectDetectionTrack, + VideoObjectDetectionTrack, +) +from labelformat.model.video import Video +from labelformat.types import JsonDict + + +class YouTubeVISObjectDetectionTrackInput(ObjectDetectionTrackInput): + @staticmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + parser.add_argument( + "--input-file", + type=Path, + required=True, + help="Path to input YouTube-VIS JSON file", + ) + + def __init__(self, input_file: Path) -> None: + with input_file.open() as file: + self._data = json.load(file) + + def get_categories(self) -> Iterable[Category]: + for category in self._data["categories"]: + yield Category( + id=category["id"], + name=category["name"], + ) + + def get_videos(self) -> Iterable[Video]: + for video in self._data["videos"]: + yield Video( + id=video["id"], + # TODO (Jonas, 1/2026): The file_names do not hold the video file extension. Solution required. + filename=Path(video["file_names"][0]).parent.name, + width=int(video["width"]), + height=int(video["height"]), + number_of_frames=int(video["length"]), + ) + + def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: + video_id_to_video = {video.id: video for video in self.get_videos()} + category_id_to_category = { + category.id: category for category in self.get_categories() + } + video_id_to_tracks: Dict[int, List[JsonDict]] = { + video_id: [] for video_id in video_id_to_video.keys() + } + for ann in self._data["annotations"]: + video_id_to_tracks[ann["video_id"]].append(ann) + + for video_id, tracks in video_id_to_tracks.items(): + video = video_id_to_video[video_id] + objects = [] + for track in tracks: + boxes = _get_object_track_boxes(ann=track) + objects.append( + SingleObjectDetectionTrack( + category=category_id_to_category[ann["category_id"]], + boxes=boxes, + ) + ) + yield VideoObjectDetectionTrack( + video=video, + objects=objects, + ) + + +def _get_object_track_boxes( + ann: JsonDict, +) -> list[BoundingBox | None]: + boxes: list[BoundingBox | None] = [] + for bbox in ann["bboxes"]: + if bbox is None or len(bbox) == 0: + boxes.append(None) + else: + boxes.append( + BoundingBox.from_format( + bbox=[float(x) for x in bbox], + format=BoundingBoxFormat.XYWH, + ) + ) + return boxes diff --git a/src/labelformat/model/instance_segmentation_track.py b/src/labelformat/model/instance_segmentation_track.py deleted file mode 100644 index 4024b6b..0000000 --- a/src/labelformat/model/instance_segmentation_track.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from argparse import ArgumentParser -from dataclasses import dataclass -from typing import Iterable - -from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation -from labelformat.model.category import Category -from labelformat.model.multipolygon import MultiPolygon -from labelformat.model.video import Video - - -@dataclass(frozen=True) -class SingleInstanceSegmentationTrack: - category: Category - segmentations: list[MultiPolygon | BinaryMaskSegmentation | None] - - -@dataclass(frozen=True) -class VideoInstanceSegmentationTrack: - """ - The base class for a video alongside with its object detection track annotations. - A video consists of N frames and M objects. Each object is defined by N boxes - one for each frame. - If an object is not present on a frame, the corresponding entry is set to None. - """ - - video: Video - objects: list[SingleInstanceSegmentationTrack] - - def __post_init__(self) -> None: - number_of_frames = self.video.number_of_frames - - for obj in self.objects: - if len(obj.segmentations) != number_of_frames: - raise ValueError( - "Length of instance segmentation track does not match the number of frames in the video." - ) - - -class InstanceSegmentationTrackInput(ABC): - @staticmethod - @abstractmethod - def add_cli_arguments(parser: ArgumentParser) -> None: - raise NotImplementedError() - - @abstractmethod - def get_categories(self) -> Iterable[Category]: - raise NotImplementedError() - - @abstractmethod - def get_videos(self) -> Iterable[Video]: - raise NotImplementedError() - - @abstractmethod - def get_labels(self) -> Iterable[VideoInstanceSegmentationTrack]: - raise NotImplementedError() - - -class InstanceSegmentationTrackOutput(ABC): - @staticmethod - @abstractmethod - def add_cli_arguments(parser: ArgumentParser) -> None: - raise NotImplementedError() - - def save(self, label_input: InstanceSegmentationTrackInput) -> None: - raise NotImplementedError() diff --git a/tests/unit/formats/test_youtubevis.py b/tests/unit/formats/test_youtubevis.py new file mode 100644 index 0000000..e176720 --- /dev/null +++ b/tests/unit/formats/test_youtubevis.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path + +from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput +from labelformat.model.bounding_box import BoundingBox +from labelformat.model.category import Category +from labelformat.model.object_detection_track import ( + SingleObjectDetectionTrack, + VideoObjectDetectionTrack, +) +from labelformat.model.video import Video + + +class TestYouTubeVISObjectDetectionTrackInput: + def test_get_categories(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_json(tmp_path / "instances.json") + label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) + + assert list(label_input.get_categories()) == [Category(id=1, name="cat")] + + def test_get_videos(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_json(tmp_path / "instances.json") + label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) + + assert list(label_input.get_videos()) == [ + Video( + id=5, + filename="video1", + width=640, + height=480, + number_of_frames=2, + ) + ] + + def test_get_labels(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_json(tmp_path / "instances.json") + label_input = YouTubeVISObjectDetectionTrackInput(input_file=input_file) + + assert list(label_input.get_labels()) == [ + VideoObjectDetectionTrack( + video=Video( + id=5, + filename="video1", + width=640, + height=480, + number_of_frames=2, + ), + objects=[ + SingleObjectDetectionTrack( + category=Category(id=1, name="cat"), + boxes=[ + BoundingBox( + xmin=10.0, + ymin=20.0, + xmax=40.0, + ymax=60.0, + ), + None, + ], + ) + ], + ) + ] + + +def _write_youtube_vis_json(input_file: Path) -> Path: + data = { + "categories": [ + {"id": 1, "name": "cat"}, + ], + "videos": [ + { + "id": 5, + "file_names": ["video1/00000.jpg", "video1/00001.jpg"], + "width": 640, + "height": 480, + "length": 2, + } + ], + "annotations": [ + { + "video_id": 5, + "category_id": 1, + "bboxes": [ + [10.0, 20.0, 30.0, 40.0], + None, + ], + } + ], + } + input_file.write_text(json.dumps(data)) + return input_file diff --git a/tests/unit/model/test_instance_segmentation_track.py b/tests/unit/model/test_instance_segmentation_track.py deleted file mode 100644 index dac742d..0000000 --- a/tests/unit/model/test_instance_segmentation_track.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import pytest - -from labelformat.model.category import Category -from labelformat.model.instance_segmentation_track import ( - SingleInstanceSegmentationTrack, - VideoInstanceSegmentationTrack, -) -from labelformat.model.multipolygon import MultiPolygon -from labelformat.model.video import Video - - -class TestVideoInstanceSegmentationTrack: - def test_post_init__frames_equal_segmentations_length__valid(self) -> None: - track_a = SingleInstanceSegmentationTrack( - category=Category(id=0, name="cat"), - segmentations=[ - MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), - None, - ], - ) - - track_b = SingleInstanceSegmentationTrack( - category=Category(id=1, name="dog"), - segmentations=[ - MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), - MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), - ], - ) - - video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) - - instance_seg = VideoInstanceSegmentationTrack( - video=video, - objects=[track_a, track_b], - ) - assert len(instance_seg.objects) == 2 - assert len(instance_seg.objects[0].segmentations) == 2 - - def test_post_init__frames_equal_segmentations_length___invalid(self) -> None: - track_a = SingleInstanceSegmentationTrack( - category=Category(id=0, name="cat"), - segmentations=[ - MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), - None, - ], - ) - - track_b = SingleInstanceSegmentationTrack( - category=Category(id=1, name="dog"), - segmentations=[ - MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), - MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), - None, - ], - ) - - video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) - - with pytest.raises( - ValueError, - match="Length of instance segmentation track does not match the number of frames in the video.", - ): - VideoInstanceSegmentationTrack( - video=video, - objects=[track_a, track_b], - ) From 214fb5383df5559ee82a556eb9fc4f2df9b1a87c Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:35:07 +0100 Subject: [PATCH 10/20] format --- src/labelformat/formats/coco.py | 12 ++++++------ src/labelformat/formats/coco_segmentation_helpers.py | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/labelformat/formats/coco.py b/src/labelformat/formats/coco.py index 07557b5..694de15 100644 --- a/src/labelformat/formats/coco.py +++ b/src/labelformat/formats/coco.py @@ -6,6 +6,12 @@ from typing import Dict, Iterable, List from labelformat.cli.registry import Task, cli_register +from labelformat.formats.coco_segmentation_helpers import ( + COCOInstanceSegmentationMultiPolygon, + COCOInstanceSegmentationRLE, + coco_segmentation_to_binary_mask_rle, + coco_segmentation_to_multipolygon, +) from labelformat.model.binary_mask_segmentation import ( BinaryMaskSegmentation, RLEDecoderEncoder, @@ -26,12 +32,6 @@ ObjectDetectionOutput, SingleObjectDetection, ) -from labelformat.formats.coco_segmentation_helpers import ( - coco_segmentation_to_binary_mask_rle, - coco_segmentation_to_multipolygon, - COCOInstanceSegmentationMultiPolygon, - COCOInstanceSegmentationRLE, -) from labelformat.types import JsonDict, ParseError diff --git a/src/labelformat/formats/coco_segmentation_helpers.py b/src/labelformat/formats/coco_segmentation_helpers.py index c7144ac..dd59309 100644 --- a/src/labelformat/formats/coco_segmentation_helpers.py +++ b/src/labelformat/formats/coco_segmentation_helpers.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import List, TypedDict from labelformat.model.binary_mask_segmentation import ( @@ -17,6 +18,7 @@ class COCOInstanceSegmentationRLE(TypedDict): COCOInstanceSegmentationMultiPolygon = List[List[float]] + def coco_segmentation_to_binary_mask_rle( segmentation: COCOInstanceSegmentationRLE, bbox: list[float] ) -> BinaryMaskSegmentation: From a27ebff67f7808ab60b03436890885a80a0c2a28 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:38:02 +0100 Subject: [PATCH 11/20] Add instance seg to youtubevis --- ...k_test_youtubevis_instance_segmentation.py | 25 ++++++ src/labelformat/formats/__init__.py | 6 +- src/labelformat/formats/youtubevis.py | 70 +++++++++++++++- tests/unit/formats/test_youtubevis.py | 79 +++++++++++++++++++ 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 quick_test_youtubevis_instance_segmentation.py diff --git a/quick_test_youtubevis_instance_segmentation.py b/quick_test_youtubevis_instance_segmentation.py new file mode 100644 index 0000000..b5e860a --- /dev/null +++ b/quick_test_youtubevis_instance_segmentation.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pathlib import Path + +from labelformat.formats.youtubevis import YouTubeVISInstanceSegmentationTrackInput + +# Update this path to your YouTube-VIS JSON file. +INPUT_PATH = Path("/Users/jonaswurst/Lightly/dataset_examples/youtube_vis_50_videos/train/instances_50.json") + + +def main() -> None: + label_input = YouTubeVISInstanceSegmentationTrackInput(input_file=INPUT_PATH) + videos = list(label_input.get_videos()) + labels = list(label_input.get_labels()) + print(f"videos={len(videos)} labels={len(labels)}") + if labels: + first = labels[0] + print(f"first video id={first.video.id} objects={len(first.objects)}") + if first.objects: + segs = first.objects[0].segmentations + print(f"first track frames={len(segs)}") + + +if __name__ == "__main__": + main() diff --git a/src/labelformat/formats/__init__.py b/src/labelformat/formats/__init__.py index 76a513c..0f20bd5 100644 --- a/src/labelformat/formats/__init__.py +++ b/src/labelformat/formats/__init__.py @@ -65,7 +65,10 @@ YOLOv26ObjectDetectionInput, YOLOv26ObjectDetectionOutput, ) -from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput +from labelformat.formats.youtubevis import ( + YouTubeVISInstanceSegmentationTrackInput, + YouTubeVISObjectDetectionTrackInput, +) __all__ = [ "COCOInstanceSegmentationInput", @@ -106,5 +109,6 @@ "YOLOv26ObjectDetectionInput", "YOLOv26ObjectDetectionOutput", "MaskPairInstanceSegmentationInput", + "YouTubeVISInstanceSegmentationTrackInput", "YouTubeVISObjectDetectionTrackInput", ] diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index be8c273..7a2f576 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -5,8 +5,19 @@ from pathlib import Path from typing import Dict, Iterable, List +from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat from labelformat.model.category import Category +from labelformat.formats.coco_segmentation_helpers import ( + coco_segmentation_to_binary_mask_rle, + coco_segmentation_to_multipolygon, +) +from labelformat.model.instance_segmentation_track import ( + InstanceSegmentationTrackInput, + SingleInstanceSegmentationTrack, + VideoInstanceSegmentationTrack, +) +from labelformat.model.multipolygon import MultiPolygon from labelformat.model.object_detection_track import ( ObjectDetectionTrackInput, SingleObjectDetectionTrack, @@ -16,7 +27,7 @@ from labelformat.types import JsonDict -class YouTubeVISObjectDetectionTrackInput(ObjectDetectionTrackInput): +class _YouTubeVISBaseInput: @staticmethod def add_cli_arguments(parser: ArgumentParser) -> None: parser.add_argument( @@ -48,6 +59,8 @@ def get_videos(self) -> Iterable[Video]: number_of_frames=int(video["length"]), ) + +class YouTubeVISObjectDetectionTrackInput(_YouTubeVISBaseInput, ObjectDetectionTrackInput): def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: video_id_to_video = {video.id: video for video in self.get_videos()} category_id_to_category = { @@ -66,7 +79,7 @@ def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: boxes = _get_object_track_boxes(ann=track) objects.append( SingleObjectDetectionTrack( - category=category_id_to_category[ann["category_id"]], + category=category_id_to_category[track["category_id"]], boxes=boxes, ) ) @@ -76,6 +89,37 @@ def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: ) +class YouTubeVISInstanceSegmentationTrackInput( + _YouTubeVISBaseInput, InstanceSegmentationTrackInput +): + def get_labels(self) -> Iterable[VideoInstanceSegmentationTrack]: + video_id_to_video = {video.id: video for video in self.get_videos()} + category_id_to_category = { + category.id: category for category in self.get_categories() + } + video_id_to_tracks: Dict[int, List[JsonDict]] = { + video_id: [] for video_id in video_id_to_video.keys() + } + for ann in self._data["annotations"]: + video_id_to_tracks[ann["video_id"]].append(ann) + + for video_id, tracks in video_id_to_tracks.items(): + video = video_id_to_video[video_id] + objects = [] + for track in tracks: + segmentations = _get_object_track_segmentations(ann=track) + objects.append( + SingleInstanceSegmentationTrack( + category=category_id_to_category[track["category_id"]], + segmentations=segmentations, + ) + ) + yield VideoInstanceSegmentationTrack( + video=video, + objects=objects, + ) + + def _get_object_track_boxes( ann: JsonDict, ) -> list[BoundingBox | None]: @@ -91,3 +135,25 @@ def _get_object_track_boxes( ) ) return boxes + + +def _get_object_track_segmentations( + ann: JsonDict, +) -> list[MultiPolygon | BinaryMaskSegmentation | None]: + segmentations: list[MultiPolygon | BinaryMaskSegmentation | None] = [] + bboxes = ann["bboxes"] + for index, segmentation in enumerate(ann["segmentations"]): + if segmentation is None or len(segmentation) == 0: + segmentations.append(None) + continue + if isinstance(segmentation, dict): + segmentations.append( + coco_segmentation_to_binary_mask_rle(segmentation=segmentation, bbox=bboxes[index]) + ) + elif isinstance(segmentation, list): + segmentations.append( + coco_segmentation_to_multipolygon(coco_segmentation=segmentation) + ) + return segmentations + + diff --git a/tests/unit/formats/test_youtubevis.py b/tests/unit/formats/test_youtubevis.py index e176720..787994c 100644 --- a/tests/unit/formats/test_youtubevis.py +++ b/tests/unit/formats/test_youtubevis.py @@ -1,9 +1,20 @@ import json from pathlib import Path +import numpy as np + from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput +from labelformat.formats.youtubevis import YouTubeVISInstanceSegmentationTrackInput +from labelformat.model.binary_mask_segmentation import ( + BinaryMaskSegmentation, + RLEDecoderEncoder, +) from labelformat.model.bounding_box import BoundingBox from labelformat.model.category import Category +from labelformat.model.instance_segmentation_track import ( + SingleInstanceSegmentationTrack, + VideoInstanceSegmentationTrack, +) from labelformat.model.object_detection_track import ( SingleObjectDetectionTrack, VideoObjectDetectionTrack, @@ -63,6 +74,39 @@ def test_get_labels(self, tmp_path: Path) -> None: ] +class TestYouTubeVISInstanceSegmentationTrackInput: + def test_get_labels(self, tmp_path: Path) -> None: + input_file = _write_youtube_vis_instance_segmentation_json( + tmp_path / "instances.json" + ) + label_input = YouTubeVISInstanceSegmentationTrackInput(input_file=input_file) + + binary_mask = np.array([[0, 1, 1], [0, 1, 1]], dtype=int) + bounding_box = BoundingBox(xmin=1.0, ymin=0.0, xmax=3.0, ymax=2.0) + expected_segmentation = BinaryMaskSegmentation.from_binary_mask( + binary_mask=binary_mask, + bounding_box=bounding_box, + ) + + assert list(label_input.get_labels()) == [ + VideoInstanceSegmentationTrack( + video=Video( + id=5, + filename="video1", + width=3, + height=2, + number_of_frames=2, + ), + objects=[ + SingleInstanceSegmentationTrack( + category=Category(id=1, name="cat"), + segmentations=[expected_segmentation, None], + ) + ], + ) + ] + + def _write_youtube_vis_json(input_file: Path) -> Path: data = { "categories": [ @@ -90,3 +134,38 @@ def _write_youtube_vis_json(input_file: Path) -> Path: } input_file.write_text(json.dumps(data)) return input_file + + +def _write_youtube_vis_instance_segmentation_json(input_file: Path) -> Path: + binary_mask = np.array([[0, 1, 1], [0, 1, 1]], dtype=int) + counts = RLEDecoderEncoder.encode_column_wise_rle(binary_mask) + data = { + "categories": [ + {"id": 1, "name": "cat"}, + ], + "videos": [ + { + "id": 5, + "file_names": ["video1/00000.jpg", "video1/00001.jpg"], + "width": 3, + "height": 2, + "length": 2, + } + ], + "annotations": [ + { + "video_id": 5, + "category_id": 1, + "bboxes": [ + [1.0, 0.0, 2.0, 2.0], + None, + ], + "segmentations": [ + {"counts": counts, "size": [2, 3]}, + None, + ], + } + ], + } + input_file.write_text(json.dumps(data)) + return input_file From 318914ec52929ec9ebfd62f4eb04d319370d51d7 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:38:42 +0100 Subject: [PATCH 12/20] remove test file --- ...k_test_youtubevis_instance_segmentation.py | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 quick_test_youtubevis_instance_segmentation.py diff --git a/quick_test_youtubevis_instance_segmentation.py b/quick_test_youtubevis_instance_segmentation.py deleted file mode 100644 index b5e860a..0000000 --- a/quick_test_youtubevis_instance_segmentation.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from labelformat.formats.youtubevis import YouTubeVISInstanceSegmentationTrackInput - -# Update this path to your YouTube-VIS JSON file. -INPUT_PATH = Path("/Users/jonaswurst/Lightly/dataset_examples/youtube_vis_50_videos/train/instances_50.json") - - -def main() -> None: - label_input = YouTubeVISInstanceSegmentationTrackInput(input_file=INPUT_PATH) - videos = list(label_input.get_videos()) - labels = list(label_input.get_labels()) - print(f"videos={len(videos)} labels={len(labels)}") - if labels: - first = labels[0] - print(f"first video id={first.video.id} objects={len(first.objects)}") - if first.objects: - segs = first.objects[0].segmentations - print(f"first track frames={len(segs)}") - - -if __name__ == "__main__": - main() From 7fe2b9643627320e086be30aa93cec54c3fe5743 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:45:02 +0100 Subject: [PATCH 13/20] adding model def --- .../model/instance_segmentation_track.py | 67 ++++++++++++++++++ .../model/test_instance_segmentation_track.py | 68 +++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/labelformat/model/instance_segmentation_track.py create mode 100644 tests/unit/model/test_instance_segmentation_track.py diff --git a/src/labelformat/model/instance_segmentation_track.py b/src/labelformat/model/instance_segmentation_track.py new file mode 100644 index 0000000..4024b6b --- /dev/null +++ b/src/labelformat/model/instance_segmentation_track.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from argparse import ArgumentParser +from dataclasses import dataclass +from typing import Iterable + +from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation +from labelformat.model.category import Category +from labelformat.model.multipolygon import MultiPolygon +from labelformat.model.video import Video + + +@dataclass(frozen=True) +class SingleInstanceSegmentationTrack: + category: Category + segmentations: list[MultiPolygon | BinaryMaskSegmentation | None] + + +@dataclass(frozen=True) +class VideoInstanceSegmentationTrack: + """ + The base class for a video alongside with its object detection track annotations. + A video consists of N frames and M objects. Each object is defined by N boxes - one for each frame. + If an object is not present on a frame, the corresponding entry is set to None. + """ + + video: Video + objects: list[SingleInstanceSegmentationTrack] + + def __post_init__(self) -> None: + number_of_frames = self.video.number_of_frames + + for obj in self.objects: + if len(obj.segmentations) != number_of_frames: + raise ValueError( + "Length of instance segmentation track does not match the number of frames in the video." + ) + + +class InstanceSegmentationTrackInput(ABC): + @staticmethod + @abstractmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + raise NotImplementedError() + + @abstractmethod + def get_categories(self) -> Iterable[Category]: + raise NotImplementedError() + + @abstractmethod + def get_videos(self) -> Iterable[Video]: + raise NotImplementedError() + + @abstractmethod + def get_labels(self) -> Iterable[VideoInstanceSegmentationTrack]: + raise NotImplementedError() + + +class InstanceSegmentationTrackOutput(ABC): + @staticmethod + @abstractmethod + def add_cli_arguments(parser: ArgumentParser) -> None: + raise NotImplementedError() + + def save(self, label_input: InstanceSegmentationTrackInput) -> None: + raise NotImplementedError() diff --git a/tests/unit/model/test_instance_segmentation_track.py b/tests/unit/model/test_instance_segmentation_track.py new file mode 100644 index 0000000..dac742d --- /dev/null +++ b/tests/unit/model/test_instance_segmentation_track.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import pytest + +from labelformat.model.category import Category +from labelformat.model.instance_segmentation_track import ( + SingleInstanceSegmentationTrack, + VideoInstanceSegmentationTrack, +) +from labelformat.model.multipolygon import MultiPolygon +from labelformat.model.video import Video + + +class TestVideoInstanceSegmentationTrack: + def test_post_init__frames_equal_segmentations_length__valid(self) -> None: + track_a = SingleInstanceSegmentationTrack( + category=Category(id=0, name="cat"), + segmentations=[ + MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), + None, + ], + ) + + track_b = SingleInstanceSegmentationTrack( + category=Category(id=1, name="dog"), + segmentations=[ + MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), + MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), + ], + ) + + video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) + + instance_seg = VideoInstanceSegmentationTrack( + video=video, + objects=[track_a, track_b], + ) + assert len(instance_seg.objects) == 2 + assert len(instance_seg.objects[0].segmentations) == 2 + + def test_post_init__frames_equal_segmentations_length___invalid(self) -> None: + track_a = SingleInstanceSegmentationTrack( + category=Category(id=0, name="cat"), + segmentations=[ + MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), + None, + ], + ) + + track_b = SingleInstanceSegmentationTrack( + category=Category(id=1, name="dog"), + segmentations=[ + MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), + MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), + None, + ], + ) + + video = Video(id=0, filename="test.mov", width=1, height=1, number_of_frames=2) + + with pytest.raises( + ValueError, + match="Length of instance segmentation track does not match the number of frames in the video.", + ): + VideoInstanceSegmentationTrack( + video=video, + objects=[track_a, track_b], + ) From 2742245bbcb93c7d9bf7dc5b55ad94d5d93a88d7 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:45:28 +0100 Subject: [PATCH 14/20] format --- src/labelformat/formats/youtubevis.py | 16 +++++++++------- tests/unit/formats/test_youtubevis.py | 6 ++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index 7a2f576..d64c24b 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -5,13 +5,13 @@ from pathlib import Path from typing import Dict, Iterable, List -from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation -from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat -from labelformat.model.category import Category from labelformat.formats.coco_segmentation_helpers import ( coco_segmentation_to_binary_mask_rle, coco_segmentation_to_multipolygon, ) +from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation +from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat +from labelformat.model.category import Category from labelformat.model.instance_segmentation_track import ( InstanceSegmentationTrackInput, SingleInstanceSegmentationTrack, @@ -60,7 +60,9 @@ def get_videos(self) -> Iterable[Video]: ) -class YouTubeVISObjectDetectionTrackInput(_YouTubeVISBaseInput, ObjectDetectionTrackInput): +class YouTubeVISObjectDetectionTrackInput( + _YouTubeVISBaseInput, ObjectDetectionTrackInput +): def get_labels(self) -> Iterable[VideoObjectDetectionTrack]: video_id_to_video = {video.id: video for video in self.get_videos()} category_id_to_category = { @@ -148,12 +150,12 @@ def _get_object_track_segmentations( continue if isinstance(segmentation, dict): segmentations.append( - coco_segmentation_to_binary_mask_rle(segmentation=segmentation, bbox=bboxes[index]) + coco_segmentation_to_binary_mask_rle( + segmentation=segmentation, bbox=bboxes[index] + ) ) elif isinstance(segmentation, list): segmentations.append( coco_segmentation_to_multipolygon(coco_segmentation=segmentation) ) return segmentations - - diff --git a/tests/unit/formats/test_youtubevis.py b/tests/unit/formats/test_youtubevis.py index 787994c..27f63fe 100644 --- a/tests/unit/formats/test_youtubevis.py +++ b/tests/unit/formats/test_youtubevis.py @@ -3,8 +3,10 @@ import numpy as np -from labelformat.formats.youtubevis import YouTubeVISObjectDetectionTrackInput -from labelformat.formats.youtubevis import YouTubeVISInstanceSegmentationTrackInput +from labelformat.formats.youtubevis import ( + YouTubeVISInstanceSegmentationTrackInput, + YouTubeVISObjectDetectionTrackInput, +) from labelformat.model.binary_mask_segmentation import ( BinaryMaskSegmentation, RLEDecoderEncoder, From 02dcd6a04e9569789a2a05646d2efdadb1c0331f Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Fri, 16 Jan 2026 14:50:05 +0100 Subject: [PATCH 15/20] update test --- tests/unit/model/test_instance_segmentation_track.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/unit/model/test_instance_segmentation_track.py b/tests/unit/model/test_instance_segmentation_track.py index dac742d..d73d0c0 100644 --- a/tests/unit/model/test_instance_segmentation_track.py +++ b/tests/unit/model/test_instance_segmentation_track.py @@ -44,14 +44,6 @@ def test_post_init__frames_equal_segmentations_length___invalid(self) -> None: segmentations=[ MultiPolygon(polygons=[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]), None, - ], - ) - - track_b = SingleInstanceSegmentationTrack( - category=Category(id=1, name="dog"), - segmentations=[ - MultiPolygon(polygons=[[(2.0, 2.0), (3.0, 2.0), (3.0, 3.0)]]), - MultiPolygon(polygons=[[(4.0, 4.0), (5.0, 4.0), (5.0, 5.0)]]), None, ], ) @@ -64,5 +56,5 @@ def test_post_init__frames_equal_segmentations_length___invalid(self) -> None: ): VideoInstanceSegmentationTrack( video=video, - objects=[track_a, track_b], + objects=[track_a], ) From 63cdb4c6060eb37bba4dcf7bb190e79701746863 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Mon, 19 Jan 2026 11:37:15 +0100 Subject: [PATCH 16/20] Do not import func --- src/labelformat/formats/youtubevis.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index d64c24b..45db5c1 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -5,10 +5,7 @@ from pathlib import Path from typing import Dict, Iterable, List -from labelformat.formats.coco_segmentation_helpers import ( - coco_segmentation_to_binary_mask_rle, - coco_segmentation_to_multipolygon, -) +import labelformat.formats.coco_segmentation_helpers as segmentation_helpers from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat from labelformat.model.category import Category @@ -150,12 +147,12 @@ def _get_object_track_segmentations( continue if isinstance(segmentation, dict): segmentations.append( - coco_segmentation_to_binary_mask_rle( + segmentation_helpers.coco_segmentation_to_binary_mask_rle( segmentation=segmentation, bbox=bboxes[index] ) ) elif isinstance(segmentation, list): segmentations.append( - coco_segmentation_to_multipolygon(coco_segmentation=segmentation) + segmentation_helpers.coco_segmentation_to_multipolygon(coco_segmentation=segmentation) ) return segmentations From 2e8f5212bab90a76ddc8c29b6423328c702c1316 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Mon, 19 Jan 2026 11:40:56 +0100 Subject: [PATCH 17/20] format --- src/labelformat/formats/youtubevis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index 45db5c1..70d8461 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -153,6 +153,8 @@ def _get_object_track_segmentations( ) elif isinstance(segmentation, list): segmentations.append( - segmentation_helpers.coco_segmentation_to_multipolygon(coco_segmentation=segmentation) + segmentation_helpers.coco_segmentation_to_multipolygon( + coco_segmentation=segmentation + ) ) return segmentations From 36fdd1352ebe0682373b9cb5e4d8481b931b78d6 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Mon, 19 Jan 2026 11:50:45 +0100 Subject: [PATCH 18/20] fix typing --- src/labelformat/formats/youtubevis.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index 70d8461..89fa9d3 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -6,6 +6,10 @@ from typing import Dict, Iterable, List import labelformat.formats.coco_segmentation_helpers as segmentation_helpers +from labelformat.formats.coco_segmentation_helpers import ( + COCOInstanceSegmentationMultiPolygon, + COCOInstanceSegmentationRLE, +) from labelformat.model.binary_mask_segmentation import BinaryMaskSegmentation from labelformat.model.bounding_box import BoundingBox, BoundingBoxFormat from labelformat.model.category import Category @@ -146,15 +150,17 @@ def _get_object_track_segmentations( segmentations.append(None) continue if isinstance(segmentation, dict): + segmentation_rle: COCOInstanceSegmentationRLE = segmentation segmentations.append( segmentation_helpers.coco_segmentation_to_binary_mask_rle( - segmentation=segmentation, bbox=bboxes[index] + segmentation=segmentation_rle, bbox=bboxes[index] ) ) elif isinstance(segmentation, list): + segmentation_multipolygon: COCOInstanceSegmentationMultiPolygon = segmentation segmentations.append( segmentation_helpers.coco_segmentation_to_multipolygon( - coco_segmentation=segmentation + coco_segmentation=segmentation_multipolygon ) ) return segmentations From f04a0d6fd958dfb8c3c65f24d6d60d9e79040a17 Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Mon, 19 Jan 2026 11:51:51 +0100 Subject: [PATCH 19/20] format --- src/labelformat/formats/youtubevis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index 89fa9d3..67c2d50 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -157,7 +157,9 @@ def _get_object_track_segmentations( ) ) elif isinstance(segmentation, list): - segmentation_multipolygon: COCOInstanceSegmentationMultiPolygon = segmentation + segmentation_multipolygon: COCOInstanceSegmentationMultiPolygon = ( + segmentation + ) segmentations.append( segmentation_helpers.coco_segmentation_to_multipolygon( coco_segmentation=segmentation_multipolygon From 7b637072ff247d990f890777b6db67307b3a65fc Mon Sep 17 00:00:00 2001 From: JonasWurst Date: Mon, 19 Jan 2026 14:52:13 +0100 Subject: [PATCH 20/20] typing --- src/labelformat/formats/youtubevis.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/labelformat/formats/youtubevis.py b/src/labelformat/formats/youtubevis.py index 67c2d50..7dc6bbd 100644 --- a/src/labelformat/formats/youtubevis.py +++ b/src/labelformat/formats/youtubevis.py @@ -3,7 +3,7 @@ import json from argparse import ArgumentParser from pathlib import Path -from typing import Dict, Iterable, List +from typing import Dict, Iterable, List, cast import labelformat.formats.coco_segmentation_helpers as segmentation_helpers from labelformat.formats.coco_segmentation_helpers import ( @@ -150,19 +150,17 @@ def _get_object_track_segmentations( segmentations.append(None) continue if isinstance(segmentation, dict): - segmentation_rle: COCOInstanceSegmentationRLE = segmentation + segmentation_rle = cast(COCOInstanceSegmentationRLE, segmentation) segmentations.append( segmentation_helpers.coco_segmentation_to_binary_mask_rle( segmentation=segmentation_rle, bbox=bboxes[index] ) ) elif isinstance(segmentation, list): - segmentation_multipolygon: COCOInstanceSegmentationMultiPolygon = ( - segmentation - ) + segmentation_mp = cast(COCOInstanceSegmentationMultiPolygon, segmentation) segmentations.append( segmentation_helpers.coco_segmentation_to_multipolygon( - coco_segmentation=segmentation_multipolygon + coco_segmentation=segmentation_mp ) ) return segmentations