added export scripts for ortho and buildings

This commit is contained in:
s0wlz (Matthias Puchstein)
2025-12-14 02:56:33 +01:00
parent da16f66d30
commit 22b5dd4fa5
4 changed files with 245 additions and 6 deletions

View File

@@ -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 importers 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/<tile>.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/<tile>.obj` (EPSG:25832 coordinates).

102
export_buildings.py Normal file
View File

@@ -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_<utm>_<easting>_<northing>.gml : source building tiles
- export_unity/tile_index.csv : manifest produced by export_heightmaps.py
Outputs
- export_unity/buildings_obj/<tile_id>.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()

View File

@@ -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)

106
export_ortho_tiles.py Normal file
View File

@@ -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/<tile_id>.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()