Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,20 @@ STRUCTURA_CELLPOSE_FLOW=0.4
# U-Nets and will NOT load here.
STRUCTURA_CELLPOSE_MODEL=

# --- 2.5D track (DEM wall tracing): gap-bridging tolerance in world units (m) ---
# --- 2.5D track (DEM wall/edge tracing) ---
# Gap-bridging tolerance in world units (m).
STRUCTURA_GAP_BRIDGE_M=0.3
# Relief smoothing scales, in PIXELS — so they depend on the DEM's resolution.
# They decide *what* is traced: a kernel near stone size makes the response
# follow individual stones and the line meander around them. A wall course is
# 50-100 cm wide, so the kernels must exceed a stone. At 2.10 cm/px the default
# (9,25,51) is only 19/53/107 cm; 101,201 is 213/423 cm.
STRUCTURA_RELIEF_KERNELS=9,25,51
# Threshold on the relief response: mean + k·std, over valid pixels only.
STRUCTURA_DEM_THRESHOLD_K=1.0
# Drop polylines shorter than this (m), and optionally simplify them (m).
STRUCTURA_DEM_MIN_LENGTH_M=0.5
STRUCTURA_DEM_SIMPLIFY_M=

# --- Output sink: "file" (GeoPackage/GeoJSON, default), "postgis" (direct) or "api" (Django) ---
STRUCTURA_SINK=file
Expand Down
18 changes: 18 additions & 0 deletions src/structura/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def _load_dotenv(path: Path) -> None:
os.environ.setdefault(key.strip(), value.strip())


def _int_tuple(name: str, default: tuple[int, ...]) -> tuple[int, ...]:
"""Read a comma-separated integer list, e.g. STRUCTURA_RELIEF_KERNELS=51,101,201."""
raw = os.environ.get(name, "").strip()
if not raw:
return default
return tuple(int(part) for part in raw.split(",") if part.strip())


def _opt_float(name: str) -> float | None:
"""Read an optional numeric setting. Unset *and* empty both mean "no value",
so a commented-out or blanked line in `.env` behaves the way it looks."""
Expand Down Expand Up @@ -53,6 +61,12 @@ class Settings:
cellpose_model: str | None
# 2.5D track: gap-bridging tolerance (world units) for wall tracing
gap_bridge_m: float
# 2.5D track: relief smoothing scales in *pixels*, and the tracing thresholds.
# The kernels decide what is traced — see WallTracer.
relief_kernels: tuple[int, ...]
dem_threshold_k: float
dem_min_length_m: float
dem_simplify_m: float | None
# File sink (default)
output_path: Path
# PostGIS
Expand Down Expand Up @@ -88,6 +102,10 @@ def from_env(cls, dotenv: str | Path = ".env") -> "Settings":
),
cellpose_model=os.environ.get("STRUCTURA_CELLPOSE_MODEL") or None,
gap_bridge_m=float(os.environ.get("STRUCTURA_GAP_BRIDGE_M", "0.3")),
relief_kernels=_int_tuple("STRUCTURA_RELIEF_KERNELS", (9, 25, 51)),
dem_threshold_k=float(os.environ.get("STRUCTURA_DEM_THRESHOLD_K", "1.0")),
dem_min_length_m=float(os.environ.get("STRUCTURA_DEM_MIN_LENGTH_M", "0.5")),
dem_simplify_m=_opt_float("STRUCTURA_DEM_SIMPLIFY_M"),
output_path=Path(
os.environ.get("STRUCTURA_OUTPUT_PATH", "./data/output/features.gpkg")
),
Expand Down
11 changes: 10 additions & 1 deletion src/structura/dem/wall_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ def __init__(
self,
gap_bridge_m: float = 0.3,
threshold_k: float = 1.0,
kernels: tuple[int, ...] = (9, 25, 51),
min_length_m: float = 0.5,
simplify_m: float | None = None,
) -> None:
self.gap_bridge_m = gap_bridge_m
# Relief smoothing scales in pixels. These decide *what* is traced: a
# kernel near stone size makes the response follow individual stones, so
# the skeleton meanders around them instead of running along the course.
# A wall is 50-100 cm wide, so the kernels have to exceed a stone for the
# stones to merge into one continuous ridge. In pixels, so scale them to
# the DEM's resolution — at 2.10 cm/px the default (9, 25, 51) is only
# 19, 52 and 107 cm.
self.kernels = kernels
self.threshold_k = threshold_k
self.min_length_m = min_length_m
self.simplify_m = simplify_m
Expand All @@ -42,7 +51,7 @@ def trace(self, dem_path: Path) -> list[Feature]:
dem, valid, transform, crs = geo.read_dem(dem_path)
res = abs(transform.a) # pixel size in world units

response = relief.multiscale_relief(dem)
response = relief.multiscale_relief(dem, kernels=self.kernels)
ridge = threshold_response(response, valid, self.threshold_k)

return relief_response_to_features(
Expand Down
14 changes: 12 additions & 2 deletions src/structura/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,18 @@ def run(settings: Settings, *, write: bool = True) -> list[Feature]:
if product.kind is RasterKind.ORTHO:
features += segmenter.segment(product.path)
elif product.kind is RasterKind.DEM:
features += WallTracer(gap_bridge_m=settings.gap_bridge_m).trace(product.path)
features += EdgeTracer().trace(product.path)
features += WallTracer(
gap_bridge_m=settings.gap_bridge_m,
threshold_k=settings.dem_threshold_k,
kernels=settings.relief_kernels,
min_length_m=settings.dem_min_length_m,
simplify_m=settings.dem_simplify_m,
).trace(product.path)
features += EdgeTracer(
threshold_k=settings.dem_threshold_k,
min_length_m=settings.dem_min_length_m,
simplify_m=settings.dem_simplify_m,
).trace(product.path)
elif product.kind is RasterKind.SEMANTIC:
# Discovered and tagged, but not consumed yet — the class field
# becomes a prior for the vector tracks in v0.8. Tagging it is what
Expand Down
4 changes: 4 additions & 0 deletions tests/test_make_segmenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def _settings(backend: str, **over: object) -> Settings:
segmentation_backend=backend,
gpu=False,
gap_bridge_m=0.3,
relief_kernels=(9, 25, 51),
dem_threshold_k=1.0,
dem_min_length_m=0.5,
dem_simplify_m=None,
**kwargs, # type: ignore[arg-type]
output_path=Path("out.gpkg"),
pg_dsn=None,
Expand Down
4 changes: 4 additions & 0 deletions tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def _settings(input_dir: Path, output_path: Path, min_area: float = 1e-4) -> Set
cellpose_cellprob_threshold=0.0,
cellpose_model=None,
gap_bridge_m=0.3,
relief_kernels=(9, 25, 51),
dem_threshold_k=1.0,
dem_min_length_m=0.5,
dem_simplify_m=None,
output_path=output_path,
pg_dsn=None,
pg_schema="public",
Expand Down