Miroslav Simko <ms@iolabs.ch> 2026-09-02T08:56:49+02:00
Commit #26 · 9 snippets
README.md | 2 +- .../_config.py | 13 +++++++++++ .../bright_points.default.json | 1 + tests/test_bright_points_config.py | 26 ++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-)
| 4 | from collections.abc import Mapping | 4 | from collections.abc import Mapping |
| 5 | from pathlib import Path | 5 | from pathlib import Path |
| 6 | from typing import Any, Literal | 6 | from typing import Any, Literal |
| 7 | 7 | ||
| 8 | import pydantic | ||
| 9 | |||
| 8 | from iolabs.common import config_loader | 10 | from iolabs.common import config_loader |
| 9 | 11 | ||
| 10 | logger = logging.getLogger(__name__) | 12 | logger = logging.getLogger(__name__) |
| 11 | 13 |
| 88 | laser_intensity_fitting: LaserIntensityFittingConfig = LaserIntensityFittingConfig() | 90 | laser_intensity_fitting: LaserIntensityFittingConfig = LaserIntensityFittingConfig() |
| 89 | color_intensity_fitting: ColorIntensityFittingConfig = ColorIntensityFittingConfig() | 91 | color_intensity_fitting: ColorIntensityFittingConfig = ColorIntensityFittingConfig() |
| 90 | file_naming: FileNamingConfig = FileNamingConfig() | 92 | file_naming: FileNamingConfig = FileNamingConfig() |
| 91 | 93 | ||
| 94 | @pydantic.field_validator( | ||
| 95 | "laser_intensity_fitting", | ||
| 96 | "color_intensity_fitting", | ||
| 97 | "file_naming", | ||
| 98 | mode="before", | ||
| 99 | ) | ||
| 100 | @classmethod | ||
| 101 | def _null_section_means_defaults(cls, value: Any) -> Any: | ||
| 102 | """Treat an explicit JSON ``null`` section as "use the section defaults".""" | ||
| 103 | return {} if value is None else value | ||
| 104 | |||
| 92 | 105 | ||
| 93 | class BrightPointsConfigError(config_loader.ConfigError): | 106 | class BrightPointsConfigError(config_loader.ConfigError): |
| 94 | """Raised when bright-points config contains unsupported keys or values.""" | 107 | """Raised when bright-points config contains unsupported keys or values.""" |
| 95 | 108 |
| 24 | "fit_width": 10000.0, | 24 | "fit_width": 10000.0, |
| 25 | "n_sigma": 4.0, | 25 | "n_sigma": 4.0, |
| 26 | "cutoff_n_sigma": 7.0, | 26 | "cutoff_n_sigma": 7.0, |
| 27 | "max_sigma": 5000.0, | 27 | "max_sigma": 5000.0, |
| 28 | "default_sigma": null, | ||
| 28 | "min_mu": 0.0, | 29 | "min_mu": 0.0, |
| 29 | "max_mu": 68000.0, | 30 | "max_mu": 68000.0, |
| 30 | "min_sigma": 200.0, | 31 | "min_sigma": 200.0, |
| 31 | "angle_min": -80.0, | 32 | "angle_min": -80.0, |
| 87 | ) | 87 | ) |
| 88 | 88 | ||
| 89 | assert config["laser_intensity_fitting"]["default_sigma"] == 1234.0 | 89 | assert config["laser_intensity_fitting"]["default_sigma"] == 1234.0 |
| 90 | assert load_bright_points_config()["laser_intensity_fitting"]["default_sigma"] is None | 90 | assert load_bright_points_config()["laser_intensity_fitting"]["default_sigma"] is None |
| 91 | |||
| 92 | |||
| 93 | @pytest.mark.parametrize( | ||
| 94 | "section", | ||
| 95 | ["laser_intensity_fitting", "color_intensity_fitting", "file_naming"], | ||
| 96 | ) | ||
| 97 | def test_normalize_bright_points_config_accepts_null_section(section): | ||
| 98 | config = normalize_bright_points_config({section: None}) | ||
| 99 | |||
| 100 | assert config[section] == _config.BrightPointsConfig().model_dump()[section] | ||
| 101 | |||
| 102 | |||
| 103 | def test_packaged_json_holds_every_model_key(): | ||
| 104 | import json | ||
| 105 | from importlib import resources | ||
| 106 | |||
| 107 | path = resources.files("iolabs_point_cloud_filtering_intensity").joinpath( | ||
| 108 | "bright_points.default.json" | ||
| 109 | ) | ||
| 110 | raw = json.loads(path.read_text(encoding="utf-8")) | ||
| 111 | model_dump = _config.BrightPointsConfig().model_dump() | ||
| 112 | |||
| 113 | assert sorted(raw) == sorted(model_dump) | ||
| 114 | for key, value in model_dump.items(): | ||
| 115 | if isinstance(value, dict): | ||
| 116 | assert sorted(raw[key]) == sorted(value), key |
| 16 | 16 | ||
| 17 | ## Requirements | 17 | ## Requirements |
| 18 | 18 | ||
| 19 | - Python ≥3.11, <3.13 | 19 | - Python ≥3.11, <3.13 |
| 20 | - numpy, open3d, torch, scipy, matplotlib, laspy, pypdf, tqdm, pydantic, iolabs-common (≥0.8.0), iolabs-geometry-geometry, iolabs-geometry-visualization, iolabs-point-cloud-filtering-surface, iolabs-point-cloud-las-tools | 20 | - numpy, open3d, torch, scipy, matplotlib, laspy, pypdf, tqdm, pydantic (≥2.7), iolabs-common (≥0.9.0), iolabs-geometry-geometry, iolabs-geometry-visualization, iolabs-point-cloud-filtering-surface, iolabs-point-cloud-las-tools |
| 21 | 21 | ||
| 22 | ## Usage | 22 | ## Usage |
| 23 | 23 | ||
| 24 | Identifies bright lane markings using intensity filtering on top of surface detection. Feeds into cluster-based middle lane detection and trajectory segmentation. | 24 | Identifies bright lane markings using intensity filtering on top of surface detection. Feeds into cluster-based middle lane detection and trajectory segmentation. |
| 4 | from collections.abc import Mapping | 4 | from collections.abc import Mapping |
| 5 | from pathlib import Path | 5 | from pathlib import Path |
| 6 | from typing import Any, Literal | 6 | from typing import Any, Literal |
| 7 | 7 | ||
| 8 | import pydantic | ||
| 9 | |||
| 8 | from iolabs.common import config_loader | 10 | from iolabs.common import config_loader |
| 9 | 11 | ||
| 10 | logger = logging.getLogger(__name__) | 12 | logger = logging.getLogger(__name__) |
| 11 | 13 |
| 88 | laser_intensity_fitting: LaserIntensityFittingConfig = LaserIntensityFittingConfig() | 90 | laser_intensity_fitting: LaserIntensityFittingConfig = LaserIntensityFittingConfig() |
| 89 | color_intensity_fitting: ColorIntensityFittingConfig = ColorIntensityFittingConfig() | 91 | color_intensity_fitting: ColorIntensityFittingConfig = ColorIntensityFittingConfig() |
| 90 | file_naming: FileNamingConfig = FileNamingConfig() | 92 | file_naming: FileNamingConfig = FileNamingConfig() |
| 91 | 93 | ||
| 94 | @pydantic.field_validator( | ||
| 95 | "laser_intensity_fitting", | ||
| 96 | "color_intensity_fitting", | ||
| 97 | "file_naming", | ||
| 98 | mode="before", | ||
| 99 | ) | ||
| 100 | @classmethod | ||
| 101 | def _null_section_means_defaults(cls, value: Any) -> Any: | ||
| 102 | """Treat an explicit JSON ``null`` section as "use the section defaults".""" | ||
| 103 | return {} if value is None else value | ||
| 104 | |||
| 92 | 105 | ||
| 93 | class BrightPointsConfigError(config_loader.ConfigError): | 106 | class BrightPointsConfigError(config_loader.ConfigError): |
| 94 | """Raised when bright-points config contains unsupported keys or values.""" | 107 | """Raised when bright-points config contains unsupported keys or values.""" |
| 95 | 108 |
| 24 | "fit_width": 10000.0, | 24 | "fit_width": 10000.0, |
| 25 | "n_sigma": 4.0, | 25 | "n_sigma": 4.0, |
| 26 | "cutoff_n_sigma": 7.0, | 26 | "cutoff_n_sigma": 7.0, |
| 27 | "max_sigma": 5000.0, | 27 | "max_sigma": 5000.0, |
| 28 | "default_sigma": null, | ||
| 28 | "min_mu": 0.0, | 29 | "min_mu": 0.0, |
| 29 | "max_mu": 68000.0, | 30 | "max_mu": 68000.0, |
| 30 | "min_sigma": 200.0, | 31 | "min_sigma": 200.0, |
| 31 | "angle_min": -80.0, | 32 | "angle_min": -80.0, |
| 87 | ) | 87 | ) |
| 88 | 88 | ||
| 89 | assert config["laser_intensity_fitting"]["default_sigma"] == 1234.0 | 89 | assert config["laser_intensity_fitting"]["default_sigma"] == 1234.0 |
| 90 | assert load_bright_points_config()["laser_intensity_fitting"]["default_sigma"] is None | 90 | assert load_bright_points_config()["laser_intensity_fitting"]["default_sigma"] is None |
| 91 | |||
| 92 | |||
| 93 | @pytest.mark.parametrize( | ||
| 94 | "section", | ||
| 95 | ["laser_intensity_fitting", "color_intensity_fitting", "file_naming"], | ||
| 96 | ) | ||
| 97 | def test_normalize_bright_points_config_accepts_null_section(section): | ||
| 98 | config = normalize_bright_points_config({section: None}) | ||
| 99 | |||
| 100 | assert config[section] == _config.BrightPointsConfig().model_dump()[section] | ||
| 101 | |||
| 102 | |||
| 103 | def test_packaged_json_holds_every_model_key(): | ||
| 104 | import json | ||
| 105 | from importlib import resources | ||
| 106 | |||
| 107 | path = resources.files("iolabs_point_cloud_filtering_intensity").joinpath( | ||
| 108 | "bright_points.default.json" | ||
| 109 | ) | ||
| 110 | raw = json.loads(path.read_text(encoding="utf-8")) | ||
| 111 | model_dump = _config.BrightPointsConfig().model_dump() | ||
| 112 | |||
| 113 | assert sorted(raw) == sorted(model_dump) | ||
| 114 | for key, value in model_dump.items(): | ||
| 115 | if isinstance(value, dict): | ||
| 116 | assert sorted(raw[key]) == sorted(value), key |
nullconfig sections fall back to section defaults (pre-migration behaviour), re-validation instead ofmodel_copy(update=), doc/test parity with the packaged JSON.