Back to report index

iolabs-common (shared config layer) a733b88: AI3D-382 Derive ColorIntensityData mask and append from its fields

Miroslav Simko <ms@iolabs.ch> 2026-09-01T14:55:33+02:00

Commit #88 ยท 6 snippets

 src/iolabs/common/color_intensity_data.py | 36 ++++++++++++--------------
 tests/test_color_intensity_data.py        | 43 +++++++++++++++++++++++++++++++
 2 files changed, 60 insertions(+), 19 deletions(-)
Importance #1: src/iolabs/common/color_intensity_data.py @@ -1,7 +1,13 @@
1"""Color, intensity, scan angle, and return-count data aligned with point clouds."""1"""Color, intensity, scan angle, and return-count data aligned with point clouds.
22
3from dataclasses import dataclass3Both transforms below (:meth:`ColorIntensityData.select_by_mask` and
4:meth:`ColorIntensityData.append`) walk :func:`dataclasses.fields`, so a new
5per-point attribute is carried through them by declaring the field alone.
6"""
7
8from collections.abc import Callable
9from dataclasses import dataclass, fields
410
5import numpy as np11import numpy as np
612
7#: Storage dtype of :attr:`ColorIntensityData.number_of_returns` (LAS values 1-7).13#: Storage dtype of :attr:`ColorIntensityData.number_of_returns` (LAS values 1-7).
Importance #2: src/iolabs/common/color_intensity_data.py @@ -36,27 +42,19 @@
36 """Fill an omitted ``number_of_returns`` with zeros (unknown) per point."""42 """Fill an omitted ``number_of_returns`` with zeros (unknown) per point."""
37 if self.number_of_returns is None:43 if self.number_of_returns is None:
38 self.number_of_returns = np.zeros(len(self.red), dtype=NUMBER_OF_RETURNS_DTYPE)44 self.number_of_returns = np.zeros(len(self.red), dtype=NUMBER_OF_RETURNS_DTYPE)
3945
46 def _map_fields(
47 self, transform: Callable[[str], np.ndarray]
48 ) -> "ColorIntensityData":
49 """Rebuild this instance's type by applying *transform* to every field name."""
50 return type(self)(**{field.name: transform(field.name) for field in fields(self)})
51
40 def select_by_mask(self, mask: np.ndarray) -> "ColorIntensityData":52 def select_by_mask(self, mask: np.ndarray) -> "ColorIntensityData":
41 """Return a new ColorIntensityData containing only points where mask is True."""53 """Return a new ColorIntensityData containing only points where mask is True."""
42 return ColorIntensityData(54 return self._map_fields(lambda name: getattr(self, name)[mask])
43 red=self.red[mask],
44 green=self.green[mask],
45 blue=self.blue[mask],
46 intensity=self.intensity[mask],
47 scan_angle_rank=self.scan_angle_rank[mask],
48 number_of_returns=self.number_of_returns[mask],
49 )
5055
51 def append(self, other: "ColorIntensityData") -> "ColorIntensityData":56 def append(self, other: "ColorIntensityData") -> "ColorIntensityData":
52 """Concatenate this instance with another, returning a new ColorIntensityData."""57 """Concatenate this instance with another, returning a new ColorIntensityData."""
53 return ColorIntensityData(58 return self._map_fields(
54 red=np.concatenate([self.red, other.red]),59 lambda name: np.concatenate([getattr(self, name), getattr(other, name)])
55 green=np.concatenate([self.green, other.green]),
56 blue=np.concatenate([self.blue, other.blue]),
57 intensity=np.concatenate([self.intensity, other.intensity]),
58 scan_angle_rank=np.concatenate([self.scan_angle_rank, other.scan_angle_rank]),
59 number_of_returns=np.concatenate(
60 [self.number_of_returns, other.number_of_returns]
61 ),
62 )60 )
Importance #3: tests/test_color_intensity_data.py @@ -1,5 +1,7 @@
1"""Tests for ColorIntensityData selection and concatenation operations."""1"""Tests for ColorIntensityData selection and concatenation operations."""
2from dataclasses import dataclass
3
2import numpy as np4import numpy as np
35
4from iolabs.common.color_intensity_data import ColorIntensityData6from iolabs.common.color_intensity_data import ColorIntensityData
57
Importance #4: tests/test_color_intensity_data.py @@ -112,4 +114,45 @@
112 np.testing.assert_array_equal(114 np.testing.assert_array_equal(
113 merged.number_of_returns,115 merged.number_of_returns,
114 np.concatenate([np.zeros(2, dtype=np.uint8), modern.number_of_returns]),116 np.concatenate([np.zeros(2, dtype=np.uint8), modern.number_of_returns]),
115 )117 )
118
119
120class TestFieldDrivenTransforms:
121 """A new per-point field must ride along without touching the methods."""
122
123 def test_extra_field_flows_through_mask_and_append(self):
124 """A subclass field is masked and concatenated by the generic transforms."""
125
126 @dataclass
127 class WithClassification(ColorIntensityData):
128 classification: np.ndarray | None = None
129
130 def _make(n: int, offset: int) -> WithClassification:
131 base = _make_sample(n, offset)
132 return WithClassification(
133 red=base.red,
134 green=base.green,
135 blue=base.blue,
136 intensity=base.intensity,
137 scan_angle_rank=base.scan_angle_rank,
138 number_of_returns=base.number_of_returns,
139 classification=np.arange(offset, offset + n, dtype=np.uint8),
140 )
141
142 first = _make(4, 0)
143 second = _make(2, 50)
144 mask = np.array([True, False, True, False])
145
146 masked = first.select_by_mask(mask)
147 merged = masked.append(second)
148
149 assert isinstance(masked, WithClassification)
150 np.testing.assert_array_equal(masked.classification, first.classification[mask])
151 np.testing.assert_array_equal(
152 merged.classification,
153 np.concatenate([first.classification[mask], second.classification]),
154 )
155 np.testing.assert_array_equal(
156 merged.number_of_returns,
157 np.concatenate([first.number_of_returns[mask], second.number_of_returns]),
158 )
Importance #5: tests/test_color_intensity_data.py @@ -1,5 +1,7 @@
1"""Tests for ColorIntensityData selection and concatenation operations."""1"""Tests for ColorIntensityData selection and concatenation operations."""
2from dataclasses import dataclass
3
2import numpy as np4import numpy as np
35
4from iolabs.common.color_intensity_data import ColorIntensityData6from iolabs.common.color_intensity_data import ColorIntensityData
57
Importance #6: tests/test_color_intensity_data.py @@ -112,4 +114,45 @@
112 np.testing.assert_array_equal(114 np.testing.assert_array_equal(
113 merged.number_of_returns,115 merged.number_of_returns,
114 np.concatenate([np.zeros(2, dtype=np.uint8), modern.number_of_returns]),116 np.concatenate([np.zeros(2, dtype=np.uint8), modern.number_of_returns]),
115 )117 )
118
119
120class TestFieldDrivenTransforms:
121 """A new per-point field must ride along without touching the methods."""
122
123 def test_extra_field_flows_through_mask_and_append(self):
124 """A subclass field is masked and concatenated by the generic transforms."""
125
126 @dataclass
127 class WithClassification(ColorIntensityData):
128 classification: np.ndarray | None = None
129
130 def _make(n: int, offset: int) -> WithClassification:
131 base = _make_sample(n, offset)
132 return WithClassification(
133 red=base.red,
134 green=base.green,
135 blue=base.blue,
136 intensity=base.intensity,
137 scan_angle_rank=base.scan_angle_rank,
138 number_of_returns=base.number_of_returns,
139 classification=np.arange(offset, offset + n, dtype=np.uint8),
140 )
141
142 first = _make(4, 0)
143 second = _make(2, 50)
144 mask = np.array([True, False, True, False])
145
146 masked = first.select_by_mask(mask)
147 merged = masked.append(second)
148
149 assert isinstance(masked, WithClassification)
150 np.testing.assert_array_equal(masked.classification, first.classification[mask])
151 np.testing.assert_array_equal(
152 merged.classification,
153 np.concatenate([first.classification[mask], second.classification]),
154 )
155 np.testing.assert_array_equal(
156 merged.number_of_returns,
157 np.concatenate([first.number_of_returns[mask], second.number_of_returns]),
158 )