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