Files
GeoData/geodata_pipeline/config.py

363 lines
12 KiB
Python

from __future__ import annotations
import os
from dataclasses import asdict, dataclass, field, fields, replace
from typing import Any, Dict
import tomllib
import tomli_w
DEFAULT_CONFIG_PATH = "geodata_config.toml"
@dataclass
class RawConfig:
dgm1_dir: str = "raw/dgm1"
dop20_dir: str = "raw/dop20/jp2"
citygml_lod1_dir: str = "raw/citygml/lod1"
citygml_lod2_dir: str = "raw/citygml/lod2"
@dataclass
class ArchiveConfig:
dgm1_dir: str = "archive/dgm1"
dop20_dir: str = "archive/dop20"
citygml_lod1_dir: str = "archive/citygml/lod1"
citygml_lod2_dir: str = "archive/citygml/lod2"
@dataclass
class WorkConfig:
work_dir: str = "work"
heightmap_vrt: str = "work/dgm.vrt"
ortho_vrt: str = "work/dop.vrt"
@dataclass
class ExportConfig:
heightmap_dir: str = "export_unity/height_png16"
ortho_dir: str = "export_unity/ortho_jpg"
manifest_path: str = "export_unity/tile_index.csv"
@dataclass
class HeightmapConfig:
out_res: int = 1025
resample: str = "bilinear"
tile_size_m: int = 1000
use_tile_minmax: bool = True
@dataclass
class RiverErosionProfileConfig:
order_field: str = "ORD_STRA"
invert_order: bool = False
invert_max_order: int = 10
min_order: int = 1
width_base_m: float = 8.0
width_per_order_m: float = 6.0
width_min_m: float = 4.0
width_max_m: float = 90.0
depth_base_m: float = 0.2
depth_per_order_m: float = 0.44
depth_min_m: float = 0.2
depth_max_m: float = 6.0
smooth_sigma_m: float = 3.0
fallback_depth_m: float = 2.0
flow_distance_field: str = "DIST_DN_KM"
flow_slope_m_per_km: float = 0.0
@dataclass
class RiverErosionLidarConfig:
enabled: bool = True
source_dir: str = "raw/bdom20rgbi"
classification_water: int = 9
depth_m: float = 3.5
bank_slope_sigma: float = 2.0
fill_holes_radius: int = 3
def _default_river_profile_vr() -> "RiverErosionProfileConfig":
return RiverErosionProfileConfig(
order_field="ORD_STRA",
invert_order=False,
invert_max_order=10,
min_order=1,
width_base_m=8.0,
width_per_order_m=6.0,
width_min_m=4.0,
width_max_m=90.0,
depth_base_m=0.2,
depth_per_order_m=0.44,
depth_min_m=0.2,
depth_max_m=6.0,
smooth_sigma_m=3.0,
fallback_depth_m=2.0,
flow_distance_field="DIST_DN_KM",
flow_slope_m_per_km=0.0,
)
def _default_river_profile_server() -> "RiverErosionProfileConfig":
return RiverErosionProfileConfig(
order_field="ORD_FLOW",
invert_order=True,
invert_max_order=10,
min_order=2,
width_base_m=5.0,
width_per_order_m=7.0,
width_min_m=3.0,
width_max_m=60.0,
depth_base_m=0.4,
depth_per_order_m=0.3,
depth_min_m=0.2,
depth_max_m=4.0,
smooth_sigma_m=1.5,
)
@dataclass
class RiverErosionConfig:
enabled: bool = True
source_path: str = "raw/HydroRIVERS_v10_eu_shp/HydroRIVERS_v10_eu.shp"
source_layer: str = "HydroRIVERS_v10_eu"
output_dir: str = "export_unity/height_png16_river"
manifest_vr: str = "export_unity/tile_index_river_vr.csv"
manifest_server: str = "export_unity/tile_index_river_server.csv"
vr: RiverErosionProfileConfig = field(default_factory=_default_river_profile_vr)
server: RiverErosionProfileConfig = field(default_factory=_default_river_profile_server)
lidar: RiverErosionLidarConfig = field(default_factory=RiverErosionLidarConfig)
@dataclass
class OrthoConfig:
out_res: int = 2048
jpeg_quality: int = 90
apply_water_mask: bool = False
water_blend: float = 0.85
water_fallback_rgb: tuple[float, float, float] = (30.0, 50.0, 60.0)
water_mask_threshold: float = 0.1
river_dir: str = "export_unity/ortho_jpg_river"
water_color_mode: str = "median"
@dataclass
class TileKeyConfig:
tile_size_x: float = 1000.0
tile_size_y: float = 1000.0
overlap_x: float = 0.5
overlap_y: float = 0.5
enabled: bool = True
@dataclass
class BuildingConfig:
out_dir: str = "export_unity/buildings_tiles"
work_cityjson_dir: str = "work/cityjson_lod2"
work_rebased_dir: str = "work/cityjson_lod2_local"
work_glb_dir: str = "work/buildings_glb_tmp"
triangle_budget_min: int = 200000
triangle_budget_max: int = 350000
roof_unlit: bool = True
@dataclass
class TreeConfig:
csv_dir: str = "export_unity/trees"
glb_dir: str = "export_unity/trees_tiles"
proxy_library: str = "export_unity/tree_proxies.glb"
grid_res_m: float = 2.0
min_height_m: float = 2.0
max_trees: int = 5000
chunk_grid: int = 4
proxy_variants: int = 16
proxy_min_tris: int = 120
proxy_max_tris: int = 180
building_buffer_m: float = 1.5
instancing: bool = False
@dataclass
class PointCloudConfig:
"""Configuration for point cloud data sources."""
lpg_dir: str = "raw/lpg"
lpo_dir: str = "raw/lpo"
lpolpg_dir: str = "raw/lpolpg"
bdom_dir: str = "raw/bdom20rgbi"
dom1_dir: str = "raw/dom1"
chunk_size: int = 5_000_000
use_lpg_validation: bool = True
use_lpo_refinement: bool = True
@dataclass
class EnhancedHeightmapConfig:
"""Configuration for enhanced heightmap export with LPG validation."""
lpg_validation_threshold_m: float = 0.5
export_quality_map: bool = True
quality_map_dir: str = "export_unity/height_quality"
@dataclass
class EnhancedBuildingConfig:
"""Configuration for enhanced building export with point cloud fusion."""
out_dir: str = "export_unity/buildings_enhanced"
ransac_threshold_m: float = 0.15
ransac_iterations: int = 1000
max_roof_planes: int = 6
min_plane_inliers: int = 50
use_bdom_roof: bool = True
use_lpo_refinement: bool = True
use_dop20_textures: bool = True
fallback_to_citygml: bool = True
@dataclass
class StreetFurnitureConfig:
"""Configuration for street furniture detection."""
enabled: bool = True
csv_dir: str = "export_unity/street_furniture"
min_height_m: float = 0.3
max_height_m: float = 8.0
min_confidence: float = 0.6
use_lpo_refinement: bool = True
lamp_height_range: tuple = (4.0, 8.0)
bench_height_range: tuple = (0.4, 1.0)
sign_height_range: tuple = (2.0, 4.0)
bollard_height_range: tuple = (0.5, 1.2)
@dataclass
class EnhancedTreeConfig:
"""Configuration for enhanced tree detection."""
csv_dir: str = "export_unity/trees_enhanced"
glb_dir: str = "export_unity/trees_enhanced_tiles"
exclude_furniture: bool = True
use_lpo_refinement: bool = True
sample_canopy_color: bool = True
lpo_search_radius_m: float = 5.0
canopy_sample_radius_factor: float = 0.5
@dataclass
class Config:
raw: RawConfig = field(default_factory=RawConfig)
archives: ArchiveConfig = field(default_factory=ArchiveConfig)
work: WorkConfig = field(default_factory=WorkConfig)
export: ExportConfig = field(default_factory=ExportConfig)
heightmap: HeightmapConfig = field(default_factory=HeightmapConfig)
river_erosion: RiverErosionConfig = field(default_factory=RiverErosionConfig)
ortho: OrthoConfig = field(default_factory=OrthoConfig)
tile_key: TileKeyConfig = field(default_factory=TileKeyConfig)
buildings: BuildingConfig = field(default_factory=BuildingConfig)
trees: TreeConfig = field(default_factory=TreeConfig)
# Enhanced pipeline configs
pointcloud: PointCloudConfig = field(default_factory=PointCloudConfig)
heightmap_enhanced: EnhancedHeightmapConfig = field(default_factory=EnhancedHeightmapConfig)
buildings_enhanced: EnhancedBuildingConfig = field(default_factory=EnhancedBuildingConfig)
street_furniture: StreetFurnitureConfig = field(default_factory=StreetFurnitureConfig)
trees_enhanced: EnhancedTreeConfig = field(default_factory=EnhancedTreeConfig)
@classmethod
def default(cls) -> "Config":
return cls()
@classmethod
def load(cls, path: str = DEFAULT_CONFIG_PATH) -> "Config":
with open(path, "rb") as f:
data = tomllib.load(f)
return cls.from_dict(data)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Config":
return cls(
raw=RawConfig(**_filter_kwargs(RawConfig, data.get("raw", {}))),
archives=ArchiveConfig(**_filter_kwargs(ArchiveConfig, data.get("archives", {}))),
work=WorkConfig(**_filter_kwargs(WorkConfig, data.get("work", {}))),
export=ExportConfig(**_filter_kwargs(ExportConfig, data.get("export", {}))),
heightmap=HeightmapConfig(**_filter_kwargs(HeightmapConfig, data.get("heightmap", {}))),
river_erosion=_river_erosion_from_dict(data.get("river_erosion", {})),
ortho=OrthoConfig(**_filter_kwargs(OrthoConfig, data.get("ortho", {}))),
tile_key=TileKeyConfig(**_filter_kwargs(TileKeyConfig, data.get("tile_key", {}))),
buildings=BuildingConfig(**_filter_kwargs(BuildingConfig, data.get("buildings", {}))),
trees=TreeConfig(**_filter_kwargs(TreeConfig, data.get("trees", {}))),
# Enhanced pipeline configs (with defaults for backward compat)
pointcloud=PointCloudConfig(**_filter_kwargs(PointCloudConfig, data.get("pointcloud", {}))),
heightmap_enhanced=EnhancedHeightmapConfig(**_filter_kwargs(
EnhancedHeightmapConfig,
data.get("heightmap_enhanced", {}),
)),
buildings_enhanced=EnhancedBuildingConfig(**_filter_kwargs(
EnhancedBuildingConfig,
data.get("buildings_enhanced", {}),
)),
street_furniture=StreetFurnitureConfig(**_filter_kwargs(
StreetFurnitureConfig,
_coerce_ranges(data.get("street_furniture", {})),
)),
trees_enhanced=EnhancedTreeConfig(**_filter_kwargs(
EnhancedTreeConfig,
data.get("trees_enhanced", {}),
)),
)
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
def save(self, path: str = DEFAULT_CONFIG_PATH) -> None:
payload = tomli_w.dumps(self.to_dict())
with open(path, "w", encoding="utf-8") as f:
f.write(payload)
def with_overrides(self, raw_dgm1_path: str | None = None, raw_dop20_path: str | None = None) -> "Config":
cfg = self
if raw_dgm1_path:
cfg = replace(cfg, raw=replace(cfg.raw, dgm1_dir=raw_dgm1_path))
if raw_dop20_path:
cfg = replace(cfg, raw=replace(cfg.raw, dop20_dir=raw_dop20_path))
return cfg
def ensure_default_config(path: str = DEFAULT_CONFIG_PATH) -> Config:
if not os.path.exists(path):
cfg = Config.default()
cfg.save(path)
return cfg
return Config.load(path)
def _filter_kwargs(cls: type, data: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(data, dict):
return {}
allowed = {field.name for field in fields(cls)}
return {key: value for key, value in data.items() if key in allowed}
def _coerce_ranges(data: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(data, dict):
return {}
out = dict(data)
for key in ("lamp_height_range", "bench_height_range", "sign_height_range", "bollard_height_range"):
value = out.get(key)
if isinstance(value, list):
out[key] = tuple(value)
value = out.get("water_fallback_rgb")
if isinstance(value, list):
out["water_fallback_rgb"] = tuple(value)
return out
def _river_erosion_from_dict(data: Dict[str, Any]) -> RiverErosionConfig:
if not isinstance(data, dict):
return RiverErosionConfig()
base = _filter_kwargs(RiverErosionConfig, data)
base.pop("vr", None)
base.pop("server", None)
base.pop("lidar", None)
vr_cfg = RiverErosionProfileConfig(**_filter_kwargs(RiverErosionProfileConfig, data.get("vr", {})))
server_cfg = RiverErosionProfileConfig(**_filter_kwargs(RiverErosionProfileConfig, data.get("server", {})))
lidar_cfg = RiverErosionLidarConfig(**_filter_kwargs(RiverErosionLidarConfig, data.get("lidar", {})))
return RiverErosionConfig(**base, vr=vr_cfg, server=server_cfg, lidar=lidar_cfg)