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.
Files changed (81) hide show
  1. voxcity/__init__.py +10 -4
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/gba.py +210 -0
  4. voxcity/downloader/gee.py +5 -1
  5. voxcity/downloader/mbfp.py +1 -1
  6. voxcity/downloader/oemj.py +80 -8
  7. voxcity/downloader/utils.py +73 -73
  8. voxcity/errors.py +30 -0
  9. voxcity/exporter/__init__.py +9 -1
  10. voxcity/exporter/cityles.py +129 -34
  11. voxcity/exporter/envimet.py +51 -26
  12. voxcity/exporter/magicavoxel.py +42 -5
  13. voxcity/exporter/netcdf.py +27 -0
  14. voxcity/exporter/obj.py +103 -28
  15. voxcity/generator/__init__.py +47 -0
  16. voxcity/generator/api.py +721 -0
  17. voxcity/generator/grids.py +381 -0
  18. voxcity/generator/io.py +94 -0
  19. voxcity/generator/pipeline.py +282 -0
  20. voxcity/generator/update.py +429 -0
  21. voxcity/generator/voxelizer.py +392 -0
  22. voxcity/geoprocessor/__init__.py +75 -6
  23. voxcity/geoprocessor/conversion.py +153 -0
  24. voxcity/geoprocessor/draw.py +1488 -1169
  25. voxcity/geoprocessor/heights.py +199 -0
  26. voxcity/geoprocessor/io.py +101 -0
  27. voxcity/geoprocessor/merge_utils.py +91 -0
  28. voxcity/geoprocessor/mesh.py +26 -10
  29. voxcity/geoprocessor/network.py +35 -6
  30. voxcity/geoprocessor/overlap.py +84 -0
  31. voxcity/geoprocessor/raster/__init__.py +82 -0
  32. voxcity/geoprocessor/raster/buildings.py +435 -0
  33. voxcity/geoprocessor/raster/canopy.py +258 -0
  34. voxcity/geoprocessor/raster/core.py +150 -0
  35. voxcity/geoprocessor/raster/export.py +93 -0
  36. voxcity/geoprocessor/raster/landcover.py +159 -0
  37. voxcity/geoprocessor/raster/raster.py +110 -0
  38. voxcity/geoprocessor/selection.py +85 -0
  39. voxcity/geoprocessor/utils.py +824 -820
  40. voxcity/models.py +113 -0
  41. voxcity/simulator/common/__init__.py +22 -0
  42. voxcity/simulator/common/geometry.py +98 -0
  43. voxcity/simulator/common/raytracing.py +450 -0
  44. voxcity/simulator/solar/__init__.py +66 -0
  45. voxcity/simulator/solar/integration.py +336 -0
  46. voxcity/simulator/solar/kernels.py +62 -0
  47. voxcity/simulator/solar/radiation.py +648 -0
  48. voxcity/simulator/solar/sky.py +668 -0
  49. voxcity/simulator/solar/temporal.py +792 -0
  50. voxcity/simulator/view.py +36 -2286
  51. voxcity/simulator/visibility/__init__.py +29 -0
  52. voxcity/simulator/visibility/landmark.py +392 -0
  53. voxcity/simulator/visibility/view.py +508 -0
  54. voxcity/utils/__init__.py +11 -0
  55. voxcity/utils/classes.py +194 -0
  56. voxcity/utils/lc.py +80 -39
  57. voxcity/utils/logging.py +61 -0
  58. voxcity/utils/orientation.py +51 -0
  59. voxcity/utils/shape.py +230 -0
  60. voxcity/utils/weather/__init__.py +26 -0
  61. voxcity/utils/weather/epw.py +146 -0
  62. voxcity/utils/weather/files.py +36 -0
  63. voxcity/utils/weather/onebuilding.py +486 -0
  64. voxcity/visualizer/__init__.py +24 -0
  65. voxcity/visualizer/builder.py +43 -0
  66. voxcity/visualizer/grids.py +141 -0
  67. voxcity/visualizer/maps.py +187 -0
  68. voxcity/visualizer/palette.py +228 -0
  69. voxcity/visualizer/renderer.py +1145 -0
  70. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
  71. voxcity-1.0.2.dist-info/RECORD +81 -0
  72. voxcity/generator.py +0 -1302
  73. voxcity/geoprocessor/grid.py +0 -1739
  74. voxcity/geoprocessor/polygon.py +0 -1344
  75. voxcity/simulator/solar.py +0 -2339
  76. voxcity/utils/visualization.py +0 -2849
  77. voxcity/utils/weather.py +0 -1038
  78. voxcity-0.6.26.dist-info/RECORD +0 -38
  79. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
  80. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {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
- )