Skip to content

Commit 7ef43ad

Browse files
Store semantic segmentation as run-length encoding. (#53)
1 parent b896cbe commit 7ef43ad

File tree

6 files changed

+68
-9
lines changed

6 files changed

+68
-9
lines changed

.github/pull_request_template.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## What has changed and why?
2+
3+
(Delete this: Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.)
4+
5+
## How has it been tested?
6+
7+
(Delete this: Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.)

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.7.16
1+
3.8

src/labelformat/formats/semantic_segmentation/pascalvoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def get_mask(self, image_filepath: str) -> SemanticSegmentationMask:
114114
valid_class_ids={c.id for c in self._categories},
115115
)
116116

117-
return SemanticSegmentationMask(array=mask_np)
117+
return SemanticSegmentationMask.from_array(array=mask_np)
118118

119119

120120
def _validate_mask(

src/labelformat/model/semantic_segmentation.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import List, Optional, Tuple
4+
35
"""Semantic segmentation core types and input interface.
46
"""
57

@@ -24,12 +26,36 @@ class SemanticSegmentationMask:
2426
array: The 2D numpy array with integer class IDs of shape (H, W).
2527
"""
2628

27-
array: NDArray[np.int_]
29+
category_id_rle: List[Tuple[int, int]]
30+
"""The mask as a run-length encoding (RLE) list of (category_id, run_length) tuples."""
31+
width: int
32+
height: int
2833

29-
def __post_init__(self) -> None:
30-
if self.array.ndim != 2:
34+
@classmethod
35+
def from_array(cls, array: NDArray[np.int_]) -> "SemanticSegmentationMask":
36+
"""Create a SemanticSegmentationMask from a 2D numpy array."""
37+
if array.ndim != 2:
3138
raise ValueError("SemSegMask.array must be 2D with shape (H, W).")
3239

40+
category_id_rle: List[Tuple[int, int]] = []
41+
42+
cur_cat_id: Optional[int] = None
43+
cur_run_length = 0
44+
for cat_id in array.flatten():
45+
if cat_id == cur_cat_id:
46+
cur_run_length += 1
47+
else:
48+
if cur_cat_id is not None:
49+
category_id_rle.append((cur_cat_id, cur_run_length))
50+
cur_cat_id = cat_id
51+
cur_run_length = 1
52+
if cur_cat_id is not None:
53+
category_id_rle.append((cur_cat_id, cur_run_length))
54+
55+
return cls(
56+
category_id_rle=category_id_rle, width=array.shape[1], height=array.shape[0]
57+
)
58+
3359

3460
class SemanticSegmentationInput(ABC):
3561

tests/unit/formats/semantic_segmentation/test_pascalvoc.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,16 @@ def test_from_dirs__builds_categories_and_images(self) -> None:
4545
filenames = {img.filename for img in imgs}
4646
assert filenames == {"2007_000032.jpg", "subdir/2007_000033.jpg"}
4747

48-
def test_get_mask__returns_int2d_and_matches_image_shape(self) -> None:
48+
def test_get_mask__returns_rle_and_matches_image_length(self) -> None:
4949
mapping = _load_class_mapping_int_keys()
5050
ds = PascalVOCSemanticSegmentationInput.from_dirs(
5151
images_dir=IMAGES_DIR, masks_dir=MASKS_DIR, class_id_to_name=mapping
5252
)
5353

5454
for img in ds.get_images():
5555
mask = ds.get_mask(img.filename)
56-
assert mask.array.ndim == 2
57-
assert np.issubdtype(mask.array.dtype, np.integer)
58-
assert mask.array.shape == (img.height, img.width)
56+
length = sum(run_length for _, run_length in mask.category_id_rle)
57+
assert length == img.width * img.height
5958

6059
def test_from_dirs__missing_mask_raises(self, tmp_path: Path) -> None:
6160
masks_tmp = tmp_path / "SegmentationClass"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
5+
from labelformat.model.semantic_segmentation import SemanticSegmentationMask
6+
7+
8+
class TestSemanticSegmentationMask:
9+
def test_from_array(self) -> None:
10+
array = np.array(
11+
[
12+
[1, 1, 2, 2],
13+
[2, 1, 1, 1],
14+
[3, 3, 3, 3],
15+
],
16+
dtype=np.int_,
17+
)
18+
expected_rle = [
19+
(1, 2),
20+
(2, 3),
21+
(1, 3),
22+
(3, 4),
23+
]
24+
mask = SemanticSegmentationMask.from_array(array=array)
25+
assert mask.category_id_rle == expected_rle
26+
assert mask.width == 4
27+
assert mask.height == 3

0 commit comments

Comments
 (0)