Back to report index

Step 3 3dsegmentation 4d72a38: AI3D-382 Use module imports (Google style) in touched files

Miroslav Simko <ms@iolabs.ch> 2026-09-02T07:50:11+02:00

Commit #107 ยท 199 snippets

 src/iolabs_point_cloud_segmentation_3d/cli.py    |   6 +-
 src/iolabs_point_cloud_segmentation_3d/io_npz.py |  47 ++--
 tests/test_fusion.py                             | 286 +++++++++++------------
 3 files changed, 157 insertions(+), 182 deletions(-)
Importance #1: src/iolabs_point_cloud_segmentation_3d/cli.py @@ -7,9 +7,9 @@
7from dataclasses import asdict, dataclass7from dataclasses import asdict, dataclass
8from pathlib import Path8from pathlib import Path
99
10import numpy as np10import numpy as np
11from iolabs.common import run_stats11from iolabs.common import run_stats, segment_points_io
12from iolabs.common.cli import add_log_level_argument, configure_logging12from iolabs.common.cli import add_log_level_argument, configure_logging
13from iolabs.common.crs import looks_georeferenced13from iolabs.common.crs import looks_georeferenced
14from iolabs.common.segments import (14from iolabs.common.segments import (
15 SEGMENT_DIR_PREFIX,15 SEGMENT_DIR_PREFIX,
Importance #2: src/iolabs_point_cloud_segmentation_3d/cli.py @@ -260,9 +260,9 @@
260 logger.error(260 logger.error(
261 "segment %s: %s exists but the cloud is already in world "261 "segment %s: %s exists but the cloud is already in world "
262 "coordinates -- refusing to apply the geoshift twice. Remove "262 "coordinates -- refusing to apply the geoshift twice. Remove "
263 "the file or run with --set las_georeference=false.",263 "the file or run with --set las_georeference=false.",
264 seg_name, io_npz.GEOSHIFT_NAME,264 seg_name, segment_points_io.GEOSHIFT_NAME,
265 )265 )
266 return None266 return None
267 world = geoshift is not None or already_world267 world = geoshift is not None or already_world
268 crs_epsg = config.las_crs_epsg if (config.las_crs_epsg and world) else None268 crs_epsg = config.las_crs_epsg if (config.las_crs_epsg and world) else None
Importance #3: src/iolabs_point_cloud_segmentation_3d/cli.py @@ -274,9 +274,9 @@
274 elif not world:274 elif not world:
275 logger.warning(275 logger.warning(
276 "segment %s: no %s found and coordinates look local -- LAS "276 "segment %s: no %s found and coordinates look local -- LAS "
277 "written in the pipeline frame with no CRS", seg_name,277 "written in the pipeline frame with no CRS", seg_name,
278 io_npz.GEOSHIFT_NAME,278 segment_points_io.GEOSHIFT_NAME,
279 )279 )
280 return geoshift, crs_epsg280 return geoshift, crs_epsg
281281
282282
Importance #4: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -8,26 +8,13 @@
8(the road-surface key set, its recall metric) and the `SegmentCloud` shape the8(the road-surface key set, its recall metric) and the `SegmentCloud` shape the
9fusion pipeline consumes.9fusion pipeline consumes.
10"""10"""
1111
12from dataclasses import dataclass, field12import dataclasses
13from pathlib import Path13import pathlib
1414
15import numpy as np15import numpy as np
16from iolabs.common.point_hash import (16from iolabs.common import point_hash, segment_points_io, segments
17 DEFAULT_UNITS_PER_M,
18 key_match_rate,
19 position_keys,
20)
21from iolabs.common.segment_points_io import (
22 GEOSHIFT_NAME,
23 RecordSpan,
24 load_run3_segment,
25)
26from iolabs.common.segment_points_io import (
27 load_geoshift as _load_geoshift_file,
28)
29from iolabs.common.segments import segment_record_files
3017
31# Storage dtypes every run3 record is coerced to on load. These are the18# Storage dtypes every run3 record is coerced to on load. These are the
32# historical seg3d coercions and they are load-bearing: `points` must stay19# historical seg3d coercions and they are load-bearing: `points` must stay
33# float64 all the way into the hash rounding, and the uint16/int8 channels are20# float64 all the way into the hash rounding, and the uint16/int8 channels are
Importance #5: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -61,13 +48,13 @@
61#: Filename suffix of the run4 road-surface records joined against run3.48#: Filename suffix of the run4 road-surface records joined against run3.
62RUN4_SURFACE_SUFFIX = "_run4_road_surface.npz"49RUN4_SURFACE_SUFFIX = "_run4_road_surface.npz"
6350
64#: One input record's identity and row range within the concatenated cloud.51#: One input record's identity and row range within the concatenated cloud.
65Record = RecordSpan52Record = segment_points_io.RecordSpan
6653
6754
68def surface_keys(55def surface_keys(
69 xyz: np.ndarray, units_per_m: float = DEFAULT_UNITS_PER_M56 xyz: np.ndarray, units_per_m: float = point_hash.DEFAULT_UNITS_PER_M
70) -> np.ndarray:57) -> np.ndarray:
71 """Returns structured integer position keys for a `(N, 3)` coordinate array.58 """Returns structured integer position keys for a `(N, 3)` coordinate array.
7259
73 Thin wrapper over :func:`iolabs.common.point_hash.position_keys`, kept so60 Thin wrapper over :func:`iolabs.common.point_hash.position_keys`, kept so
Importance #6: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -81,12 +68,12 @@
8168
82 Returns:69 Returns:
83 `(N,)` array of packed position keys, in input order.70 `(N,)` array of packed position keys, in input order.
84 """71 """
85 return position_keys(xyz, units_per_m)72 return point_hash.position_keys(xyz, units_per_m)
8673
8774
88@dataclass75@dataclasses.dataclass
89class SegmentCloud:76class SegmentCloud:
90 """A segment's concatenated run3 point cloud plus run4 surface tagging.77 """A segment's concatenated run3 point cloud plus run4 surface tagging.
9178
92 Attributes:79 Attributes:
Importance #7: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -116,9 +103,9 @@
116 scan_angle: np.ndarray103 scan_angle: np.ndarray
117 is_surface: np.ndarray104 is_surface: np.ndarray
118 records: list[Record]105 records: list[Record]
119 surface_match_rate: float106 surface_match_rate: float
120 extra: dict[str, np.ndarray] = field(default_factory=dict)107 extra: dict[str, np.ndarray] = dataclasses.field(default_factory=dict)
121108
122 @property109 @property
123 def n(self) -> int:110 def n(self) -> int:
124 """Number of points in the concatenated cloud."""111 """Number of points in the concatenated cloud."""
Importance #8: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -146,9 +133,9 @@
146 """Returns the contributing record basenames, in concatenation order."""133 """Returns the contributing record basenames, in concatenation order."""
147 return [r.name for r in self.records]134 return [r.name for r in self.records]
148135
149136
150def load_geoshift(seg_dir: Path) -> np.ndarray | None:137def load_geoshift(seg_dir: pathlib.Path) -> np.ndarray | None:
151 """Loads the per-dataset geoshift that run3 subtracted, if present.138 """Loads the per-dataset geoshift that run3 subtracted, if present.
152139
153 The upstream trajectory step writes run3 points as `source - geoshift`140 The upstream trajectory step writes run3 points as `source - geoshift`
154 (the geoshift is the spline centroid) and records the offset in141 (the geoshift is the spline centroid) and records the offset in
Importance #9: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -172,16 +159,16 @@
172159
173 Raises:160 Raises:
174 ValueError: The file exists but is not a `{"x", "y", "z"}` object.161 ValueError: The file exists but is not a `{"x", "y", "z"}` object.
175 """162 """
176 path = Path(seg_dir).parent / GEOSHIFT_NAME163 path = pathlib.Path(seg_dir).parent / segment_points_io.GEOSHIFT_NAME
177 if not path.exists():164 if not path.exists():
178 return None165 return None
179 return _load_geoshift_file(path)166 return segment_points_io.load_geoshift(path)
180167
181168
182def _load_surface_key_set(169def _load_surface_key_set(
183 files: list[Path], units_per_m: float170 files: list[pathlib.Path], units_per_m: float
184) -> np.ndarray | None:171) -> np.ndarray | None:
185 """Builds the unique run4 road-surface key set for a segment.172 """Builds the unique run4 road-surface key set for a segment.
186173
187 Args:174 Args:
Importance #10: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -204,9 +191,9 @@
204 return out191 return out
205192
206193
207def load_segment_cloud(194def load_segment_cloud(
208 seg_dir: Path, units_per_m: float = DEFAULT_UNITS_PER_M195 seg_dir: pathlib.Path, units_per_m: float = point_hash.DEFAULT_UNITS_PER_M
209) -> SegmentCloud:196) -> SegmentCloud:
210 """Loads and concatenates all run3 records, tagging run4 surface membership.197 """Loads and concatenates all run3 records, tagging run4 surface membership.
211198
212 Records are read in sorted glob order; record boundaries (name, offset,199 Records are read in sorted glob order; record boundaries (name, offset,
Importance #11: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -224,24 +211,24 @@
224 Raises:211 Raises:
225 FileNotFoundError: The directory holds no complete run3 record.212 FileNotFoundError: The directory holds no complete run3 record.
226 ValueError: A record violates the point-record schema.213 ValueError: A record violates the point-record schema.
227 """214 """
228 seg_dir = Path(seg_dir)215 seg_dir = pathlib.Path(seg_dir)
229 record, records = load_run3_segment(216 record, records = segment_points_io.load_run3_segment(
230 seg_dir, target_dtypes=RUN3_TARGET_DTYPES217 seg_dir, target_dtypes=RUN3_TARGET_DTYPES
231 )218 )
232219
233 points = record["points"]220 points = record["points"]
234 surf_set = _load_surface_key_set(221 surf_set = _load_surface_key_set(
235 segment_record_files(seg_dir, RUN4_SURFACE_SUFFIX), units_per_m222 segments.segment_record_files(seg_dir, RUN4_SURFACE_SUFFIX), units_per_m
236 )223 )
237 if surf_set is None:224 if surf_set is None:
238 is_surface = np.zeros(points.shape[0], dtype=bool)225 is_surface = np.zeros(points.shape[0], dtype=bool)
239 surface_match_rate = 0.0226 surface_match_rate = 0.0
240 else:227 else:
241 keys = surface_keys(points, units_per_m)228 keys = surface_keys(points, units_per_m)
242 is_surface = np.isin(keys, surf_set, assume_unique=False)229 is_surface = np.isin(keys, surf_set, assume_unique=False)
243 surface_match_rate = key_match_rate(surf_set, keys)230 surface_match_rate = point_hash.key_match_rate(surf_set, keys)
244 del keys231 del keys
245232
246 return SegmentCloud(233 return SegmentCloud(
247 points=points,234 points=points,
Importance #12: tests/test_fusion.py @@ -1,19 +1,21 @@
1"""Synthetic micro-cloud tests for the fusion pipeline (no real data)."""1"""Synthetic micro-cloud tests for the fusion pipeline (no real data)."""
22
3import json3import json
4import logging4import logging
5import pathlib
56
6import numpy as np7import numpy as np
7import pytest8import pytest
8from iolabs.common import crs9import shapely
9from iolabs.common.segments import parse_segment_ids, parse_segment_names10from iolabs.common import crs, segment_points_io, segments
10from scipy.spatial import cKDTree11from scipy import spatial
11from shapely import Polygon
1212
13from iolabs_point_cloud_segmentation_3d import (13from iolabs_point_cloud_segmentation_3d import (
14 classes,14 classes,
15 clusters,15 clusters,
16 config,
17 fuse,
16 ground,18 ground,
17 guardrail_json,19 guardrail_json,
18 io_npz,20 io_npz,
19 lines_xml,21 lines_xml,
Importance #13: tests/test_fusion.py @@ -22,20 +24,8 @@
22 vegetation,24 vegetation,
23 voxel,25 voxel,
24 writer,26 writer,
25)27)
26from iolabs_point_cloud_segmentation_3d import fuse as fuse_mod
27from iolabs_point_cloud_segmentation_3d.config import (
28 Seg3dConfig,
29 config_from_dict,
30)
31from iolabs_point_cloud_segmentation_3d.fuse import (
32 AlignmentError,
33 FuseResult,
34 _paint_lines,
35 fuse_segment,
36 paint_signs_from_json,
37)
3828
39# The legacy output base name for segment 007: the writers take a resolved29# The legacy output base name for segment 007: the writers take a resolved
40# base (see `naming`), not a segment id.30# base (see `naming`), not a segment id.
41BASE_007 = "segment_007_seg3d"31BASE_007 = "segment_007_seg3d"
Importance #14: tests/test_fusion.py @@ -95,9 +85,9 @@
95 Splitting them would let a tube voxel lose the count tie-break to the85 Splitting them would let a tube voxel lose the count tie-break to the
96 rail it was carved out of, which is the exact failure the tier exists86 rail it was carved out of, which is the exact failure the tier exists
97 to prevent.87 to prevent.
98 """88 """
99 cfg = Seg3dConfig(priority_support=9)89 cfg = config.Seg3dConfig(priority_support=9)
100 plut = classes.priority_lut(cfg)90 plut = classes.priority_lut(cfg)
101 assert plut[classes.BY_NAME["guardrail_support"].las_code] == 991 assert plut[classes.BY_NAME["guardrail_support"].las_code] == 9
102 assert plut[classes.BY_NAME["guardrail_top_rail"].las_code] == 992 assert plut[classes.BY_NAME["guardrail_top_rail"].las_code] == 9
103 assert plut[classes.BY_NAME["guardrail"].las_code] == 493 assert plut[classes.BY_NAME["guardrail"].las_code] == 4
Importance #15: tests/test_fusion.py @@ -157,9 +147,9 @@
157 """A record key the loader grows reaches consumers with no edit here."""147 """A record key the loader grows reaches consumers with no edit here."""
158 seg = tmp_path / "segment_004"148 seg = tmp_path / "segment_004"
159 seg.mkdir()149 seg.mkdir()
160 _write_run3(seg / "a_run3_points.npz", np.zeros((3, 3)))150 _write_run3(seg / "a_run3_points.npz", np.zeros((3, 3)))
161 real_load = io_npz.load_run3_segment151 real_load = segment_points_io.load_run3_segment
162152
163 def _with_future_key(seg_dir, **kwargs):153 def _with_future_key(seg_dir, **kwargs):
164 record, records = real_load(seg_dir, **kwargs)154 record, records = real_load(seg_dir, **kwargs)
165 record = dict(record)155 record = dict(record)
Importance #16: tests/test_fusion.py @@ -167,9 +157,9 @@
167 record["points"].shape[0], dtype=np.uint8157 record["points"].shape[0], dtype=np.uint8
168 )158 )
169 return record, records159 return record, records
170160
171 monkeypatch.setattr(io_npz, "load_run3_segment", _with_future_key)161 monkeypatch.setattr(segment_points_io, "load_run3_segment", _with_future_key)
172 cloud = io_npz.load_segment_cloud(seg)162 cloud = io_npz.load_segment_cloud(seg)
173163
174 expected = np.arange(3, dtype=np.uint8)164 expected = np.arange(3, dtype=np.uint8)
175 np.testing.assert_array_equal(cloud.extra["future_channel"], expected)165 np.testing.assert_array_equal(cloud.extra["future_channel"], expected)
Importance #17: tests/test_fusion.py @@ -180,9 +170,9 @@
180# pavement / polygon classify170# pavement / polygon classify
181# --------------------------------------------------------------------------- #171# --------------------------------------------------------------------------- #
182def test_polygon_classify():172def test_polygon_classify():
183 corridor = pavement.Corridor(173 corridor = pavement.Corridor(
184 polygon=Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),174 polygon=shapely.Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),
185 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),175 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),
186 )176 )
187 pts = np.array(177 pts = np.array(
188 [178 [
Importance #18: tests/test_fusion.py @@ -197,9 +187,9 @@
197187
198188
199def test_classify_above_corridor_ignores_surface_gate():189def test_classify_above_corridor_ignores_surface_gate():
200 corridor = pavement.Corridor(190 corridor = pavement.Corridor(
201 polygon=Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),191 polygon=shapely.Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),
202 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),192 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),
203 )193 )
204 pts = np.array(194 pts = np.array(
205 [195 [
Importance #19: tests/test_fusion.py @@ -220,9 +210,9 @@
220 p = tmp_path / "segment_003_edges.npz"210 p = tmp_path / "segment_003_edges.npz"
221 np.savez(p, left_polyline_points=left, right_polyline_points=right)211 np.savez(p, left_polyline_points=left, right_polyline_points=right)
222 corr = pavement.build_corridor(p)212 corr = pavement.build_corridor(p)
223 assert corr is not None213 assert corr is not None
224 assert corr.polygon.contains(Polygon([(1, 1), (2, 1), (2, 3)]).centroid)214 assert corr.polygon.contains(shapely.Polygon([(1, 1), (2, 1), (2, 3)]).centroid)
225215
226216
227def test_build_corridor_bowtie_keeps_both_lobes(tmp_path):217def test_build_corridor_bowtie_keeps_both_lobes(tmp_path):
228 # Self-intersecting (bowtie) ring: buffer(0) would silently keep only218 # Self-intersecting (bowtie) ring: buffer(0) would silently keep only
Importance #20: tests/test_fusion.py @@ -257,9 +247,9 @@
257 [0.10, 0.0, 1.0], # within XY but dz 1.0 > 0.5 -> no247 [0.10, 0.0, 1.0], # within XY but dz 1.0 > 0.5 -> no
258 ]248 ]
259 )249 )
260 is_asphalt = np.ones(3, dtype=bool)250 is_asphalt = np.ones(3, dtype=bool)
261 solid, dashed = _paint_lines(pts, is_asphalt, verts)251 solid, dashed = fuse._paint_lines(pts, is_asphalt, verts)
262 np.testing.assert_array_equal(solid, [True, False, False])252 np.testing.assert_array_equal(solid, [True, False, False])
263 assert not dashed.any()253 assert not dashed.any()
264254
265255
Importance #21: tests/test_fusion.py @@ -271,9 +261,9 @@
271 solid_xyz=np.array([[0.01, 0.0, 1.0], [0.10, 0.0, 0.0]]),261 solid_xyz=np.array([[0.01, 0.0, 1.0], [0.10, 0.0, 0.0]]),
272 dashed_xyz=np.empty((0, 3)),262 dashed_xyz=np.empty((0, 3)),
273 )263 )
274 pts = np.array([[0.0, 0.0, 0.0]])264 pts = np.array([[0.0, 0.0, 0.0]])
275 solid, dashed = _paint_lines(pts, np.ones(1, bool), verts)265 solid, dashed = fuse._paint_lines(pts, np.ones(1, bool), verts)
276 assert solid[0]266 assert solid[0]
277 assert not dashed[0]267 assert not dashed[0]
278268
279269
Importance #22: tests/test_fusion.py @@ -282,9 +272,9 @@
282 solid_xyz=np.array([[0.0, 0.0, 0.0]]),272 solid_xyz=np.array([[0.0, 0.0, 0.0]]),
283 dashed_xyz=np.array([[0.0, 0.0, 0.0]]),273 dashed_xyz=np.array([[0.0, 0.0, 0.0]]),
284 )274 )
285 pts = np.array([[0.05, 0.0, 0.0]])275 pts = np.array([[0.05, 0.0, 0.0]])
286 solid, dashed = _paint_lines(pts, np.ones(1, bool), verts)276 solid, dashed = fuse._paint_lines(pts, np.ones(1, bool), verts)
287 assert solid[0] and not dashed[0]277 assert solid[0] and not dashed[0]
288278
289279
290def test_paint_lines_only_asphalt():280def test_paint_lines_only_asphalt():
Importance #23: tests/test_fusion.py @@ -292,9 +282,9 @@
292 solid_xyz=np.array([[0.0, 0.0, 0.0]]),282 solid_xyz=np.array([[0.0, 0.0, 0.0]]),
293 dashed_xyz=np.empty((0, 3)),283 dashed_xyz=np.empty((0, 3)),
294 )284 )
295 pts = np.array([[0.0, 0.0, 0.0]])285 pts = np.array([[0.0, 0.0, 0.0]])
296 solid, _ = _paint_lines(pts, np.zeros(1, bool), verts)286 solid, _ = fuse._paint_lines(pts, np.zeros(1, bool), verts)
297 assert not solid.any() # not asphalt -> never painted287 assert not solid.any() # not asphalt -> never painted
298288
299289
300# --------------------------------------------------------------------------- #290# --------------------------------------------------------------------------- #
Importance #24: tests/test_fusion.py @@ -509,9 +499,9 @@
509 "<EndPoint><X>4.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"499 "<EndPoint><X>4.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"
510 "</Line></Lines></Feature></HighwayData>"500 "</Line></Lines></Feature></HighwayData>"
511 )501 )
512502
513 result = fuse_segment(503 result = fuse.fuse_segment(
514 seg_dir=seg_dir,504 seg_dir=seg_dir,
515 seg_name="066",505 seg_name="066",
516 edges_dirs=[edges],506 edges_dirs=[edges],
517 xml_path=xml_path,507 xml_path=xml_path,
Importance #25: tests/test_fusion.py @@ -558,9 +548,9 @@
558 "<EndPoint><X>3.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"548 "<EndPoint><X>3.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"
559 "</Line></Lines></Feature></HighwayData>"549 "</Line></Lines></Feature></HighwayData>"
560 )550 )
561551
562 result = fuse_segment(552 result = fuse.fuse_segment(
563 seg_dir=seg_dir,553 seg_dir=seg_dir,
564 seg_name="067",554 seg_name="067",
565 edges_dirs=[edges],555 edges_dirs=[edges],
566 xml_path=xml_path,556 xml_path=xml_path,
Importance #26: tests/test_fusion.py @@ -597,10 +587,10 @@
597 "<EndPoint><X>50001.0</X><Y>50000.0</Y><Z>0.0</Z></EndPoint>"587 "<EndPoint><X>50001.0</X><Y>50000.0</Y><Z>0.0</Z></EndPoint>"
598 "</Line></Lines></Feature></HighwayData>"588 "</Line></Lines></Feature></HighwayData>"
599 )589 )
600590
601 with pytest.raises(AlignmentError):591 with pytest.raises(fuse.AlignmentError):
602 fuse_segment(592 fuse.fuse_segment(
603 seg_dir=seg_dir,593 seg_dir=seg_dir,
604 seg_name="068",594 seg_name="068",
605 edges_dirs=[edges],595 edges_dirs=[edges],
606 xml_path=xml_path,596 xml_path=xml_path,
Importance #27: tests/test_fusion.py @@ -637,9 +627,9 @@
637 "<EndPoint><X>7.1</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"627 "<EndPoint><X>7.1</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"
638 "</Line></Lines></Feature></HighwayData>"628 "</Line></Lines></Feature></HighwayData>"
639 )629 )
640630
641 result = fuse_segment(631 result = fuse.fuse_segment(
642 seg_dir=seg_dir,632 seg_dir=seg_dir,
643 seg_name="069",633 seg_name="069",
644 edges_dirs=[edges],634 edges_dirs=[edges],
645 xml_path=xml_path,635 xml_path=xml_path,
Importance #28: tests/test_fusion.py @@ -667,11 +657,9 @@
667657
668 import tempfile658 import tempfile
669659
670 with tempfile.TemporaryDirectory() as d:660 with tempfile.TemporaryDirectory() as d:
671 from pathlib import Path661 path = pathlib.Path(d) / "point_masks.npz"
672
673 path = Path(d) / "point_masks.npz"
674 np.savez(662 np.savez(
675 path,663 path,
676 record_names=np.array(["a_run3_points.npz", "b_run3_points.npz"]),664 record_names=np.array(["a_run3_points.npz", "b_run3_points.npz"]),
677 record_id=np.array([0, 1, 1], dtype=np.uint16),665 record_id=np.array([0, 1, 1], dtype=np.uint16),
Importance #29: tests/test_fusion.py @@ -791,9 +779,9 @@
791 instance_type=np.array(["w_beam", "guardrail_support"]),779 instance_type=np.array(["w_beam", "guardrail_support"]),
792 instance_json_index=np.array([0, 1], dtype=np.int32),780 instance_json_index=np.array([0, 1], dtype=np.int32),
793 )781 )
794782
795 result = fuse_segment(783 result = fuse.fuse_segment(
796 seg_dir=seg_dir,784 seg_dir=seg_dir,
797 seg_name="081",785 seg_name="081",
798 edges_dirs=[],786 edges_dirs=[],
799 xml_path=None,787 xml_path=None,
Importance #30: tests/test_fusion.py @@ -848,9 +836,9 @@
848 instance_type=np.array(["delineator"]),836 instance_type=np.array(["delineator"]),
849 instance_json_index=np.array([0], dtype=np.int32),837 instance_json_index=np.array([0], dtype=np.int32),
850 )838 )
851839
852 result = fuse_segment(840 result = fuse.fuse_segment(
853 seg_dir=seg_dir,841 seg_dir=seg_dir,
854 seg_name="082",842 seg_name="082",
855 edges_dirs=[],843 edges_dirs=[],
856 xml_path=None,844 xml_path=None,
Importance #31: tests/test_fusion.py @@ -953,10 +941,10 @@
953 detections = [941 detections = [
954 _sign_detection(10.0, 10.0),942 _sign_detection(10.0, 10.0),
955 _sign_detection(20.0, 10.0),943 _sign_detection(20.0, 10.0),
956 ]944 ]
957 instances, metrics = paint_signs_from_json(945 instances, metrics = fuse.paint_signs_from_json(
958 pts, cls, detections, [covered], Seg3dConfig(), seg_name="001"946 pts, cls, detections, [covered], config.Seg3dConfig(), seg_name="001"
959 )947 )
960 assert len(instances) == 1948 assert len(instances) == 1
961 inst = instances[0]949 inst = instances[0]
962 assert inst.kind == "sign"950 assert inst.kind == "sign"
Importance #32: tests/test_fusion.py @@ -982,10 +970,10 @@
982 json_index=0,970 json_index=0,
983 local_index=0,971 local_index=0,
984 global_rows=np.empty(0, dtype=np.int64),972 global_rows=np.empty(0, dtype=np.int64),
985 )973 )
986 instances, metrics = paint_signs_from_json(974 instances, metrics = fuse.paint_signs_from_json(
987 pts, cls, [_sign_detection(10.0, 10.0)], [empty], Seg3dConfig()975 pts, cls, [_sign_detection(10.0, 10.0)], [empty], config.Seg3dConfig()
988 )976 )
989 assert len(instances) == 1977 assert len(instances) == 1
990 assert instances[0].json_index == 0978 assert instances[0].json_index == 0
991 assert instances[0].local_index == 1979 assert instances[0].local_index == 1
Importance #33: tests/test_fusion.py @@ -1010,10 +998,10 @@
1010 local_index=0,998 local_index=0,
1011 global_rows=np.arange(20),999 global_rows=np.arange(20),
1012 )1000 )
1013 detections = [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0)]1001 detections = [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0)]
1014 instances, metrics = paint_signs_from_json(1002 instances, metrics = fuse.paint_signs_from_json(
1015 pts, cls, detections, [legacy], Seg3dConfig()1003 pts, cls, detections, [legacy], config.Seg3dConfig()
1016 )1004 )
1017 assert [i.json_index for i in instances] == [1]1005 assert [i.json_index for i in instances] == [1]
1018 assert metrics["signs_json_painted"] == 1.01006 assert metrics["signs_json_painted"] == 1.0
1019 assert metrics["signs_json_skipped"] == 0.01007 assert metrics["signs_json_skipped"] == 0.0
Importance #34: tests/test_fusion.py @@ -1067,10 +1055,10 @@
1067 )1055 )
1068 with caplog.at_level(1056 with caplog.at_level(
1069 logging.WARNING, logger="iolabs_point_cloud_segmentation_3d.fuse"1057 logging.WARNING, logger="iolabs_point_cloud_segmentation_3d.fuse"
1070 ):1058 ):
1071 instances, metrics = paint_signs_from_json(1059 instances, metrics = fuse.paint_signs_from_json(
1072 pts, cls, detections, mask, Seg3dConfig(), seg_name="023"1060 pts, cls, detections, mask, config.Seg3dConfig(), seg_name="023"
1073 )1061 )
1074 # The shifted half-post is painted; every mask-covered detection is1062 # The shifted half-post is painted; every mask-covered detection is
1075 # recognised by geometry and not painted a second time.1063 # recognised by geometry and not painted a second time.
1076 assert [i.json_index for i in instances] == [7]1064 assert [i.json_index for i in instances] == [7]
Importance #35: tests/test_fusion.py @@ -1101,10 +1089,10 @@
1101 json_index=0,1089 json_index=0,
1102 local_index=0,1090 local_index=0,
1103 global_rows=np.empty(0, dtype=np.int64),1091 global_rows=np.empty(0, dtype=np.int64),
1104 )1092 )
1105 instances, _ = paint_signs_from_json(1093 instances, _ = fuse.paint_signs_from_json(
1106 pts, cls, [_sign_detection(10.0, 10.0)], [rail], Seg3dConfig()1094 pts, cls, [_sign_detection(10.0, 10.0)], [rail], config.Seg3dConfig()
1107 )1095 )
1108 assert [i.json_index for i in instances] == [0]1096 assert [i.json_index for i in instances] == [0]
11091097
11101098
Importance #36: tests/test_fusion.py @@ -1121,10 +1109,10 @@
1121 json_index=0,1109 json_index=0,
1122 local_index=0,1110 local_index=0,
1123 global_rows=np.arange(pts.shape[0]),1111 global_rows=np.arange(pts.shape[0]),
1124 )1112 )
1125 instances, metrics = paint_signs_from_json(1113 instances, metrics = fuse.paint_signs_from_json(
1126 pts, cls, [_sign_detection(10.0, 10.0)], [covered], Seg3dConfig()1114 pts, cls, [_sign_detection(10.0, 10.0)], [covered], config.Seg3dConfig()
1127 )1115 )
1128 assert instances == []1116 assert instances == []
1129 assert metrics["signs_json_painted"] == 0.01117 assert metrics["signs_json_painted"] == 0.0
1130 assert metrics["signs_json_skipped"] == 0.01118 assert metrics["signs_json_skipped"] == 0.0
Importance #37: tests/test_fusion.py @@ -1145,10 +1133,10 @@
1145 4: "solid_line",1133 4: "solid_line",
1146 }1134 }
1147 for row, name in keep.items():1135 for row, name in keep.items():
1148 cls[row] = classes.BY_NAME[name].las_code1136 cls[row] = classes.BY_NAME[name].las_code
1149 instances, _ = paint_signs_from_json(1137 instances, _ = fuse.paint_signs_from_json(
1150 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1138 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1151 )1139 )
1152 assert len(instances) == 11140 assert len(instances) == 1
1153 np.testing.assert_array_equal(np.sort(instances[0].global_rows),1141 np.testing.assert_array_equal(np.sort(instances[0].global_rows),
1154 np.arange(5, 25))1142 np.arange(5, 25))
Importance #38: tests/test_fusion.py @@ -1160,10 +1148,10 @@
1160def test_paint_signs_from_json_skips_null_z_top():1148def test_paint_signs_from_json_skips_null_z_top():
1161 pts = _post_points(10.0, 10.0)1149 pts = _post_points(10.0, 10.0)
1162 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1150 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1163 detections = [_sign_detection(10.0, 10.0, z_top=None)]1151 detections = [_sign_detection(10.0, 10.0, z_top=None)]
1164 instances, metrics = paint_signs_from_json(1152 instances, metrics = fuse.paint_signs_from_json(
1165 pts, cls, detections, [], Seg3dConfig()1153 pts, cls, detections, [], config.Seg3dConfig()
1166 )1154 )
1167 assert instances == []1155 assert instances == []
1168 assert metrics["signs_json_skipped"] == 1.01156 assert metrics["signs_json_skipped"] == 1.0
1169 assert np.all(cls == classes.UNCLASSIFIED_CODE)1157 assert np.all(cls == classes.UNCLASSIFIED_CODE)
Importance #39: tests/test_fusion.py @@ -1175,10 +1163,10 @@
1175 detections = [1163 detections = [
1176 _sign_detection(10.0, 10.0, seg_type="tree", experimental=True),1164 _sign_detection(10.0, 10.0, seg_type="tree", experimental=True),
1177 _sign_detection(10.0, 10.0, seg_type="field_stake"),1165 _sign_detection(10.0, 10.0, seg_type="field_stake"),
1178 ]1166 ]
1179 instances, metrics = paint_signs_from_json(1167 instances, metrics = fuse.paint_signs_from_json(
1180 pts, cls, detections, [], Seg3dConfig()1168 pts, cls, detections, [], config.Seg3dConfig()
1181 )1169 )
1182 assert instances == []1170 assert instances == []
1183 assert metrics["signs_json_skipped"] == 2.01171 assert metrics["signs_json_skipped"] == 2.0
1184 assert np.all(cls == classes.UNCLASSIFIED_CODE)1172 assert np.all(cls == classes.UNCLASSIFIED_CODE)
Importance #40: tests/test_fusion.py @@ -1238,10 +1226,10 @@
1238 _sign_detection(10.0, 10.0),1226 _sign_detection(10.0, 10.0),
1239 "junk",1227 "junk",
1240 _sign_detection(20.0, 10.0),1228 _sign_detection(20.0, 10.0),
1241 ]1229 ]
1242 instances, metrics = paint_signs_from_json(1230 instances, metrics = fuse.paint_signs_from_json(
1243 pts, cls, detections, [covered], Seg3dConfig()1231 pts, cls, detections, [covered], config.Seg3dConfig()
1244 )1232 )
1245 # Only the genuinely uncovered detection 0 is painted.1233 # Only the genuinely uncovered detection 0 is painted.
1246 assert [i.json_index for i in instances] == [0]1234 assert [i.json_index for i in instances] == [0]
1247 np.testing.assert_array_equal(np.sort(instances[0].global_rows),1235 np.testing.assert_array_equal(np.sort(instances[0].global_rows),
Importance #41: tests/test_fusion.py @@ -1253,11 +1241,11 @@
1253 # min_points=0 must not turn a detection that matched nothing into a1241 # min_points=0 must not turn a detection that matched nothing into a
1254 # zero-row instance.1242 # zero-row instance.
1255 pts = _post_points(500.0, 500.0, n=20) # nowhere near the detection1243 pts = _post_points(500.0, 500.0, n=20) # nowhere near the detection
1256 cls = np.full(20, classes.UNCLASSIFIED_CODE, dtype=np.uint8)1244 cls = np.full(20, classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1257 config = config_from_dict({"signs_json_paint_min_points": 0})1245 cfg = config.config_from_dict({"signs_json_paint_min_points": 0})
1258 instances, metrics = paint_signs_from_json(1246 instances, metrics = fuse.paint_signs_from_json(
1259 pts, cls, [_sign_detection(10.0, 10.0)], [], config1247 pts, cls, [_sign_detection(10.0, 10.0)], [], cfg
1260 )1248 )
1261 assert instances == []1249 assert instances == []
1262 assert metrics["signs_json_painted"] == 0.01250 assert metrics["signs_json_painted"] == 0.0
1263 assert metrics["signs_json_skipped"] == 1.01251 assert metrics["signs_json_skipped"] == 1.0
Importance #42: tests/test_fusion.py @@ -1268,18 +1256,18 @@
1268 # 5 points in the cylinder, below the default floor of 10 -> no phantom1256 # 5 points in the cylinder, below the default floor of 10 -> no phantom
1269 # instance and nothing painted.1257 # instance and nothing painted.
1270 pts = _post_points(10.0, 10.0, n=5)1258 pts = _post_points(10.0, 10.0, n=5)
1271 cls = np.full(5, classes.UNCLASSIFIED_CODE, dtype=np.uint8)1259 cls = np.full(5, classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1272 instances, metrics = paint_signs_from_json(1260 instances, metrics = fuse.paint_signs_from_json(
1273 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1261 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1274 )1262 )
1275 assert instances == []1263 assert instances == []
1276 assert metrics["signs_json_skipped"] == 1.01264 assert metrics["signs_json_skipped"] == 1.0
1277 assert np.all(cls == classes.UNCLASSIFIED_CODE)1265 assert np.all(cls == classes.UNCLASSIFIED_CODE)
1278 # Lowering the floor paints the same detection.1266 # Lowering the floor paints the same detection.
1279 instances, _ = paint_signs_from_json(1267 instances, _ = fuse.paint_signs_from_json(
1280 pts, cls, [_sign_detection(10.0, 10.0)], [],1268 pts, cls, [_sign_detection(10.0, 10.0)], [],
1281 config_from_dict({"signs_json_paint_min_points": 3}),1269 config.config_from_dict({"signs_json_paint_min_points": 3}),
1282 )1270 )
1283 assert len(instances) == 11271 assert len(instances) == 1
12841272
12851273
Importance #43: tests/test_fusion.py @@ -1291,10 +1279,10 @@
1291 outside_xy = _post_points(10.5, 10.0, n=12) # 0.5 m out -> outside1279 outside_xy = _post_points(10.5, 10.0, n=12) # 0.5 m out -> outside
1292 above = _post_points(10.0, 10.0, n=12, z0=1.9, z1=3.0) # above z_top+0.301280 above = _post_points(10.0, 10.0, n=12, z0=1.9, z1=3.0) # above z_top+0.30
1293 pts = np.vstack([inside, far_xy, outside_xy, above])1281 pts = np.vstack([inside, far_xy, outside_xy, above])
1294 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1282 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1295 instances, _ = paint_signs_from_json(1283 instances, _ = fuse.paint_signs_from_json(
1296 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1284 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1297 )1285 )
1298 assert len(instances) == 11286 assert len(instances) == 1
1299 painted = np.zeros(pts.shape[0], dtype=bool)1287 painted = np.zeros(pts.shape[0], dtype=bool)
1300 painted[instances[0].global_rows] = True1288 painted[instances[0].global_rows] = True
Importance #44: tests/test_fusion.py @@ -1313,10 +1301,10 @@
1313 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1301 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1314 gantry = _sign_detection(1302 gantry = _sign_detection(
1315 10.0, 10.0, "gantry_or_gate", footprint_m=[10.0, 0.5]1303 10.0, 10.0, "gantry_or_gate", footprint_m=[10.0, 0.5]
1316 )1304 )
1317 instances, metrics = paint_signs_from_json(1305 instances, metrics = fuse.paint_signs_from_json(
1318 pts, cls, [gantry], [], Seg3dConfig()1306 pts, cls, [gantry], [], config.Seg3dConfig()
1319 )1307 )
1320 assert instances == []1308 assert instances == []
1321 assert metrics["signs_json_painted"] == 0.01309 assert metrics["signs_json_painted"] == 0.0
1322 assert metrics["signs_json_skipped"] == 1.01310 assert metrics["signs_json_skipped"] == 1.0
Importance #45: tests/test_fusion.py @@ -1329,20 +1317,20 @@
1329 below = _post_points(10.0, 10.0, n=12, z0=-0.14, z1=-0.01)1317 below = _post_points(10.0, 10.0, n=12, z0=-0.14, z1=-0.01)
1330 above = _post_points(10.0, 10.0, n=12, z0=0.0, z1=1.5)1318 above = _post_points(10.0, 10.0, n=12, z0=0.0, z1=1.5)
1331 pts = np.vstack([below, above])1319 pts = np.vstack([below, above])
1332 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1320 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1333 instances, _ = paint_signs_from_json(1321 instances, _ = fuse.paint_signs_from_json(
1334 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1322 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1335 )1323 )
1336 assert len(instances) == 11324 assert len(instances) == 1
1337 np.testing.assert_array_equal(1325 np.testing.assert_array_equal(
1338 np.sort(instances[0].global_rows), np.arange(12, 24)1326 np.sort(instances[0].global_rows), np.arange(12, 24)
1339 )1327 )
1340 # The knob still reaches under the ground when a run asks for it.1328 # The knob still reaches under the ground when a run asks for it.
1341 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1329 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1342 instances, _ = paint_signs_from_json(1330 instances, _ = fuse.paint_signs_from_json(
1343 pts, cls, [_sign_detection(10.0, 10.0)], [],1331 pts, cls, [_sign_detection(10.0, 10.0)], [],
1344 config_from_dict({"signs_json_paint_z_pad_bottom_m": 0.15}),1332 config.config_from_dict({"signs_json_paint_z_pad_bottom_m": 0.15}),
1345 )1333 )
1346 np.testing.assert_array_equal(1334 np.testing.assert_array_equal(
1347 np.sort(instances[0].global_rows), np.arange(24)1335 np.sort(instances[0].global_rows), np.arange(24)
1348 )1336 )
Importance #46: tests/test_fusion.py @@ -1360,9 +1348,9 @@
1360 signs, "080",1348 signs, "080",
1361 [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0, "sign_post")],1349 [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0, "sign_post")],
1362 )1350 )
13631351
1364 result = fuse_segment(1352 result = fuse.fuse_segment(
1365 seg_dir=seg_dir,1353 seg_dir=seg_dir,
1366 seg_name="080",1354 seg_name="080",
1367 edges_dirs=[],1355 edges_dirs=[],
1368 xml_path=None,1356 xml_path=None,
Importance #47: tests/test_fusion.py @@ -1401,29 +1389,29 @@
1401 seg_dir = tmp_path / "segment_007"1389 seg_dir = tmp_path / "segment_007"
1402 seg_dir.mkdir()1390 seg_dir.mkdir()
1403 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))1391 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))
14041392
1405 config = config_from_dict(1393 cfg = config.config_from_dict(
1406 {"edge_extend_m": 12.5, "priority_detector": 7, "las_crs_epsg": 2056}1394 {"edge_extend_m": 12.5, "priority_detector": 7, "las_crs_epsg": 2056}
1407 )1395 )
1408 result = fuse_segment(1396 result = fuse.fuse_segment(
1409 seg_dir=seg_dir,1397 seg_dir=seg_dir,
1410 seg_name="007",1398 seg_name="007",
1411 edges_dirs=[],1399 edges_dirs=[],
1412 xml_path=None,1400 xml_path=None,
1413 guardrail_masks_dir=None,1401 guardrail_masks_dir=None,
1414 signs_masks_dir=None,1402 signs_masks_dir=None,
1415 voxel=0.25,1403 voxel=0.25,
1416 config=config,1404 config=cfg,
1417 )1405 )
14181406
1419 params = result.stats["params"]1407 params = result.stats["params"]
1420 assert params["edge_extend_m"] == 12.51408 assert params["edge_extend_m"] == 12.5
1421 assert params["priority_detector"] == 71409 assert params["priority_detector"] == 7
1422 assert params["las_crs_epsg"] == 20561410 assert params["las_crs_epsg"] == 2056
1423 # `--voxel` beats `config.voxel_size_m`, and the params say so.1411 # `--voxel` beats `config.voxel_size_m`, and the params say so.
1424 assert params["voxel_size_m"] == 0.251412 assert params["voxel_size_m"] == 0.25
1425 assert config.voxel_size_m != 0.251413 assert cfg.voxel_size_m != 0.25
1426 # Whatever ends up in the params has to survive the stats json.1414 # Whatever ends up in the params has to survive the stats json.
1427 assert json.loads(json.dumps(params)) == params1415 assert json.loads(json.dumps(params)) == params
14281416
14291417
Importance #48: tests/test_fusion.py @@ -1441,26 +1429,26 @@
1441 seg_dir = tmp_path / "segment_007"1429 seg_dir = tmp_path / "segment_007"
1442 seg_dir.mkdir()1430 seg_dir.mkdir()
1443 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))1431 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))
14441432
1445 config = config_from_dict({"las_split": "none", "write_ply": False})1433 cfg = config.config_from_dict({"las_split": "none", "write_ply": False})
1446 result = fuse_segment(1434 result = fuse.fuse_segment(
1447 seg_dir=seg_dir,1435 seg_dir=seg_dir,
1448 seg_name="007",1436 seg_name="007",
1449 edges_dirs=[],1437 edges_dirs=[],
1450 xml_path=None,1438 xml_path=None,
1451 guardrail_masks_dir=None,1439 guardrail_masks_dir=None,
1452 signs_masks_dir=None,1440 signs_masks_dir=None,
1453 voxel=0.25,1441 voxel=0.25,
1454 config=config,1442 config=cfg,
1455 )1443 )
1456 summary = {1444 summary = {
1457 "segment": "007",1445 "segment": "007",
1458 "peak_rss_gb": 0.1,1446 "peak_rss_gb": 0.1,
1459 "params": result.stats["params"],1447 "params": result.stats["params"],
1460 }1448 }
1461 path = cli._write_run_summary(1449 path = cli._write_run_summary(
1462 tmp_path / "out", config, [summary],1450 tmp_path / "out", cfg, [summary],
1463 voxel_override=0.25,1451 voxel_override=0.25,
1464 las_split_override="class",1452 las_split_override="class",
1465 write_ply_override=True,1453 write_ply_override=True,
1466 )1454 )
Importance #49: tests/test_fusion.py @@ -1489,9 +1477,9 @@
1489 _write_signs_sidecars(1477 _write_signs_sidecars(
1490 signs, "081", detections, mask=[(0, list(range(20)))]1478 signs, "081", detections, mask=[(0, list(range(20)))]
1491 )1479 )
14921480
1493 result = fuse_segment(1481 result = fuse.fuse_segment(
1494 seg_dir=seg_dir,1482 seg_dir=seg_dir,
1495 seg_name="081",1483 seg_name="081",
1496 edges_dirs=[],1484 edges_dirs=[],
1497 xml_path=None,1485 xml_path=None,
Importance #50: tests/test_fusion.py @@ -1535,9 +1523,9 @@
15351523
1536 signs = tmp_path / "signs"1524 signs = tmp_path / "signs"
1537 _write_signs_sidecars(signs, "083", [_sign_detection(10.0, 10.0)])1525 _write_signs_sidecars(signs, "083", [_sign_detection(10.0, 10.0)])
15381526
1539 result = fuse_segment(1527 result = fuse.fuse_segment(
1540 seg_dir=seg_dir,1528 seg_dir=seg_dir,
1541 seg_name="083",1529 seg_name="083",
1542 edges_dirs=[],1530 edges_dirs=[],
1543 xml_path=None,1531 xml_path=None,
Importance #51: tests/test_fusion.py @@ -1558,16 +1546,16 @@
15581546
1559 signs = tmp_path / "signs"1547 signs = tmp_path / "signs"
1560 _write_signs_sidecars(signs, "082", [_sign_detection(10.0, 10.0)])1548 _write_signs_sidecars(signs, "082", [_sign_detection(10.0, 10.0)])
15611549
1562 result = fuse_segment(1550 result = fuse.fuse_segment(
1563 seg_dir=seg_dir,1551 seg_dir=seg_dir,
1564 seg_name="082",1552 seg_name="082",
1565 edges_dirs=[],1553 edges_dirs=[],
1566 xml_path=None,1554 xml_path=None,
1567 guardrail_masks_dir=None,1555 guardrail_masks_dir=None,
1568 signs_masks_dir=signs,1556 signs_masks_dir=signs,
1569 config=config_from_dict({"signs_json_paint_enabled": False}),1557 config=config.config_from_dict({"signs_json_paint_enabled": False}),
1570 )1558 )
1571 assert np.all(result.classification == classes.UNCLASSIFIED_CODE)1559 assert np.all(result.classification == classes.UNCLASSIFIED_CODE)
1572 assert "signs_json_painted" not in result.stats["guard_metrics"]1560 assert "signs_json_painted" not in result.stats["guard_metrics"]
15731561
Importance #52: tests/test_fusion.py @@ -1589,9 +1577,9 @@
1589 instance_type=np.array(["delineator"]),1577 instance_type=np.array(["delineator"]),
1590 instance_json_index=np.array([0], dtype=np.int32),1578 instance_json_index=np.array([0], dtype=np.int32),
1591 )1579 )
15921580
1593 result = fuse_segment(1581 result = fuse.fuse_segment(
1594 seg_dir=seg_dir,1582 seg_dir=seg_dir,
1595 seg_name="084",1583 seg_name="084",
1596 edges_dirs=[],1584 edges_dirs=[],
1597 xml_path=None,1585 xml_path=None,
Importance #53: tests/test_fusion.py @@ -2102,9 +2090,9 @@
2102 (mdir / "guardrails.json").write_text(2090 (mdir / "guardrails.json").write_text(
2103 json.dumps({"guardrails": [_rail_entry(0), _support_entry(1)]})2091 json.dumps({"guardrails": [_rail_entry(0), _support_entry(1)]})
2104 )2092 )
21052093
2106 result = fuse_segment(2094 result = fuse.fuse_segment(
2107 seg_dir=seg_dir,2095 seg_dir=seg_dir,
2108 seg_name="082",2096 seg_name="082",
2109 edges_dirs=[],2097 edges_dirs=[],
2110 xml_path=None,2098 xml_path=None,
Importance #54: tests/test_fusion.py @@ -2144,9 +2132,9 @@
2144 assert masks.guardrails_json_file(tmp_path / "gmasks", "084") == (2132 assert masks.guardrails_json_file(tmp_path / "gmasks", "084") == (
2145 mdir / "guardrails.json"2133 mdir / "guardrails.json"
2146 )2134 )
21472135
2148 result = fuse_segment(2136 result = fuse.fuse_segment(
2149 seg_dir=seg_dir,2137 seg_dir=seg_dir,
2150 seg_name="084",2138 seg_name="084",
2151 edges_dirs=[],2139 edges_dirs=[],
2152 xml_path=None,2140 xml_path=None,
Importance #55: tests/test_fusion.py @@ -2179,9 +2167,9 @@
2179 mdir = tmp_path / "gmasks" / "segment_084"2167 mdir = tmp_path / "gmasks" / "segment_084"
2180 mdir.mkdir(parents=True)2168 mdir.mkdir(parents=True)
2181 (mdir / "guardrails.json").write_text('{"guardrails": [{"id": 0,')2169 (mdir / "guardrails.json").write_text('{"guardrails": [{"id": 0,')
21822170
2183 result = fuse_segment(2171 result = fuse.fuse_segment(
2184 seg_dir=seg_dir,2172 seg_dir=seg_dir,
2185 seg_name="084",2173 seg_name="084",
2186 edges_dirs=[],2174 edges_dirs=[],
2187 xml_path=None,2175 xml_path=None,
Importance #56: tests/test_fusion.py @@ -2227,9 +2215,9 @@
2227 (mdir / "guardrails.json").write_text(json.dumps({"guardrails": [2215 (mdir / "guardrails.json").write_text(json.dumps({"guardrails": [
2228 _rail_entry(0), _support_entry(1), _top_rail_entry(2),2216 _rail_entry(0), _support_entry(1), _top_rail_entry(2),
2229 ]}))2217 ]}))
22302218
2231 result = fuse_segment(2219 result = fuse.fuse_segment(
2232 seg_dir=seg_dir,2220 seg_dir=seg_dir,
2233 seg_name="083",2221 seg_name="083",
2234 edges_dirs=[],2222 edges_dirs=[],
2235 xml_path=None,2223 xml_path=None,
Importance #57: tests/test_fusion.py @@ -2370,9 +2358,9 @@
2370 _write_tablecloth_mask(2358 _write_tablecloth_mask(
2371 gm / "a_tablecloth_masks.npz", np.ones(6, dtype=bool)2359 gm / "a_tablecloth_masks.npz", np.ones(6, dtype=bool)
2372 )2360 )
23732361
2374 result = fuse_segment(2362 result = fuse.fuse_segment(
2375 seg_dir=seg_dir,2363 seg_dir=seg_dir,
2376 seg_name="070",2364 seg_name="070",
2377 edges_dirs=[edges],2365 edges_dirs=[edges],
2378 xml_path=None,2366 xml_path=None,
Importance #58: tests/test_fusion.py @@ -2430,10 +2418,10 @@
2430 xml_path=None,2418 xml_path=None,
2431 guardrail_masks_dir=None,2419 guardrail_masks_dir=None,
2432 signs_masks_dir=None,2420 signs_masks_dir=None,
2433 )2421 )
2434 without = fuse_segment(**kwargs).stats["counts_full"]2422 without = fuse.fuse_segment(**kwargs).stats["counts_full"]
2435 with_g = fuse_segment(2423 with_g = fuse.fuse_segment(
2436 **kwargs, ground_masks_dir=tmp_path / "ground"2424 **kwargs, ground_masks_dir=tmp_path / "ground"
2437 ).stats["counts_full"]2425 ).stats["counts_full"]
24382426
2439 for name in ("asphalt", "solid_line", "dashed_line"):2427 for name in ("asphalt", "solid_line", "dashed_line"):
Importance #59: tests/test_fusion.py @@ -2451,9 +2439,9 @@
24512439
2452 gm = tmp_path / "ground" / "segment_071"2440 gm = tmp_path / "ground" / "segment_071"
2453 gm.mkdir(parents=True) # subdir exists but holds no mask files2441 gm.mkdir(parents=True) # subdir exists but holds no mask files
24542442
2455 result = fuse_segment(2443 result = fuse.fuse_segment(
2456 seg_dir=seg_dir,2444 seg_dir=seg_dir,
2457 seg_name="071",2445 seg_name="071",
2458 edges_dirs=[],2446 edges_dirs=[],
2459 xml_path=None,2447 xml_path=None,
Importance #60: tests/test_fusion.py @@ -2493,9 +2481,9 @@
24932481
2494def test_fuse_segment_vehicle_paints_unclassified_inside_corridor(tmp_path):2482def test_fuse_segment_vehicle_paints_unclassified_inside_corridor(tmp_path):
2495 seg_dir, edges = _vehicle_segment(tmp_path, "073")2483 seg_dir, edges = _vehicle_segment(tmp_path, "073")
24962484
2497 result = fuse_segment(2485 result = fuse.fuse_segment(
2498 seg_dir=seg_dir,2486 seg_dir=seg_dir,
2499 seg_name="073",2487 seg_name="073",
2500 edges_dirs=[edges],2488 edges_dirs=[edges],
2501 xml_path=None,2489 xml_path=None,
Importance #61: tests/test_fusion.py @@ -2552,9 +2540,9 @@
2552 gm / "a_tablecloth_masks.npz",2540 gm / "a_tablecloth_masks.npz",
2553 [True] * 7 + [False, False],2541 [True] * 7 + [False, False],
2554 )2542 )
25552543
2556 result = fuse_segment(2544 result = fuse.fuse_segment(
2557 seg_dir=seg_dir,2545 seg_dir=seg_dir,
2558 seg_name="074",2546 seg_name="074",
2559 edges_dirs=[edges],2547 edges_dirs=[edges],
2560 xml_path=None,2548 xml_path=None,
Importance #62: tests/test_fusion.py @@ -2573,9 +2561,9 @@
25732561
2574def test_fuse_segment_vehicle_skipped_without_corridor(tmp_path):2562def test_fuse_segment_vehicle_skipped_without_corridor(tmp_path):
2575 seg_dir, _ = _vehicle_segment(tmp_path, "075")2563 seg_dir, _ = _vehicle_segment(tmp_path, "075")
25762564
2577 result = fuse_segment(2565 result = fuse.fuse_segment(
2578 seg_dir=seg_dir,2566 seg_dir=seg_dir,
2579 seg_name="075",2567 seg_name="075",
2580 edges_dirs=[],2568 edges_dirs=[],
2581 xml_path=None,2569 xml_path=None,
Importance #63: tests/test_fusion.py @@ -2593,9 +2581,9 @@
2593 # so the corridor interior is the *un-painted* carriageway -- sweeping it2581 # so the corridor interior is the *un-painted* carriageway -- sweeping it
2594 # would relabel the whole road as vehicles.2582 # would relabel the whole road as vehicles.
2595 seg_dir, edges = _vehicle_segment(tmp_path, "076", with_run4=False)2583 seg_dir, edges = _vehicle_segment(tmp_path, "076", with_run4=False)
25962584
2597 result = fuse_segment(2585 result = fuse.fuse_segment(
2598 seg_dir=seg_dir,2586 seg_dir=seg_dir,
2599 seg_name="076",2587 seg_name="076",
2600 edges_dirs=[edges],2588 edges_dirs=[edges],
2601 xml_path=None,2589 xml_path=None,
Importance #64: tests/test_fusion.py @@ -2610,16 +2598,16 @@
26102598
2611def test_fuse_segment_vehicle_disabled_by_config(tmp_path):2599def test_fuse_segment_vehicle_disabled_by_config(tmp_path):
2612 seg_dir, edges = _vehicle_segment(tmp_path, "077")2600 seg_dir, edges = _vehicle_segment(tmp_path, "077")
26132601
2614 result = fuse_segment(2602 result = fuse.fuse_segment(
2615 seg_dir=seg_dir,2603 seg_dir=seg_dir,
2616 seg_name="077",2604 seg_name="077",
2617 edges_dirs=[edges],2605 edges_dirs=[edges],
2618 xml_path=None,2606 xml_path=None,
2619 guardrail_masks_dir=None,2607 guardrail_masks_dir=None,
2620 signs_masks_dir=None,2608 signs_masks_dir=None,
2621 config=Seg3dConfig(vehicle_enabled=False),2609 config=config.Seg3dConfig(vehicle_enabled=False),
2622 )2610 )
2623 assert (2611 assert (
2624 "vehicle: disabled by config (vehicle_enabled=false)"2612 "vehicle: disabled by config (vehicle_enabled=false)"
2625 in result.stats["skips"]2613 in result.stats["skips"]
Importance #65: tests/test_fusion.py @@ -2702,12 +2690,12 @@
2702 guard = classes.BY_NAME["guardrail"].las_code2690 guard = classes.BY_NAME["guardrail"].las_code
2703 support = classes.BY_NAME["guardrail_support"].las_code2691 support = classes.BY_NAME["guardrail_support"].las_code
2704 pts = np.array([[0.1, 0.1, 0.0], [0.2, 0.2, 0.0], [0.9, 0.9, 0.0]])2692 pts = np.array([[0.1, 0.1, 0.0], [0.2, 0.2, 0.0], [0.9, 0.9, 0.0]])
2705 cls = np.array([guard, guard, support], dtype=np.uint8)2693 cls = np.array([guard, guard, support], dtype=np.uint8)
2706 plut = classes.priority_lut(Seg3dConfig())2694 plut = classes.priority_lut(config.Seg3dConfig())
2707 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut)2695 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut)
2708 assert cls[rep[0]] == support2696 assert cls[rep[0]] == support
2709 plut_flat = classes.priority_lut(Seg3dConfig(priority_support=4))2697 plut_flat = classes.priority_lut(config.Seg3dConfig(priority_support=4))
2710 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut_flat)2698 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut_flat)
2711 assert cls[rep[0]] == guard2699 assert cls[rep[0]] == guard
27122700
27132701
Importance #66: tests/test_fusion.py @@ -2762,9 +2750,9 @@
2762 is_surface=np.zeros(pts.shape[0], bool),2750 is_surface=np.zeros(pts.shape[0], bool),
2763 records=[io_npz.Record("a_run3_points.npz", 0, pts.shape[0])],2751 records=[io_npz.Record("a_run3_points.npz", 0, pts.shape[0])],
2764 surface_match_rate=0.0,2752 surface_match_rate=0.0,
2765 )2753 )
2766 return FuseResult(2754 return fuse.FuseResult(
2767 cloud=cloud,2755 cloud=cloud,
2768 classification=cls,2756 classification=cls,
2769 rep_index=np.arange(pts.shape[0]),2757 rep_index=np.arange(pts.shape[0]),
2770 instances=[],2758 instances=[],
Importance #67: tests/test_fusion.py @@ -3058,13 +3046,13 @@
3058# --------------------------------------------------------------------------- #3046# --------------------------------------------------------------------------- #
3059def test_parse_segments():3047def test_parse_segments():
3060 # seg3d semantics the CLI relies on: first-seen order, ascending-only3048 # seg3d semantics the CLI relies on: first-seen order, ascending-only
3061 # ranges, duplicates dropped -- the shared parser's defaults.3049 # ranges, duplicates dropped -- the shared parser's defaults.
3062 assert parse_segment_ids("066-069,038") == [66, 67, 68, 69, 38]3050 assert segments.parse_segment_ids("066-069,038") == [66, 67, 68, 69, 38]
3063 assert parse_segment_ids("5") == [5]3051 assert segments.parse_segment_ids("5") == [5]
3064 assert parse_segment_ids("1,1,2") == [1, 2]3052 assert segments.parse_segment_ids("1,1,2") == [1, 2]
3065 assert parse_segment_ids("074-066") == []3053 assert segments.parse_segment_ids("074-066") == []
3066 assert parse_segment_names("066-068,38") == ["066", "067", "068", "038"]3054 assert segments.parse_segment_names("066-068,38") == ["066", "067", "068", "038"]
30673055
30683056
3069def test_extend_polyline_reaches_beyond_ends():3057def test_extend_polyline_reaches_beyond_ends():
3070 xy = np.column_stack([np.linspace(0, 10, 11), np.zeros(11)])3058 xy = np.column_stack([np.linspace(0, 10, 11), np.zeros(11)])
Importance #68: tests/test_fusion.py @@ -3137,9 +3125,9 @@
3137 io_npz.Record("a_run3_points.npz", 0, 3),3125 io_npz.Record("a_run3_points.npz", 0, 3),
3138 io_npz.Record("b_run3_points.npz", 3, 3),3126 io_npz.Record("b_run3_points.npz", 3, 3),
3139 ]3127 ]
3140 rep, vop = voxel.decimate_with_map(pts, cls, voxel_size)3128 rep, vop = voxel.decimate_with_map(pts, cls, voxel_size)
3141 return FuseResult(3129 return fuse.FuseResult(
3142 cloud=_map_cloud(pts, records),3130 cloud=_map_cloud(pts, records),
3143 classification=cls,3131 classification=cls,
3144 rep_index=rep,3132 rep_index=rep,
3145 instances=[],3133 instances=[],
Importance #69: tests/test_fusion.py @@ -3373,9 +3361,9 @@
3373 [[0.0, 1.0, 0.0], [0.3, 1.2, 0.1], [4.0, 1.0, 0.0], [9.0, 1.0, 0.0]]3361 [[0.0, 1.0, 0.0], [0.3, 1.2, 0.1], [4.0, 1.0, 0.0], [9.0, 1.0, 0.0]]
3374 )3362 )
3375 _write_run3(seg_dir / "a_run3_points.npz", pts)3363 _write_run3(seg_dir / "a_run3_points.npz", pts)
33763364
3377 result = fuse_segment(3365 result = fuse.fuse_segment(
3378 seg_dir=seg_dir,3366 seg_dir=seg_dir,
3379 seg_name="078",3367 seg_name="078",
3380 edges_dirs=[],3368 edges_dirs=[],
3381 xml_path=None,3369 xml_path=None,
Importance #70: tests/test_fusion.py @@ -3456,9 +3444,9 @@
34563444
3457 pts = np.empty((0, 3))3445 pts = np.empty((0, 3))
3458 cls = np.empty(0, dtype=np.uint8)3446 cls = np.empty(0, dtype=np.uint8)
3459 rep, vop = voxel.decimate_with_map(pts, cls, 1.0)3447 rep, vop = voxel.decimate_with_map(pts, cls, 1.0)
3460 result = FuseResult(3448 result = fuse.FuseResult(
3461 cloud=_map_cloud(pts, []),3449 cloud=_map_cloud(pts, []),
3462 classification=cls,3450 classification=cls,
3463 rep_index=rep,3451 rep_index=rep,
3464 instances=[],3452 instances=[],
Importance #71: tests/test_fusion.py @@ -3528,10 +3516,10 @@
3528 pts[0] = [10.0, 20.0, 30.0]3516 pts[0] = [10.0, 20.0, 30.0]
3529 pts[1] = [10.009, 20.009, 30.009]3517 pts[1] = [10.009, 20.009, 30.009]
3530 pts[2] = [10.011, 20.0, 30.0]3518 pts[2] = [10.011, 20.0, 30.0]
35313519
3532 keys = voxel._voxel_keys(pts, Seg3dConfig().voxel_size_m)3520 keys = voxel._voxel_keys(pts, config.Seg3dConfig().voxel_size_m)
3533 ijk = np.floor(pts / Seg3dConfig().voxel_size_m).astype(np.int64)3521 ijk = np.floor(pts / config.Seg3dConfig().voxel_size_m).astype(np.int64)
3534 # Same voxel <=> same key, in both directions: the two labelings3522 # Same voxel <=> same key, in both directions: the two labelings
3535 # differ only by a permutation, so their pairing has as many distinct3523 # differ only by a permutation, so their pairing has as many distinct
3536 # values as either side alone (no merge, no split).3524 # values as either side alone (no merge, no split).
3537 _, want = np.unique(ijk, axis=0, return_inverse=True)3525 _, want = np.unique(ijk, axis=0, return_inverse=True)
Importance #72: tests/test_fusion.py @@ -3586,9 +3574,9 @@
3586 """Config with the tiny-cloud DTM floor, plus the test's overrides."""3574 """Config with the tiny-cloud DTM floor, plus the test's overrides."""
3587 # Five asphalt rows are a ground surface here; production wants 1000.3575 # Five asphalt rows are a ground surface here; production wants 1000.
3588 params = {"vegetation_min_ground_points": 4, **_VEG_LEGACY}3576 params = {"vegetation_min_ground_points": 4, **_VEG_LEGACY}
3589 params.update(overrides)3577 params.update(overrides)
3590 return Seg3dConfig(**params)3578 return config.Seg3dConfig(**params)
35913579
35923580
3593def _veg_segment(tmp_path, seg_name, *, extra=(), kept_mask=None):3581def _veg_segment(tmp_path, seg_name, *, extra=(), kept_mask=None):
3594 """Builds the vegetation scenario segment.3582 """Builds the vegetation scenario segment.
Importance #73: tests/test_fusion.py @@ -3635,22 +3623,22 @@
3635 )3623 )
3636 return seg_dir, edges, ground_dir3624 return seg_dir, edges, ground_dir
36373625
36383626
3639def _fuse_veg(tmp_path, seg_name, *, config=None, extra=(), kept_mask=None,3627def _fuse_veg(tmp_path, seg_name, *, config_override=None, extra=(), kept_mask=None,
3640 signs_dir=None, guardrail_dir=None):3628 signs_dir=None, guardrail_dir=None):
3641 seg_dir, edges, ground_dir = _veg_segment(3629 seg_dir, edges, ground_dir = _veg_segment(
3642 tmp_path, seg_name, extra=extra, kept_mask=kept_mask3630 tmp_path, seg_name, extra=extra, kept_mask=kept_mask
3643 )3631 )
3644 return fuse_segment(3632 return fuse.fuse_segment(
3645 seg_dir=seg_dir,3633 seg_dir=seg_dir,
3646 seg_name=seg_name,3634 seg_name=seg_name,
3647 edges_dirs=[edges],3635 edges_dirs=[edges],
3648 xml_path=None,3636 xml_path=None,
3649 guardrail_masks_dir=guardrail_dir,3637 guardrail_masks_dir=guardrail_dir,
3650 signs_masks_dir=signs_dir,3638 signs_masks_dir=signs_dir,
3651 ground_masks_dir=ground_dir,3639 ground_masks_dir=ground_dir,
3652 config=config or _veg_config(),3640 config=config_override or _veg_config(),
3653 )3641 )
36543642
36553643
3656def _write_guardrail_mask(tmp_path, seg_name, instances, record_name="a"):3644def _write_guardrail_mask(tmp_path, seg_name, instances, record_name="a"):
Importance #74: tests/test_fusion.py @@ -3711,9 +3699,9 @@
37113699
37123700
3713def test_fuse_segment_vegetation_tall_class_tree(tmp_path):3701def test_fuse_segment_vegetation_tall_class_tree(tmp_path):
3714 result = _fuse_veg(3702 result = _fuse_veg(
3715 tmp_path, "081", config=_veg_config(vegetation_tall_class="tree")3703 tmp_path, "081", config_override=_veg_config(vegetation_tall_class="tree")
3716 )3704 )
3717 cls = result.classification3705 cls = result.classification
3718 assert cls[5] == classes.BY_NAME["low_vegetation"].las_code3706 assert cls[5] == classes.BY_NAME["low_vegetation"].las_code
3719 assert cls[6] == classes.BY_NAME["medium_vegetation"].las_code3707 assert cls[6] == classes.BY_NAME["medium_vegetation"].las_code
Importance #75: tests/test_fusion.py @@ -3722,9 +3710,9 @@
37223710
3723def test_fuse_segment_vegetation_tall_class_unclassified(tmp_path):3711def test_fuse_segment_vegetation_tall_class_unclassified(tmp_path):
3724 result = _fuse_veg(3712 result = _fuse_veg(
3725 tmp_path, "082",3713 tmp_path, "082",
3726 config=_veg_config(vegetation_tall_class="unclassified"),3714 config_override=_veg_config(vegetation_tall_class="unclassified"),
3727 )3715 )
3728 # "unclassified" LEAVES the tall band as it was rather than stripping it.3716 # "unclassified" LEAVES the tall band as it was rather than stripping it.
3729 assert result.classification[7] == classes.UNCLASSIFIED_CODE3717 assert result.classification[7] == classes.UNCLASSIFIED_CODE
3730 assert result.stats["guard_metrics"]["vegetation_tall_points"] == 1.03718 assert result.stats["guard_metrics"]["vegetation_tall_points"] == 1.0
Importance #76: tests/test_fusion.py @@ -3732,9 +3720,9 @@
37323720
3733def test_fuse_segment_vegetation_corridor_rule_drops_the_median(tmp_path):3721def test_fuse_segment_vegetation_corridor_rule_drops_the_median(tmp_path):
3734 result = _fuse_veg(3722 result = _fuse_veg(
3735 tmp_path, "083",3723 tmp_path, "083",
3736 config=_veg_config(vegetation_asphalt_rule="corridor"),3724 config_override=_veg_config(vegetation_asphalt_rule="corridor"),
3737 )3725 )
3738 cls = result.classification3726 cls = result.classification
3739 # Inside the polygon -> not vegetation, and the sweep takes it instead.3727 # Inside the polygon -> not vegetation, and the sweep takes it instead.
3740 assert cls[10] == classes.BY_NAME["vehicle"].las_code3728 assert cls[10] == classes.BY_NAME["vehicle"].las_code
Importance #77: tests/test_fusion.py @@ -3760,9 +3748,9 @@
3760 assert np.all(column.classification[rows] == medium)3748 assert np.all(column.classification[rows] == medium)
37613749
3762 per_point = _fuse_veg(3750 per_point = _fuse_veg(
3763 tmp_path, "085", extra=hedge,3751 tmp_path, "085", extra=hedge,
3764 config=_veg_config(vegetation_band_mode="point"),3752 config_override=_veg_config(vegetation_band_mode="point"),
3765 )3753 )
3766 # Per point the same hedge grows a low skirt -- what the column rule is3754 # Per point the same hedge grows a low skirt -- what the column rule is
3767 # there to prevent.3755 # there to prevent.
3768 assert per_point.classification[11] == low3756 assert per_point.classification[11] == low
Importance #78: tests/test_fusion.py @@ -3775,15 +3763,15 @@
3775 ground_code = classes.BY_NAME["ground"].las_code3763 ground_code = classes.BY_NAME["ground"].las_code
37763764
3777 on = _fuse_veg(3765 on = _fuse_veg(
3778 tmp_path, "086", kept_mask=kept,3766 tmp_path, "086", kept_mask=kept,
3779 config=_veg_config(vegetation_from_ground=True),3767 config_override=_veg_config(vegetation_from_ground=True),
3780 )3768 )
3781 assert on.classification[5] == classes.BY_NAME["low_vegetation"].las_code3769 assert on.classification[5] == classes.BY_NAME["low_vegetation"].las_code
37823770
3783 off = _fuse_veg(3771 off = _fuse_veg(
3784 tmp_path, "087", kept_mask=kept,3772 tmp_path, "087", kept_mask=kept,
3785 config=_veg_config(vegetation_from_ground=False),3773 config_override=_veg_config(vegetation_from_ground=False),
3786 )3774 )
3787 assert off.classification[5] == ground_code3775 assert off.classification[5] == ground_code
3788 assert off.stats["guard_metrics"]["vegetation_candidates"] == 5.03776 assert off.stats["guard_metrics"]["vegetation_candidates"] == 5.0
37893777
Importance #79: tests/test_fusion.py @@ -3797,9 +3785,9 @@
37973785
37983786
3799def test_fuse_segment_vegetation_disabled(tmp_path):3787def test_fuse_segment_vegetation_disabled(tmp_path):
3800 result = _fuse_veg(3788 result = _fuse_veg(
3801 tmp_path, "088", config=_veg_config(vegetation_enabled=False)3789 tmp_path, "088", config_override=_veg_config(vegetation_enabled=False)
3802 )3790 )
3803 cls = result.classification3791 cls = result.classification
3804 assert np.all(cls[5:9] == classes.UNCLASSIFIED_CODE)3792 assert np.all(cls[5:9] == classes.UNCLASSIFIED_CODE)
3805 # A fuse-level skip carries the SAME metric keys as a run, all zero,3793 # A fuse-level skip carries the SAME metric keys as a run, all zero,
Importance #80: tests/test_fusion.py @@ -3826,9 +3814,9 @@
3826 _write_edges(3814 _write_edges(
3827 edges / "segment_089_edges.npz",3815 edges / "segment_089_edges.npz",
3828 left=[[0, -4, 0], [4, -4, 0]], right=[[0, 4, 0], [4, 4, 0]],3816 left=[[0, -4, 0], [4, -4, 0]], right=[[0, 4, 0], [4, 4, 0]],
3829 )3817 )
3830 result = fuse_segment(3818 result = fuse.fuse_segment(
3831 seg_dir=seg_dir,3819 seg_dir=seg_dir,
3832 seg_name="089",3820 seg_name="089",
3833 edges_dirs=[edges],3821 edges_dirs=[edges],
3834 xml_path=None,3822 xml_path=None,
Importance #81: tests/test_fusion.py @@ -3841,9 +3829,9 @@
38413829
38423830
3843def test_fuse_segment_vegetation_skips_without_a_ground_surface(tmp_path):3831def test_fuse_segment_vegetation_skips_without_a_ground_surface(tmp_path):
3844 # The packaged floor (1000 hard points) against five asphalt rows.3832 # The packaged floor (1000 hard points) against five asphalt rows.
3845 result = _fuse_veg(tmp_path, "090", config=Seg3dConfig())3833 result = _fuse_veg(tmp_path, "090", config_override=config.Seg3dConfig())
3846 skip = [s for s in result.stats["skips"] if s.startswith("vegetation")]3834 skip = [s for s in result.stats["skips"] if s.startswith("vegetation")]
3847 assert skip == ["vegetation: too few hard-surface points (5 < 1000)"]3835 assert skip == ["vegetation: too few hard-surface points (5 < 1000)"]
3848 assert result.classification[5] == classes.UNCLASSIFIED_CODE3836 assert result.classification[5] == classes.UNCLASSIFIED_CODE
3849 assert result.stats["guard_metrics"]["vegetation_candidates"] == 0.03837 assert result.stats["guard_metrics"]["vegetation_candidates"] == 0.0
Importance #82: tests/test_fusion.py @@ -3877,9 +3865,9 @@
3877 mask=[(0, list(range(11, 16)))],3865 mask=[(0, list(range(11, 16)))],
3878 )3866 )
3879 result = _fuse_veg(3867 result = _fuse_veg(
3880 tmp_path, "092", extra=tree, signs_dir=signs,3868 tmp_path, "092", extra=tree, signs_dir=signs,
3881 config=_veg_config(vegetation_tree_min_height_m=3.0),3869 config_override=_veg_config(vegetation_tree_min_height_m=3.0),
3882 )3870 )
3883 medium = classes.BY_NAME["medium_vegetation"].las_code3871 medium = classes.BY_NAME["medium_vegetation"].las_code
3884 # The whole instance moves, never point by point.3872 # The whole instance moves, never point by point.
3885 assert np.all(result.classification[11:16] == medium)3873 assert np.all(result.classification[11:16] == medium)
Importance #83: tests/test_fusion.py @@ -3895,9 +3883,9 @@
3895 # stage shipped with, limiters or not.3883 # stage shipped with, limiters or not.
3896 result = _fuse_veg(3884 result = _fuse_veg(
3897 tmp_path,3885 tmp_path,
3898 "089",3886 "089",
3899 config=Seg3dConfig(3887 config_override=config.Seg3dConfig(
3900 vegetation_min_ground_points=4,3888 vegetation_min_ground_points=4,
3901 vegetation_green_rg_ratio=1.0,3889 vegetation_green_rg_ratio=1.0,
3902 vegetation_asphalt_cell_m=0.5,3890 vegetation_asphalt_cell_m=0.5,
3903 vegetation_asphalt_dilate_cells=1,3891 vegetation_asphalt_dilate_cells=1,
Importance #84: tests/test_fusion.py @@ -3936,9 +3924,9 @@
3936 # points (so the column guard rejects nothing), and the scene has no3924 # points (so the column guard rejects nothing), and the scene has no
3937 # barrier, so the corridor-barrier rule takes both in-corridor rows3925 # barrier, so the corridor-barrier rule takes both in-corridor rows
3938 # instead and the sweep gets them.3926 # instead and the sweep gets them.
3939 result = _fuse_veg(3927 result = _fuse_veg(
3940 tmp_path, "090", config=Seg3dConfig(vegetation_min_ground_points=4)3928 tmp_path, "090", config_override=config.Seg3dConfig(vegetation_min_ground_points=4)
3941 )3929 )
3942 cls = result.classification3930 cls = result.classification
3943 vehicle = classes.BY_NAME["vehicle"].las_code3931 vehicle = classes.BY_NAME["vehicle"].las_code
3944 assert list(cls[5:9]) == [classes.UNCLASSIFIED_CODE] * 43932 assert list(cls[5:9]) == [classes.UNCLASSIFIED_CODE] * 4
Importance #85: tests/test_fusion.py @@ -3980,9 +3968,9 @@
3980 # leaves behind. With no barrier next to it the rule hands it back to3968 # leaves behind. With no barrier next to it the rule hands it back to
3981 # the vehicle sweep.3969 # the vehicle sweep.
3982 result = _fuse_veg(3970 result = _fuse_veg(
3983 tmp_path, "093",3971 tmp_path, "093",
3984 config=_veg_config(vegetation_corridor_rail_m=2.0),3972 config_override=_veg_config(vegetation_corridor_rail_m=2.0),
3985 )3973 )
3986 cls = result.classification3974 cls = result.classification
3987 vehicle = classes.BY_NAME["vehicle"].las_code3975 vehicle = classes.BY_NAME["vehicle"].las_code
3988 assert cls[10] == vehicle3976 assert cls[10] == vehicle
Importance #86: tests/test_fusion.py @@ -4004,9 +3992,9 @@
4004 rail = [(2.5, -2.0, 0.5, GREY_RGB)] # row 113992 rail = [(2.5, -2.0, 0.5, GREY_RGB)] # row 11
4005 guardrails = _write_guardrail_mask(tmp_path, "094", [("w_beam", [11])])3993 guardrails = _write_guardrail_mask(tmp_path, "094", [("w_beam", [11])])
4006 result = _fuse_veg(3994 result = _fuse_veg(
4007 tmp_path, "094", extra=rail, guardrail_dir=guardrails,3995 tmp_path, "094", extra=rail, guardrail_dir=guardrails,
4008 config=_veg_config(vegetation_corridor_rail_m=2.0),3996 config_override=_veg_config(vegetation_corridor_rail_m=2.0),
4009 )3997 )
4010 cls = result.classification3998 cls = result.classification
4011 assert cls[11] == classes.BY_NAME["guardrail"].las_code3999 assert cls[11] == classes.BY_NAME["guardrail"].las_code
4012 assert cls[10] == classes.BY_NAME["medium_vegetation"].las_code4000 assert cls[10] == classes.BY_NAME["medium_vegetation"].las_code
Importance #87: tests/test_fusion.py @@ -4027,9 +4015,9 @@
4027 ]4015 ]
4028 guardrails = _write_guardrail_mask(tmp_path, "095", [("w_beam", [11])])4016 guardrails = _write_guardrail_mask(tmp_path, "095", [("w_beam", [11])])
4029 result = _fuse_veg(4017 result = _fuse_veg(
4030 tmp_path, "095", extra=extra, guardrail_dir=guardrails,4018 tmp_path, "095", extra=extra, guardrail_dir=guardrails,
4031 config=_veg_config(4019 config_override=_veg_config(
4032 vegetation_corridor_rail_m=2.0,4020 vegetation_corridor_rail_m=2.0,
4033 vegetation_corridor_max_height_m=0.5,4021 vegetation_corridor_max_height_m=0.5,
4034 ),4022 ),
4035 )4023 )
Importance #88: tests/test_fusion.py @@ -4053,9 +4041,9 @@
4053 ]4041 ]
4054 guardrails = _write_guardrail_mask(tmp_path, "096", [("w_beam", [11])])4042 guardrails = _write_guardrail_mask(tmp_path, "096", [("w_beam", [11])])
4055 result = _fuse_veg(4043 result = _fuse_veg(
4056 tmp_path, "096", extra=extra, guardrail_dir=guardrails,4044 tmp_path, "096", extra=extra, guardrail_dir=guardrails,
4057 config=_veg_config(4045 config_override=_veg_config(
4058 vegetation_corridor_rail_m=2.0,4046 vegetation_corridor_rail_m=2.0,
4059 vegetation_corridor_max_height_m=0.0,4047 vegetation_corridor_max_height_m=0.0,
4060 ),4048 ),
4061 )4049 )
Importance #89: tests/test_fusion.py @@ -4068,9 +4056,9 @@
40684056
4069# --------------------------------------------------------------------------- #4057# --------------------------------------------------------------------------- #
4070# vegetation: re-band ownership and instance identity (AI3D-373)4058# vegetation: re-band ownership and instance identity (AI3D-373)
4071# --------------------------------------------------------------------------- #4059# --------------------------------------------------------------------------- #
4072def _short_tree_with_a_support(tmp_path, seg_name, config):4060def _short_tree_with_a_support(tmp_path, seg_name, config_override):
4073 """Fuses a short detector tree whose last two rows a support took."""4061 """Fuses a short detector tree whose last two rows a support took."""
4074 tree = [(8.0, 10.0, float(z), GREEN_RGB) for z in np.linspace(0.0, 1.0, 5)]4062 tree = [(8.0, 10.0, float(z), GREEN_RGB) for z in np.linspace(0.0, 1.0, 5)]
4075 signs = tmp_path / f"signs_{seg_name}"4063 signs = tmp_path / f"signs_{seg_name}"
4076 _write_signs_sidecars(4064 _write_signs_sidecars(
Importance #90: tests/test_fusion.py @@ -4082,9 +4070,9 @@
4082 tmp_path, seg_name, [("guardrail_support", [14, 15])]4070 tmp_path, seg_name, [("guardrail_support", [14, 15])]
4083 )4071 )
4084 return _fuse_veg(4072 return _fuse_veg(
4085 tmp_path, seg_name, extra=tree, signs_dir=signs,4073 tmp_path, seg_name, extra=tree, signs_dir=signs,
4086 guardrail_dir=guardrails, config=config,4074 guardrail_dir=guardrails, config_override=config_override,
4087 )4075 )
40884076
40894077
4090def test_fuse_segment_reband_leaves_rows_a_support_took_from_the_tree(4078def test_fuse_segment_reband_leaves_rows_a_support_took_from_the_tree(
Importance #91: tests/test_fusion.py @@ -4177,9 +4165,9 @@
4177 seg_dir / "a_run3_points.npz", pts,4165 seg_dir / "a_run3_points.npz", pts,
4178 rgb=np.zeros((pts.shape[0], 3), dtype=np.uint16),4166 rgb=np.zeros((pts.shape[0], 3), dtype=np.uint16),
4179 )4167 )
4180 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])4168 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])
4181 result = fuse_segment(4169 result = fuse.fuse_segment(
4182 seg_dir=seg_dir,4170 seg_dir=seg_dir,
4183 seg_name="101",4171 seg_name="101",
4184 edges_dirs=[],4172 edges_dirs=[],
4185 xml_path=None,4173 xml_path=None,
Importance #92: tests/test_fusion.py @@ -4207,9 +4195,9 @@
4207 rgb = np.zeros((pts.shape[0], 3), dtype=np.uint16)4195 rgb = np.zeros((pts.shape[0], 3), dtype=np.uint16)
4208 rgb[:, 0] = 300004196 rgb[:, 0] = 30000
4209 _write_run3(seg_dir / "a_run3_points.npz", pts, rgb=rgb)4197 _write_run3(seg_dir / "a_run3_points.npz", pts, rgb=rgb)
4210 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])4198 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])
4211 result = fuse_segment(4199 result = fuse.fuse_segment(
4212 seg_dir=seg_dir,4200 seg_dir=seg_dir,
4213 seg_name="102",4201 seg_name="102",
4214 edges_dirs=[],4202 edges_dirs=[],
4215 xml_path=None,4203 xml_path=None,
Importance #93: tests/test_fusion.py @@ -4230,22 +4218,22 @@
4230 # a stage skip: without it a run that could not guard the carriageway4218 # a stage skip: without it a run that could not guard the carriageway
4231 # looked exactly like one whose guards found nothing.4219 # looked exactly like one whose guards found nothing.
4232 kept = [True] * 5 + [False] * 6 # the tablecloth carries the DTM here4220 kept = [True] * 5 + [False] * 6 # the tablecloth carries the DTM here
4233 seg_dir, _, ground_dir = _veg_segment(tmp_path, "103", kept_mask=kept)4221 seg_dir, _, ground_dir = _veg_segment(tmp_path, "103", kept_mask=kept)
4234 config = _veg_config(4222 cfg = _veg_config(
4235 vegetation_corridor_rail_m=2.0,4223 vegetation_corridor_rail_m=2.0,
4236 vegetation_corridor_max_height_m=0.5,4224 vegetation_corridor_max_height_m=0.5,
4237 )4225 )
42384226
4239 result = fuse_segment(4227 result = fuse.fuse_segment(
4240 seg_dir=seg_dir,4228 seg_dir=seg_dir,
4241 seg_name="103",4229 seg_name="103",
4242 edges_dirs=[],4230 edges_dirs=[],
4243 xml_path=None,4231 xml_path=None,
4244 guardrail_masks_dir=None,4232 guardrail_masks_dir=None,
4245 signs_masks_dir=None,4233 signs_masks_dir=None,
4246 ground_masks_dir=ground_dir,4234 ground_masks_dir=ground_dir,
4247 config=config,4235 config=cfg,
4248 )4236 )
42494237
4250 assert (4238 assert (
4251 "vegetation: no corridor -> corridor guards inert"4239 "vegetation: no corridor -> corridor guards inert"
Importance #94: tests/test_fusion.py @@ -4271,9 +4259,9 @@
4271 # corridor means no exclusion at all.4259 # corridor means no exclusion at all.
4272 kept = [True] * 5 + [False] * 64260 kept = [True] * 5 + [False] * 6
4273 seg_dir, _, ground_dir = _veg_segment(tmp_path, "104", kept_mask=kept)4261 seg_dir, _, ground_dir = _veg_segment(tmp_path, "104", kept_mask=kept)
42744262
4275 result = fuse_segment(4263 result = fuse.fuse_segment(
4276 seg_dir=seg_dir,4264 seg_dir=seg_dir,
4277 seg_name="104",4265 seg_name="104",
4278 edges_dirs=[],4266 edges_dirs=[],
4279 xml_path=None,4267 xml_path=None,
Importance #95: tests/test_fusion.py @@ -4300,9 +4288,9 @@
4300 d = np.full(axy.shape[0], np.inf)4288 d = np.full(axy.shape[0], np.inf)
4301 ok = np.zeros(axy.shape[0], dtype=bool)4289 ok = np.zeros(axy.shape[0], dtype=bool)
4302 if vxyz.shape[0] == 0:4290 if vxyz.shape[0] == 0:
4303 return d, ok4291 return d, ok
4304 tree = cKDTree(vxyz[:, :2])4292 tree = spatial.cKDTree(vxyz[:, :2])
4305 vz = vxyz[:, 2]4293 vz = vxyz[:, 2]
4306 for idx, cand in enumerate(tree.query_ball_point(axy, r=xy_radius)):4294 for idx, cand in enumerate(tree.query_ball_point(axy, r=xy_radius)):
4307 if not cand:4295 if not cand:
4308 continue4296 continue
Importance #96: tests/test_fusion.py @@ -4321,9 +4309,9 @@
4321def test_nearest_gated_vertex_matches_the_per_point_reference(seed):4309def test_nearest_gated_vertex_matches_the_per_point_reference(seed):
4322 rng = np.random.default_rng(seed)4310 rng = np.random.default_rng(seed)
4323 pts = rng.uniform(-1.0, 1.0, size=(120, 3))4311 pts = rng.uniform(-1.0, 1.0, size=(120, 3))
4324 verts = rng.uniform(-1.0, 1.0, size=(40, 3))4312 verts = rng.uniform(-1.0, 1.0, size=(40, 3))
4325 got_d, got_ok = fuse_mod._nearest_gated_vertex(4313 got_d, got_ok = fuse._nearest_gated_vertex(
4326 pts[:, :2], pts[:, 2], verts, 0.25, 0.44314 pts[:, :2], pts[:, 2], verts, 0.25, 0.4
4327 )4315 )
4328 want_d, want_ok = _reference_nearest_gated_vertex(4316 want_d, want_ok = _reference_nearest_gated_vertex(
4329 pts[:, :2], pts[:, 2], verts, 0.25, 0.44317 pts[:, :2], pts[:, 2], verts, 0.25, 0.4
Importance #97: tests/test_fusion.py @@ -4337,13 +4325,13 @@
4337 """Blocking the radius join must not change a single distance."""4325 """Blocking the radius join must not change a single distance."""
4338 rng = np.random.default_rng(7)4326 rng = np.random.default_rng(7)
4339 pts = rng.uniform(-1.0, 1.0, size=(200, 3))4327 pts = rng.uniform(-1.0, 1.0, size=(200, 3))
4340 verts = rng.uniform(-1.0, 1.0, size=(60, 3))4328 verts = rng.uniform(-1.0, 1.0, size=(60, 3))
4341 one_d, one_ok = fuse_mod._nearest_gated_vertex(4329 one_d, one_ok = fuse._nearest_gated_vertex(
4342 pts[:, :2], pts[:, 2], verts, 0.3, 0.54330 pts[:, :2], pts[:, 2], verts, 0.3, 0.5
4343 )4331 )
4344 monkeypatch.setattr(fuse_mod, "_LINE_QUERY_BLOCK", 13)4332 monkeypatch.setattr(fuse, "_LINE_QUERY_BLOCK", 13)
4345 many_d, many_ok = fuse_mod._nearest_gated_vertex(4333 many_d, many_ok = fuse._nearest_gated_vertex(
4346 pts[:, :2], pts[:, 2], verts, 0.3, 0.54334 pts[:, :2], pts[:, 2], verts, 0.3, 0.5
4347 )4335 )
4348 np.testing.assert_array_equal(one_ok, many_ok)4336 np.testing.assert_array_equal(one_ok, many_ok)
4349 np.testing.assert_allclose(one_d, many_d)4337 np.testing.assert_allclose(one_d, many_d)
Importance #98: tests/test_fusion.py @@ -4359,9 +4347,9 @@
4359 verts = lines_xml.LineVertices(4347 verts = lines_xml.LineVertices(
4360 solid_xyz=solid_xyz, dashed_xyz=dashed_xyz4348 solid_xyz=solid_xyz, dashed_xyz=dashed_xyz
4361 )4349 )
43624350
4363 solid, dashed = _paint_lines(4351 solid, dashed = fuse._paint_lines(
4364 pts, is_asphalt, verts, xy_radius=0.3, z_gate=0.44352 pts, is_asphalt, verts, xy_radius=0.3, z_gate=0.4
4365 )4353 )
43664354
4367 asph_idx = np.nonzero(is_asphalt)[0]4355 asph_idx = np.nonzero(is_asphalt)[0]
Importance #99: tests/test_fusion.py @@ -4437,9 +4425,9 @@
4437 dtype=np.uint8,4425 dtype=np.uint8,
4438 )4426 )
44394427
4440 got = start.copy()4428 got = start.copy()
4441 fuse_mod._paint_mask_rows(got, rows, seg_classes)4429 fuse._paint_mask_rows(got, rows, seg_classes)
4442 want = start.copy()4430 want = start.copy()
4443 _reference_paint_mask_rows(want, rows, seg_classes)4431 _reference_paint_mask_rows(want, rows, seg_classes)
44444432
4445 np.testing.assert_array_equal(got, want)4433 np.testing.assert_array_equal(got, want)
Importance #100: tests/test_fusion.py @@ -4455,9 +4443,9 @@
4455 ],4443 ],
4456 dtype=np.uint8,4444 dtype=np.uint8,
4457 )4445 )
4458 got = start.copy()4446 got = start.copy()
4459 fuse_mod._paint_mask_rows(4447 fuse._paint_mask_rows(
4460 got,4448 got,
4461 np.array([0, 1, 2]),4449 np.array([0, 1, 2]),
4462 np.array(["low_vegetation"] * 3, dtype=np.str_),4450 np.array(["low_vegetation"] * 3, dtype=np.str_),
4463 )4451 )
Importance #101: tests/test_fusion.py @@ -4472,9 +4460,9 @@
44724460
44734461
4474def test_paint_mask_rows_gives_a_contested_row_to_the_support():4462def test_paint_mask_rows_gives_a_contested_row_to_the_support():
4475 got = np.array([classes.UNCLASSIFIED_CODE] * 2, dtype=np.uint8)4463 got = np.array([classes.UNCLASSIFIED_CODE] * 2, dtype=np.uint8)
4476 fuse_mod._paint_mask_rows(4464 fuse._paint_mask_rows(
4477 got,4465 got,
4478 np.array([0, 0, 1, 1]),4466 np.array([0, 0, 1, 1]),
4479 np.array(4467 np.array(
4480 ["guardrail_support", "guardrail", "guardrail_top_rail",4468 ["guardrail_support", "guardrail", "guardrail_top_rail",
Importance #102: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -8,26 +8,13 @@
8(the road-surface key set, its recall metric) and the `SegmentCloud` shape the8(the road-surface key set, its recall metric) and the `SegmentCloud` shape the
9fusion pipeline consumes.9fusion pipeline consumes.
10"""10"""
1111
12from dataclasses import dataclass, field12import dataclasses
13from pathlib import Path13import pathlib
1414
15import numpy as np15import numpy as np
16from iolabs.common.point_hash import (16from iolabs.common import point_hash, segment_points_io, segments
17 DEFAULT_UNITS_PER_M,
18 key_match_rate,
19 position_keys,
20)
21from iolabs.common.segment_points_io import (
22 GEOSHIFT_NAME,
23 RecordSpan,
24 load_run3_segment,
25)
26from iolabs.common.segment_points_io import (
27 load_geoshift as _load_geoshift_file,
28)
29from iolabs.common.segments import segment_record_files
3017
31# Storage dtypes every run3 record is coerced to on load. These are the18# Storage dtypes every run3 record is coerced to on load. These are the
32# historical seg3d coercions and they are load-bearing: `points` must stay19# historical seg3d coercions and they are load-bearing: `points` must stay
33# float64 all the way into the hash rounding, and the uint16/int8 channels are20# float64 all the way into the hash rounding, and the uint16/int8 channels are
Importance #103: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -61,13 +48,13 @@
61#: Filename suffix of the run4 road-surface records joined against run3.48#: Filename suffix of the run4 road-surface records joined against run3.
62RUN4_SURFACE_SUFFIX = "_run4_road_surface.npz"49RUN4_SURFACE_SUFFIX = "_run4_road_surface.npz"
6350
64#: One input record's identity and row range within the concatenated cloud.51#: One input record's identity and row range within the concatenated cloud.
65Record = RecordSpan52Record = segment_points_io.RecordSpan
6653
6754
68def surface_keys(55def surface_keys(
69 xyz: np.ndarray, units_per_m: float = DEFAULT_UNITS_PER_M56 xyz: np.ndarray, units_per_m: float = point_hash.DEFAULT_UNITS_PER_M
70) -> np.ndarray:57) -> np.ndarray:
71 """Returns structured integer position keys for a `(N, 3)` coordinate array.58 """Returns structured integer position keys for a `(N, 3)` coordinate array.
7259
73 Thin wrapper over :func:`iolabs.common.point_hash.position_keys`, kept so60 Thin wrapper over :func:`iolabs.common.point_hash.position_keys`, kept so
Importance #104: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -81,12 +68,12 @@
8168
82 Returns:69 Returns:
83 `(N,)` array of packed position keys, in input order.70 `(N,)` array of packed position keys, in input order.
84 """71 """
85 return position_keys(xyz, units_per_m)72 return point_hash.position_keys(xyz, units_per_m)
8673
8774
88@dataclass75@dataclasses.dataclass
89class SegmentCloud:76class SegmentCloud:
90 """A segment's concatenated run3 point cloud plus run4 surface tagging.77 """A segment's concatenated run3 point cloud plus run4 surface tagging.
9178
92 Attributes:79 Attributes:
Importance #105: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -116,9 +103,9 @@
116 scan_angle: np.ndarray103 scan_angle: np.ndarray
117 is_surface: np.ndarray104 is_surface: np.ndarray
118 records: list[Record]105 records: list[Record]
119 surface_match_rate: float106 surface_match_rate: float
120 extra: dict[str, np.ndarray] = field(default_factory=dict)107 extra: dict[str, np.ndarray] = dataclasses.field(default_factory=dict)
121108
122 @property109 @property
123 def n(self) -> int:110 def n(self) -> int:
124 """Number of points in the concatenated cloud."""111 """Number of points in the concatenated cloud."""
Importance #106: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -146,9 +133,9 @@
146 """Returns the contributing record basenames, in concatenation order."""133 """Returns the contributing record basenames, in concatenation order."""
147 return [r.name for r in self.records]134 return [r.name for r in self.records]
148135
149136
150def load_geoshift(seg_dir: Path) -> np.ndarray | None:137def load_geoshift(seg_dir: pathlib.Path) -> np.ndarray | None:
151 """Loads the per-dataset geoshift that run3 subtracted, if present.138 """Loads the per-dataset geoshift that run3 subtracted, if present.
152139
153 The upstream trajectory step writes run3 points as `source - geoshift`140 The upstream trajectory step writes run3 points as `source - geoshift`
154 (the geoshift is the spline centroid) and records the offset in141 (the geoshift is the spline centroid) and records the offset in
Importance #107: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -172,16 +159,16 @@
172159
173 Raises:160 Raises:
174 ValueError: The file exists but is not a `{"x", "y", "z"}` object.161 ValueError: The file exists but is not a `{"x", "y", "z"}` object.
175 """162 """
176 path = Path(seg_dir).parent / GEOSHIFT_NAME163 path = pathlib.Path(seg_dir).parent / segment_points_io.GEOSHIFT_NAME
177 if not path.exists():164 if not path.exists():
178 return None165 return None
179 return _load_geoshift_file(path)166 return segment_points_io.load_geoshift(path)
180167
181168
182def _load_surface_key_set(169def _load_surface_key_set(
183 files: list[Path], units_per_m: float170 files: list[pathlib.Path], units_per_m: float
184) -> np.ndarray | None:171) -> np.ndarray | None:
185 """Builds the unique run4 road-surface key set for a segment.172 """Builds the unique run4 road-surface key set for a segment.
186173
187 Args:174 Args:
Importance #108: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -204,9 +191,9 @@
204 return out191 return out
205192
206193
207def load_segment_cloud(194def load_segment_cloud(
208 seg_dir: Path, units_per_m: float = DEFAULT_UNITS_PER_M195 seg_dir: pathlib.Path, units_per_m: float = point_hash.DEFAULT_UNITS_PER_M
209) -> SegmentCloud:196) -> SegmentCloud:
210 """Loads and concatenates all run3 records, tagging run4 surface membership.197 """Loads and concatenates all run3 records, tagging run4 surface membership.
211198
212 Records are read in sorted glob order; record boundaries (name, offset,199 Records are read in sorted glob order; record boundaries (name, offset,
Importance #109: src/iolabs_point_cloud_segmentation_3d/io_npz.py @@ -224,24 +211,24 @@
224 Raises:211 Raises:
225 FileNotFoundError: The directory holds no complete run3 record.212 FileNotFoundError: The directory holds no complete run3 record.
226 ValueError: A record violates the point-record schema.213 ValueError: A record violates the point-record schema.
227 """214 """
228 seg_dir = Path(seg_dir)215 seg_dir = pathlib.Path(seg_dir)
229 record, records = load_run3_segment(216 record, records = segment_points_io.load_run3_segment(
230 seg_dir, target_dtypes=RUN3_TARGET_DTYPES217 seg_dir, target_dtypes=RUN3_TARGET_DTYPES
231 )218 )
232219
233 points = record["points"]220 points = record["points"]
234 surf_set = _load_surface_key_set(221 surf_set = _load_surface_key_set(
235 segment_record_files(seg_dir, RUN4_SURFACE_SUFFIX), units_per_m222 segments.segment_record_files(seg_dir, RUN4_SURFACE_SUFFIX), units_per_m
236 )223 )
237 if surf_set is None:224 if surf_set is None:
238 is_surface = np.zeros(points.shape[0], dtype=bool)225 is_surface = np.zeros(points.shape[0], dtype=bool)
239 surface_match_rate = 0.0226 surface_match_rate = 0.0
240 else:227 else:
241 keys = surface_keys(points, units_per_m)228 keys = surface_keys(points, units_per_m)
242 is_surface = np.isin(keys, surf_set, assume_unique=False)229 is_surface = np.isin(keys, surf_set, assume_unique=False)
243 surface_match_rate = key_match_rate(surf_set, keys)230 surface_match_rate = point_hash.key_match_rate(surf_set, keys)
244 del keys231 del keys
245232
246 return SegmentCloud(233 return SegmentCloud(
247 points=points,234 points=points,
Importance #110: tests/test_fusion.py @@ -1,19 +1,21 @@
1"""Synthetic micro-cloud tests for the fusion pipeline (no real data)."""1"""Synthetic micro-cloud tests for the fusion pipeline (no real data)."""
22
3import json3import json
4import logging4import logging
5import pathlib
56
6import numpy as np7import numpy as np
7import pytest8import pytest
8from iolabs.common import crs9import shapely
9from iolabs.common.segments import parse_segment_ids, parse_segment_names10from iolabs.common import crs, segment_points_io, segments
10from scipy.spatial import cKDTree11from scipy import spatial
11from shapely import Polygon
1212
13from iolabs_point_cloud_segmentation_3d import (13from iolabs_point_cloud_segmentation_3d import (
14 classes,14 classes,
15 clusters,15 clusters,
16 config,
17 fuse,
16 ground,18 ground,
17 guardrail_json,19 guardrail_json,
18 io_npz,20 io_npz,
19 lines_xml,21 lines_xml,
Importance #111: tests/test_fusion.py @@ -22,20 +24,8 @@
22 vegetation,24 vegetation,
23 voxel,25 voxel,
24 writer,26 writer,
25)27)
26from iolabs_point_cloud_segmentation_3d import fuse as fuse_mod
27from iolabs_point_cloud_segmentation_3d.config import (
28 Seg3dConfig,
29 config_from_dict,
30)
31from iolabs_point_cloud_segmentation_3d.fuse import (
32 AlignmentError,
33 FuseResult,
34 _paint_lines,
35 fuse_segment,
36 paint_signs_from_json,
37)
3828
39# The legacy output base name for segment 007: the writers take a resolved29# The legacy output base name for segment 007: the writers take a resolved
40# base (see `naming`), not a segment id.30# base (see `naming`), not a segment id.
41BASE_007 = "segment_007_seg3d"31BASE_007 = "segment_007_seg3d"
Importance #112: tests/test_fusion.py @@ -95,9 +85,9 @@
95 Splitting them would let a tube voxel lose the count tie-break to the85 Splitting them would let a tube voxel lose the count tie-break to the
96 rail it was carved out of, which is the exact failure the tier exists86 rail it was carved out of, which is the exact failure the tier exists
97 to prevent.87 to prevent.
98 """88 """
99 cfg = Seg3dConfig(priority_support=9)89 cfg = config.Seg3dConfig(priority_support=9)
100 plut = classes.priority_lut(cfg)90 plut = classes.priority_lut(cfg)
101 assert plut[classes.BY_NAME["guardrail_support"].las_code] == 991 assert plut[classes.BY_NAME["guardrail_support"].las_code] == 9
102 assert plut[classes.BY_NAME["guardrail_top_rail"].las_code] == 992 assert plut[classes.BY_NAME["guardrail_top_rail"].las_code] == 9
103 assert plut[classes.BY_NAME["guardrail"].las_code] == 493 assert plut[classes.BY_NAME["guardrail"].las_code] == 4
Importance #113: tests/test_fusion.py @@ -157,9 +147,9 @@
157 """A record key the loader grows reaches consumers with no edit here."""147 """A record key the loader grows reaches consumers with no edit here."""
158 seg = tmp_path / "segment_004"148 seg = tmp_path / "segment_004"
159 seg.mkdir()149 seg.mkdir()
160 _write_run3(seg / "a_run3_points.npz", np.zeros((3, 3)))150 _write_run3(seg / "a_run3_points.npz", np.zeros((3, 3)))
161 real_load = io_npz.load_run3_segment151 real_load = segment_points_io.load_run3_segment
162152
163 def _with_future_key(seg_dir, **kwargs):153 def _with_future_key(seg_dir, **kwargs):
164 record, records = real_load(seg_dir, **kwargs)154 record, records = real_load(seg_dir, **kwargs)
165 record = dict(record)155 record = dict(record)
Importance #114: tests/test_fusion.py @@ -167,9 +157,9 @@
167 record["points"].shape[0], dtype=np.uint8157 record["points"].shape[0], dtype=np.uint8
168 )158 )
169 return record, records159 return record, records
170160
171 monkeypatch.setattr(io_npz, "load_run3_segment", _with_future_key)161 monkeypatch.setattr(segment_points_io, "load_run3_segment", _with_future_key)
172 cloud = io_npz.load_segment_cloud(seg)162 cloud = io_npz.load_segment_cloud(seg)
173163
174 expected = np.arange(3, dtype=np.uint8)164 expected = np.arange(3, dtype=np.uint8)
175 np.testing.assert_array_equal(cloud.extra["future_channel"], expected)165 np.testing.assert_array_equal(cloud.extra["future_channel"], expected)
Importance #115: tests/test_fusion.py @@ -180,9 +170,9 @@
180# pavement / polygon classify170# pavement / polygon classify
181# --------------------------------------------------------------------------- #171# --------------------------------------------------------------------------- #
182def test_polygon_classify():172def test_polygon_classify():
183 corridor = pavement.Corridor(173 corridor = pavement.Corridor(
184 polygon=Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),174 polygon=shapely.Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),
185 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),175 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),
186 )176 )
187 pts = np.array(177 pts = np.array(
188 [178 [
Importance #116: tests/test_fusion.py @@ -197,9 +187,9 @@
197187
198188
199def test_classify_above_corridor_ignores_surface_gate():189def test_classify_above_corridor_ignores_surface_gate():
200 corridor = pavement.Corridor(190 corridor = pavement.Corridor(
201 polygon=Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),191 polygon=shapely.Polygon([(0, 0), (10, 0), (10, 4), (0, 4)]),
202 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),192 vertices_xy=np.array([[0, 0], [10, 0], [10, 4], [0, 4]], dtype=float),
203 )193 )
204 pts = np.array(194 pts = np.array(
205 [195 [
Importance #117: tests/test_fusion.py @@ -220,9 +210,9 @@
220 p = tmp_path / "segment_003_edges.npz"210 p = tmp_path / "segment_003_edges.npz"
221 np.savez(p, left_polyline_points=left, right_polyline_points=right)211 np.savez(p, left_polyline_points=left, right_polyline_points=right)
222 corr = pavement.build_corridor(p)212 corr = pavement.build_corridor(p)
223 assert corr is not None213 assert corr is not None
224 assert corr.polygon.contains(Polygon([(1, 1), (2, 1), (2, 3)]).centroid)214 assert corr.polygon.contains(shapely.Polygon([(1, 1), (2, 1), (2, 3)]).centroid)
225215
226216
227def test_build_corridor_bowtie_keeps_both_lobes(tmp_path):217def test_build_corridor_bowtie_keeps_both_lobes(tmp_path):
228 # Self-intersecting (bowtie) ring: buffer(0) would silently keep only218 # Self-intersecting (bowtie) ring: buffer(0) would silently keep only
Importance #118: tests/test_fusion.py @@ -257,9 +247,9 @@
257 [0.10, 0.0, 1.0], # within XY but dz 1.0 > 0.5 -> no247 [0.10, 0.0, 1.0], # within XY but dz 1.0 > 0.5 -> no
258 ]248 ]
259 )249 )
260 is_asphalt = np.ones(3, dtype=bool)250 is_asphalt = np.ones(3, dtype=bool)
261 solid, dashed = _paint_lines(pts, is_asphalt, verts)251 solid, dashed = fuse._paint_lines(pts, is_asphalt, verts)
262 np.testing.assert_array_equal(solid, [True, False, False])252 np.testing.assert_array_equal(solid, [True, False, False])
263 assert not dashed.any()253 assert not dashed.any()
264254
265255
Importance #119: tests/test_fusion.py @@ -271,9 +261,9 @@
271 solid_xyz=np.array([[0.01, 0.0, 1.0], [0.10, 0.0, 0.0]]),261 solid_xyz=np.array([[0.01, 0.0, 1.0], [0.10, 0.0, 0.0]]),
272 dashed_xyz=np.empty((0, 3)),262 dashed_xyz=np.empty((0, 3)),
273 )263 )
274 pts = np.array([[0.0, 0.0, 0.0]])264 pts = np.array([[0.0, 0.0, 0.0]])
275 solid, dashed = _paint_lines(pts, np.ones(1, bool), verts)265 solid, dashed = fuse._paint_lines(pts, np.ones(1, bool), verts)
276 assert solid[0]266 assert solid[0]
277 assert not dashed[0]267 assert not dashed[0]
278268
279269
Importance #120: tests/test_fusion.py @@ -282,9 +272,9 @@
282 solid_xyz=np.array([[0.0, 0.0, 0.0]]),272 solid_xyz=np.array([[0.0, 0.0, 0.0]]),
283 dashed_xyz=np.array([[0.0, 0.0, 0.0]]),273 dashed_xyz=np.array([[0.0, 0.0, 0.0]]),
284 )274 )
285 pts = np.array([[0.05, 0.0, 0.0]])275 pts = np.array([[0.05, 0.0, 0.0]])
286 solid, dashed = _paint_lines(pts, np.ones(1, bool), verts)276 solid, dashed = fuse._paint_lines(pts, np.ones(1, bool), verts)
287 assert solid[0] and not dashed[0]277 assert solid[0] and not dashed[0]
288278
289279
290def test_paint_lines_only_asphalt():280def test_paint_lines_only_asphalt():
Importance #121: tests/test_fusion.py @@ -292,9 +282,9 @@
292 solid_xyz=np.array([[0.0, 0.0, 0.0]]),282 solid_xyz=np.array([[0.0, 0.0, 0.0]]),
293 dashed_xyz=np.empty((0, 3)),283 dashed_xyz=np.empty((0, 3)),
294 )284 )
295 pts = np.array([[0.0, 0.0, 0.0]])285 pts = np.array([[0.0, 0.0, 0.0]])
296 solid, _ = _paint_lines(pts, np.zeros(1, bool), verts)286 solid, _ = fuse._paint_lines(pts, np.zeros(1, bool), verts)
297 assert not solid.any() # not asphalt -> never painted287 assert not solid.any() # not asphalt -> never painted
298288
299289
300# --------------------------------------------------------------------------- #290# --------------------------------------------------------------------------- #
Importance #122: tests/test_fusion.py @@ -509,9 +499,9 @@
509 "<EndPoint><X>4.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"499 "<EndPoint><X>4.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"
510 "</Line></Lines></Feature></HighwayData>"500 "</Line></Lines></Feature></HighwayData>"
511 )501 )
512502
513 result = fuse_segment(503 result = fuse.fuse_segment(
514 seg_dir=seg_dir,504 seg_dir=seg_dir,
515 seg_name="066",505 seg_name="066",
516 edges_dirs=[edges],506 edges_dirs=[edges],
517 xml_path=xml_path,507 xml_path=xml_path,
Importance #123: tests/test_fusion.py @@ -558,9 +548,9 @@
558 "<EndPoint><X>3.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"548 "<EndPoint><X>3.0</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"
559 "</Line></Lines></Feature></HighwayData>"549 "</Line></Lines></Feature></HighwayData>"
560 )550 )
561551
562 result = fuse_segment(552 result = fuse.fuse_segment(
563 seg_dir=seg_dir,553 seg_dir=seg_dir,
564 seg_name="067",554 seg_name="067",
565 edges_dirs=[edges],555 edges_dirs=[edges],
566 xml_path=xml_path,556 xml_path=xml_path,
Importance #124: tests/test_fusion.py @@ -597,10 +587,10 @@
597 "<EndPoint><X>50001.0</X><Y>50000.0</Y><Z>0.0</Z></EndPoint>"587 "<EndPoint><X>50001.0</X><Y>50000.0</Y><Z>0.0</Z></EndPoint>"
598 "</Line></Lines></Feature></HighwayData>"588 "</Line></Lines></Feature></HighwayData>"
599 )589 )
600590
601 with pytest.raises(AlignmentError):591 with pytest.raises(fuse.AlignmentError):
602 fuse_segment(592 fuse.fuse_segment(
603 seg_dir=seg_dir,593 seg_dir=seg_dir,
604 seg_name="068",594 seg_name="068",
605 edges_dirs=[edges],595 edges_dirs=[edges],
606 xml_path=xml_path,596 xml_path=xml_path,
Importance #125: tests/test_fusion.py @@ -637,9 +627,9 @@
637 "<EndPoint><X>7.1</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"627 "<EndPoint><X>7.1</X><Y>1.0</Y><Z>0.0</Z></EndPoint>"
638 "</Line></Lines></Feature></HighwayData>"628 "</Line></Lines></Feature></HighwayData>"
639 )629 )
640630
641 result = fuse_segment(631 result = fuse.fuse_segment(
642 seg_dir=seg_dir,632 seg_dir=seg_dir,
643 seg_name="069",633 seg_name="069",
644 edges_dirs=[edges],634 edges_dirs=[edges],
645 xml_path=xml_path,635 xml_path=xml_path,
Importance #126: tests/test_fusion.py @@ -667,11 +657,9 @@
667657
668 import tempfile658 import tempfile
669659
670 with tempfile.TemporaryDirectory() as d:660 with tempfile.TemporaryDirectory() as d:
671 from pathlib import Path661 path = pathlib.Path(d) / "point_masks.npz"
672
673 path = Path(d) / "point_masks.npz"
674 np.savez(662 np.savez(
675 path,663 path,
676 record_names=np.array(["a_run3_points.npz", "b_run3_points.npz"]),664 record_names=np.array(["a_run3_points.npz", "b_run3_points.npz"]),
677 record_id=np.array([0, 1, 1], dtype=np.uint16),665 record_id=np.array([0, 1, 1], dtype=np.uint16),
Importance #127: tests/test_fusion.py @@ -791,9 +779,9 @@
791 instance_type=np.array(["w_beam", "guardrail_support"]),779 instance_type=np.array(["w_beam", "guardrail_support"]),
792 instance_json_index=np.array([0, 1], dtype=np.int32),780 instance_json_index=np.array([0, 1], dtype=np.int32),
793 )781 )
794782
795 result = fuse_segment(783 result = fuse.fuse_segment(
796 seg_dir=seg_dir,784 seg_dir=seg_dir,
797 seg_name="081",785 seg_name="081",
798 edges_dirs=[],786 edges_dirs=[],
799 xml_path=None,787 xml_path=None,
Importance #128: tests/test_fusion.py @@ -848,9 +836,9 @@
848 instance_type=np.array(["delineator"]),836 instance_type=np.array(["delineator"]),
849 instance_json_index=np.array([0], dtype=np.int32),837 instance_json_index=np.array([0], dtype=np.int32),
850 )838 )
851839
852 result = fuse_segment(840 result = fuse.fuse_segment(
853 seg_dir=seg_dir,841 seg_dir=seg_dir,
854 seg_name="082",842 seg_name="082",
855 edges_dirs=[],843 edges_dirs=[],
856 xml_path=None,844 xml_path=None,
Importance #129: tests/test_fusion.py @@ -953,10 +941,10 @@
953 detections = [941 detections = [
954 _sign_detection(10.0, 10.0),942 _sign_detection(10.0, 10.0),
955 _sign_detection(20.0, 10.0),943 _sign_detection(20.0, 10.0),
956 ]944 ]
957 instances, metrics = paint_signs_from_json(945 instances, metrics = fuse.paint_signs_from_json(
958 pts, cls, detections, [covered], Seg3dConfig(), seg_name="001"946 pts, cls, detections, [covered], config.Seg3dConfig(), seg_name="001"
959 )947 )
960 assert len(instances) == 1948 assert len(instances) == 1
961 inst = instances[0]949 inst = instances[0]
962 assert inst.kind == "sign"950 assert inst.kind == "sign"
Importance #130: tests/test_fusion.py @@ -982,10 +970,10 @@
982 json_index=0,970 json_index=0,
983 local_index=0,971 local_index=0,
984 global_rows=np.empty(0, dtype=np.int64),972 global_rows=np.empty(0, dtype=np.int64),
985 )973 )
986 instances, metrics = paint_signs_from_json(974 instances, metrics = fuse.paint_signs_from_json(
987 pts, cls, [_sign_detection(10.0, 10.0)], [empty], Seg3dConfig()975 pts, cls, [_sign_detection(10.0, 10.0)], [empty], config.Seg3dConfig()
988 )976 )
989 assert len(instances) == 1977 assert len(instances) == 1
990 assert instances[0].json_index == 0978 assert instances[0].json_index == 0
991 assert instances[0].local_index == 1979 assert instances[0].local_index == 1
Importance #131: tests/test_fusion.py @@ -1010,10 +998,10 @@
1010 local_index=0,998 local_index=0,
1011 global_rows=np.arange(20),999 global_rows=np.arange(20),
1012 )1000 )
1013 detections = [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0)]1001 detections = [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0)]
1014 instances, metrics = paint_signs_from_json(1002 instances, metrics = fuse.paint_signs_from_json(
1015 pts, cls, detections, [legacy], Seg3dConfig()1003 pts, cls, detections, [legacy], config.Seg3dConfig()
1016 )1004 )
1017 assert [i.json_index for i in instances] == [1]1005 assert [i.json_index for i in instances] == [1]
1018 assert metrics["signs_json_painted"] == 1.01006 assert metrics["signs_json_painted"] == 1.0
1019 assert metrics["signs_json_skipped"] == 0.01007 assert metrics["signs_json_skipped"] == 0.0
Importance #132: tests/test_fusion.py @@ -1067,10 +1055,10 @@
1067 )1055 )
1068 with caplog.at_level(1056 with caplog.at_level(
1069 logging.WARNING, logger="iolabs_point_cloud_segmentation_3d.fuse"1057 logging.WARNING, logger="iolabs_point_cloud_segmentation_3d.fuse"
1070 ):1058 ):
1071 instances, metrics = paint_signs_from_json(1059 instances, metrics = fuse.paint_signs_from_json(
1072 pts, cls, detections, mask, Seg3dConfig(), seg_name="023"1060 pts, cls, detections, mask, config.Seg3dConfig(), seg_name="023"
1073 )1061 )
1074 # The shifted half-post is painted; every mask-covered detection is1062 # The shifted half-post is painted; every mask-covered detection is
1075 # recognised by geometry and not painted a second time.1063 # recognised by geometry and not painted a second time.
1076 assert [i.json_index for i in instances] == [7]1064 assert [i.json_index for i in instances] == [7]
Importance #133: tests/test_fusion.py @@ -1101,10 +1089,10 @@
1101 json_index=0,1089 json_index=0,
1102 local_index=0,1090 local_index=0,
1103 global_rows=np.empty(0, dtype=np.int64),1091 global_rows=np.empty(0, dtype=np.int64),
1104 )1092 )
1105 instances, _ = paint_signs_from_json(1093 instances, _ = fuse.paint_signs_from_json(
1106 pts, cls, [_sign_detection(10.0, 10.0)], [rail], Seg3dConfig()1094 pts, cls, [_sign_detection(10.0, 10.0)], [rail], config.Seg3dConfig()
1107 )1095 )
1108 assert [i.json_index for i in instances] == [0]1096 assert [i.json_index for i in instances] == [0]
11091097
11101098
Importance #134: tests/test_fusion.py @@ -1121,10 +1109,10 @@
1121 json_index=0,1109 json_index=0,
1122 local_index=0,1110 local_index=0,
1123 global_rows=np.arange(pts.shape[0]),1111 global_rows=np.arange(pts.shape[0]),
1124 )1112 )
1125 instances, metrics = paint_signs_from_json(1113 instances, metrics = fuse.paint_signs_from_json(
1126 pts, cls, [_sign_detection(10.0, 10.0)], [covered], Seg3dConfig()1114 pts, cls, [_sign_detection(10.0, 10.0)], [covered], config.Seg3dConfig()
1127 )1115 )
1128 assert instances == []1116 assert instances == []
1129 assert metrics["signs_json_painted"] == 0.01117 assert metrics["signs_json_painted"] == 0.0
1130 assert metrics["signs_json_skipped"] == 0.01118 assert metrics["signs_json_skipped"] == 0.0
Importance #135: tests/test_fusion.py @@ -1145,10 +1133,10 @@
1145 4: "solid_line",1133 4: "solid_line",
1146 }1134 }
1147 for row, name in keep.items():1135 for row, name in keep.items():
1148 cls[row] = classes.BY_NAME[name].las_code1136 cls[row] = classes.BY_NAME[name].las_code
1149 instances, _ = paint_signs_from_json(1137 instances, _ = fuse.paint_signs_from_json(
1150 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1138 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1151 )1139 )
1152 assert len(instances) == 11140 assert len(instances) == 1
1153 np.testing.assert_array_equal(np.sort(instances[0].global_rows),1141 np.testing.assert_array_equal(np.sort(instances[0].global_rows),
1154 np.arange(5, 25))1142 np.arange(5, 25))
Importance #136: tests/test_fusion.py @@ -1160,10 +1148,10 @@
1160def test_paint_signs_from_json_skips_null_z_top():1148def test_paint_signs_from_json_skips_null_z_top():
1161 pts = _post_points(10.0, 10.0)1149 pts = _post_points(10.0, 10.0)
1162 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1150 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1163 detections = [_sign_detection(10.0, 10.0, z_top=None)]1151 detections = [_sign_detection(10.0, 10.0, z_top=None)]
1164 instances, metrics = paint_signs_from_json(1152 instances, metrics = fuse.paint_signs_from_json(
1165 pts, cls, detections, [], Seg3dConfig()1153 pts, cls, detections, [], config.Seg3dConfig()
1166 )1154 )
1167 assert instances == []1155 assert instances == []
1168 assert metrics["signs_json_skipped"] == 1.01156 assert metrics["signs_json_skipped"] == 1.0
1169 assert np.all(cls == classes.UNCLASSIFIED_CODE)1157 assert np.all(cls == classes.UNCLASSIFIED_CODE)
Importance #137: tests/test_fusion.py @@ -1175,10 +1163,10 @@
1175 detections = [1163 detections = [
1176 _sign_detection(10.0, 10.0, seg_type="tree", experimental=True),1164 _sign_detection(10.0, 10.0, seg_type="tree", experimental=True),
1177 _sign_detection(10.0, 10.0, seg_type="field_stake"),1165 _sign_detection(10.0, 10.0, seg_type="field_stake"),
1178 ]1166 ]
1179 instances, metrics = paint_signs_from_json(1167 instances, metrics = fuse.paint_signs_from_json(
1180 pts, cls, detections, [], Seg3dConfig()1168 pts, cls, detections, [], config.Seg3dConfig()
1181 )1169 )
1182 assert instances == []1170 assert instances == []
1183 assert metrics["signs_json_skipped"] == 2.01171 assert metrics["signs_json_skipped"] == 2.0
1184 assert np.all(cls == classes.UNCLASSIFIED_CODE)1172 assert np.all(cls == classes.UNCLASSIFIED_CODE)
Importance #138: tests/test_fusion.py @@ -1238,10 +1226,10 @@
1238 _sign_detection(10.0, 10.0),1226 _sign_detection(10.0, 10.0),
1239 "junk",1227 "junk",
1240 _sign_detection(20.0, 10.0),1228 _sign_detection(20.0, 10.0),
1241 ]1229 ]
1242 instances, metrics = paint_signs_from_json(1230 instances, metrics = fuse.paint_signs_from_json(
1243 pts, cls, detections, [covered], Seg3dConfig()1231 pts, cls, detections, [covered], config.Seg3dConfig()
1244 )1232 )
1245 # Only the genuinely uncovered detection 0 is painted.1233 # Only the genuinely uncovered detection 0 is painted.
1246 assert [i.json_index for i in instances] == [0]1234 assert [i.json_index for i in instances] == [0]
1247 np.testing.assert_array_equal(np.sort(instances[0].global_rows),1235 np.testing.assert_array_equal(np.sort(instances[0].global_rows),
Importance #139: tests/test_fusion.py @@ -1253,11 +1241,11 @@
1253 # min_points=0 must not turn a detection that matched nothing into a1241 # min_points=0 must not turn a detection that matched nothing into a
1254 # zero-row instance.1242 # zero-row instance.
1255 pts = _post_points(500.0, 500.0, n=20) # nowhere near the detection1243 pts = _post_points(500.0, 500.0, n=20) # nowhere near the detection
1256 cls = np.full(20, classes.UNCLASSIFIED_CODE, dtype=np.uint8)1244 cls = np.full(20, classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1257 config = config_from_dict({"signs_json_paint_min_points": 0})1245 cfg = config.config_from_dict({"signs_json_paint_min_points": 0})
1258 instances, metrics = paint_signs_from_json(1246 instances, metrics = fuse.paint_signs_from_json(
1259 pts, cls, [_sign_detection(10.0, 10.0)], [], config1247 pts, cls, [_sign_detection(10.0, 10.0)], [], cfg
1260 )1248 )
1261 assert instances == []1249 assert instances == []
1262 assert metrics["signs_json_painted"] == 0.01250 assert metrics["signs_json_painted"] == 0.0
1263 assert metrics["signs_json_skipped"] == 1.01251 assert metrics["signs_json_skipped"] == 1.0
Importance #140: tests/test_fusion.py @@ -1268,18 +1256,18 @@
1268 # 5 points in the cylinder, below the default floor of 10 -> no phantom1256 # 5 points in the cylinder, below the default floor of 10 -> no phantom
1269 # instance and nothing painted.1257 # instance and nothing painted.
1270 pts = _post_points(10.0, 10.0, n=5)1258 pts = _post_points(10.0, 10.0, n=5)
1271 cls = np.full(5, classes.UNCLASSIFIED_CODE, dtype=np.uint8)1259 cls = np.full(5, classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1272 instances, metrics = paint_signs_from_json(1260 instances, metrics = fuse.paint_signs_from_json(
1273 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1261 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1274 )1262 )
1275 assert instances == []1263 assert instances == []
1276 assert metrics["signs_json_skipped"] == 1.01264 assert metrics["signs_json_skipped"] == 1.0
1277 assert np.all(cls == classes.UNCLASSIFIED_CODE)1265 assert np.all(cls == classes.UNCLASSIFIED_CODE)
1278 # Lowering the floor paints the same detection.1266 # Lowering the floor paints the same detection.
1279 instances, _ = paint_signs_from_json(1267 instances, _ = fuse.paint_signs_from_json(
1280 pts, cls, [_sign_detection(10.0, 10.0)], [],1268 pts, cls, [_sign_detection(10.0, 10.0)], [],
1281 config_from_dict({"signs_json_paint_min_points": 3}),1269 config.config_from_dict({"signs_json_paint_min_points": 3}),
1282 )1270 )
1283 assert len(instances) == 11271 assert len(instances) == 1
12841272
12851273
Importance #141: tests/test_fusion.py @@ -1291,10 +1279,10 @@
1291 outside_xy = _post_points(10.5, 10.0, n=12) # 0.5 m out -> outside1279 outside_xy = _post_points(10.5, 10.0, n=12) # 0.5 m out -> outside
1292 above = _post_points(10.0, 10.0, n=12, z0=1.9, z1=3.0) # above z_top+0.301280 above = _post_points(10.0, 10.0, n=12, z0=1.9, z1=3.0) # above z_top+0.30
1293 pts = np.vstack([inside, far_xy, outside_xy, above])1281 pts = np.vstack([inside, far_xy, outside_xy, above])
1294 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1282 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1295 instances, _ = paint_signs_from_json(1283 instances, _ = fuse.paint_signs_from_json(
1296 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1284 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1297 )1285 )
1298 assert len(instances) == 11286 assert len(instances) == 1
1299 painted = np.zeros(pts.shape[0], dtype=bool)1287 painted = np.zeros(pts.shape[0], dtype=bool)
1300 painted[instances[0].global_rows] = True1288 painted[instances[0].global_rows] = True
Importance #142: tests/test_fusion.py @@ -1313,10 +1301,10 @@
1313 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1301 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1314 gantry = _sign_detection(1302 gantry = _sign_detection(
1315 10.0, 10.0, "gantry_or_gate", footprint_m=[10.0, 0.5]1303 10.0, 10.0, "gantry_or_gate", footprint_m=[10.0, 0.5]
1316 )1304 )
1317 instances, metrics = paint_signs_from_json(1305 instances, metrics = fuse.paint_signs_from_json(
1318 pts, cls, [gantry], [], Seg3dConfig()1306 pts, cls, [gantry], [], config.Seg3dConfig()
1319 )1307 )
1320 assert instances == []1308 assert instances == []
1321 assert metrics["signs_json_painted"] == 0.01309 assert metrics["signs_json_painted"] == 0.0
1322 assert metrics["signs_json_skipped"] == 1.01310 assert metrics["signs_json_skipped"] == 1.0
Importance #143: tests/test_fusion.py @@ -1329,20 +1317,20 @@
1329 below = _post_points(10.0, 10.0, n=12, z0=-0.14, z1=-0.01)1317 below = _post_points(10.0, 10.0, n=12, z0=-0.14, z1=-0.01)
1330 above = _post_points(10.0, 10.0, n=12, z0=0.0, z1=1.5)1318 above = _post_points(10.0, 10.0, n=12, z0=0.0, z1=1.5)
1331 pts = np.vstack([below, above])1319 pts = np.vstack([below, above])
1332 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1320 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1333 instances, _ = paint_signs_from_json(1321 instances, _ = fuse.paint_signs_from_json(
1334 pts, cls, [_sign_detection(10.0, 10.0)], [], Seg3dConfig()1322 pts, cls, [_sign_detection(10.0, 10.0)], [], config.Seg3dConfig()
1335 )1323 )
1336 assert len(instances) == 11324 assert len(instances) == 1
1337 np.testing.assert_array_equal(1325 np.testing.assert_array_equal(
1338 np.sort(instances[0].global_rows), np.arange(12, 24)1326 np.sort(instances[0].global_rows), np.arange(12, 24)
1339 )1327 )
1340 # The knob still reaches under the ground when a run asks for it.1328 # The knob still reaches under the ground when a run asks for it.
1341 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)1329 cls = np.full(pts.shape[0], classes.UNCLASSIFIED_CODE, dtype=np.uint8)
1342 instances, _ = paint_signs_from_json(1330 instances, _ = fuse.paint_signs_from_json(
1343 pts, cls, [_sign_detection(10.0, 10.0)], [],1331 pts, cls, [_sign_detection(10.0, 10.0)], [],
1344 config_from_dict({"signs_json_paint_z_pad_bottom_m": 0.15}),1332 config.config_from_dict({"signs_json_paint_z_pad_bottom_m": 0.15}),
1345 )1333 )
1346 np.testing.assert_array_equal(1334 np.testing.assert_array_equal(
1347 np.sort(instances[0].global_rows), np.arange(24)1335 np.sort(instances[0].global_rows), np.arange(24)
1348 )1336 )
Importance #144: tests/test_fusion.py @@ -1360,9 +1348,9 @@
1360 signs, "080",1348 signs, "080",
1361 [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0, "sign_post")],1349 [_sign_detection(10.0, 10.0), _sign_detection(20.0, 10.0, "sign_post")],
1362 )1350 )
13631351
1364 result = fuse_segment(1352 result = fuse.fuse_segment(
1365 seg_dir=seg_dir,1353 seg_dir=seg_dir,
1366 seg_name="080",1354 seg_name="080",
1367 edges_dirs=[],1355 edges_dirs=[],
1368 xml_path=None,1356 xml_path=None,
Importance #145: tests/test_fusion.py @@ -1401,29 +1389,29 @@
1401 seg_dir = tmp_path / "segment_007"1389 seg_dir = tmp_path / "segment_007"
1402 seg_dir.mkdir()1390 seg_dir.mkdir()
1403 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))1391 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))
14041392
1405 config = config_from_dict(1393 cfg = config.config_from_dict(
1406 {"edge_extend_m": 12.5, "priority_detector": 7, "las_crs_epsg": 2056}1394 {"edge_extend_m": 12.5, "priority_detector": 7, "las_crs_epsg": 2056}
1407 )1395 )
1408 result = fuse_segment(1396 result = fuse.fuse_segment(
1409 seg_dir=seg_dir,1397 seg_dir=seg_dir,
1410 seg_name="007",1398 seg_name="007",
1411 edges_dirs=[],1399 edges_dirs=[],
1412 xml_path=None,1400 xml_path=None,
1413 guardrail_masks_dir=None,1401 guardrail_masks_dir=None,
1414 signs_masks_dir=None,1402 signs_masks_dir=None,
1415 voxel=0.25,1403 voxel=0.25,
1416 config=config,1404 config=cfg,
1417 )1405 )
14181406
1419 params = result.stats["params"]1407 params = result.stats["params"]
1420 assert params["edge_extend_m"] == 12.51408 assert params["edge_extend_m"] == 12.5
1421 assert params["priority_detector"] == 71409 assert params["priority_detector"] == 7
1422 assert params["las_crs_epsg"] == 20561410 assert params["las_crs_epsg"] == 2056
1423 # `--voxel` beats `config.voxel_size_m`, and the params say so.1411 # `--voxel` beats `config.voxel_size_m`, and the params say so.
1424 assert params["voxel_size_m"] == 0.251412 assert params["voxel_size_m"] == 0.25
1425 assert config.voxel_size_m != 0.251413 assert cfg.voxel_size_m != 0.25
1426 # Whatever ends up in the params has to survive the stats json.1414 # Whatever ends up in the params has to survive the stats json.
1427 assert json.loads(json.dumps(params)) == params1415 assert json.loads(json.dumps(params)) == params
14281416
14291417
Importance #146: tests/test_fusion.py @@ -1441,26 +1429,26 @@
1441 seg_dir = tmp_path / "segment_007"1429 seg_dir = tmp_path / "segment_007"
1442 seg_dir.mkdir()1430 seg_dir.mkdir()
1443 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))1431 _write_run3(seg_dir / "a_run3_points.npz", np.zeros((3, 3)))
14441432
1445 config = config_from_dict({"las_split": "none", "write_ply": False})1433 cfg = config.config_from_dict({"las_split": "none", "write_ply": False})
1446 result = fuse_segment(1434 result = fuse.fuse_segment(
1447 seg_dir=seg_dir,1435 seg_dir=seg_dir,
1448 seg_name="007",1436 seg_name="007",
1449 edges_dirs=[],1437 edges_dirs=[],
1450 xml_path=None,1438 xml_path=None,
1451 guardrail_masks_dir=None,1439 guardrail_masks_dir=None,
1452 signs_masks_dir=None,1440 signs_masks_dir=None,
1453 voxel=0.25,1441 voxel=0.25,
1454 config=config,1442 config=cfg,
1455 )1443 )
1456 summary = {1444 summary = {
1457 "segment": "007",1445 "segment": "007",
1458 "peak_rss_gb": 0.1,1446 "peak_rss_gb": 0.1,
1459 "params": result.stats["params"],1447 "params": result.stats["params"],
1460 }1448 }
1461 path = cli._write_run_summary(1449 path = cli._write_run_summary(
1462 tmp_path / "out", config, [summary],1450 tmp_path / "out", cfg, [summary],
1463 voxel_override=0.25,1451 voxel_override=0.25,
1464 las_split_override="class",1452 las_split_override="class",
1465 write_ply_override=True,1453 write_ply_override=True,
1466 )1454 )
Importance #147: tests/test_fusion.py @@ -1489,9 +1477,9 @@
1489 _write_signs_sidecars(1477 _write_signs_sidecars(
1490 signs, "081", detections, mask=[(0, list(range(20)))]1478 signs, "081", detections, mask=[(0, list(range(20)))]
1491 )1479 )
14921480
1493 result = fuse_segment(1481 result = fuse.fuse_segment(
1494 seg_dir=seg_dir,1482 seg_dir=seg_dir,
1495 seg_name="081",1483 seg_name="081",
1496 edges_dirs=[],1484 edges_dirs=[],
1497 xml_path=None,1485 xml_path=None,
Importance #148: tests/test_fusion.py @@ -1535,9 +1523,9 @@
15351523
1536 signs = tmp_path / "signs"1524 signs = tmp_path / "signs"
1537 _write_signs_sidecars(signs, "083", [_sign_detection(10.0, 10.0)])1525 _write_signs_sidecars(signs, "083", [_sign_detection(10.0, 10.0)])
15381526
1539 result = fuse_segment(1527 result = fuse.fuse_segment(
1540 seg_dir=seg_dir,1528 seg_dir=seg_dir,
1541 seg_name="083",1529 seg_name="083",
1542 edges_dirs=[],1530 edges_dirs=[],
1543 xml_path=None,1531 xml_path=None,
Importance #149: tests/test_fusion.py @@ -1558,16 +1546,16 @@
15581546
1559 signs = tmp_path / "signs"1547 signs = tmp_path / "signs"
1560 _write_signs_sidecars(signs, "082", [_sign_detection(10.0, 10.0)])1548 _write_signs_sidecars(signs, "082", [_sign_detection(10.0, 10.0)])
15611549
1562 result = fuse_segment(1550 result = fuse.fuse_segment(
1563 seg_dir=seg_dir,1551 seg_dir=seg_dir,
1564 seg_name="082",1552 seg_name="082",
1565 edges_dirs=[],1553 edges_dirs=[],
1566 xml_path=None,1554 xml_path=None,
1567 guardrail_masks_dir=None,1555 guardrail_masks_dir=None,
1568 signs_masks_dir=signs,1556 signs_masks_dir=signs,
1569 config=config_from_dict({"signs_json_paint_enabled": False}),1557 config=config.config_from_dict({"signs_json_paint_enabled": False}),
1570 )1558 )
1571 assert np.all(result.classification == classes.UNCLASSIFIED_CODE)1559 assert np.all(result.classification == classes.UNCLASSIFIED_CODE)
1572 assert "signs_json_painted" not in result.stats["guard_metrics"]1560 assert "signs_json_painted" not in result.stats["guard_metrics"]
15731561
Importance #150: tests/test_fusion.py @@ -1589,9 +1577,9 @@
1589 instance_type=np.array(["delineator"]),1577 instance_type=np.array(["delineator"]),
1590 instance_json_index=np.array([0], dtype=np.int32),1578 instance_json_index=np.array([0], dtype=np.int32),
1591 )1579 )
15921580
1593 result = fuse_segment(1581 result = fuse.fuse_segment(
1594 seg_dir=seg_dir,1582 seg_dir=seg_dir,
1595 seg_name="084",1583 seg_name="084",
1596 edges_dirs=[],1584 edges_dirs=[],
1597 xml_path=None,1585 xml_path=None,
Importance #151: tests/test_fusion.py @@ -2102,9 +2090,9 @@
2102 (mdir / "guardrails.json").write_text(2090 (mdir / "guardrails.json").write_text(
2103 json.dumps({"guardrails": [_rail_entry(0), _support_entry(1)]})2091 json.dumps({"guardrails": [_rail_entry(0), _support_entry(1)]})
2104 )2092 )
21052093
2106 result = fuse_segment(2094 result = fuse.fuse_segment(
2107 seg_dir=seg_dir,2095 seg_dir=seg_dir,
2108 seg_name="082",2096 seg_name="082",
2109 edges_dirs=[],2097 edges_dirs=[],
2110 xml_path=None,2098 xml_path=None,
Importance #152: tests/test_fusion.py @@ -2144,9 +2132,9 @@
2144 assert masks.guardrails_json_file(tmp_path / "gmasks", "084") == (2132 assert masks.guardrails_json_file(tmp_path / "gmasks", "084") == (
2145 mdir / "guardrails.json"2133 mdir / "guardrails.json"
2146 )2134 )
21472135
2148 result = fuse_segment(2136 result = fuse.fuse_segment(
2149 seg_dir=seg_dir,2137 seg_dir=seg_dir,
2150 seg_name="084",2138 seg_name="084",
2151 edges_dirs=[],2139 edges_dirs=[],
2152 xml_path=None,2140 xml_path=None,
Importance #153: tests/test_fusion.py @@ -2179,9 +2167,9 @@
2179 mdir = tmp_path / "gmasks" / "segment_084"2167 mdir = tmp_path / "gmasks" / "segment_084"
2180 mdir.mkdir(parents=True)2168 mdir.mkdir(parents=True)
2181 (mdir / "guardrails.json").write_text('{"guardrails": [{"id": 0,')2169 (mdir / "guardrails.json").write_text('{"guardrails": [{"id": 0,')
21822170
2183 result = fuse_segment(2171 result = fuse.fuse_segment(
2184 seg_dir=seg_dir,2172 seg_dir=seg_dir,
2185 seg_name="084",2173 seg_name="084",
2186 edges_dirs=[],2174 edges_dirs=[],
2187 xml_path=None,2175 xml_path=None,
Importance #154: tests/test_fusion.py @@ -2227,9 +2215,9 @@
2227 (mdir / "guardrails.json").write_text(json.dumps({"guardrails": [2215 (mdir / "guardrails.json").write_text(json.dumps({"guardrails": [
2228 _rail_entry(0), _support_entry(1), _top_rail_entry(2),2216 _rail_entry(0), _support_entry(1), _top_rail_entry(2),
2229 ]}))2217 ]}))
22302218
2231 result = fuse_segment(2219 result = fuse.fuse_segment(
2232 seg_dir=seg_dir,2220 seg_dir=seg_dir,
2233 seg_name="083",2221 seg_name="083",
2234 edges_dirs=[],2222 edges_dirs=[],
2235 xml_path=None,2223 xml_path=None,
Importance #155: tests/test_fusion.py @@ -2370,9 +2358,9 @@
2370 _write_tablecloth_mask(2358 _write_tablecloth_mask(
2371 gm / "a_tablecloth_masks.npz", np.ones(6, dtype=bool)2359 gm / "a_tablecloth_masks.npz", np.ones(6, dtype=bool)
2372 )2360 )
23732361
2374 result = fuse_segment(2362 result = fuse.fuse_segment(
2375 seg_dir=seg_dir,2363 seg_dir=seg_dir,
2376 seg_name="070",2364 seg_name="070",
2377 edges_dirs=[edges],2365 edges_dirs=[edges],
2378 xml_path=None,2366 xml_path=None,
Importance #156: tests/test_fusion.py @@ -2430,10 +2418,10 @@
2430 xml_path=None,2418 xml_path=None,
2431 guardrail_masks_dir=None,2419 guardrail_masks_dir=None,
2432 signs_masks_dir=None,2420 signs_masks_dir=None,
2433 )2421 )
2434 without = fuse_segment(**kwargs).stats["counts_full"]2422 without = fuse.fuse_segment(**kwargs).stats["counts_full"]
2435 with_g = fuse_segment(2423 with_g = fuse.fuse_segment(
2436 **kwargs, ground_masks_dir=tmp_path / "ground"2424 **kwargs, ground_masks_dir=tmp_path / "ground"
2437 ).stats["counts_full"]2425 ).stats["counts_full"]
24382426
2439 for name in ("asphalt", "solid_line", "dashed_line"):2427 for name in ("asphalt", "solid_line", "dashed_line"):
Importance #157: tests/test_fusion.py @@ -2451,9 +2439,9 @@
24512439
2452 gm = tmp_path / "ground" / "segment_071"2440 gm = tmp_path / "ground" / "segment_071"
2453 gm.mkdir(parents=True) # subdir exists but holds no mask files2441 gm.mkdir(parents=True) # subdir exists but holds no mask files
24542442
2455 result = fuse_segment(2443 result = fuse.fuse_segment(
2456 seg_dir=seg_dir,2444 seg_dir=seg_dir,
2457 seg_name="071",2445 seg_name="071",
2458 edges_dirs=[],2446 edges_dirs=[],
2459 xml_path=None,2447 xml_path=None,
Importance #158: tests/test_fusion.py @@ -2493,9 +2481,9 @@
24932481
2494def test_fuse_segment_vehicle_paints_unclassified_inside_corridor(tmp_path):2482def test_fuse_segment_vehicle_paints_unclassified_inside_corridor(tmp_path):
2495 seg_dir, edges = _vehicle_segment(tmp_path, "073")2483 seg_dir, edges = _vehicle_segment(tmp_path, "073")
24962484
2497 result = fuse_segment(2485 result = fuse.fuse_segment(
2498 seg_dir=seg_dir,2486 seg_dir=seg_dir,
2499 seg_name="073",2487 seg_name="073",
2500 edges_dirs=[edges],2488 edges_dirs=[edges],
2501 xml_path=None,2489 xml_path=None,
Importance #159: tests/test_fusion.py @@ -2552,9 +2540,9 @@
2552 gm / "a_tablecloth_masks.npz",2540 gm / "a_tablecloth_masks.npz",
2553 [True] * 7 + [False, False],2541 [True] * 7 + [False, False],
2554 )2542 )
25552543
2556 result = fuse_segment(2544 result = fuse.fuse_segment(
2557 seg_dir=seg_dir,2545 seg_dir=seg_dir,
2558 seg_name="074",2546 seg_name="074",
2559 edges_dirs=[edges],2547 edges_dirs=[edges],
2560 xml_path=None,2548 xml_path=None,
Importance #160: tests/test_fusion.py @@ -2573,9 +2561,9 @@
25732561
2574def test_fuse_segment_vehicle_skipped_without_corridor(tmp_path):2562def test_fuse_segment_vehicle_skipped_without_corridor(tmp_path):
2575 seg_dir, _ = _vehicle_segment(tmp_path, "075")2563 seg_dir, _ = _vehicle_segment(tmp_path, "075")
25762564
2577 result = fuse_segment(2565 result = fuse.fuse_segment(
2578 seg_dir=seg_dir,2566 seg_dir=seg_dir,
2579 seg_name="075",2567 seg_name="075",
2580 edges_dirs=[],2568 edges_dirs=[],
2581 xml_path=None,2569 xml_path=None,
Importance #161: tests/test_fusion.py @@ -2593,9 +2581,9 @@
2593 # so the corridor interior is the *un-painted* carriageway -- sweeping it2581 # so the corridor interior is the *un-painted* carriageway -- sweeping it
2594 # would relabel the whole road as vehicles.2582 # would relabel the whole road as vehicles.
2595 seg_dir, edges = _vehicle_segment(tmp_path, "076", with_run4=False)2583 seg_dir, edges = _vehicle_segment(tmp_path, "076", with_run4=False)
25962584
2597 result = fuse_segment(2585 result = fuse.fuse_segment(
2598 seg_dir=seg_dir,2586 seg_dir=seg_dir,
2599 seg_name="076",2587 seg_name="076",
2600 edges_dirs=[edges],2588 edges_dirs=[edges],
2601 xml_path=None,2589 xml_path=None,
Importance #162: tests/test_fusion.py @@ -2610,16 +2598,16 @@
26102598
2611def test_fuse_segment_vehicle_disabled_by_config(tmp_path):2599def test_fuse_segment_vehicle_disabled_by_config(tmp_path):
2612 seg_dir, edges = _vehicle_segment(tmp_path, "077")2600 seg_dir, edges = _vehicle_segment(tmp_path, "077")
26132601
2614 result = fuse_segment(2602 result = fuse.fuse_segment(
2615 seg_dir=seg_dir,2603 seg_dir=seg_dir,
2616 seg_name="077",2604 seg_name="077",
2617 edges_dirs=[edges],2605 edges_dirs=[edges],
2618 xml_path=None,2606 xml_path=None,
2619 guardrail_masks_dir=None,2607 guardrail_masks_dir=None,
2620 signs_masks_dir=None,2608 signs_masks_dir=None,
2621 config=Seg3dConfig(vehicle_enabled=False),2609 config=config.Seg3dConfig(vehicle_enabled=False),
2622 )2610 )
2623 assert (2611 assert (
2624 "vehicle: disabled by config (vehicle_enabled=false)"2612 "vehicle: disabled by config (vehicle_enabled=false)"
2625 in result.stats["skips"]2613 in result.stats["skips"]
Importance #163: tests/test_fusion.py @@ -2702,12 +2690,12 @@
2702 guard = classes.BY_NAME["guardrail"].las_code2690 guard = classes.BY_NAME["guardrail"].las_code
2703 support = classes.BY_NAME["guardrail_support"].las_code2691 support = classes.BY_NAME["guardrail_support"].las_code
2704 pts = np.array([[0.1, 0.1, 0.0], [0.2, 0.2, 0.0], [0.9, 0.9, 0.0]])2692 pts = np.array([[0.1, 0.1, 0.0], [0.2, 0.2, 0.0], [0.9, 0.9, 0.0]])
2705 cls = np.array([guard, guard, support], dtype=np.uint8)2693 cls = np.array([guard, guard, support], dtype=np.uint8)
2706 plut = classes.priority_lut(Seg3dConfig())2694 plut = classes.priority_lut(config.Seg3dConfig())
2707 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut)2695 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut)
2708 assert cls[rep[0]] == support2696 assert cls[rep[0]] == support
2709 plut_flat = classes.priority_lut(Seg3dConfig(priority_support=4))2697 plut_flat = classes.priority_lut(config.Seg3dConfig(priority_support=4))
2710 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut_flat)2698 rep = voxel.decimate(pts, cls, voxel=1.0, priorities=plut_flat)
2711 assert cls[rep[0]] == guard2699 assert cls[rep[0]] == guard
27122700
27132701
Importance #164: tests/test_fusion.py @@ -2762,9 +2750,9 @@
2762 is_surface=np.zeros(pts.shape[0], bool),2750 is_surface=np.zeros(pts.shape[0], bool),
2763 records=[io_npz.Record("a_run3_points.npz", 0, pts.shape[0])],2751 records=[io_npz.Record("a_run3_points.npz", 0, pts.shape[0])],
2764 surface_match_rate=0.0,2752 surface_match_rate=0.0,
2765 )2753 )
2766 return FuseResult(2754 return fuse.FuseResult(
2767 cloud=cloud,2755 cloud=cloud,
2768 classification=cls,2756 classification=cls,
2769 rep_index=np.arange(pts.shape[0]),2757 rep_index=np.arange(pts.shape[0]),
2770 instances=[],2758 instances=[],
Importance #165: tests/test_fusion.py @@ -3058,13 +3046,13 @@
3058# --------------------------------------------------------------------------- #3046# --------------------------------------------------------------------------- #
3059def test_parse_segments():3047def test_parse_segments():
3060 # seg3d semantics the CLI relies on: first-seen order, ascending-only3048 # seg3d semantics the CLI relies on: first-seen order, ascending-only
3061 # ranges, duplicates dropped -- the shared parser's defaults.3049 # ranges, duplicates dropped -- the shared parser's defaults.
3062 assert parse_segment_ids("066-069,038") == [66, 67, 68, 69, 38]3050 assert segments.parse_segment_ids("066-069,038") == [66, 67, 68, 69, 38]
3063 assert parse_segment_ids("5") == [5]3051 assert segments.parse_segment_ids("5") == [5]
3064 assert parse_segment_ids("1,1,2") == [1, 2]3052 assert segments.parse_segment_ids("1,1,2") == [1, 2]
3065 assert parse_segment_ids("074-066") == []3053 assert segments.parse_segment_ids("074-066") == []
3066 assert parse_segment_names("066-068,38") == ["066", "067", "068", "038"]3054 assert segments.parse_segment_names("066-068,38") == ["066", "067", "068", "038"]
30673055
30683056
3069def test_extend_polyline_reaches_beyond_ends():3057def test_extend_polyline_reaches_beyond_ends():
3070 xy = np.column_stack([np.linspace(0, 10, 11), np.zeros(11)])3058 xy = np.column_stack([np.linspace(0, 10, 11), np.zeros(11)])
Importance #166: tests/test_fusion.py @@ -3137,9 +3125,9 @@
3137 io_npz.Record("a_run3_points.npz", 0, 3),3125 io_npz.Record("a_run3_points.npz", 0, 3),
3138 io_npz.Record("b_run3_points.npz", 3, 3),3126 io_npz.Record("b_run3_points.npz", 3, 3),
3139 ]3127 ]
3140 rep, vop = voxel.decimate_with_map(pts, cls, voxel_size)3128 rep, vop = voxel.decimate_with_map(pts, cls, voxel_size)
3141 return FuseResult(3129 return fuse.FuseResult(
3142 cloud=_map_cloud(pts, records),3130 cloud=_map_cloud(pts, records),
3143 classification=cls,3131 classification=cls,
3144 rep_index=rep,3132 rep_index=rep,
3145 instances=[],3133 instances=[],
Importance #167: tests/test_fusion.py @@ -3373,9 +3361,9 @@
3373 [[0.0, 1.0, 0.0], [0.3, 1.2, 0.1], [4.0, 1.0, 0.0], [9.0, 1.0, 0.0]]3361 [[0.0, 1.0, 0.0], [0.3, 1.2, 0.1], [4.0, 1.0, 0.0], [9.0, 1.0, 0.0]]
3374 )3362 )
3375 _write_run3(seg_dir / "a_run3_points.npz", pts)3363 _write_run3(seg_dir / "a_run3_points.npz", pts)
33763364
3377 result = fuse_segment(3365 result = fuse.fuse_segment(
3378 seg_dir=seg_dir,3366 seg_dir=seg_dir,
3379 seg_name="078",3367 seg_name="078",
3380 edges_dirs=[],3368 edges_dirs=[],
3381 xml_path=None,3369 xml_path=None,
Importance #168: tests/test_fusion.py @@ -3456,9 +3444,9 @@
34563444
3457 pts = np.empty((0, 3))3445 pts = np.empty((0, 3))
3458 cls = np.empty(0, dtype=np.uint8)3446 cls = np.empty(0, dtype=np.uint8)
3459 rep, vop = voxel.decimate_with_map(pts, cls, 1.0)3447 rep, vop = voxel.decimate_with_map(pts, cls, 1.0)
3460 result = FuseResult(3448 result = fuse.FuseResult(
3461 cloud=_map_cloud(pts, []),3449 cloud=_map_cloud(pts, []),
3462 classification=cls,3450 classification=cls,
3463 rep_index=rep,3451 rep_index=rep,
3464 instances=[],3452 instances=[],
Importance #169: tests/test_fusion.py @@ -3528,10 +3516,10 @@
3528 pts[0] = [10.0, 20.0, 30.0]3516 pts[0] = [10.0, 20.0, 30.0]
3529 pts[1] = [10.009, 20.009, 30.009]3517 pts[1] = [10.009, 20.009, 30.009]
3530 pts[2] = [10.011, 20.0, 30.0]3518 pts[2] = [10.011, 20.0, 30.0]
35313519
3532 keys = voxel._voxel_keys(pts, Seg3dConfig().voxel_size_m)3520 keys = voxel._voxel_keys(pts, config.Seg3dConfig().voxel_size_m)
3533 ijk = np.floor(pts / Seg3dConfig().voxel_size_m).astype(np.int64)3521 ijk = np.floor(pts / config.Seg3dConfig().voxel_size_m).astype(np.int64)
3534 # Same voxel <=> same key, in both directions: the two labelings3522 # Same voxel <=> same key, in both directions: the two labelings
3535 # differ only by a permutation, so their pairing has as many distinct3523 # differ only by a permutation, so their pairing has as many distinct
3536 # values as either side alone (no merge, no split).3524 # values as either side alone (no merge, no split).
3537 _, want = np.unique(ijk, axis=0, return_inverse=True)3525 _, want = np.unique(ijk, axis=0, return_inverse=True)
Importance #170: tests/test_fusion.py @@ -3586,9 +3574,9 @@
3586 """Config with the tiny-cloud DTM floor, plus the test's overrides."""3574 """Config with the tiny-cloud DTM floor, plus the test's overrides."""
3587 # Five asphalt rows are a ground surface here; production wants 1000.3575 # Five asphalt rows are a ground surface here; production wants 1000.
3588 params = {"vegetation_min_ground_points": 4, **_VEG_LEGACY}3576 params = {"vegetation_min_ground_points": 4, **_VEG_LEGACY}
3589 params.update(overrides)3577 params.update(overrides)
3590 return Seg3dConfig(**params)3578 return config.Seg3dConfig(**params)
35913579
35923580
3593def _veg_segment(tmp_path, seg_name, *, extra=(), kept_mask=None):3581def _veg_segment(tmp_path, seg_name, *, extra=(), kept_mask=None):
3594 """Builds the vegetation scenario segment.3582 """Builds the vegetation scenario segment.
Importance #171: tests/test_fusion.py @@ -3635,22 +3623,22 @@
3635 )3623 )
3636 return seg_dir, edges, ground_dir3624 return seg_dir, edges, ground_dir
36373625
36383626
3639def _fuse_veg(tmp_path, seg_name, *, config=None, extra=(), kept_mask=None,3627def _fuse_veg(tmp_path, seg_name, *, config_override=None, extra=(), kept_mask=None,
3640 signs_dir=None, guardrail_dir=None):3628 signs_dir=None, guardrail_dir=None):
3641 seg_dir, edges, ground_dir = _veg_segment(3629 seg_dir, edges, ground_dir = _veg_segment(
3642 tmp_path, seg_name, extra=extra, kept_mask=kept_mask3630 tmp_path, seg_name, extra=extra, kept_mask=kept_mask
3643 )3631 )
3644 return fuse_segment(3632 return fuse.fuse_segment(
3645 seg_dir=seg_dir,3633 seg_dir=seg_dir,
3646 seg_name=seg_name,3634 seg_name=seg_name,
3647 edges_dirs=[edges],3635 edges_dirs=[edges],
3648 xml_path=None,3636 xml_path=None,
3649 guardrail_masks_dir=guardrail_dir,3637 guardrail_masks_dir=guardrail_dir,
3650 signs_masks_dir=signs_dir,3638 signs_masks_dir=signs_dir,
3651 ground_masks_dir=ground_dir,3639 ground_masks_dir=ground_dir,
3652 config=config or _veg_config(),3640 config=config_override or _veg_config(),
3653 )3641 )
36543642
36553643
3656def _write_guardrail_mask(tmp_path, seg_name, instances, record_name="a"):3644def _write_guardrail_mask(tmp_path, seg_name, instances, record_name="a"):
Importance #172: tests/test_fusion.py @@ -3711,9 +3699,9 @@
37113699
37123700
3713def test_fuse_segment_vegetation_tall_class_tree(tmp_path):3701def test_fuse_segment_vegetation_tall_class_tree(tmp_path):
3714 result = _fuse_veg(3702 result = _fuse_veg(
3715 tmp_path, "081", config=_veg_config(vegetation_tall_class="tree")3703 tmp_path, "081", config_override=_veg_config(vegetation_tall_class="tree")
3716 )3704 )
3717 cls = result.classification3705 cls = result.classification
3718 assert cls[5] == classes.BY_NAME["low_vegetation"].las_code3706 assert cls[5] == classes.BY_NAME["low_vegetation"].las_code
3719 assert cls[6] == classes.BY_NAME["medium_vegetation"].las_code3707 assert cls[6] == classes.BY_NAME["medium_vegetation"].las_code
Importance #173: tests/test_fusion.py @@ -3722,9 +3710,9 @@
37223710
3723def test_fuse_segment_vegetation_tall_class_unclassified(tmp_path):3711def test_fuse_segment_vegetation_tall_class_unclassified(tmp_path):
3724 result = _fuse_veg(3712 result = _fuse_veg(
3725 tmp_path, "082",3713 tmp_path, "082",
3726 config=_veg_config(vegetation_tall_class="unclassified"),3714 config_override=_veg_config(vegetation_tall_class="unclassified"),
3727 )3715 )
3728 # "unclassified" LEAVES the tall band as it was rather than stripping it.3716 # "unclassified" LEAVES the tall band as it was rather than stripping it.
3729 assert result.classification[7] == classes.UNCLASSIFIED_CODE3717 assert result.classification[7] == classes.UNCLASSIFIED_CODE
3730 assert result.stats["guard_metrics"]["vegetation_tall_points"] == 1.03718 assert result.stats["guard_metrics"]["vegetation_tall_points"] == 1.0
Importance #174: tests/test_fusion.py @@ -3732,9 +3720,9 @@
37323720
3733def test_fuse_segment_vegetation_corridor_rule_drops_the_median(tmp_path):3721def test_fuse_segment_vegetation_corridor_rule_drops_the_median(tmp_path):
3734 result = _fuse_veg(3722 result = _fuse_veg(
3735 tmp_path, "083",3723 tmp_path, "083",
3736 config=_veg_config(vegetation_asphalt_rule="corridor"),3724 config_override=_veg_config(vegetation_asphalt_rule="corridor"),
3737 )3725 )
3738 cls = result.classification3726 cls = result.classification
3739 # Inside the polygon -> not vegetation, and the sweep takes it instead.3727 # Inside the polygon -> not vegetation, and the sweep takes it instead.
3740 assert cls[10] == classes.BY_NAME["vehicle"].las_code3728 assert cls[10] == classes.BY_NAME["vehicle"].las_code
Importance #175: tests/test_fusion.py @@ -3760,9 +3748,9 @@
3760 assert np.all(column.classification[rows] == medium)3748 assert np.all(column.classification[rows] == medium)
37613749
3762 per_point = _fuse_veg(3750 per_point = _fuse_veg(
3763 tmp_path, "085", extra=hedge,3751 tmp_path, "085", extra=hedge,
3764 config=_veg_config(vegetation_band_mode="point"),3752 config_override=_veg_config(vegetation_band_mode="point"),
3765 )3753 )
3766 # Per point the same hedge grows a low skirt -- what the column rule is3754 # Per point the same hedge grows a low skirt -- what the column rule is
3767 # there to prevent.3755 # there to prevent.
3768 assert per_point.classification[11] == low3756 assert per_point.classification[11] == low
Importance #176: tests/test_fusion.py @@ -3775,15 +3763,15 @@
3775 ground_code = classes.BY_NAME["ground"].las_code3763 ground_code = classes.BY_NAME["ground"].las_code
37763764
3777 on = _fuse_veg(3765 on = _fuse_veg(
3778 tmp_path, "086", kept_mask=kept,3766 tmp_path, "086", kept_mask=kept,
3779 config=_veg_config(vegetation_from_ground=True),3767 config_override=_veg_config(vegetation_from_ground=True),
3780 )3768 )
3781 assert on.classification[5] == classes.BY_NAME["low_vegetation"].las_code3769 assert on.classification[5] == classes.BY_NAME["low_vegetation"].las_code
37823770
3783 off = _fuse_veg(3771 off = _fuse_veg(
3784 tmp_path, "087", kept_mask=kept,3772 tmp_path, "087", kept_mask=kept,
3785 config=_veg_config(vegetation_from_ground=False),3773 config_override=_veg_config(vegetation_from_ground=False),
3786 )3774 )
3787 assert off.classification[5] == ground_code3775 assert off.classification[5] == ground_code
3788 assert off.stats["guard_metrics"]["vegetation_candidates"] == 5.03776 assert off.stats["guard_metrics"]["vegetation_candidates"] == 5.0
37893777
Importance #177: tests/test_fusion.py @@ -3797,9 +3785,9 @@
37973785
37983786
3799def test_fuse_segment_vegetation_disabled(tmp_path):3787def test_fuse_segment_vegetation_disabled(tmp_path):
3800 result = _fuse_veg(3788 result = _fuse_veg(
3801 tmp_path, "088", config=_veg_config(vegetation_enabled=False)3789 tmp_path, "088", config_override=_veg_config(vegetation_enabled=False)
3802 )3790 )
3803 cls = result.classification3791 cls = result.classification
3804 assert np.all(cls[5:9] == classes.UNCLASSIFIED_CODE)3792 assert np.all(cls[5:9] == classes.UNCLASSIFIED_CODE)
3805 # A fuse-level skip carries the SAME metric keys as a run, all zero,3793 # A fuse-level skip carries the SAME metric keys as a run, all zero,
Importance #178: tests/test_fusion.py @@ -3826,9 +3814,9 @@
3826 _write_edges(3814 _write_edges(
3827 edges / "segment_089_edges.npz",3815 edges / "segment_089_edges.npz",
3828 left=[[0, -4, 0], [4, -4, 0]], right=[[0, 4, 0], [4, 4, 0]],3816 left=[[0, -4, 0], [4, -4, 0]], right=[[0, 4, 0], [4, 4, 0]],
3829 )3817 )
3830 result = fuse_segment(3818 result = fuse.fuse_segment(
3831 seg_dir=seg_dir,3819 seg_dir=seg_dir,
3832 seg_name="089",3820 seg_name="089",
3833 edges_dirs=[edges],3821 edges_dirs=[edges],
3834 xml_path=None,3822 xml_path=None,
Importance #179: tests/test_fusion.py @@ -3841,9 +3829,9 @@
38413829
38423830
3843def test_fuse_segment_vegetation_skips_without_a_ground_surface(tmp_path):3831def test_fuse_segment_vegetation_skips_without_a_ground_surface(tmp_path):
3844 # The packaged floor (1000 hard points) against five asphalt rows.3832 # The packaged floor (1000 hard points) against five asphalt rows.
3845 result = _fuse_veg(tmp_path, "090", config=Seg3dConfig())3833 result = _fuse_veg(tmp_path, "090", config_override=config.Seg3dConfig())
3846 skip = [s for s in result.stats["skips"] if s.startswith("vegetation")]3834 skip = [s for s in result.stats["skips"] if s.startswith("vegetation")]
3847 assert skip == ["vegetation: too few hard-surface points (5 < 1000)"]3835 assert skip == ["vegetation: too few hard-surface points (5 < 1000)"]
3848 assert result.classification[5] == classes.UNCLASSIFIED_CODE3836 assert result.classification[5] == classes.UNCLASSIFIED_CODE
3849 assert result.stats["guard_metrics"]["vegetation_candidates"] == 0.03837 assert result.stats["guard_metrics"]["vegetation_candidates"] == 0.0
Importance #180: tests/test_fusion.py @@ -3877,9 +3865,9 @@
3877 mask=[(0, list(range(11, 16)))],3865 mask=[(0, list(range(11, 16)))],
3878 )3866 )
3879 result = _fuse_veg(3867 result = _fuse_veg(
3880 tmp_path, "092", extra=tree, signs_dir=signs,3868 tmp_path, "092", extra=tree, signs_dir=signs,
3881 config=_veg_config(vegetation_tree_min_height_m=3.0),3869 config_override=_veg_config(vegetation_tree_min_height_m=3.0),
3882 )3870 )
3883 medium = classes.BY_NAME["medium_vegetation"].las_code3871 medium = classes.BY_NAME["medium_vegetation"].las_code
3884 # The whole instance moves, never point by point.3872 # The whole instance moves, never point by point.
3885 assert np.all(result.classification[11:16] == medium)3873 assert np.all(result.classification[11:16] == medium)
Importance #181: tests/test_fusion.py @@ -3895,9 +3883,9 @@
3895 # stage shipped with, limiters or not.3883 # stage shipped with, limiters or not.
3896 result = _fuse_veg(3884 result = _fuse_veg(
3897 tmp_path,3885 tmp_path,
3898 "089",3886 "089",
3899 config=Seg3dConfig(3887 config_override=config.Seg3dConfig(
3900 vegetation_min_ground_points=4,3888 vegetation_min_ground_points=4,
3901 vegetation_green_rg_ratio=1.0,3889 vegetation_green_rg_ratio=1.0,
3902 vegetation_asphalt_cell_m=0.5,3890 vegetation_asphalt_cell_m=0.5,
3903 vegetation_asphalt_dilate_cells=1,3891 vegetation_asphalt_dilate_cells=1,
Importance #182: tests/test_fusion.py @@ -3936,9 +3924,9 @@
3936 # points (so the column guard rejects nothing), and the scene has no3924 # points (so the column guard rejects nothing), and the scene has no
3937 # barrier, so the corridor-barrier rule takes both in-corridor rows3925 # barrier, so the corridor-barrier rule takes both in-corridor rows
3938 # instead and the sweep gets them.3926 # instead and the sweep gets them.
3939 result = _fuse_veg(3927 result = _fuse_veg(
3940 tmp_path, "090", config=Seg3dConfig(vegetation_min_ground_points=4)3928 tmp_path, "090", config_override=config.Seg3dConfig(vegetation_min_ground_points=4)
3941 )3929 )
3942 cls = result.classification3930 cls = result.classification
3943 vehicle = classes.BY_NAME["vehicle"].las_code3931 vehicle = classes.BY_NAME["vehicle"].las_code
3944 assert list(cls[5:9]) == [classes.UNCLASSIFIED_CODE] * 43932 assert list(cls[5:9]) == [classes.UNCLASSIFIED_CODE] * 4
Importance #183: tests/test_fusion.py @@ -3980,9 +3968,9 @@
3980 # leaves behind. With no barrier next to it the rule hands it back to3968 # leaves behind. With no barrier next to it the rule hands it back to
3981 # the vehicle sweep.3969 # the vehicle sweep.
3982 result = _fuse_veg(3970 result = _fuse_veg(
3983 tmp_path, "093",3971 tmp_path, "093",
3984 config=_veg_config(vegetation_corridor_rail_m=2.0),3972 config_override=_veg_config(vegetation_corridor_rail_m=2.0),
3985 )3973 )
3986 cls = result.classification3974 cls = result.classification
3987 vehicle = classes.BY_NAME["vehicle"].las_code3975 vehicle = classes.BY_NAME["vehicle"].las_code
3988 assert cls[10] == vehicle3976 assert cls[10] == vehicle
Importance #184: tests/test_fusion.py @@ -4004,9 +3992,9 @@
4004 rail = [(2.5, -2.0, 0.5, GREY_RGB)] # row 113992 rail = [(2.5, -2.0, 0.5, GREY_RGB)] # row 11
4005 guardrails = _write_guardrail_mask(tmp_path, "094", [("w_beam", [11])])3993 guardrails = _write_guardrail_mask(tmp_path, "094", [("w_beam", [11])])
4006 result = _fuse_veg(3994 result = _fuse_veg(
4007 tmp_path, "094", extra=rail, guardrail_dir=guardrails,3995 tmp_path, "094", extra=rail, guardrail_dir=guardrails,
4008 config=_veg_config(vegetation_corridor_rail_m=2.0),3996 config_override=_veg_config(vegetation_corridor_rail_m=2.0),
4009 )3997 )
4010 cls = result.classification3998 cls = result.classification
4011 assert cls[11] == classes.BY_NAME["guardrail"].las_code3999 assert cls[11] == classes.BY_NAME["guardrail"].las_code
4012 assert cls[10] == classes.BY_NAME["medium_vegetation"].las_code4000 assert cls[10] == classes.BY_NAME["medium_vegetation"].las_code
Importance #185: tests/test_fusion.py @@ -4027,9 +4015,9 @@
4027 ]4015 ]
4028 guardrails = _write_guardrail_mask(tmp_path, "095", [("w_beam", [11])])4016 guardrails = _write_guardrail_mask(tmp_path, "095", [("w_beam", [11])])
4029 result = _fuse_veg(4017 result = _fuse_veg(
4030 tmp_path, "095", extra=extra, guardrail_dir=guardrails,4018 tmp_path, "095", extra=extra, guardrail_dir=guardrails,
4031 config=_veg_config(4019 config_override=_veg_config(
4032 vegetation_corridor_rail_m=2.0,4020 vegetation_corridor_rail_m=2.0,
4033 vegetation_corridor_max_height_m=0.5,4021 vegetation_corridor_max_height_m=0.5,
4034 ),4022 ),
4035 )4023 )
Importance #186: tests/test_fusion.py @@ -4053,9 +4041,9 @@
4053 ]4041 ]
4054 guardrails = _write_guardrail_mask(tmp_path, "096", [("w_beam", [11])])4042 guardrails = _write_guardrail_mask(tmp_path, "096", [("w_beam", [11])])
4055 result = _fuse_veg(4043 result = _fuse_veg(
4056 tmp_path, "096", extra=extra, guardrail_dir=guardrails,4044 tmp_path, "096", extra=extra, guardrail_dir=guardrails,
4057 config=_veg_config(4045 config_override=_veg_config(
4058 vegetation_corridor_rail_m=2.0,4046 vegetation_corridor_rail_m=2.0,
4059 vegetation_corridor_max_height_m=0.0,4047 vegetation_corridor_max_height_m=0.0,
4060 ),4048 ),
4061 )4049 )
Importance #187: tests/test_fusion.py @@ -4068,9 +4056,9 @@
40684056
4069# --------------------------------------------------------------------------- #4057# --------------------------------------------------------------------------- #
4070# vegetation: re-band ownership and instance identity (AI3D-373)4058# vegetation: re-band ownership and instance identity (AI3D-373)
4071# --------------------------------------------------------------------------- #4059# --------------------------------------------------------------------------- #
4072def _short_tree_with_a_support(tmp_path, seg_name, config):4060def _short_tree_with_a_support(tmp_path, seg_name, config_override):
4073 """Fuses a short detector tree whose last two rows a support took."""4061 """Fuses a short detector tree whose last two rows a support took."""
4074 tree = [(8.0, 10.0, float(z), GREEN_RGB) for z in np.linspace(0.0, 1.0, 5)]4062 tree = [(8.0, 10.0, float(z), GREEN_RGB) for z in np.linspace(0.0, 1.0, 5)]
4075 signs = tmp_path / f"signs_{seg_name}"4063 signs = tmp_path / f"signs_{seg_name}"
4076 _write_signs_sidecars(4064 _write_signs_sidecars(
Importance #188: tests/test_fusion.py @@ -4082,9 +4070,9 @@
4082 tmp_path, seg_name, [("guardrail_support", [14, 15])]4070 tmp_path, seg_name, [("guardrail_support", [14, 15])]
4083 )4071 )
4084 return _fuse_veg(4072 return _fuse_veg(
4085 tmp_path, seg_name, extra=tree, signs_dir=signs,4073 tmp_path, seg_name, extra=tree, signs_dir=signs,
4086 guardrail_dir=guardrails, config=config,4074 guardrail_dir=guardrails, config_override=config_override,
4087 )4075 )
40884076
40894077
4090def test_fuse_segment_reband_leaves_rows_a_support_took_from_the_tree(4078def test_fuse_segment_reband_leaves_rows_a_support_took_from_the_tree(
Importance #189: tests/test_fusion.py @@ -4177,9 +4165,9 @@
4177 seg_dir / "a_run3_points.npz", pts,4165 seg_dir / "a_run3_points.npz", pts,
4178 rgb=np.zeros((pts.shape[0], 3), dtype=np.uint16),4166 rgb=np.zeros((pts.shape[0], 3), dtype=np.uint16),
4179 )4167 )
4180 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])4168 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])
4181 result = fuse_segment(4169 result = fuse.fuse_segment(
4182 seg_dir=seg_dir,4170 seg_dir=seg_dir,
4183 seg_name="101",4171 seg_name="101",
4184 edges_dirs=[],4172 edges_dirs=[],
4185 xml_path=None,4173 xml_path=None,
Importance #190: tests/test_fusion.py @@ -4207,9 +4195,9 @@
4207 rgb = np.zeros((pts.shape[0], 3), dtype=np.uint16)4195 rgb = np.zeros((pts.shape[0], 3), dtype=np.uint16)
4208 rgb[:, 0] = 300004196 rgb[:, 0] = 30000
4209 _write_run3(seg_dir / "a_run3_points.npz", pts, rgb=rgb)4197 _write_run3(seg_dir / "a_run3_points.npz", pts, rgb=rgb)
4210 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])4198 _write_run4(seg_dir / "a_run4_road_surface.npz", pts[:5])
4211 result = fuse_segment(4199 result = fuse.fuse_segment(
4212 seg_dir=seg_dir,4200 seg_dir=seg_dir,
4213 seg_name="102",4201 seg_name="102",
4214 edges_dirs=[],4202 edges_dirs=[],
4215 xml_path=None,4203 xml_path=None,
Importance #191: tests/test_fusion.py @@ -4230,22 +4218,22 @@
4230 # a stage skip: without it a run that could not guard the carriageway4218 # a stage skip: without it a run that could not guard the carriageway
4231 # looked exactly like one whose guards found nothing.4219 # looked exactly like one whose guards found nothing.
4232 kept = [True] * 5 + [False] * 6 # the tablecloth carries the DTM here4220 kept = [True] * 5 + [False] * 6 # the tablecloth carries the DTM here
4233 seg_dir, _, ground_dir = _veg_segment(tmp_path, "103", kept_mask=kept)4221 seg_dir, _, ground_dir = _veg_segment(tmp_path, "103", kept_mask=kept)
4234 config = _veg_config(4222 cfg = _veg_config(
4235 vegetation_corridor_rail_m=2.0,4223 vegetation_corridor_rail_m=2.0,
4236 vegetation_corridor_max_height_m=0.5,4224 vegetation_corridor_max_height_m=0.5,
4237 )4225 )
42384226
4239 result = fuse_segment(4227 result = fuse.fuse_segment(
4240 seg_dir=seg_dir,4228 seg_dir=seg_dir,
4241 seg_name="103",4229 seg_name="103",
4242 edges_dirs=[],4230 edges_dirs=[],
4243 xml_path=None,4231 xml_path=None,
4244 guardrail_masks_dir=None,4232 guardrail_masks_dir=None,
4245 signs_masks_dir=None,4233 signs_masks_dir=None,
4246 ground_masks_dir=ground_dir,4234 ground_masks_dir=ground_dir,
4247 config=config,4235 config=cfg,
4248 )4236 )
42494237
4250 assert (4238 assert (
4251 "vegetation: no corridor -> corridor guards inert"4239 "vegetation: no corridor -> corridor guards inert"
Importance #192: tests/test_fusion.py @@ -4271,9 +4259,9 @@
4271 # corridor means no exclusion at all.4259 # corridor means no exclusion at all.
4272 kept = [True] * 5 + [False] * 64260 kept = [True] * 5 + [False] * 6
4273 seg_dir, _, ground_dir = _veg_segment(tmp_path, "104", kept_mask=kept)4261 seg_dir, _, ground_dir = _veg_segment(tmp_path, "104", kept_mask=kept)
42744262
4275 result = fuse_segment(4263 result = fuse.fuse_segment(
4276 seg_dir=seg_dir,4264 seg_dir=seg_dir,
4277 seg_name="104",4265 seg_name="104",
4278 edges_dirs=[],4266 edges_dirs=[],
4279 xml_path=None,4267 xml_path=None,
Importance #193: tests/test_fusion.py @@ -4300,9 +4288,9 @@
4300 d = np.full(axy.shape[0], np.inf)4288 d = np.full(axy.shape[0], np.inf)
4301 ok = np.zeros(axy.shape[0], dtype=bool)4289 ok = np.zeros(axy.shape[0], dtype=bool)
4302 if vxyz.shape[0] == 0:4290 if vxyz.shape[0] == 0:
4303 return d, ok4291 return d, ok
4304 tree = cKDTree(vxyz[:, :2])4292 tree = spatial.cKDTree(vxyz[:, :2])
4305 vz = vxyz[:, 2]4293 vz = vxyz[:, 2]
4306 for idx, cand in enumerate(tree.query_ball_point(axy, r=xy_radius)):4294 for idx, cand in enumerate(tree.query_ball_point(axy, r=xy_radius)):
4307 if not cand:4295 if not cand:
4308 continue4296 continue
Importance #194: tests/test_fusion.py @@ -4321,9 +4309,9 @@
4321def test_nearest_gated_vertex_matches_the_per_point_reference(seed):4309def test_nearest_gated_vertex_matches_the_per_point_reference(seed):
4322 rng = np.random.default_rng(seed)4310 rng = np.random.default_rng(seed)
4323 pts = rng.uniform(-1.0, 1.0, size=(120, 3))4311 pts = rng.uniform(-1.0, 1.0, size=(120, 3))
4324 verts = rng.uniform(-1.0, 1.0, size=(40, 3))4312 verts = rng.uniform(-1.0, 1.0, size=(40, 3))
4325 got_d, got_ok = fuse_mod._nearest_gated_vertex(4313 got_d, got_ok = fuse._nearest_gated_vertex(
4326 pts[:, :2], pts[:, 2], verts, 0.25, 0.44314 pts[:, :2], pts[:, 2], verts, 0.25, 0.4
4327 )4315 )
4328 want_d, want_ok = _reference_nearest_gated_vertex(4316 want_d, want_ok = _reference_nearest_gated_vertex(
4329 pts[:, :2], pts[:, 2], verts, 0.25, 0.44317 pts[:, :2], pts[:, 2], verts, 0.25, 0.4
Importance #195: tests/test_fusion.py @@ -4337,13 +4325,13 @@
4337 """Blocking the radius join must not change a single distance."""4325 """Blocking the radius join must not change a single distance."""
4338 rng = np.random.default_rng(7)4326 rng = np.random.default_rng(7)
4339 pts = rng.uniform(-1.0, 1.0, size=(200, 3))4327 pts = rng.uniform(-1.0, 1.0, size=(200, 3))
4340 verts = rng.uniform(-1.0, 1.0, size=(60, 3))4328 verts = rng.uniform(-1.0, 1.0, size=(60, 3))
4341 one_d, one_ok = fuse_mod._nearest_gated_vertex(4329 one_d, one_ok = fuse._nearest_gated_vertex(
4342 pts[:, :2], pts[:, 2], verts, 0.3, 0.54330 pts[:, :2], pts[:, 2], verts, 0.3, 0.5
4343 )4331 )
4344 monkeypatch.setattr(fuse_mod, "_LINE_QUERY_BLOCK", 13)4332 monkeypatch.setattr(fuse, "_LINE_QUERY_BLOCK", 13)
4345 many_d, many_ok = fuse_mod._nearest_gated_vertex(4333 many_d, many_ok = fuse._nearest_gated_vertex(
4346 pts[:, :2], pts[:, 2], verts, 0.3, 0.54334 pts[:, :2], pts[:, 2], verts, 0.3, 0.5
4347 )4335 )
4348 np.testing.assert_array_equal(one_ok, many_ok)4336 np.testing.assert_array_equal(one_ok, many_ok)
4349 np.testing.assert_allclose(one_d, many_d)4337 np.testing.assert_allclose(one_d, many_d)
Importance #196: tests/test_fusion.py @@ -4359,9 +4347,9 @@
4359 verts = lines_xml.LineVertices(4347 verts = lines_xml.LineVertices(
4360 solid_xyz=solid_xyz, dashed_xyz=dashed_xyz4348 solid_xyz=solid_xyz, dashed_xyz=dashed_xyz
4361 )4349 )
43624350
4363 solid, dashed = _paint_lines(4351 solid, dashed = fuse._paint_lines(
4364 pts, is_asphalt, verts, xy_radius=0.3, z_gate=0.44352 pts, is_asphalt, verts, xy_radius=0.3, z_gate=0.4
4365 )4353 )
43664354
4367 asph_idx = np.nonzero(is_asphalt)[0]4355 asph_idx = np.nonzero(is_asphalt)[0]
Importance #197: tests/test_fusion.py @@ -4437,9 +4425,9 @@
4437 dtype=np.uint8,4425 dtype=np.uint8,
4438 )4426 )
44394427
4440 got = start.copy()4428 got = start.copy()
4441 fuse_mod._paint_mask_rows(got, rows, seg_classes)4429 fuse._paint_mask_rows(got, rows, seg_classes)
4442 want = start.copy()4430 want = start.copy()
4443 _reference_paint_mask_rows(want, rows, seg_classes)4431 _reference_paint_mask_rows(want, rows, seg_classes)
44444432
4445 np.testing.assert_array_equal(got, want)4433 np.testing.assert_array_equal(got, want)
Importance #198: tests/test_fusion.py @@ -4455,9 +4443,9 @@
4455 ],4443 ],
4456 dtype=np.uint8,4444 dtype=np.uint8,
4457 )4445 )
4458 got = start.copy()4446 got = start.copy()
4459 fuse_mod._paint_mask_rows(4447 fuse._paint_mask_rows(
4460 got,4448 got,
4461 np.array([0, 1, 2]),4449 np.array([0, 1, 2]),
4462 np.array(["low_vegetation"] * 3, dtype=np.str_),4450 np.array(["low_vegetation"] * 3, dtype=np.str_),
4463 )4451 )
Importance #199: tests/test_fusion.py @@ -4472,9 +4460,9 @@
44724460
44734461
4474def test_paint_mask_rows_gives_a_contested_row_to_the_support():4462def test_paint_mask_rows_gives_a_contested_row_to_the_support():
4475 got = np.array([classes.UNCLASSIFIED_CODE] * 2, dtype=np.uint8)4463 got = np.array([classes.UNCLASSIFIED_CODE] * 2, dtype=np.uint8)
4476 fuse_mod._paint_mask_rows(4464 fuse._paint_mask_rows(
4477 got,4465 got,
4478 np.array([0, 0, 1, 1]),4466 np.array([0, 0, 1, 1]),
4479 np.array(4467 np.array(
4480 ["guardrail_support", "guardrail", "guardrail_top_rail",4468 ["guardrail_support", "guardrail", "guardrail_top_rail",