From 22b5dd4fa5aefca3a3b93f97da4f800690502c47 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Sun, 14 Dec 2025 02:56:33 +0100 Subject: [PATCH] added export scripts for ortho and buildings --- README.md | 26 +++++++++-- export_buildings.py | 102 ++++++++++++++++++++++++++++++++++++++++ export_heightmaps.py | 17 ++++++- export_ortho_tiles.py | 106 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 export_buildings.py create mode 100644 export_ortho_tiles.py diff --git a/README.md b/README.md index 5461237..1976712 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,15 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height - `work/` — intermediates such as `dgm.vrt` and `_tmp.tif` files; safe to delete/regenerate. - `export_unity/height_png16/` — final 16-bit PNG heightmaps for Unity import. - `export_unity/tile_index.csv` — manifest mapping tile IDs to world bounds and global min/max used for scaling. +- `export_unity/ortho_jpg/` — cropped orthophoto tiles aligned to the terrain grid (JPEG + worldfiles). +- `export_unity/buildings_obj/` — per-tile clipped CityGML exports as OBJ meshes. - `export_heightmaps.py` — main export script. +- `export_ortho_tiles.py` — exports orthophoto tiles from DOP JP2 inputs using the terrain manifest. +- `export_buildings.py` — clips CityGML LoD2 tiles per terrain tile and writes OBJ meshes. - `AGENTS.md` — contributor guide. ### Quick Start -1. Build the VRT mosaic from raw tiles: - ```bash - gdalbuildvrt work/dgm.vrt raw_dgm1/*.tif - ``` -2. Export Unity heightmaps and manifest: +1. Export Unity heightmaps and manifest (builds `work/dgm.vrt` automatically if missing): ```bash python3 export_heightmaps.py ``` @@ -38,3 +38,19 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height - Large raw datasets are intentionally excluded from version control—document download sources or scripts instead of committing data. - Additional inputs: `raw_dop/` contains an HTTPS download helper (`raw_dop/dlscript.sh`) that fetches JP2/J2W/XML orthophotos listed in `filelist.txt`; `3dgeblod1/` and `3dgeblod2/` hold zipped 3D building tiles with object lists for future use. - Handoff to Unity: copy/sync `export_unity/height_png16/` and `export_unity/tile_index.csv` into `DTrierFlood/Assets/GeoData/` before running the Unity-side importer. Keep `OUT_RES` aligned with the importer’s expected resolution (currently 1025). + +### Orthophotos (textures) +1. Ensure DOP JP2s are present in `raw_dop/jp2/` (use `raw_dop/dlscript.sh` if needed). +2. From `GeoData/`, run: + ```bash + python3 export_ortho_tiles.py + ``` + This builds `work/dop.vrt` if missing and writes `export_unity/ortho_jpg/.jpg` + `.jgw` aligned to `tile_index.csv`. + +### Buildings (CityGML LoD2 → OBJ) +1. Ensure LoD2 GML tiles live in `raw_3dgeb_lod2/` (unzipped from `3dgeblod2` bundle). +2. From `GeoData/`, run: + ```bash + python3 export_buildings.py + ``` + This clips each GML to the terrain tile footprint and exports `export_unity/buildings_obj/.obj` (EPSG:25832 coordinates). diff --git a/export_buildings.py b/export_buildings.py new file mode 100644 index 0000000..cb33d88 --- /dev/null +++ b/export_buildings.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Clip CityGML LoD2 buildings per terrain tile and export as OBJ meshes. + +Inputs +- raw_3dgeb_lod2/LoD2___.gml : source building tiles +- export_unity/tile_index.csv : manifest produced by export_heightmaps.py + +Outputs +- export_unity/buildings_obj/.obj : clipped building geometries per tile +""" +from __future__ import annotations + +import csv +import os +from pathlib import Path + +from osgeo import gdal + +RAW_GML_DIR = Path("raw_3dgeb_lod2") +TILE_INDEX = Path("export_unity/tile_index.csv") +OUT_DIR = Path("export_unity/buildings_obj") + +OUTPUT_FORMAT = "OBJ" # glTF driver is not always available; OBJ is widely supported. + +gdal.UseExceptions() +OUT_DIR.mkdir(parents=True, exist_ok=True) + + +def gml_path_for_tile(tile_id: str) -> Path: + """Map a DGM tile id (dgm1_utm_easting_northing) to the corresponding CityGML file.""" + parts = tile_id.split("_") + if len(parts) != 4 or parts[0] != "dgm1": + raise ValueError(f"Unexpected tile_id format: {tile_id}") + utm_zone, easting, northing = parts[1:] + return RAW_GML_DIR / f"LoD2_{utm_zone}_{easting}_{northing}.gml" + + +def open_dataset(path: str, purpose: str): + """Open a dataset and fail fast with context.""" + try: + ds = gdal.OpenEx(path) + except RuntimeError as exc: + raise SystemExit(f"{purpose}: {exc}") from exc + if ds is None: + raise SystemExit(f"{purpose}: GDAL returned None for {path}") + return ds + + +def main() -> None: + if not TILE_INDEX.exists(): + raise SystemExit(f"Tile index missing: {TILE_INDEX}. Run export_heightmaps.py first.") + + written = 0 + skipped = 0 + + with TILE_INDEX.open(newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + try: + tile_id = row["tile_id"] + xmin = float(row["xmin"]) + ymin = float(row["ymin"]) + xmax = float(row["xmax"]) + ymax = float(row["ymax"]) + except (KeyError, ValueError) as exc: + print(f"Skipping malformed row {row}: {exc}") + skipped += 1 + continue + + gml_path = gml_path_for_tile(tile_id) + if not gml_path.exists(): + print(f"Missing CityGML for {tile_id}: {gml_path}") + skipped += 1 + continue + + out_path = OUT_DIR / f"{tile_id}.obj" + + src_ds = open_dataset(str(gml_path), f"Could not open {gml_path}") + + opts = gdal.VectorTranslateOptions( + format=OUTPUT_FORMAT, + spatFilter=(xmin, ymin, xmax, ymax), + layerName="Building", # matches GML layer name + ) + + try: + gdal.VectorTranslate(destNameOrDestDS=str(out_path), srcDS=src_ds, options=opts) + except RuntimeError as exc: + print(f"VectorTranslate failed for {tile_id}: {exc}") + skipped += 1 + continue + + written += 1 + print(f"Wrote {out_path}") + + print(f"Summary: wrote {written} building tiles; skipped {skipped}.") + if skipped: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/export_heightmaps.py b/export_heightmaps.py index 0a6320c..9ec3afe 100644 --- a/export_heightmaps.py +++ b/export_heightmaps.py @@ -18,6 +18,20 @@ os.makedirs(OUT_DIR, exist_ok=True) gdal.UseExceptions() +def build_dgm_vrt_if_needed(): + """Build the DGM VRT automatically when missing.""" + if os.path.exists(VRT_PATH): + return + tif_paths = sorted(glob.glob(os.path.join(RAW_DIR, "*.tif"))) + if not tif_paths: + raise SystemExit(f"No TIFFs found in {RAW_DIR}; cannot build {VRT_PATH}.") + print(f"Building {VRT_PATH} from {len(tif_paths)} GeoTIFFs...") + try: + gdal.BuildVRT(VRT_PATH, tif_paths) + except RuntimeError as exc: + raise SystemExit(f"Could not build {VRT_PATH}: {exc}") from exc + + def open_dataset(path, purpose): """Open a dataset and fail fast with context.""" try: @@ -57,7 +71,8 @@ def cleanup_aux_files(): print(f"Cleanup removed {removed} temporary files/sidecars.") -ds = open_dataset(VRT_PATH, f"Could not open {VRT_PATH}. Did you run gdalbuildvrt?") +build_dgm_vrt_if_needed() +ds = open_dataset(VRT_PATH, f"Could not open {VRT_PATH} after attempting to build it.") band = ds.GetRasterBand(1) diff --git a/export_ortho_tiles.py b/export_ortho_tiles.py new file mode 100644 index 0000000..c923814 --- /dev/null +++ b/export_ortho_tiles.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Export orthophoto tiles aligned to the terrain grid. + +Inputs +- raw_dop/jp2/*.jp2 : orthophoto source tiles (DOP20 RGB) +- export_unity/tile_index.csv : manifest produced by export_heightmaps.py + +Outputs +- work/dop.vrt : auto-built VRT mosaic of all JP2 tiles +- export_unity/ortho_jpg/.jpg : cropped JPEG tiles + .jgw worldfiles +""" +from __future__ import annotations + +import csv +import glob +import os +from typing import Iterable + +from osgeo import gdal + +RAW_ORTHO_DIR = "raw_dop/jp2" +VRT_PATH = "work/dop.vrt" +TILE_INDEX = "export_unity/tile_index.csv" +OUT_DIR = "export_unity/ortho_jpg" + +# 1000 m tiles at ~0.5 m/pixel give good visual quality in Unity while staying light. +OUT_RES = 2048 +JPEG_QUALITY = 90 + +gdal.UseExceptions() +os.makedirs("work", exist_ok=True) +os.makedirs(OUT_DIR, exist_ok=True) + + +def build_vrt(jp2_paths: Iterable[str]) -> None: + """Build the orthophoto VRT if missing.""" + print(f"Building VRT at {VRT_PATH} from {len(list(jp2_paths))} JP2 files...") + gdal.BuildVRT(VRT_PATH, list(jp2_paths)) + + +def open_dataset(path: str, purpose: str): + """Open a dataset and fail fast with context.""" + try: + ds = gdal.Open(path) + except RuntimeError as exc: + raise SystemExit(f"{purpose}: {exc}") from exc + if ds is None: + raise SystemExit(f"{purpose}: GDAL returned None for {path}") + return ds + + +def main() -> None: + jp2_paths = sorted(glob.glob(os.path.join(RAW_ORTHO_DIR, "*.jp2"))) + if not jp2_paths: + raise SystemExit(f"No JP2 files found in {RAW_ORTHO_DIR}. Run raw_dop/dlscript.sh first.") + + if not os.path.exists(VRT_PATH): + build_vrt(jp2_paths) + + vrt_ds = open_dataset(VRT_PATH, f"Could not open VRT at {VRT_PATH}") + + if not os.path.exists(TILE_INDEX): + raise SystemExit(f"Tile index missing: {TILE_INDEX}. Run export_heightmaps.py first.") + + with open(TILE_INDEX, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + written = 0 + skipped = 0 + + for row in reader: + try: + tile_id = row["tile_id"] + xmin = float(row["xmin"]) + ymin = float(row["ymin"]) + xmax = float(row["xmax"]) + ymax = float(row["ymax"]) + except (KeyError, ValueError) as exc: + print(f"Skipping malformed row {row}: {exc}") + skipped += 1 + continue + + out_path = os.path.join(OUT_DIR, f"{tile_id}.jpg") + opts = gdal.TranslateOptions( + format="JPEG", + width=OUT_RES, + height=OUT_RES, + projWin=(xmin, ymax, xmax, ymin), # xmin,xmax,ymax,ymin (upper-left origin) + creationOptions=[f"QUALITY={JPEG_QUALITY}", "WORLDFILE=YES"], + ) + try: + gdal.Translate(out_path, vrt_ds, options=opts) + except RuntimeError as exc: + print(f"Translate failed for {tile_id}: {exc}") + skipped += 1 + continue + + written += 1 + print(f"Wrote {out_path}") + + print(f"Summary: wrote {written} orthophoto tiles; skipped {skipped}.") + if skipped: + raise SystemExit(1) + + +if __name__ == "__main__": + main()