voxcity 0.6.26__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxcity/__init__.py +10 -4
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +9 -1
- voxcity/exporter/cityles.py +129 -34
- voxcity/exporter/envimet.py +51 -26
- voxcity/exporter/magicavoxel.py +42 -5
- voxcity/exporter/netcdf.py +27 -0
- voxcity/exporter/obj.py +103 -28
- voxcity/generator/__init__.py +47 -0
- voxcity/generator/api.py +721 -0
- voxcity/generator/grids.py +381 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +392 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +1488 -1169
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +26 -10
- voxcity/geoprocessor/network.py +35 -6
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +435 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +159 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +824 -820
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +66 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/shape.py +230 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +1145 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
- voxcity-1.0.2.dist-info/RECORD +81 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
voxcity/generator.py
DELETED
|
@@ -1,1302 +0,0 @@
|
|
|
1
|
-
"""Main module for voxcity.
|
|
2
|
-
|
|
3
|
-
This module provides functions to generate 3D voxel representations of cities using various data sources.
|
|
4
|
-
It handles land cover, building heights, canopy heights, and digital elevation models to create detailed
|
|
5
|
-
3D city models.
|
|
6
|
-
|
|
7
|
-
The main functions are:
|
|
8
|
-
- get_land_cover_grid: Creates a grid of land cover classifications
|
|
9
|
-
- get_building_height_grid: Creates a grid of building heights (supports GeoDataFrame input)
|
|
10
|
-
- get_canopy_height_grid: Creates a grid of tree canopy heights
|
|
11
|
-
- get_dem_grid: Creates a digital elevation model grid
|
|
12
|
-
- create_3d_voxel: Combines the grids into a 3D voxel representation
|
|
13
|
-
- create_3d_voxel_individuals: Creates separate voxel grids for each component
|
|
14
|
-
- get_voxcity: Main function to generate a complete voxel city model (supports GeoDataFrame input)
|
|
15
|
-
|
|
16
|
-
Key Features:
|
|
17
|
-
- Support for multiple data sources (OpenStreetMap, ESA WorldCover, Google Earth Engine, etc.)
|
|
18
|
-
- Direct GeoDataFrame input for building data (useful for custom datasets)
|
|
19
|
-
- 3D voxel generation with configurable resolution
|
|
20
|
-
- Visualization capabilities for both 2D grids and 3D models
|
|
21
|
-
- Data export in various formats (GeoTIFF, GeoJSON, pickle)
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
# Standard library imports
|
|
25
|
-
import numpy as np
|
|
26
|
-
import os
|
|
27
|
-
try:
|
|
28
|
-
from numba import jit, prange
|
|
29
|
-
import numba
|
|
30
|
-
NUMBA_AVAILABLE = True
|
|
31
|
-
except ImportError:
|
|
32
|
-
NUMBA_AVAILABLE = False
|
|
33
|
-
print("Numba not available. Using optimized version without JIT compilation.")
|
|
34
|
-
# Define dummy decorators
|
|
35
|
-
def jit(*args, **kwargs):
|
|
36
|
-
def decorator(func):
|
|
37
|
-
return func
|
|
38
|
-
return decorator
|
|
39
|
-
prange = range
|
|
40
|
-
|
|
41
|
-
# Local application/library specific imports
|
|
42
|
-
|
|
43
|
-
# Data downloaders - modules for fetching geospatial data from various sources
|
|
44
|
-
from .downloader.mbfp import get_mbfp_gdf
|
|
45
|
-
from .downloader.osm import load_gdf_from_openstreetmap, load_land_cover_gdf_from_osm
|
|
46
|
-
from .downloader.oemj import save_oemj_as_geotiff
|
|
47
|
-
# from .downloader.omt import load_gdf_from_openmaptiles
|
|
48
|
-
from .downloader.eubucco import load_gdf_from_eubucco
|
|
49
|
-
from .downloader.overture import load_gdf_from_overture
|
|
50
|
-
from .downloader.citygml import load_buid_dem_veg_from_citygml
|
|
51
|
-
|
|
52
|
-
# Google Earth Engine related imports - for satellite and elevation data
|
|
53
|
-
from .downloader.gee import (
|
|
54
|
-
initialize_earth_engine,
|
|
55
|
-
get_roi,
|
|
56
|
-
get_ee_image_collection,
|
|
57
|
-
get_ee_image,
|
|
58
|
-
save_geotiff,
|
|
59
|
-
get_dem_image,
|
|
60
|
-
save_geotiff_esa_land_cover,
|
|
61
|
-
save_geotiff_esri_landcover,
|
|
62
|
-
save_geotiff_dynamic_world_v1,
|
|
63
|
-
save_geotiff_open_buildings_temporal,
|
|
64
|
-
save_geotiff_dsm_minus_dtm
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
# Grid processing functions - for converting geodata to raster grids
|
|
68
|
-
from .geoprocessor.grid import (
|
|
69
|
-
group_and_label_cells,
|
|
70
|
-
process_grid,
|
|
71
|
-
create_land_cover_grid_from_geotiff_polygon,
|
|
72
|
-
create_height_grid_from_geotiff_polygon,
|
|
73
|
-
create_building_height_grid_from_gdf_polygon,
|
|
74
|
-
create_dem_grid_from_geotiff_polygon,
|
|
75
|
-
create_land_cover_grid_from_gdf_polygon,
|
|
76
|
-
create_building_height_grid_from_open_building_temporal_polygon,
|
|
77
|
-
create_vegetation_height_grid_from_gdf_polygon,
|
|
78
|
-
create_canopy_grids_from_tree_gdf,
|
|
79
|
-
create_dem_grid_from_gdf_polygon
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Utility functions
|
|
83
|
-
from .utils.lc import convert_land_cover, convert_land_cover_array
|
|
84
|
-
from .geoprocessor.polygon import get_gdf_from_gpkg, save_geojson
|
|
85
|
-
|
|
86
|
-
# Visualization functions - for creating plots and 3D visualizations
|
|
87
|
-
from .utils.visualization import (
|
|
88
|
-
get_land_cover_classes,
|
|
89
|
-
visualize_land_cover_grid,
|
|
90
|
-
visualize_numerical_grid,
|
|
91
|
-
visualize_landcover_grid_on_basemap,
|
|
92
|
-
visualize_numerical_grid_on_basemap,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
96
|
-
"""Creates a grid of land cover classifications.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
100
|
-
meshsize: Size of each grid cell in meters
|
|
101
|
-
source: Data source for land cover (e.g. 'ESA WorldCover', 'OpenStreetMap')
|
|
102
|
-
output_dir: Directory to save output files
|
|
103
|
-
**kwargs: Additional arguments including:
|
|
104
|
-
- esri_landcover_year: Year for ESRI land cover data
|
|
105
|
-
- dynamic_world_date: Date for Dynamic World data
|
|
106
|
-
- gridvis: Whether to visualize the grid
|
|
107
|
-
- default_land_cover_class: Default class for grid cells with no intersecting polygons (default: 'Developed space')
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
numpy.ndarray: Grid of land cover classifications as integer values
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
print("Creating Land Use Land Cover grid\n ")
|
|
114
|
-
print(f"Data source: {source}")
|
|
115
|
-
|
|
116
|
-
# Initialize Earth Engine for satellite-based data sources
|
|
117
|
-
# Skip initialization for local/vector data sources
|
|
118
|
-
if source not in ["OpenStreetMap", "OpenEarthMapJapan"]:
|
|
119
|
-
initialize_earth_engine()
|
|
120
|
-
|
|
121
|
-
# Ensure output directory exists for saving intermediate files
|
|
122
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
123
|
-
geotiff_path = os.path.join(output_dir, "land_cover.tif")
|
|
124
|
-
|
|
125
|
-
# Handle different data sources - each requires specific processing
|
|
126
|
-
# Satellite/raster-based sources are saved as GeoTIFF files
|
|
127
|
-
if source == 'Urbanwatch':
|
|
128
|
-
# Urban-focused land cover from satellite imagery
|
|
129
|
-
roi = get_roi(rectangle_vertices)
|
|
130
|
-
collection_name = "projects/sat-io/open-datasets/HRLC/urban-watch-cities"
|
|
131
|
-
image = get_ee_image_collection(collection_name, roi)
|
|
132
|
-
save_geotiff(image, geotiff_path)
|
|
133
|
-
elif source == 'ESA WorldCover':
|
|
134
|
-
# Global land cover from European Space Agency
|
|
135
|
-
roi = get_roi(rectangle_vertices)
|
|
136
|
-
save_geotiff_esa_land_cover(roi, geotiff_path)
|
|
137
|
-
elif source == 'ESRI 10m Annual Land Cover':
|
|
138
|
-
# High-resolution annual land cover from ESRI
|
|
139
|
-
esri_landcover_year = kwargs.get("esri_landcover_year")
|
|
140
|
-
roi = get_roi(rectangle_vertices)
|
|
141
|
-
save_geotiff_esri_landcover(roi, geotiff_path, year=esri_landcover_year)
|
|
142
|
-
elif source == 'Dynamic World V1':
|
|
143
|
-
# Near real-time land cover from Google's Dynamic World
|
|
144
|
-
dynamic_world_date = kwargs.get("dynamic_world_date")
|
|
145
|
-
roi = get_roi(rectangle_vertices)
|
|
146
|
-
save_geotiff_dynamic_world_v1(roi, geotiff_path, dynamic_world_date)
|
|
147
|
-
elif source == 'OpenEarthMapJapan':
|
|
148
|
-
# Japan-specific land cover dataset
|
|
149
|
-
save_oemj_as_geotiff(rectangle_vertices, geotiff_path)
|
|
150
|
-
elif source == 'OpenStreetMap':
|
|
151
|
-
# Vector-based land cover from OpenStreetMap
|
|
152
|
-
# This bypasses the GeoTIFF workflow and gets data directly as GeoJSON
|
|
153
|
-
land_cover_gdf = load_land_cover_gdf_from_osm(rectangle_vertices)
|
|
154
|
-
|
|
155
|
-
# Get the classification scheme for the selected data source
|
|
156
|
-
# Each source has its own land cover categories and color coding
|
|
157
|
-
land_cover_classes = get_land_cover_classes(source)
|
|
158
|
-
|
|
159
|
-
# Convert geospatial data to regular grid format
|
|
160
|
-
# Different processing for vector vs raster data sources
|
|
161
|
-
if source == 'OpenStreetMap':
|
|
162
|
-
# Process vector data directly from GeoDataFrame
|
|
163
|
-
default_class = kwargs.get('default_land_cover_class', 'Developed space')
|
|
164
|
-
land_cover_grid_str = create_land_cover_grid_from_gdf_polygon(land_cover_gdf, meshsize, source, rectangle_vertices, default_class=default_class)
|
|
165
|
-
else:
|
|
166
|
-
# Process raster data from GeoTIFF file
|
|
167
|
-
land_cover_grid_str = create_land_cover_grid_from_geotiff_polygon(geotiff_path, meshsize, land_cover_classes, rectangle_vertices)
|
|
168
|
-
|
|
169
|
-
# Prepare color mapping for visualization
|
|
170
|
-
# Convert RGB values from 0-255 range to 0-1 range for matplotlib
|
|
171
|
-
color_map = {cls: [r/255, g/255, b/255] for (r,g,b), cls in land_cover_classes.items()}
|
|
172
|
-
|
|
173
|
-
# Generate visualization if requested
|
|
174
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
175
|
-
if grid_vis:
|
|
176
|
-
# Flip grid vertically for correct display orientation
|
|
177
|
-
visualize_land_cover_grid(np.flipud(land_cover_grid_str), meshsize, color_map, land_cover_classes)
|
|
178
|
-
|
|
179
|
-
# Convert string-based land cover labels to integer codes for processing
|
|
180
|
-
# This enables efficient numerical operations on the grid
|
|
181
|
-
land_cover_grid_int = convert_land_cover_array(land_cover_grid_str, land_cover_classes)
|
|
182
|
-
|
|
183
|
-
return land_cover_grid_int
|
|
184
|
-
|
|
185
|
-
# def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir="output", visualization=True, maptiler_API_key=None, file_path=None):
|
|
186
|
-
def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, building_gdf=None, **kwargs):
|
|
187
|
-
"""Creates a grid of building heights.
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
191
|
-
meshsize: Size of each grid cell in meters
|
|
192
|
-
source: Data source for buildings (e.g. 'OpenStreetMap', 'Microsoft Building Footprints', 'GeoDataFrame')
|
|
193
|
-
output_dir: Directory to save output files
|
|
194
|
-
building_gdf: Optional GeoDataFrame with building footprint, height and other information
|
|
195
|
-
**kwargs: Additional arguments including:
|
|
196
|
-
- maptiler_API_key: API key for MapTiler
|
|
197
|
-
- building_path: Path to local building data file
|
|
198
|
-
- building_complementary_source: Additional building data source
|
|
199
|
-
- gridvis: Whether to visualize the grid
|
|
200
|
-
|
|
201
|
-
Returns:
|
|
202
|
-
tuple:
|
|
203
|
-
- numpy.ndarray: Grid of building heights
|
|
204
|
-
- numpy.ndarray: Grid of building minimum heights
|
|
205
|
-
- numpy.ndarray: Grid of building IDs
|
|
206
|
-
- list: Filtered building features
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
# Initialize Earth Engine for satellite-based building data sources
|
|
210
|
-
if source not in ["OpenStreetMap", "Overture", "Local file", "GeoDataFrame"]:
|
|
211
|
-
initialize_earth_engine()
|
|
212
|
-
|
|
213
|
-
print("Creating Building Height grid\n ")
|
|
214
|
-
print(f"Data source: {source}")
|
|
215
|
-
|
|
216
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
217
|
-
|
|
218
|
-
# If building_gdf is provided, use it directly
|
|
219
|
-
if building_gdf is not None:
|
|
220
|
-
gdf = building_gdf
|
|
221
|
-
print("Using provided GeoDataFrame for building data")
|
|
222
|
-
else:
|
|
223
|
-
# Fetch building data from primary source
|
|
224
|
-
# Each source has different data formats and processing requirements
|
|
225
|
-
# Floor height (m) for inferring heights from floors/levels
|
|
226
|
-
floor_height = kwargs.get("floor_height", 3.0)
|
|
227
|
-
if source == 'Microsoft Building Footprints':
|
|
228
|
-
# Machine learning-derived building footprints from satellite imagery
|
|
229
|
-
gdf = get_mbfp_gdf(output_dir, rectangle_vertices)
|
|
230
|
-
elif source == 'OpenStreetMap':
|
|
231
|
-
# Crowd-sourced building data with varying completeness
|
|
232
|
-
gdf = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
|
|
233
|
-
elif source == "Open Building 2.5D Temporal":
|
|
234
|
-
# Special case: this source provides both footprints and heights
|
|
235
|
-
# Skip GeoDataFrame processing and create grids directly
|
|
236
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_open_building_temporal_polygon(meshsize, rectangle_vertices, output_dir)
|
|
237
|
-
elif source == 'EUBUCCO v0.1':
|
|
238
|
-
# European building database with height information
|
|
239
|
-
gdf = load_gdf_from_eubucco(rectangle_vertices, output_dir)
|
|
240
|
-
# elif source == "OpenMapTiles":
|
|
241
|
-
# # Vector tiles service for building data
|
|
242
|
-
# gdf = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
|
|
243
|
-
elif source == "Overture":
|
|
244
|
-
# Open building dataset from Overture Maps Foundation
|
|
245
|
-
gdf = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
|
|
246
|
-
elif source == "Local file":
|
|
247
|
-
# Handle user-provided local building data files
|
|
248
|
-
_, extension = os.path.splitext(kwargs["building_path"])
|
|
249
|
-
if extension == ".gpkg":
|
|
250
|
-
gdf = get_gdf_from_gpkg(kwargs["building_path"], rectangle_vertices)
|
|
251
|
-
elif source == "GeoDataFrame":
|
|
252
|
-
# This case is handled by the building_gdf parameter above
|
|
253
|
-
raise ValueError("When source is 'GeoDataFrame', building_gdf parameter must be provided")
|
|
254
|
-
|
|
255
|
-
# Handle complementary data sources to fill gaps or provide additional information
|
|
256
|
-
# This allows combining multiple sources for better coverage or accuracy
|
|
257
|
-
building_complementary_source = kwargs.get("building_complementary_source")
|
|
258
|
-
building_complement_height = kwargs.get("building_complement_height")
|
|
259
|
-
overlapping_footprint = kwargs.get("overlapping_footprint")
|
|
260
|
-
|
|
261
|
-
if (building_complementary_source is None) or (building_complementary_source=='None'):
|
|
262
|
-
# Use only the primary data source
|
|
263
|
-
if source != "Open Building 2.5D Temporal":
|
|
264
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
265
|
-
else:
|
|
266
|
-
# Combine primary source with complementary data
|
|
267
|
-
if building_complementary_source == "Open Building 2.5D Temporal":
|
|
268
|
-
# Use temporal height data to complement footprint data
|
|
269
|
-
roi = get_roi(rectangle_vertices)
|
|
270
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
271
|
-
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
272
|
-
save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
|
|
273
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
274
|
-
elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
|
|
275
|
-
# Use digital surface model minus digital terrain model for height estimation
|
|
276
|
-
roi = get_roi(rectangle_vertices)
|
|
277
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
278
|
-
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
279
|
-
save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
|
|
280
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
281
|
-
else:
|
|
282
|
-
# Fetch complementary data from another vector source
|
|
283
|
-
if building_complementary_source == 'Microsoft Building Footprints':
|
|
284
|
-
gdf_comp = get_mbfp_gdf(output_dir, rectangle_vertices)
|
|
285
|
-
elif building_complementary_source == 'OpenStreetMap':
|
|
286
|
-
gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
|
|
287
|
-
elif building_complementary_source == 'EUBUCCO v0.1':
|
|
288
|
-
gdf_comp = load_gdf_from_eubucco(rectangle_vertices, output_dir)
|
|
289
|
-
# elif building_complementary_source == "OpenMapTiles":
|
|
290
|
-
# gdf_comp = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
|
|
291
|
-
elif building_complementary_source == "Overture":
|
|
292
|
-
gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
|
|
293
|
-
elif building_complementary_source == "Local file":
|
|
294
|
-
_, extension = os.path.splitext(kwargs["building_complementary_path"])
|
|
295
|
-
if extension == ".gpkg":
|
|
296
|
-
gdf_comp = get_gdf_from_gpkg(kwargs["building_complementary_path"], rectangle_vertices)
|
|
297
|
-
|
|
298
|
-
# Configure how to combine the complementary data
|
|
299
|
-
# Can complement footprints only or both footprints and heights
|
|
300
|
-
complement_building_footprints = kwargs.get("complement_building_footprints")
|
|
301
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, gdf_comp=gdf_comp, complement_building_footprints=complement_building_footprints, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
302
|
-
|
|
303
|
-
# Generate visualization if requested
|
|
304
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
305
|
-
if grid_vis:
|
|
306
|
-
# Replace zeros with NaN for better visualization (don't show empty areas)
|
|
307
|
-
building_height_grid_nan = building_height_grid.copy()
|
|
308
|
-
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
309
|
-
# Flip grid vertically for correct display orientation
|
|
310
|
-
visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
|
|
311
|
-
|
|
312
|
-
return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
|
|
313
|
-
|
|
314
|
-
def get_canopy_height_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
315
|
-
"""Creates canopy top and bottom height grids.
|
|
316
|
-
|
|
317
|
-
Supports satellite sources and a GeoDataFrame source (from draw_additional_trees).
|
|
318
|
-
|
|
319
|
-
Args:
|
|
320
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
321
|
-
meshsize: Size of each grid cell in meters
|
|
322
|
-
source: Data source for canopy heights. Use 'GeoDataFrame' or 'tree_gdf' for tree_gdf path/object.
|
|
323
|
-
output_dir: Directory to save output files
|
|
324
|
-
**kwargs: Additional arguments including:
|
|
325
|
-
- gridvis: Whether to visualize the grid
|
|
326
|
-
- tree_gdf: GeoDataFrame of trees (optional when source='GeoDataFrame')
|
|
327
|
-
- tree_gdf_path: Path to a local file (e.g., .gpkg) with trees when source='GeoDataFrame'
|
|
328
|
-
- trunk_height_ratio: Fraction of top height used as canopy bottom for non-GDF sources
|
|
329
|
-
|
|
330
|
-
Returns:
|
|
331
|
-
tuple[numpy.ndarray, numpy.ndarray]: (canopy_top_height_grid, canopy_bottom_height_grid)
|
|
332
|
-
"""
|
|
333
|
-
|
|
334
|
-
print("Creating Canopy Height grid\n ")
|
|
335
|
-
print(f"Data source: {source}")
|
|
336
|
-
|
|
337
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
338
|
-
|
|
339
|
-
# Branch: compute from a provided GeoDataFrame (draw_additional_trees output)
|
|
340
|
-
if source in ('GeoDataFrame', 'tree_gdf', 'Tree_GeoDataFrame', 'GDF'):
|
|
341
|
-
tree_gdf = kwargs.get('tree_gdf')
|
|
342
|
-
tree_gdf_path = kwargs.get('tree_gdf_path')
|
|
343
|
-
if tree_gdf is None and tree_gdf_path is not None:
|
|
344
|
-
_, ext = os.path.splitext(tree_gdf_path)
|
|
345
|
-
if ext.lower() == '.gpkg':
|
|
346
|
-
tree_gdf = get_gdf_from_gpkg(tree_gdf_path, rectangle_vertices)
|
|
347
|
-
else:
|
|
348
|
-
raise ValueError("Unsupported tree file format. Use .gpkg or pass a GeoDataFrame.")
|
|
349
|
-
if tree_gdf is None:
|
|
350
|
-
raise ValueError("When source='GeoDataFrame', provide 'tree_gdf' or 'tree_gdf_path'.")
|
|
351
|
-
|
|
352
|
-
canopy_top, canopy_bottom = create_canopy_grids_from_tree_gdf(tree_gdf, meshsize, rectangle_vertices)
|
|
353
|
-
|
|
354
|
-
# Visualization
|
|
355
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
356
|
-
if grid_vis:
|
|
357
|
-
vis = canopy_top.copy()
|
|
358
|
-
vis[vis == 0] = np.nan
|
|
359
|
-
visualize_numerical_grid(np.flipud(vis), meshsize, "Tree canopy height (top)", cmap='Greens', label='Tree canopy height (m)')
|
|
360
|
-
|
|
361
|
-
return canopy_top, canopy_bottom
|
|
362
|
-
|
|
363
|
-
# Default: satellite/remote sensing sources
|
|
364
|
-
print("Data source: High Resolution Canopy Height Maps by WRI and Meta")
|
|
365
|
-
initialize_earth_engine()
|
|
366
|
-
|
|
367
|
-
geotiff_path = os.path.join(output_dir, "canopy_height.tif")
|
|
368
|
-
|
|
369
|
-
roi = get_roi(rectangle_vertices)
|
|
370
|
-
if source == 'High Resolution 1m Global Canopy Height Maps':
|
|
371
|
-
collection_name = "projects/meta-forest-monitoring-okw37/assets/CanopyHeight"
|
|
372
|
-
image = get_ee_image_collection(collection_name, roi)
|
|
373
|
-
elif source == 'ETH Global Sentinel-2 10m Canopy Height (2020)':
|
|
374
|
-
collection_name = "users/nlang/ETH_GlobalCanopyHeight_2020_10m_v1"
|
|
375
|
-
image = get_ee_image(collection_name, roi)
|
|
376
|
-
else:
|
|
377
|
-
raise ValueError(f"Unsupported canopy source: {source}")
|
|
378
|
-
|
|
379
|
-
save_geotiff(image, geotiff_path, resolution=meshsize)
|
|
380
|
-
canopy_height_grid = create_height_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices)
|
|
381
|
-
|
|
382
|
-
# Derive bottom grid using trunk_height_ratio (consistent with create_3d_voxel)
|
|
383
|
-
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
384
|
-
if trunk_height_ratio is None:
|
|
385
|
-
trunk_height_ratio = 11.76 / 19.98
|
|
386
|
-
canopy_bottom_grid = canopy_height_grid * float(trunk_height_ratio)
|
|
387
|
-
|
|
388
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
389
|
-
if grid_vis:
|
|
390
|
-
canopy_height_grid_nan = canopy_height_grid.copy()
|
|
391
|
-
canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
|
|
392
|
-
visualize_numerical_grid(np.flipud(canopy_height_grid_nan), meshsize, "Tree canopy height", cmap='Greens', label='Tree canopy height (m)')
|
|
393
|
-
return canopy_height_grid, canopy_bottom_grid
|
|
394
|
-
|
|
395
|
-
def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
396
|
-
"""Creates a digital elevation model grid.
|
|
397
|
-
|
|
398
|
-
Args:
|
|
399
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
400
|
-
meshsize: Size of each grid cell in meters
|
|
401
|
-
source: Data source for DEM
|
|
402
|
-
output_dir: Directory to save output files
|
|
403
|
-
**kwargs: Additional arguments including:
|
|
404
|
-
- dem_interpolation: Interpolation method for DEM
|
|
405
|
-
- gridvis: Whether to visualize the grid
|
|
406
|
-
|
|
407
|
-
Returns:
|
|
408
|
-
numpy.ndarray: Grid of elevation values
|
|
409
|
-
"""
|
|
410
|
-
|
|
411
|
-
print("Creating Digital Elevation Model (DEM) grid\n ")
|
|
412
|
-
print(f"Data source: {source}")
|
|
413
|
-
|
|
414
|
-
if source == "Local file":
|
|
415
|
-
# Use user-provided local DEM file
|
|
416
|
-
geotiff_path = kwargs["dem_path"]
|
|
417
|
-
else:
|
|
418
|
-
# Fetch DEM data from various satellite/government sources
|
|
419
|
-
initialize_earth_engine()
|
|
420
|
-
|
|
421
|
-
geotiff_path = os.path.join(output_dir, "dem.tif")
|
|
422
|
-
|
|
423
|
-
# Add buffer around region of interest to ensure smooth interpolation at edges
|
|
424
|
-
# This prevents edge artifacts in the final grid
|
|
425
|
-
buffer_distance = 100
|
|
426
|
-
roi = get_roi(rectangle_vertices)
|
|
427
|
-
roi_buffered = roi.buffer(buffer_distance)
|
|
428
|
-
|
|
429
|
-
# Fetch elevation data from selected source
|
|
430
|
-
image = get_dem_image(roi_buffered, source)
|
|
431
|
-
|
|
432
|
-
# Save DEM data with appropriate resolution based on source capabilities
|
|
433
|
-
if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM', 'Netherlands 0.5m DTM']:
|
|
434
|
-
# High-resolution elevation models - use specified mesh size
|
|
435
|
-
save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
|
|
436
|
-
elif source == 'USGS 3DEP 1m':
|
|
437
|
-
# US Geological Survey 3D Elevation Program
|
|
438
|
-
# Ensure minimum scale of 1.25m due to data limitations
|
|
439
|
-
scale = max(meshsize, 1.25)
|
|
440
|
-
save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
|
|
441
|
-
else:
|
|
442
|
-
# Default to 30m resolution for global/lower resolution sources
|
|
443
|
-
save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
|
|
444
|
-
|
|
445
|
-
# Convert GeoTIFF to regular grid with optional interpolation
|
|
446
|
-
# Interpolation helps fill gaps and smooth transitions
|
|
447
|
-
dem_interpolation = kwargs.get("dem_interpolation")
|
|
448
|
-
dem_grid = create_dem_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices, dem_interpolation=dem_interpolation)
|
|
449
|
-
|
|
450
|
-
# Generate visualization if requested
|
|
451
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
452
|
-
if grid_vis:
|
|
453
|
-
# Use terrain color scheme appropriate for elevation data
|
|
454
|
-
visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
|
|
455
|
-
|
|
456
|
-
return dem_grid
|
|
457
|
-
|
|
458
|
-
def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori,
|
|
459
|
-
building_id_grid_ori, land_cover_grid_ori, dem_grid_ori,
|
|
460
|
-
tree_grid_ori, voxel_size, land_cover_source, canopy_bottom_height_grid_ori=None, **kwargs):
|
|
461
|
-
"""Creates a 3D voxel representation combining all input grids.
|
|
462
|
-
Args:
|
|
463
|
-
building_height_grid_ori: Grid of building heights
|
|
464
|
-
building_min_height_grid_ori: Grid of building minimum heights
|
|
465
|
-
building_id_grid_ori: Grid of building IDs
|
|
466
|
-
land_cover_grid_ori: Grid of land cover classifications
|
|
467
|
-
dem_grid_ori: Grid of elevation values
|
|
468
|
-
tree_grid_ori: Grid of tree heights
|
|
469
|
-
voxel_size: Size of each voxel in meters
|
|
470
|
-
land_cover_source: Source of land cover data
|
|
471
|
-
kwargs: Additional arguments including:
|
|
472
|
-
- trunk_height_ratio: Ratio of trunk height to total tree height
|
|
473
|
-
Returns:
|
|
474
|
-
numpy.ndarray: 3D voxel grid with encoded values for different features
|
|
475
|
-
"""
|
|
476
|
-
print("Generating 3D voxel data")
|
|
477
|
-
|
|
478
|
-
# Convert land cover values to standardized format if needed
|
|
479
|
-
# OpenStreetMap data is already in the correct format
|
|
480
|
-
if (land_cover_source == 'OpenStreetMap'):
|
|
481
|
-
land_cover_grid_converted = land_cover_grid_ori
|
|
482
|
-
else:
|
|
483
|
-
land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
|
|
484
|
-
# Prepare all input grids for 3D processing
|
|
485
|
-
# Flip vertically to align with standard geographic orientation (north-up)
|
|
486
|
-
# Handle missing data appropriately for each grid type
|
|
487
|
-
building_height_grid = np.flipud(np.nan_to_num(building_height_grid_ori, nan=10.0)) # Replace NaN values with 10m height
|
|
488
|
-
building_min_height_grid = np.flipud(replace_nan_in_nested(building_min_height_grid_ori)) # Replace NaN in nested arrays
|
|
489
|
-
building_id_grid = np.flipud(building_id_grid_ori)
|
|
490
|
-
land_cover_grid = np.flipud(land_cover_grid_converted.copy()) + 1 # Add 1 to avoid 0 values in land cover
|
|
491
|
-
dem_grid = np.flipud(dem_grid_ori.copy()) - np.min(dem_grid_ori) # Normalize DEM to start at 0
|
|
492
|
-
dem_grid = process_grid(building_id_grid, dem_grid) # Process DEM based on building footprints
|
|
493
|
-
tree_grid = np.flipud(tree_grid_ori.copy())
|
|
494
|
-
canopy_bottom_grid = None
|
|
495
|
-
if canopy_bottom_height_grid_ori is not None:
|
|
496
|
-
canopy_bottom_grid = np.flipud(canopy_bottom_height_grid_ori.copy())
|
|
497
|
-
# Validate that all input grids have consistent dimensions
|
|
498
|
-
assert building_height_grid.shape == land_cover_grid.shape == dem_grid.shape == tree_grid.shape, "Input grids must have the same shape"
|
|
499
|
-
rows, cols = building_height_grid.shape
|
|
500
|
-
# Calculate the required height for the 3D voxel grid
|
|
501
|
-
# Add 1 voxel layer to ensure sufficient vertical space
|
|
502
|
-
max_height = int(np.ceil(np.max(building_height_grid + dem_grid + tree_grid) / voxel_size))+1
|
|
503
|
-
# Initialize the 3D voxel grid with zeros
|
|
504
|
-
# Use int8 by default to reduce memory (values range from about -99 to small positives)
|
|
505
|
-
# Allow override via kwarg 'voxel_dtype'
|
|
506
|
-
voxel_dtype = kwargs.get("voxel_dtype", np.int8)
|
|
507
|
-
|
|
508
|
-
# Optional: estimate memory and allow a soft limit before allocating
|
|
509
|
-
try:
|
|
510
|
-
bytes_per_elem = np.dtype(voxel_dtype).itemsize
|
|
511
|
-
est_mb = rows * cols * max_height * bytes_per_elem / (1024 ** 2)
|
|
512
|
-
print(f"Voxel grid shape: ({rows}, {cols}, {max_height}), dtype: {voxel_dtype}, ~{est_mb:.1f} MB")
|
|
513
|
-
max_ram_mb = kwargs.get("max_voxel_ram_mb")
|
|
514
|
-
if (max_ram_mb is not None) and (est_mb > max_ram_mb):
|
|
515
|
-
raise MemoryError(f"Estimated voxel grid memory {est_mb:.1f} MB exceeds limit {max_ram_mb} MB. Increase mesh size or restrict ROI.")
|
|
516
|
-
except Exception:
|
|
517
|
-
pass
|
|
518
|
-
|
|
519
|
-
voxel_grid = np.zeros((rows, cols, max_height), dtype=voxel_dtype)
|
|
520
|
-
# Configure tree trunk-to-crown ratio
|
|
521
|
-
# This determines how much of the tree is trunk vs canopy
|
|
522
|
-
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
523
|
-
if trunk_height_ratio is None:
|
|
524
|
-
trunk_height_ratio = 11.76 / 19.98 # Default ratio based on typical tree proportions
|
|
525
|
-
# Process each grid cell to build the 3D voxel representation
|
|
526
|
-
for i in range(rows):
|
|
527
|
-
for j in range(cols):
|
|
528
|
-
# Calculate ground level in voxel units
|
|
529
|
-
# Add 1 to ensure space for surface features
|
|
530
|
-
ground_level = int(dem_grid[i, j] / voxel_size + 0.5) + 1
|
|
531
|
-
# Extract current cell values
|
|
532
|
-
tree_height = tree_grid[i, j]
|
|
533
|
-
land_cover = land_cover_grid[i, j]
|
|
534
|
-
# Fill underground voxels with -1 (represents subsurface)
|
|
535
|
-
voxel_grid[i, j, :ground_level] = -1
|
|
536
|
-
# Set the ground surface to the land cover type
|
|
537
|
-
voxel_grid[i, j, ground_level-1] = land_cover
|
|
538
|
-
# Process tree canopy if trees are present
|
|
539
|
-
if tree_height > 0:
|
|
540
|
-
# Calculate tree structure: trunk base to crown base to crown top
|
|
541
|
-
if canopy_bottom_grid is not None:
|
|
542
|
-
crown_base_height = canopy_bottom_grid[i, j]
|
|
543
|
-
else:
|
|
544
|
-
crown_base_height = (tree_height * trunk_height_ratio)
|
|
545
|
-
crown_base_height_level = int(crown_base_height / voxel_size + 0.5)
|
|
546
|
-
crown_top_height = tree_height
|
|
547
|
-
crown_top_height_level = int(crown_top_height / voxel_size + 0.5)
|
|
548
|
-
|
|
549
|
-
# Ensure minimum crown height of 1 voxel
|
|
550
|
-
# Prevent crown base and top from being at the same level
|
|
551
|
-
if (crown_top_height_level == crown_base_height_level) and (crown_base_height_level>0):
|
|
552
|
-
crown_base_height_level -= 1
|
|
553
|
-
|
|
554
|
-
# Calculate absolute positions relative to ground level
|
|
555
|
-
tree_start = ground_level + crown_base_height_level
|
|
556
|
-
tree_end = ground_level + crown_top_height_level
|
|
557
|
-
|
|
558
|
-
# Fill tree crown voxels with -2 (represents vegetation canopy)
|
|
559
|
-
voxel_grid[i, j, tree_start:tree_end] = -2
|
|
560
|
-
# Process buildings - handle multiple height segments per building
|
|
561
|
-
# Some buildings may have multiple levels or complex height profiles
|
|
562
|
-
for k in building_min_height_grid[i, j]:
|
|
563
|
-
building_min_height = int(k[0] / voxel_size + 0.5) # Lower height of building segment
|
|
564
|
-
building_height = int(k[1] / voxel_size + 0.5) # Upper height of building segment
|
|
565
|
-
# Fill building voxels with -3 (represents built structures)
|
|
566
|
-
voxel_grid[i, j, ground_level+building_min_height:ground_level+building_height] = -3
|
|
567
|
-
return voxel_grid
|
|
568
|
-
|
|
569
|
-
def create_3d_voxel_individuals(building_height_grid_ori, land_cover_grid_ori, dem_grid_ori, tree_grid_ori, voxel_size, land_cover_source, layered_interval=None):
|
|
570
|
-
"""Creates separate 3D voxel grids for each component.
|
|
571
|
-
|
|
572
|
-
Args:
|
|
573
|
-
building_height_grid_ori: Grid of building heights
|
|
574
|
-
land_cover_grid_ori: Grid of land cover classifications
|
|
575
|
-
dem_grid_ori: Grid of elevation values
|
|
576
|
-
tree_grid_ori: Grid of tree heights
|
|
577
|
-
voxel_size: Size of each voxel in meters
|
|
578
|
-
land_cover_source: Source of land cover data
|
|
579
|
-
layered_interval: Interval for layered output
|
|
580
|
-
|
|
581
|
-
Returns:
|
|
582
|
-
tuple:
|
|
583
|
-
- numpy.ndarray: Land cover voxel grid
|
|
584
|
-
- numpy.ndarray: Building voxel grid
|
|
585
|
-
- numpy.ndarray: Tree voxel grid
|
|
586
|
-
- numpy.ndarray: DEM voxel grid
|
|
587
|
-
- numpy.ndarray: Combined layered voxel grid
|
|
588
|
-
"""
|
|
589
|
-
|
|
590
|
-
print("Generating 3D voxel data")
|
|
591
|
-
# Convert land cover values if not from OpenEarthMapJapan
|
|
592
|
-
if land_cover_source != 'OpenEarthMapJapan':
|
|
593
|
-
land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
|
|
594
|
-
else:
|
|
595
|
-
land_cover_grid_converted = land_cover_grid_ori
|
|
596
|
-
|
|
597
|
-
# Prepare and flip all input grids vertically
|
|
598
|
-
building_height_grid = np.flipud(building_height_grid_ori.copy())
|
|
599
|
-
land_cover_grid = np.flipud(land_cover_grid_converted.copy()) + 1 # Add 1 to avoid 0 values
|
|
600
|
-
dem_grid = np.flipud(dem_grid_ori.copy()) - np.min(dem_grid_ori) # Normalize DEM to start at 0
|
|
601
|
-
building_nr_grid = group_and_label_cells(np.flipud(building_height_grid_ori.copy()))
|
|
602
|
-
dem_grid = process_grid(building_nr_grid, dem_grid) # Process DEM based on building footprints
|
|
603
|
-
tree_grid = np.flipud(tree_grid_ori.copy())
|
|
604
|
-
|
|
605
|
-
# Validate input dimensions
|
|
606
|
-
assert building_height_grid.shape == land_cover_grid.shape == dem_grid.shape == tree_grid.shape, "Input grids must have the same shape"
|
|
607
|
-
|
|
608
|
-
rows, cols = building_height_grid.shape
|
|
609
|
-
|
|
610
|
-
# Calculate required height for 3D grid
|
|
611
|
-
max_height = int(np.ceil(np.max(building_height_grid + dem_grid + tree_grid) / voxel_size))
|
|
612
|
-
|
|
613
|
-
# Initialize empty 3D grids for each component
|
|
614
|
-
land_cover_voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
|
|
615
|
-
building_voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
|
|
616
|
-
tree_voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
|
|
617
|
-
dem_voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
|
|
618
|
-
|
|
619
|
-
# Fill individual component grids
|
|
620
|
-
for i in range(rows):
|
|
621
|
-
for j in range(cols):
|
|
622
|
-
ground_level = int(dem_grid[i, j] / voxel_size + 0.5)
|
|
623
|
-
building_height = int(building_height_grid[i, j] / voxel_size + 0.5)
|
|
624
|
-
tree_height = int(tree_grid[i, j] / voxel_size + 0.5)
|
|
625
|
-
land_cover = land_cover_grid[i, j]
|
|
626
|
-
|
|
627
|
-
# Fill underground cells with -1
|
|
628
|
-
dem_voxel_grid[i, j, :ground_level+1] = -1
|
|
629
|
-
|
|
630
|
-
# Set ground level cell to land cover
|
|
631
|
-
land_cover_voxel_grid[i, j, 0] = land_cover
|
|
632
|
-
|
|
633
|
-
# Fill tree crown with value -2
|
|
634
|
-
if tree_height > 0:
|
|
635
|
-
tree_voxel_grid[i, j, :tree_height] = -2
|
|
636
|
-
|
|
637
|
-
# Fill building with value -3
|
|
638
|
-
if building_height > 0:
|
|
639
|
-
building_voxel_grid[i, j, :building_height] = -3
|
|
640
|
-
|
|
641
|
-
# Set default layered interval if not provided
|
|
642
|
-
if not layered_interval:
|
|
643
|
-
layered_interval = max(max_height, int(dem_grid.shape[0]/4 + 0.5))
|
|
644
|
-
|
|
645
|
-
# Create combined layered visualization
|
|
646
|
-
extract_height = min(layered_interval, max_height)
|
|
647
|
-
layered_voxel_grid = np.zeros((rows, cols, layered_interval*4), dtype=np.int32)
|
|
648
|
-
|
|
649
|
-
# Stack components in layers with equal spacing
|
|
650
|
-
layered_voxel_grid[:, :, :extract_height] = dem_voxel_grid[:, :, :extract_height]
|
|
651
|
-
layered_voxel_grid[:, :, layered_interval:layered_interval+extract_height] = land_cover_voxel_grid[:, :, :extract_height]
|
|
652
|
-
layered_voxel_grid[:, :, 2*layered_interval:2*layered_interval+extract_height] = building_voxel_grid[:, :, :extract_height]
|
|
653
|
-
layered_voxel_grid[:, :, 3*layered_interval:3*layered_interval+extract_height] = tree_voxel_grid[:, :, :extract_height]
|
|
654
|
-
|
|
655
|
-
return land_cover_voxel_grid, building_voxel_grid, tree_voxel_grid, dem_voxel_grid, layered_voxel_grid
|
|
656
|
-
|
|
657
|
-
def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_height_source, dem_source, meshsize, building_gdf=None, **kwargs):
|
|
658
|
-
"""Main function to generate a complete voxel city model.
|
|
659
|
-
|
|
660
|
-
Args:
|
|
661
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
662
|
-
building_source: Source for building height data (e.g. 'OSM', 'EUBUCCO', 'GeoDataFrame')
|
|
663
|
-
land_cover_source: Source for land cover data (e.g. 'ESA', 'ESRI')
|
|
664
|
-
canopy_height_source: Source for tree canopy height data
|
|
665
|
-
dem_source: Source for digital elevation model data ('Flat' or other source)
|
|
666
|
-
meshsize: Size of each grid cell in meters
|
|
667
|
-
building_gdf: Optional GeoDataFrame with building footprint, height and other information
|
|
668
|
-
**kwargs: Additional keyword arguments including:
|
|
669
|
-
- output_dir: Directory to save output files (default: 'output')
|
|
670
|
-
- min_canopy_height: Minimum height threshold for tree canopy
|
|
671
|
-
- remove_perimeter_object: Factor to remove objects near perimeter
|
|
672
|
-
- mapvis: Whether to visualize grids on map
|
|
673
|
-
- voxelvis: Whether to visualize 3D voxel model
|
|
674
|
-
- voxelvis_img_save_path: Path to save 3D visualization
|
|
675
|
-
- default_land_cover_class: Default class for land cover grid cells with no intersecting polygons (default: 'Developed space')
|
|
676
|
-
|
|
677
|
-
Returns:
|
|
678
|
-
tuple containing:
|
|
679
|
-
- voxcity_grid: 3D voxel grid of the complete city model
|
|
680
|
-
- building_height_grid: 2D grid of building heights
|
|
681
|
-
- building_min_height_grid: 2D grid of minimum building heights
|
|
682
|
-
- building_id_grid: 2D grid of building IDs
|
|
683
|
-
- canopy_height_grid: 2D grid of tree canopy top heights
|
|
684
|
-
- canopy_bottom_height_grid: 2D grid of tree canopy bottom heights
|
|
685
|
-
- land_cover_grid: 2D grid of land cover classifications
|
|
686
|
-
- dem_grid: 2D grid of ground elevation
|
|
687
|
-
- building_geojson: GeoJSON of building footprints and metadata
|
|
688
|
-
"""
|
|
689
|
-
# Set up output directory for intermediate and final files
|
|
690
|
-
output_dir = kwargs.get("output_dir", "output")
|
|
691
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
692
|
-
|
|
693
|
-
# Remove 'output_dir' from kwargs to prevent duplication in function calls
|
|
694
|
-
kwargs.pop('output_dir', None)
|
|
695
|
-
|
|
696
|
-
# STEP 1: Generate all required 2D grids from various data sources
|
|
697
|
-
# These grids form the foundation for the 3D voxel model
|
|
698
|
-
|
|
699
|
-
# Land cover classification grid (e.g., urban, forest, water, agriculture)
|
|
700
|
-
land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
|
|
701
|
-
|
|
702
|
-
# Building footprints and height information
|
|
703
|
-
building_height_grid, building_min_height_grid, building_id_grid, building_gdf = get_building_height_grid(rectangle_vertices, meshsize, building_source, output_dir, building_gdf=building_gdf, **kwargs)
|
|
704
|
-
|
|
705
|
-
# Save building data to file for later analysis or visualization
|
|
706
|
-
if not building_gdf.empty:
|
|
707
|
-
save_path = f"{output_dir}/building.gpkg"
|
|
708
|
-
building_gdf.to_file(save_path, driver='GPKG')
|
|
709
|
-
|
|
710
|
-
# STEP 2: Handle canopy height data
|
|
711
|
-
# Either use static values or fetch from satellite sources
|
|
712
|
-
if canopy_height_source == "Static":
|
|
713
|
-
# Create uniform canopy height for all tree-covered areas (top grid)
|
|
714
|
-
canopy_height_grid = np.zeros_like(land_cover_grid, dtype=float)
|
|
715
|
-
static_tree_height = kwargs.get("static_tree_height", 10.0)
|
|
716
|
-
# Determine tree class indices based on source-specific class names
|
|
717
|
-
_classes = get_land_cover_classes(land_cover_source)
|
|
718
|
-
_class_to_int = {name: i for i, name in enumerate(_classes.values())}
|
|
719
|
-
_tree_labels = ["Tree", "Trees", "Tree Canopy"]
|
|
720
|
-
_tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
|
|
721
|
-
tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
|
|
722
|
-
canopy_height_grid[tree_mask] = static_tree_height
|
|
723
|
-
|
|
724
|
-
# Derive bottom from trunk_height_ratio
|
|
725
|
-
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
726
|
-
if trunk_height_ratio is None:
|
|
727
|
-
trunk_height_ratio = 11.76 / 19.98
|
|
728
|
-
canopy_bottom_height_grid = canopy_height_grid * float(trunk_height_ratio)
|
|
729
|
-
else:
|
|
730
|
-
# Fetch canopy top/bottom from source
|
|
731
|
-
canopy_height_grid, canopy_bottom_height_grid = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
|
|
732
|
-
|
|
733
|
-
# STEP 3: Handle digital elevation model (terrain)
|
|
734
|
-
if dem_source == "Flat":
|
|
735
|
-
# Create flat terrain for simplified modeling
|
|
736
|
-
dem_grid = np.zeros_like(land_cover_grid)
|
|
737
|
-
else:
|
|
738
|
-
# Fetch terrain elevation from various sources
|
|
739
|
-
dem_grid = get_dem_grid(rectangle_vertices, meshsize, dem_source, output_dir, **kwargs)
|
|
740
|
-
|
|
741
|
-
# STEP 4: Apply optional data filtering and cleaning
|
|
742
|
-
|
|
743
|
-
# Filter out low vegetation that may be noise in the data
|
|
744
|
-
min_canopy_height = kwargs.get("min_canopy_height")
|
|
745
|
-
if min_canopy_height is not None:
|
|
746
|
-
canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
|
|
747
|
-
if 'canopy_bottom_height_grid' in locals():
|
|
748
|
-
canopy_bottom_height_grid[canopy_height_grid == 0] = 0
|
|
749
|
-
|
|
750
|
-
# Remove objects near the boundary to avoid edge effects
|
|
751
|
-
# This is useful when the area of interest is part of a larger urban area
|
|
752
|
-
remove_perimeter_object = kwargs.get("remove_perimeter_object")
|
|
753
|
-
if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
|
|
754
|
-
print("apply perimeter removal")
|
|
755
|
-
# Calculate perimeter width based on grid dimensions
|
|
756
|
-
w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
|
|
757
|
-
h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
|
|
758
|
-
|
|
759
|
-
# Clear canopy heights in perimeter areas
|
|
760
|
-
canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
|
|
761
|
-
if 'canopy_bottom_height_grid' in locals():
|
|
762
|
-
canopy_bottom_height_grid[:w_peri, :] = canopy_bottom_height_grid[-w_peri:, :] = canopy_bottom_height_grid[:, :h_peri] = canopy_bottom_height_grid[:, -h_peri:] = 0
|
|
763
|
-
|
|
764
|
-
# Identify buildings that intersect with perimeter areas
|
|
765
|
-
ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
|
|
766
|
-
ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
|
|
767
|
-
ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
|
|
768
|
-
ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
|
|
769
|
-
remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
|
|
770
|
-
|
|
771
|
-
# Remove identified buildings from all grids
|
|
772
|
-
for remove_id in remove_ids:
|
|
773
|
-
positions = np.where(building_id_grid == remove_id)
|
|
774
|
-
building_height_grid[positions] = 0
|
|
775
|
-
building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
|
|
776
|
-
|
|
777
|
-
# Visualize grids after optional perimeter removal
|
|
778
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
779
|
-
if grid_vis:
|
|
780
|
-
# Building height grid visualization (zeros hidden)
|
|
781
|
-
building_height_grid_nan = building_height_grid.copy()
|
|
782
|
-
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
783
|
-
visualize_numerical_grid(
|
|
784
|
-
np.flipud(building_height_grid_nan),
|
|
785
|
-
meshsize,
|
|
786
|
-
"building height (m)",
|
|
787
|
-
cmap='viridis',
|
|
788
|
-
label='Value'
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
# Canopy height grid visualization (zeros hidden)
|
|
792
|
-
canopy_height_grid_nan = canopy_height_grid.copy()
|
|
793
|
-
canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
|
|
794
|
-
visualize_numerical_grid(
|
|
795
|
-
np.flipud(canopy_height_grid_nan),
|
|
796
|
-
meshsize,
|
|
797
|
-
"Tree canopy height (m)",
|
|
798
|
-
cmap='Greens',
|
|
799
|
-
label='Tree canopy height (m)'
|
|
800
|
-
)
|
|
801
|
-
|
|
802
|
-
# STEP 5: Generate optional 2D visualizations on interactive maps
|
|
803
|
-
mapvis = kwargs.get("mapvis")
|
|
804
|
-
if mapvis:
|
|
805
|
-
# Create map-based visualizations of all data layers
|
|
806
|
-
# These help users understand the input data before 3D modeling
|
|
807
|
-
|
|
808
|
-
# Visualize land cover using the new function
|
|
809
|
-
visualize_landcover_grid_on_basemap(
|
|
810
|
-
land_cover_grid,
|
|
811
|
-
rectangle_vertices,
|
|
812
|
-
meshsize,
|
|
813
|
-
source=land_cover_source,
|
|
814
|
-
alpha=0.7,
|
|
815
|
-
figsize=(12, 8),
|
|
816
|
-
basemap='CartoDB light',
|
|
817
|
-
show_edge=False
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
# Visualize building heights using the new function
|
|
821
|
-
visualize_numerical_grid_on_basemap(
|
|
822
|
-
building_height_grid,
|
|
823
|
-
rectangle_vertices,
|
|
824
|
-
meshsize,
|
|
825
|
-
value_name="Building Heights (m)",
|
|
826
|
-
cmap='viridis',
|
|
827
|
-
alpha=0.7,
|
|
828
|
-
figsize=(12, 8),
|
|
829
|
-
basemap='CartoDB light',
|
|
830
|
-
show_edge=False
|
|
831
|
-
)
|
|
832
|
-
|
|
833
|
-
# Visualize canopy heights using the new function
|
|
834
|
-
visualize_numerical_grid_on_basemap(
|
|
835
|
-
canopy_height_grid,
|
|
836
|
-
rectangle_vertices,
|
|
837
|
-
meshsize,
|
|
838
|
-
value_name="Canopy Heights (m)",
|
|
839
|
-
cmap='Greens',
|
|
840
|
-
alpha=0.7,
|
|
841
|
-
figsize=(12, 8),
|
|
842
|
-
basemap='CartoDB light',
|
|
843
|
-
show_edge=False
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
# Visualize DEM using the new function
|
|
847
|
-
visualize_numerical_grid_on_basemap(
|
|
848
|
-
dem_grid,
|
|
849
|
-
rectangle_vertices,
|
|
850
|
-
meshsize,
|
|
851
|
-
value_name="Terrain Elevation (m)",
|
|
852
|
-
cmap='terrain',
|
|
853
|
-
alpha=0.7,
|
|
854
|
-
figsize=(12, 8),
|
|
855
|
-
basemap='CartoDB light',
|
|
856
|
-
show_edge=False
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
# STEP 6: Generate the final 3D voxel model
|
|
860
|
-
# This combines all 2D grids into a comprehensive 3D representation
|
|
861
|
-
voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source, canopy_bottom_height_grid)
|
|
862
|
-
|
|
863
|
-
# STEP 7: Generate optional 3D visualization
|
|
864
|
-
# voxelvis = kwargs.get("voxelvis")
|
|
865
|
-
# if voxelvis:
|
|
866
|
-
# # Create a taller grid for better visualization
|
|
867
|
-
# # Fixed height ensures consistent camera positioning
|
|
868
|
-
# new_height = int(550/meshsize+0.5)
|
|
869
|
-
# voxcity_grid_vis = np.zeros((voxcity_grid.shape[0], voxcity_grid.shape[1], new_height))
|
|
870
|
-
# voxcity_grid_vis[:, :, :voxcity_grid.shape[2]] = voxcity_grid
|
|
871
|
-
# voxcity_grid_vis[-1, -1, -1] = -99 # Add marker to fix camera location and angle of view
|
|
872
|
-
# visualize_3d_voxel(voxcity_grid_vis, voxel_size=meshsize, save_path=kwargs["voxelvis_img_save_path"])
|
|
873
|
-
|
|
874
|
-
# STEP 8: Save all generated data for future use
|
|
875
|
-
save_voxcity = kwargs.get("save_voxctiy_data", True)
|
|
876
|
-
if save_voxcity:
|
|
877
|
-
save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity_data.pkl")
|
|
878
|
-
save_voxcity_data(save_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
879
|
-
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
880
|
-
building_gdf, meshsize, rectangle_vertices)
|
|
881
|
-
|
|
882
|
-
return voxcity_grid, building_height_grid, building_min_height_grid, building_id_grid, canopy_height_grid, canopy_bottom_height_grid, land_cover_grid, dem_grid, building_gdf
|
|
883
|
-
|
|
884
|
-
def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_source, meshsize, url_citygml=None, citygml_path=None, **kwargs):
|
|
885
|
-
"""Main function to generate a complete voxel city model.
|
|
886
|
-
|
|
887
|
-
Args:
|
|
888
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
889
|
-
building_source: Source for building height data (e.g. 'OSM', 'EUBUCCO')
|
|
890
|
-
land_cover_source: Source for land cover data (e.g. 'ESA', 'ESRI')
|
|
891
|
-
canopy_height_source: Source for tree canopy height data
|
|
892
|
-
dem_source: Source for digital elevation model data ('Flat' or other source)
|
|
893
|
-
meshsize: Size of each grid cell in meters
|
|
894
|
-
**kwargs: Additional keyword arguments including:
|
|
895
|
-
- output_dir: Directory to save output files (default: 'output')
|
|
896
|
-
- min_canopy_height: Minimum height threshold for tree canopy
|
|
897
|
-
- remove_perimeter_object: Factor to remove objects near perimeter
|
|
898
|
-
- mapvis: Whether to visualize grids on map
|
|
899
|
-
- voxelvis: Whether to visualize 3D voxel model
|
|
900
|
-
- voxelvis_img_save_path: Path to save 3D visualization
|
|
901
|
-
|
|
902
|
-
Returns:
|
|
903
|
-
tuple containing:
|
|
904
|
-
- voxcity_grid: 3D voxel grid of the complete city model
|
|
905
|
-
- building_height_grid: 2D grid of building heights
|
|
906
|
-
- building_min_height_grid: 2D grid of minimum building heights
|
|
907
|
-
- building_id_grid: 2D grid of building IDs
|
|
908
|
-
- canopy_height_grid: 2D grid of tree canopy heights
|
|
909
|
-
- land_cover_grid: 2D grid of land cover classifications
|
|
910
|
-
- dem_grid: 2D grid of ground elevation
|
|
911
|
-
- building_geojson: GeoJSON of building footprints and metadata
|
|
912
|
-
"""
|
|
913
|
-
# Create output directory if it doesn't exist
|
|
914
|
-
output_dir = kwargs.get("output_dir", "output")
|
|
915
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
916
|
-
|
|
917
|
-
# Remove 'output_dir' from kwargs to prevent duplication
|
|
918
|
-
kwargs.pop('output_dir', None)
|
|
919
|
-
|
|
920
|
-
# SSL/HTTP options for CityGML download (optional)
|
|
921
|
-
# Backward compatible: accept 'verify' but prefer 'ssl_verify'
|
|
922
|
-
ssl_verify = kwargs.pop('ssl_verify', kwargs.pop('verify', True))
|
|
923
|
-
ca_bundle = kwargs.pop('ca_bundle', None)
|
|
924
|
-
timeout = kwargs.pop('timeout', 60)
|
|
925
|
-
|
|
926
|
-
# get all required gdfs
|
|
927
|
-
building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(
|
|
928
|
-
url=url_citygml,
|
|
929
|
-
citygml_path=citygml_path,
|
|
930
|
-
base_dir=output_dir,
|
|
931
|
-
rectangle_vertices=rectangle_vertices,
|
|
932
|
-
ssl_verify=ssl_verify,
|
|
933
|
-
ca_bundle=ca_bundle,
|
|
934
|
-
timeout=timeout
|
|
935
|
-
)
|
|
936
|
-
|
|
937
|
-
# Normalize CRS to WGS84 (EPSG:4326) to ensure consistent operations downstream
|
|
938
|
-
try:
|
|
939
|
-
import geopandas as gpd # noqa: F401
|
|
940
|
-
if building_gdf is not None:
|
|
941
|
-
if building_gdf.crs is None:
|
|
942
|
-
building_gdf = building_gdf.set_crs(epsg=4326)
|
|
943
|
-
elif getattr(building_gdf.crs, 'to_epsg', lambda: None)() != 4326 and building_gdf.crs != "EPSG:4326":
|
|
944
|
-
building_gdf = building_gdf.to_crs(epsg=4326)
|
|
945
|
-
if terrain_gdf is not None:
|
|
946
|
-
if terrain_gdf.crs is None:
|
|
947
|
-
terrain_gdf = terrain_gdf.set_crs(epsg=4326)
|
|
948
|
-
elif getattr(terrain_gdf.crs, 'to_epsg', lambda: None)() != 4326 and terrain_gdf.crs != "EPSG:4326":
|
|
949
|
-
terrain_gdf = terrain_gdf.to_crs(epsg=4326)
|
|
950
|
-
if vegetation_gdf is not None:
|
|
951
|
-
if vegetation_gdf.crs is None:
|
|
952
|
-
vegetation_gdf = vegetation_gdf.set_crs(epsg=4326)
|
|
953
|
-
elif getattr(vegetation_gdf.crs, 'to_epsg', lambda: None)() != 4326 and vegetation_gdf.crs != "EPSG:4326":
|
|
954
|
-
vegetation_gdf = vegetation_gdf.to_crs(epsg=4326)
|
|
955
|
-
except Exception:
|
|
956
|
-
pass
|
|
957
|
-
|
|
958
|
-
land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
|
|
959
|
-
|
|
960
|
-
# building_height_grid, building_min_height_grid, building_id_grid, building_gdf = get_building_height_grid(rectangle_vertices, meshsize, building_source, output_dir, **kwargs)
|
|
961
|
-
print("Creating building height grid")
|
|
962
|
-
# Prepare complementary building source if provided
|
|
963
|
-
building_complementary_source = kwargs.get("building_complementary_source")
|
|
964
|
-
gdf_comp = None
|
|
965
|
-
geotiff_path_comp = None
|
|
966
|
-
complement_building_footprints = kwargs.get("complement_building_footprints")
|
|
967
|
-
# Default to complement footprints when a complementary source is specified
|
|
968
|
-
if complement_building_footprints is None and (building_complementary_source not in (None, "None")):
|
|
969
|
-
complement_building_footprints = True
|
|
970
|
-
|
|
971
|
-
if (building_complementary_source is not None) and (building_complementary_source != "None"):
|
|
972
|
-
# Vector complementary sources
|
|
973
|
-
floor_height = kwargs.get("floor_height", 3.0)
|
|
974
|
-
if building_complementary_source == 'Microsoft Building Footprints':
|
|
975
|
-
gdf_comp = get_mbfp_gdf(kwargs.get("output_dir", "output"), rectangle_vertices)
|
|
976
|
-
elif building_complementary_source == 'OpenStreetMap':
|
|
977
|
-
gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
|
|
978
|
-
elif building_complementary_source == 'EUBUCCO v0.1':
|
|
979
|
-
gdf_comp = load_gdf_from_eubucco(rectangle_vertices, kwargs.get("output_dir", "output"))
|
|
980
|
-
elif building_complementary_source == 'Overture':
|
|
981
|
-
gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
|
|
982
|
-
elif building_complementary_source == 'Local file':
|
|
983
|
-
comp_path = kwargs.get("building_complementary_path")
|
|
984
|
-
if comp_path is not None:
|
|
985
|
-
_, extension = os.path.splitext(comp_path)
|
|
986
|
-
if extension == ".gpkg":
|
|
987
|
-
gdf_comp = get_gdf_from_gpkg(comp_path, rectangle_vertices)
|
|
988
|
-
# Ensure complementary GDF uses WGS84
|
|
989
|
-
if gdf_comp is not None:
|
|
990
|
-
try:
|
|
991
|
-
if gdf_comp.crs is None:
|
|
992
|
-
gdf_comp = gdf_comp.set_crs(epsg=4326)
|
|
993
|
-
elif getattr(gdf_comp.crs, 'to_epsg', lambda: None)() != 4326 and gdf_comp.crs != "EPSG:4326":
|
|
994
|
-
gdf_comp = gdf_comp.to_crs(epsg=4326)
|
|
995
|
-
except Exception:
|
|
996
|
-
pass
|
|
997
|
-
# Raster complementary sources (height only)
|
|
998
|
-
elif building_complementary_source == "Open Building 2.5D Temporal":
|
|
999
|
-
roi = get_roi(rectangle_vertices)
|
|
1000
|
-
os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
|
|
1001
|
-
geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
|
|
1002
|
-
save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
|
|
1003
|
-
elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
|
|
1004
|
-
roi = get_roi(rectangle_vertices)
|
|
1005
|
-
os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
|
|
1006
|
-
geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
|
|
1007
|
-
save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
|
|
1008
|
-
|
|
1009
|
-
# Filter and assemble kwargs accepted by the grid function
|
|
1010
|
-
_allowed_building_kwargs = {
|
|
1011
|
-
"overlapping_footprint",
|
|
1012
|
-
"gdf_comp",
|
|
1013
|
-
"geotiff_path_comp",
|
|
1014
|
-
"complement_building_footprints",
|
|
1015
|
-
"complement_height",
|
|
1016
|
-
}
|
|
1017
|
-
_building_kwargs = {k: v for k, v in kwargs.items() if k in _allowed_building_kwargs}
|
|
1018
|
-
if gdf_comp is not None:
|
|
1019
|
-
_building_kwargs["gdf_comp"] = gdf_comp
|
|
1020
|
-
if geotiff_path_comp is not None:
|
|
1021
|
-
_building_kwargs["geotiff_path_comp"] = geotiff_path_comp
|
|
1022
|
-
if complement_building_footprints is not None:
|
|
1023
|
-
_building_kwargs["complement_building_footprints"] = complement_building_footprints
|
|
1024
|
-
|
|
1025
|
-
# Map user-provided building_complement_height -> complement_height for grid builder
|
|
1026
|
-
comp_height_user = kwargs.get("building_complement_height")
|
|
1027
|
-
if comp_height_user is not None:
|
|
1028
|
-
_building_kwargs["complement_height"] = comp_height_user
|
|
1029
|
-
# If footprints are being complemented and no height provided, default to 10
|
|
1030
|
-
if _building_kwargs.get("complement_building_footprints") and ("complement_height" not in _building_kwargs):
|
|
1031
|
-
_building_kwargs["complement_height"] = 10.0
|
|
1032
|
-
|
|
1033
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(
|
|
1034
|
-
building_gdf, meshsize, rectangle_vertices, **_building_kwargs
|
|
1035
|
-
)
|
|
1036
|
-
|
|
1037
|
-
# Visualize grid if requested
|
|
1038
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
1039
|
-
if grid_vis:
|
|
1040
|
-
building_height_grid_nan = building_height_grid.copy()
|
|
1041
|
-
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
1042
|
-
visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
|
|
1043
|
-
|
|
1044
|
-
# Save building data to GeoJSON
|
|
1045
|
-
if not building_gdf.empty:
|
|
1046
|
-
save_path = f"{output_dir}/building.gpkg"
|
|
1047
|
-
building_gdf.to_file(save_path, driver='GPKG')
|
|
1048
|
-
|
|
1049
|
-
# Get canopy height data
|
|
1050
|
-
if canopy_height_source == "Static":
|
|
1051
|
-
# Create canopy height grid with same shape as land cover grid
|
|
1052
|
-
canopy_height_grid_comp = np.zeros_like(land_cover_grid, dtype=float)
|
|
1053
|
-
|
|
1054
|
-
# Set default static height for trees (20 meters is a typical average tree height)
|
|
1055
|
-
static_tree_height = kwargs.get("static_tree_height", 10.0)
|
|
1056
|
-
# Determine tree class indices based on source-specific class names
|
|
1057
|
-
_classes = get_land_cover_classes(land_cover_source)
|
|
1058
|
-
_class_to_int = {name: i for i, name in enumerate(_classes.values())}
|
|
1059
|
-
_tree_labels = ["Tree", "Trees", "Tree Canopy"]
|
|
1060
|
-
_tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
|
|
1061
|
-
tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
|
|
1062
|
-
|
|
1063
|
-
# Set static height for tree cells
|
|
1064
|
-
canopy_height_grid_comp[tree_mask] = static_tree_height
|
|
1065
|
-
|
|
1066
|
-
# Bottom comp from trunk ratio
|
|
1067
|
-
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
1068
|
-
if trunk_height_ratio is None:
|
|
1069
|
-
trunk_height_ratio = 11.76 / 19.98
|
|
1070
|
-
canopy_bottom_height_grid_comp = canopy_height_grid_comp * float(trunk_height_ratio)
|
|
1071
|
-
else:
|
|
1072
|
-
canopy_height_grid_comp, canopy_bottom_height_grid_comp = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
|
|
1073
|
-
|
|
1074
|
-
# In the get_voxcity_CityGML function, modify it to handle None vegetation_gdf
|
|
1075
|
-
if vegetation_gdf is not None:
|
|
1076
|
-
canopy_height_grid = create_vegetation_height_grid_from_gdf_polygon(vegetation_gdf, meshsize, rectangle_vertices)
|
|
1077
|
-
# Base bottom grid from ratio
|
|
1078
|
-
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
1079
|
-
if trunk_height_ratio is None:
|
|
1080
|
-
trunk_height_ratio = 11.76 / 19.98
|
|
1081
|
-
canopy_bottom_height_grid = canopy_height_grid * float(trunk_height_ratio)
|
|
1082
|
-
else:
|
|
1083
|
-
# Create an empty canopy_height_grid with the same shape as your other grids
|
|
1084
|
-
# This depends on the expected shape, you might need to adjust
|
|
1085
|
-
canopy_height_grid = np.zeros_like(building_height_grid)
|
|
1086
|
-
canopy_bottom_height_grid = np.zeros_like(building_height_grid)
|
|
1087
|
-
|
|
1088
|
-
mask = (canopy_height_grid == 0) & (canopy_height_grid_comp != 0)
|
|
1089
|
-
canopy_height_grid[mask] = canopy_height_grid_comp[mask]
|
|
1090
|
-
# Apply same complementation to bottom grid
|
|
1091
|
-
mask_b = (canopy_bottom_height_grid == 0) & (canopy_bottom_height_grid_comp != 0)
|
|
1092
|
-
canopy_bottom_height_grid[mask_b] = canopy_bottom_height_grid_comp[mask_b]
|
|
1093
|
-
|
|
1094
|
-
# Ensure bottom <= top
|
|
1095
|
-
canopy_bottom_height_grid = np.minimum(canopy_bottom_height_grid, canopy_height_grid)
|
|
1096
|
-
|
|
1097
|
-
# Handle DEM - either flat or from source
|
|
1098
|
-
if kwargs.pop('flat_dem', None):
|
|
1099
|
-
dem_grid = np.zeros_like(land_cover_grid)
|
|
1100
|
-
else:
|
|
1101
|
-
print("Creating Digital Elevation Model (DEM) grid")
|
|
1102
|
-
dem_grid = create_dem_grid_from_gdf_polygon(terrain_gdf, meshsize, rectangle_vertices)
|
|
1103
|
-
|
|
1104
|
-
# Visualize grid if requested
|
|
1105
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
1106
|
-
if grid_vis:
|
|
1107
|
-
visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
# Apply minimum canopy height threshold if specified
|
|
1111
|
-
min_canopy_height = kwargs.get("min_canopy_height")
|
|
1112
|
-
if min_canopy_height is not None:
|
|
1113
|
-
canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
|
|
1114
|
-
canopy_bottom_height_grid[canopy_height_grid == 0] = 0
|
|
1115
|
-
|
|
1116
|
-
# Remove objects near perimeter if specified
|
|
1117
|
-
remove_perimeter_object = kwargs.get("remove_perimeter_object")
|
|
1118
|
-
if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
|
|
1119
|
-
print("apply perimeter removal")
|
|
1120
|
-
# Calculate perimeter width based on grid dimensions
|
|
1121
|
-
w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
|
|
1122
|
-
h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
|
|
1123
|
-
|
|
1124
|
-
# Clear canopy heights in perimeter
|
|
1125
|
-
canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
|
|
1126
|
-
canopy_bottom_height_grid[:w_peri, :] = canopy_bottom_height_grid[-w_peri:, :] = canopy_bottom_height_grid[:, :h_peri] = canopy_bottom_height_grid[:, -h_peri:] = 0
|
|
1127
|
-
|
|
1128
|
-
# Find building IDs in perimeter regions
|
|
1129
|
-
ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
|
|
1130
|
-
ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
|
|
1131
|
-
ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
|
|
1132
|
-
ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
|
|
1133
|
-
remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
|
|
1134
|
-
|
|
1135
|
-
# Remove buildings in perimeter
|
|
1136
|
-
for remove_id in remove_ids:
|
|
1137
|
-
positions = np.where(building_id_grid == remove_id)
|
|
1138
|
-
building_height_grid[positions] = 0
|
|
1139
|
-
building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
|
|
1140
|
-
|
|
1141
|
-
# Visualize grids after optional perimeter removal
|
|
1142
|
-
grid_vis = kwargs.get("gridvis", True)
|
|
1143
|
-
if grid_vis:
|
|
1144
|
-
# Building height grid visualization (zeros hidden)
|
|
1145
|
-
building_height_grid_nan = building_height_grid.copy()
|
|
1146
|
-
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
1147
|
-
visualize_numerical_grid(
|
|
1148
|
-
np.flipud(building_height_grid_nan),
|
|
1149
|
-
meshsize,
|
|
1150
|
-
"building height (m)",
|
|
1151
|
-
cmap='viridis',
|
|
1152
|
-
label='Value'
|
|
1153
|
-
)
|
|
1154
|
-
|
|
1155
|
-
# Canopy height grid visualization (zeros hidden)
|
|
1156
|
-
canopy_height_grid_nan = canopy_height_grid.copy()
|
|
1157
|
-
canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
|
|
1158
|
-
visualize_numerical_grid(
|
|
1159
|
-
np.flipud(canopy_height_grid_nan),
|
|
1160
|
-
meshsize,
|
|
1161
|
-
"Tree canopy height (m)",
|
|
1162
|
-
cmap='Greens',
|
|
1163
|
-
label='Tree canopy height (m)'
|
|
1164
|
-
)
|
|
1165
|
-
|
|
1166
|
-
# Generate 3D voxel grid
|
|
1167
|
-
voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source, canopy_bottom_height_grid)
|
|
1168
|
-
|
|
1169
|
-
# Save all data if a save path is provided
|
|
1170
|
-
save_voxcity = kwargs.get("save_voxctiy_data", True)
|
|
1171
|
-
if save_voxcity:
|
|
1172
|
-
save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity_data.pkl")
|
|
1173
|
-
save_voxcity_data(save_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
1174
|
-
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
1175
|
-
building_gdf, meshsize, rectangle_vertices)
|
|
1176
|
-
|
|
1177
|
-
return voxcity_grid, building_height_grid, building_min_height_grid, building_id_grid, canopy_height_grid, canopy_bottom_height_grid, land_cover_grid, dem_grid, filtered_buildings
|
|
1178
|
-
|
|
1179
|
-
def replace_nan_in_nested(arr, replace_value=10.0):
|
|
1180
|
-
"""
|
|
1181
|
-
Optimized version that avoids converting to Python lists.
|
|
1182
|
-
Works directly with numpy arrays.
|
|
1183
|
-
"""
|
|
1184
|
-
if not isinstance(arr, np.ndarray):
|
|
1185
|
-
return arr
|
|
1186
|
-
|
|
1187
|
-
# Create output array
|
|
1188
|
-
result = np.empty_like(arr, dtype=object)
|
|
1189
|
-
|
|
1190
|
-
# Vectorized operation for empty cells
|
|
1191
|
-
for i in range(arr.shape[0]):
|
|
1192
|
-
for j in range(arr.shape[1]):
|
|
1193
|
-
cell = arr[i, j]
|
|
1194
|
-
|
|
1195
|
-
if cell is None or (isinstance(cell, list) and len(cell) == 0):
|
|
1196
|
-
result[i, j] = []
|
|
1197
|
-
elif isinstance(cell, list):
|
|
1198
|
-
# Process list without converting entire array
|
|
1199
|
-
new_cell = []
|
|
1200
|
-
for segment in cell:
|
|
1201
|
-
if isinstance(segment, (list, np.ndarray)):
|
|
1202
|
-
# Use numpy operations where possible
|
|
1203
|
-
if isinstance(segment, np.ndarray):
|
|
1204
|
-
new_segment = np.where(np.isnan(segment), replace_value, segment).tolist()
|
|
1205
|
-
else:
|
|
1206
|
-
new_segment = [replace_value if (isinstance(v, float) and np.isnan(v)) else v for v in segment]
|
|
1207
|
-
new_cell.append(new_segment)
|
|
1208
|
-
else:
|
|
1209
|
-
new_cell.append(segment)
|
|
1210
|
-
result[i, j] = new_cell
|
|
1211
|
-
else:
|
|
1212
|
-
result[i, j] = cell
|
|
1213
|
-
|
|
1214
|
-
return result
|
|
1215
|
-
|
|
1216
|
-
def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
1217
|
-
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
1218
|
-
building_gdf, meshsize, rectangle_vertices):
|
|
1219
|
-
"""Save voxcity data to a file for later loading.
|
|
1220
|
-
|
|
1221
|
-
Args:
|
|
1222
|
-
output_path: Path to save the data file
|
|
1223
|
-
voxcity_grid: 3D voxel grid of the complete city model
|
|
1224
|
-
building_height_grid: 2D grid of building heights
|
|
1225
|
-
building_min_height_grid: 2D grid of minimum building heights
|
|
1226
|
-
building_id_grid: 2D grid of building IDs
|
|
1227
|
-
canopy_height_grid: 2D grid of tree canopy heights
|
|
1228
|
-
land_cover_grid: 2D grid of land cover classifications
|
|
1229
|
-
dem_grid: 2D grid of ground elevation
|
|
1230
|
-
building_gdf: GeoDataFrame of building footprints and metadata
|
|
1231
|
-
meshsize: Size of each grid cell in meters
|
|
1232
|
-
rectangle_vertices: List of coordinates defining the area of interest
|
|
1233
|
-
"""
|
|
1234
|
-
import pickle
|
|
1235
|
-
import os
|
|
1236
|
-
|
|
1237
|
-
# Ensure the output directory exists
|
|
1238
|
-
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
1239
|
-
|
|
1240
|
-
# Create a comprehensive dictionary containing all voxcity data
|
|
1241
|
-
# This preserves all components needed to reconstruct or analyze the model
|
|
1242
|
-
data_dict = {
|
|
1243
|
-
'voxcity_grid': voxcity_grid,
|
|
1244
|
-
'building_height_grid': building_height_grid,
|
|
1245
|
-
'building_min_height_grid': building_min_height_grid,
|
|
1246
|
-
'building_id_grid': building_id_grid,
|
|
1247
|
-
'canopy_height_grid': canopy_height_grid,
|
|
1248
|
-
'land_cover_grid': land_cover_grid,
|
|
1249
|
-
'dem_grid': dem_grid,
|
|
1250
|
-
'building_gdf': building_gdf,
|
|
1251
|
-
'meshsize': meshsize,
|
|
1252
|
-
'rectangle_vertices': rectangle_vertices
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
# Serialize and save the data using pickle for efficient storage
|
|
1256
|
-
# Pickle preserves exact data types and structures
|
|
1257
|
-
with open(output_path, 'wb') as f:
|
|
1258
|
-
pickle.dump(data_dict, f)
|
|
1259
|
-
|
|
1260
|
-
print(f"Voxcity data saved to {output_path}")
|
|
1261
|
-
|
|
1262
|
-
def load_voxcity_data(input_path):
|
|
1263
|
-
"""Load voxcity data from a saved file.
|
|
1264
|
-
|
|
1265
|
-
Args:
|
|
1266
|
-
input_path: Path to the saved data file
|
|
1267
|
-
|
|
1268
|
-
Returns:
|
|
1269
|
-
tuple: All the voxcity data components including:
|
|
1270
|
-
- voxcity_grid: 3D voxel grid of the complete city model
|
|
1271
|
-
- building_height_grid: 2D grid of building heights
|
|
1272
|
-
- building_min_height_grid: 2D grid of minimum building heights
|
|
1273
|
-
- building_id_grid: 2D grid of building IDs
|
|
1274
|
-
- canopy_height_grid: 2D grid of tree canopy heights
|
|
1275
|
-
- land_cover_grid: 2D grid of land cover classifications
|
|
1276
|
-
- dem_grid: 2D grid of ground elevation
|
|
1277
|
-
- building_gdf: GeoDataFrame of building footprints and metadata
|
|
1278
|
-
- meshsize: Size of each grid cell in meters
|
|
1279
|
-
- rectangle_vertices: List of coordinates defining the area of interest
|
|
1280
|
-
"""
|
|
1281
|
-
import pickle
|
|
1282
|
-
|
|
1283
|
-
# Deserialize the data from the saved file
|
|
1284
|
-
with open(input_path, 'rb') as f:
|
|
1285
|
-
data_dict = pickle.load(f)
|
|
1286
|
-
|
|
1287
|
-
print(f"Voxcity data loaded from {input_path}")
|
|
1288
|
-
|
|
1289
|
-
# Return all components as a tuple in the same order as the main function
|
|
1290
|
-
# This ensures compatibility with existing code that expects this structure
|
|
1291
|
-
return (
|
|
1292
|
-
data_dict['voxcity_grid'],
|
|
1293
|
-
data_dict['building_height_grid'],
|
|
1294
|
-
data_dict['building_min_height_grid'],
|
|
1295
|
-
data_dict['building_id_grid'],
|
|
1296
|
-
data_dict['canopy_height_grid'],
|
|
1297
|
-
data_dict['land_cover_grid'],
|
|
1298
|
-
data_dict['dem_grid'],
|
|
1299
|
-
data_dict['building_gdf'],
|
|
1300
|
-
data_dict['meshsize'],
|
|
1301
|
-
data_dict['rectangle_vertices']
|
|
1302
|
-
)
|