voxcity 0.5.13__py3-none-any.whl → 0.5.15__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.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

voxcity/generator.py CHANGED
@@ -14,9 +14,13 @@ The main functions are:
14
14
  - get_voxcity: Main function to generate a complete voxel city model
15
15
  """
16
16
 
17
+ # Standard library imports
17
18
  import numpy as np
18
19
  import os
20
+
19
21
  # Local application/library specific imports
22
+
23
+ # Data downloaders - modules for fetching geospatial data from various sources
20
24
  from .downloader.mbfp import get_mbfp_gdf
21
25
  from .downloader.osm import load_gdf_from_openstreetmap, load_land_cover_gdf_from_osm
22
26
  from .downloader.oemj import save_oemj_as_geotiff
@@ -24,6 +28,8 @@ from .downloader.omt import load_gdf_from_openmaptiles
24
28
  from .downloader.eubucco import load_gdf_from_eubucco
25
29
  from .downloader.overture import load_gdf_from_overture
26
30
  from .downloader.citygml import load_buid_dem_veg_from_citygml
31
+
32
+ # Google Earth Engine related imports - for satellite and elevation data
27
33
  from .downloader.gee import (
28
34
  initialize_earth_engine,
29
35
  get_roi,
@@ -37,6 +43,8 @@ from .downloader.gee import (
37
43
  save_geotiff_open_buildings_temporal,
38
44
  save_geotiff_dsm_minus_dtm
39
45
  )
46
+
47
+ # Grid processing functions - for converting geodata to raster grids
40
48
  from .geoprocessor.grid import (
41
49
  group_and_label_cells,
42
50
  process_grid,
@@ -49,8 +57,12 @@ from .geoprocessor.grid import (
49
57
  create_vegetation_height_grid_from_gdf_polygon,
50
58
  create_dem_grid_from_gdf_polygon
51
59
  )
60
+
61
+ # Utility functions
52
62
  from .utils.lc import convert_land_cover, convert_land_cover_array
53
63
  from .geoprocessor.polygon import get_gdf_from_gpkg, save_geojson
64
+
65
+ # Visualization functions - for creating plots and 3D visualizations
54
66
  from .utils.visualization import (
55
67
  get_land_cover_classes,
56
68
  visualize_land_cover_grid,
@@ -83,55 +95,70 @@ def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwar
83
95
  print("Creating Land Use Land Cover grid\n ")
84
96
  print(f"Data source: {source}")
85
97
 
86
- # Initialize Earth Engine for accessing satellite data
98
+ # Initialize Earth Engine for satellite-based data sources
99
+ # Skip initialization for local/vector data sources
87
100
  if source not in ["OpenStreetMap", "OpenEarthMapJapan"]:
88
101
  initialize_earth_engine()
89
102
 
90
- # Create output directory if it doesn't exist
103
+ # Ensure output directory exists for saving intermediate files
91
104
  os.makedirs(output_dir, exist_ok=True)
92
105
  geotiff_path = os.path.join(output_dir, "land_cover.tif")
93
106
 
94
- # Get land cover data based on selected source
107
+ # Handle different data sources - each requires specific processing
108
+ # Satellite/raster-based sources are saved as GeoTIFF files
95
109
  if source == 'Urbanwatch':
110
+ # Urban-focused land cover from satellite imagery
96
111
  roi = get_roi(rectangle_vertices)
97
112
  collection_name = "projects/sat-io/open-datasets/HRLC/urban-watch-cities"
98
113
  image = get_ee_image_collection(collection_name, roi)
99
114
  save_geotiff(image, geotiff_path)
100
115
  elif source == 'ESA WorldCover':
116
+ # Global land cover from European Space Agency
101
117
  roi = get_roi(rectangle_vertices)
102
118
  save_geotiff_esa_land_cover(roi, geotiff_path)
103
119
  elif source == 'ESRI 10m Annual Land Cover':
120
+ # High-resolution annual land cover from ESRI
104
121
  esri_landcover_year = kwargs.get("esri_landcover_year")
105
122
  roi = get_roi(rectangle_vertices)
106
123
  save_geotiff_esri_landcover(roi, geotiff_path, year=esri_landcover_year)
107
124
  elif source == 'Dynamic World V1':
125
+ # Near real-time land cover from Google's Dynamic World
108
126
  dynamic_world_date = kwargs.get("dynamic_world_date")
109
127
  roi = get_roi(rectangle_vertices)
110
128
  save_geotiff_dynamic_world_v1(roi, geotiff_path, dynamic_world_date)
111
129
  elif source == 'OpenEarthMapJapan':
130
+ # Japan-specific land cover dataset
112
131
  save_oemj_as_geotiff(rectangle_vertices, geotiff_path)
113
132
  elif source == 'OpenStreetMap':
114
- # For OSM, we get data directly as GeoJSON instead of GeoTIFF
133
+ # Vector-based land cover from OpenStreetMap
134
+ # This bypasses the GeoTIFF workflow and gets data directly as GeoJSON
115
135
  land_cover_gdf = load_land_cover_gdf_from_osm(rectangle_vertices)
116
136
 
117
- # Get mapping of land cover classes for the selected source
137
+ # Get the classification scheme for the selected data source
138
+ # Each source has its own land cover categories and color coding
118
139
  land_cover_classes = get_land_cover_classes(source)
119
140
 
120
- # Create grid from either GeoJSON (OSM) or GeoTIFF (other sources)
141
+ # Convert geospatial data to regular grid format
142
+ # Different processing for vector vs raster data sources
121
143
  if source == 'OpenStreetMap':
144
+ # Process vector data directly from GeoDataFrame
122
145
  land_cover_grid_str = create_land_cover_grid_from_gdf_polygon(land_cover_gdf, meshsize, source, rectangle_vertices)
123
146
  else:
147
+ # Process raster data from GeoTIFF file
124
148
  land_cover_grid_str = create_land_cover_grid_from_geotiff_polygon(geotiff_path, meshsize, land_cover_classes, rectangle_vertices)
125
149
 
126
- # Create color map for visualization, scaling RGB values to 0-1 range
150
+ # Prepare color mapping for visualization
151
+ # Convert RGB values from 0-255 range to 0-1 range for matplotlib
127
152
  color_map = {cls: [r/255, g/255, b/255] for (r,g,b), cls in land_cover_classes.items()}
128
153
 
129
- # Visualize grid if requested
154
+ # Generate visualization if requested
130
155
  grid_vis = kwargs.get("gridvis", True)
131
156
  if grid_vis:
157
+ # Flip grid vertically for correct display orientation
132
158
  visualize_land_cover_grid(np.flipud(land_cover_grid_str), meshsize, color_map, land_cover_classes)
133
159
 
134
- # Convert string labels to integer codes
160
+ # Convert string-based land cover labels to integer codes for processing
161
+ # This enables efficient numerical operations on the grid
135
162
  land_cover_grid_int = convert_land_cover_array(land_cover_grid_str, land_cover_classes)
136
163
 
137
164
  return land_cover_grid_int
@@ -159,7 +186,7 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
159
186
  - list: Filtered building features
160
187
  """
161
188
 
162
- # Initialize Earth Engine for accessing satellite data
189
+ # Initialize Earth Engine for satellite-based building data sources
163
190
  if source not in ["OpenStreetMap", "Overture", "Local file"]:
164
191
  initialize_earth_engine()
165
192
 
@@ -168,52 +195,60 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
168
195
 
169
196
  os.makedirs(output_dir, exist_ok=True)
170
197
 
171
- # Get building data from primary source
198
+ # Fetch building data from primary source
199
+ # Each source has different data formats and processing requirements
172
200
  if source == 'Microsoft Building Footprints':
201
+ # Machine learning-derived building footprints from satellite imagery
173
202
  gdf = get_mbfp_gdf(output_dir, rectangle_vertices)
174
203
  elif source == 'OpenStreetMap':
204
+ # Crowd-sourced building data with varying completeness
175
205
  gdf = load_gdf_from_openstreetmap(rectangle_vertices)
176
206
  elif source == "Open Building 2.5D Temporal":
177
- # Special case: directly creates grids without intermediate GeoJSON
207
+ # Special case: this source provides both footprints and heights
208
+ # Skip GeoDataFrame processing and create grids directly
178
209
  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)
179
210
  elif source == 'EUBUCCO v0.1':
211
+ # European building database with height information
180
212
  gdf = load_gdf_from_eubucco(rectangle_vertices, output_dir)
181
213
  elif source == "OpenMapTiles":
214
+ # Vector tiles service for building data
182
215
  gdf = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
183
216
  elif source == "Overture":
217
+ # Open building dataset from Overture Maps Foundation
184
218
  gdf = load_gdf_from_overture(rectangle_vertices)
185
219
  elif source == "Local file":
186
- # Handle local GPKG files
220
+ # Handle user-provided local building data files
187
221
  _, extension = os.path.splitext(kwargs["building_path"])
188
222
  if extension == ".gpkg":
189
223
  gdf = get_gdf_from_gpkg(kwargs["building_path"], rectangle_vertices)
190
224
 
191
- # Check for complementary building data source
225
+ # Handle complementary data sources to fill gaps or provide additional information
226
+ # This allows combining multiple sources for better coverage or accuracy
192
227
  building_complementary_source = kwargs.get("building_complementary_source")
193
228
  building_complement_height = kwargs.get("building_complement_height")
194
229
 
195
230
  if (building_complementary_source is None) or (building_complementary_source=='None'):
196
- # Use only primary source
231
+ # Use only the primary data source
197
232
  if source != "Open Building 2.5D Temporal":
198
233
  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)
199
234
  else:
200
- # Handle complementary source
235
+ # Combine primary source with complementary data
201
236
  if building_complementary_source == "Open Building 2.5D Temporal":
202
- # Special case: use temporal height data as complement
237
+ # Use temporal height data to complement footprint data
203
238
  roi = get_roi(rectangle_vertices)
204
239
  os.makedirs(output_dir, exist_ok=True)
205
240
  geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
206
241
  save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
207
242
  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)
208
243
  elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
209
- # Special case: use temporal height data as complement
244
+ # Use digital surface model minus digital terrain model for height estimation
210
245
  roi = get_roi(rectangle_vertices)
211
246
  os.makedirs(output_dir, exist_ok=True)
212
247
  geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
213
248
  save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
214
249
  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)
215
250
  else:
216
- # Get complementary data from other sources
251
+ # Fetch complementary data from another vector source
217
252
  if building_complementary_source == 'Microsoft Building Footprints':
218
253
  gdf_comp = get_mbfp_gdf(output_dir, rectangle_vertices)
219
254
  elif building_complementary_source == 'OpenStreetMap':
@@ -229,15 +264,18 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
229
264
  if extension == ".gpkg":
230
265
  gdf_comp = get_gdf_from_gpkg(kwargs["building_complementary_path"], rectangle_vertices)
231
266
 
232
- # Option to complement footprints only or both footprints and heights
267
+ # Configure how to combine the complementary data
268
+ # Can complement footprints only or both footprints and heights
233
269
  complement_building_footprints = kwargs.get("complement_building_footprints")
234
270
  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)
235
271
 
236
- # Visualize grid if requested
272
+ # Generate visualization if requested
237
273
  grid_vis = kwargs.get("gridvis", True)
238
274
  if grid_vis:
275
+ # Replace zeros with NaN for better visualization (don't show empty areas)
239
276
  building_height_grid_nan = building_height_grid.copy()
240
277
  building_height_grid_nan[building_height_grid_nan == 0] = np.nan
278
+ # Flip grid vertically for correct display orientation
241
279
  visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
242
280
 
243
281
  return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
@@ -260,32 +298,38 @@ def get_canopy_height_grid(rectangle_vertices, meshsize, source, output_dir, **k
260
298
  print("Creating Canopy Height grid\n ")
261
299
  print(f"Data source: High Resolution Canopy Height Maps by WRI and Meta")
262
300
 
263
- # Initialize Earth Engine for accessing satellite data
301
+ # Initialize Earth Engine for satellite-based canopy height data
264
302
  initialize_earth_engine()
265
303
 
266
304
  os.makedirs(output_dir, exist_ok=True)
267
305
  geotiff_path = os.path.join(output_dir, "canopy_height.tif")
268
306
 
269
- # Get region of interest and canopy height data
307
+ # Get region of interest and fetch canopy height data
270
308
  roi = get_roi(rectangle_vertices)
271
309
  if source == 'High Resolution 1m Global Canopy Height Maps':
310
+ # High-resolution (1m) global canopy height maps from Meta and WRI
311
+ # Based on satellite imagery and machine learning models
272
312
  collection_name = "projects/meta-forest-monitoring-okw37/assets/CanopyHeight"
273
313
  image = get_ee_image_collection(collection_name, roi)
274
314
  elif source == 'ETH Global Sentinel-2 10m Canopy Height (2020)':
315
+ # Medium-resolution (10m) canopy height from ETH Zurich
316
+ # Derived from Sentinel-2 satellite data
275
317
  collection_name = "users/nlang/ETH_GlobalCanopyHeight_2020_10m_v1"
276
318
  image = get_ee_image(collection_name, roi)
277
319
 
278
- # Save canopy height data as GeoTIFF
320
+ # Save canopy height data as GeoTIFF with specified resolution
279
321
  save_geotiff(image, geotiff_path, resolution=meshsize)
280
322
 
281
- # Create height grid from GeoTIFF
323
+ # Convert GeoTIFF to regular grid format
282
324
  canopy_height_grid = create_height_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices)
283
325
 
284
- # Visualize grid if requested
326
+ # Generate visualization if requested
285
327
  grid_vis = kwargs.get("gridvis", True)
286
328
  if grid_vis:
329
+ # Replace zeros with NaN for better visualization (show only areas with trees)
287
330
  canopy_height_grid_nan = canopy_height_grid.copy()
288
331
  canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
332
+ # Use green color scheme appropriate for vegetation
289
333
  visualize_numerical_grid(np.flipud(canopy_height_grid_nan), meshsize, "Tree canopy height", cmap='Greens', label='Tree canopy height (m)')
290
334
 
291
335
  return canopy_height_grid
@@ -310,38 +354,45 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
310
354
  print(f"Data source: {source}")
311
355
 
312
356
  if source == "Local file":
357
+ # Use user-provided local DEM file
313
358
  geotiff_path = kwargs["dem_path"]
314
359
  else:
315
- # Initialize Earth Engine for accessing elevation data
360
+ # Fetch DEM data from various satellite/government sources
316
361
  initialize_earth_engine()
317
362
 
318
363
  geotiff_path = os.path.join(output_dir, "dem.tif")
319
364
 
320
- # Add buffer around ROI to ensure smooth interpolation at edges
365
+ # Add buffer around region of interest to ensure smooth interpolation at edges
366
+ # This prevents edge artifacts in the final grid
321
367
  buffer_distance = 100
322
368
  roi = get_roi(rectangle_vertices)
323
369
  roi_buffered = roi.buffer(buffer_distance)
324
370
 
325
- # Get DEM data
371
+ # Fetch elevation data from selected source
326
372
  image = get_dem_image(roi_buffered, source)
327
373
 
328
- # Save DEM data with appropriate resolution based on source
374
+ # Save DEM data with appropriate resolution based on source capabilities
329
375
  if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM', 'Netherlands 0.5m DTM']:
376
+ # High-resolution elevation models - use specified mesh size
330
377
  save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
331
378
  elif source == 'USGS 3DEP 1m':
379
+ # US Geological Survey 3D Elevation Program
380
+ # Ensure minimum scale of 1.25m due to data limitations
332
381
  scale = max(meshsize, 1.25)
333
382
  save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
334
383
  else:
335
- # Default to 30m resolution for other sources
384
+ # Default to 30m resolution for global/lower resolution sources
336
385
  save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
337
386
 
338
- # Create DEM grid with optional interpolation method
387
+ # Convert GeoTIFF to regular grid with optional interpolation
388
+ # Interpolation helps fill gaps and smooth transitions
339
389
  dem_interpolation = kwargs.get("dem_interpolation")
340
390
  dem_grid = create_dem_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices, dem_interpolation=dem_interpolation)
341
391
 
342
- # Visualize grid if requested
392
+ # Generate visualization if requested
343
393
  grid_vis = kwargs.get("gridvis", True)
344
394
  if grid_vis:
395
+ # Use terrain color scheme appropriate for elevation data
345
396
  visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
346
397
 
347
398
  return dem_grid
@@ -367,13 +418,16 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
367
418
 
368
419
  print("Generating 3D voxel data")
369
420
 
370
- # Convert land cover values if not from OpenStreetMap
421
+ # Convert land cover values to standardized format if needed
422
+ # OpenStreetMap data is already in the correct format
371
423
  if (land_cover_source == 'OpenStreetMap'):
372
424
  land_cover_grid_converted = land_cover_grid_ori
373
425
  else:
374
426
  land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
375
427
 
376
- # Prepare and flip all input grids vertically for consistent orientation
428
+ # Prepare all input grids for 3D processing
429
+ # Flip vertically to align with standard geographic orientation (north-up)
430
+ # Handle missing data appropriately for each grid type
377
431
  building_height_grid = np.flipud(np.nan_to_num(building_height_grid_ori, nan=10.0)) # Replace NaN values with 10m height
378
432
  building_min_height_grid = np.flipud(replace_nan_in_nested(building_min_height_grid_ori)) # Replace NaN in nested arrays
379
433
  building_id_grid = np.flipud(building_id_grid_ori)
@@ -382,61 +436,68 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
382
436
  dem_grid = process_grid(building_id_grid, dem_grid) # Process DEM based on building footprints
383
437
  tree_grid = np.flipud(tree_grid_ori.copy())
384
438
 
385
- # Validate input dimensions
439
+ # Validate that all input grids have consistent dimensions
386
440
  assert building_height_grid.shape == land_cover_grid.shape == dem_grid.shape == tree_grid.shape, "Input grids must have the same shape"
387
441
 
388
442
  rows, cols = building_height_grid.shape
389
443
 
390
- # Calculate required height for 3D grid - add 1 to ensure enough space
444
+ # Calculate the required height for the 3D voxel grid
445
+ # Add 1 voxel layer to ensure sufficient vertical space
391
446
  max_height = int(np.ceil(np.max(building_height_grid + dem_grid + tree_grid) / voxel_size))+1
392
447
 
393
- # Initialize empty 3D grid
448
+ # Initialize the 3D voxel grid with zeros
449
+ # Dimensions: (rows, columns, height_layers)
394
450
  voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
395
451
 
396
- # Get trunk height ratio for trees, default based on typical tree proportions
452
+ # Configure tree trunk-to-crown ratio
453
+ # This determines how much of the tree is trunk vs canopy
397
454
  trunk_height_ratio = kwargs.get("trunk_height_ratio")
398
455
  if trunk_height_ratio is None:
399
456
  trunk_height_ratio = 11.76 / 19.98 # Default ratio based on typical tree proportions
400
457
 
401
- # Fill the 3D grid cell by cell
458
+ # Process each grid cell to build the 3D voxel representation
402
459
  for i in range(rows):
403
460
  for j in range(cols):
404
- # Calculate ground level in voxel units (+1 to ensure space for surface features)
461
+ # Calculate ground level in voxel units
462
+ # Add 1 to ensure space for surface features
405
463
  ground_level = int(dem_grid[i, j] / voxel_size + 0.5) + 1
406
464
 
465
+ # Extract current cell values
407
466
  tree_height = tree_grid[i, j]
408
467
  land_cover = land_cover_grid[i, j]
409
468
 
410
- # Fill underground voxels with -1
469
+ # Fill underground voxels with -1 (represents subsurface)
411
470
  voxel_grid[i, j, :ground_level] = -1
412
471
 
413
- # Set surface land cover value
472
+ # Set the ground surface to the land cover type
414
473
  voxel_grid[i, j, ground_level-1] = land_cover
415
474
 
416
- # Process trees - split into trunk and crown sections
475
+ # Process tree canopy if trees are present
417
476
  if tree_height > 0:
418
- # Calculate crown base and top heights
477
+ # Calculate tree structure: trunk base to crown base to crown top
419
478
  crown_base_height = (tree_height * trunk_height_ratio)
420
479
  crown_base_height_level = int(crown_base_height / voxel_size + 0.5)
421
480
  crown_top_height = tree_height
422
481
  crown_top_height_level = int(crown_top_height / voxel_size + 0.5)
423
482
 
424
483
  # Ensure minimum crown height of 1 voxel
484
+ # Prevent crown base and top from being at the same level
425
485
  if (crown_top_height_level == crown_base_height_level) and (crown_base_height_level>0):
426
486
  crown_base_height_level -= 1
427
487
 
428
- # Calculate tree start and end positions relative to ground level
488
+ # Calculate absolute positions relative to ground level
429
489
  tree_start = ground_level + crown_base_height_level
430
490
  tree_end = ground_level + crown_top_height_level
431
491
 
432
- # Fill tree crown voxels with -2
492
+ # Fill tree crown voxels with -2 (represents vegetation canopy)
433
493
  voxel_grid[i, j, tree_start:tree_end] = -2
434
494
 
435
- # Process buildings - handle multiple height segments
495
+ # Process buildings - handle multiple height segments per building
496
+ # Some buildings may have multiple levels or complex height profiles
436
497
  for k in building_min_height_grid[i, j]:
437
498
  building_min_height = int(k[0] / voxel_size + 0.5) # Lower height of building segment
438
499
  building_height = int(k[1] / voxel_size + 0.5) # Upper height of building segment
439
- # Fill building voxels with -3
500
+ # Fill building voxels with -3 (represents built structures)
440
501
  voxel_grid[i, j, ground_level+building_min_height:ground_level+building_height] = -3
441
502
 
442
503
  return voxel_grid
@@ -558,73 +619,89 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
558
619
  - dem_grid: 2D grid of ground elevation
559
620
  - building_geojson: GeoJSON of building footprints and metadata
560
621
  """
561
- # Create output directory if it doesn't exist
622
+ # Set up output directory for intermediate and final files
562
623
  output_dir = kwargs.get("output_dir", "output")
563
624
  os.makedirs(output_dir, exist_ok=True)
564
625
 
565
- # Remove 'output_dir' from kwargs to prevent duplication
626
+ # Remove 'output_dir' from kwargs to prevent duplication in function calls
566
627
  kwargs.pop('output_dir', None)
567
628
 
568
- # Generate all required 2D grids
629
+ # STEP 1: Generate all required 2D grids from various data sources
630
+ # These grids form the foundation for the 3D voxel model
631
+
632
+ # Land cover classification grid (e.g., urban, forest, water, agriculture)
569
633
  land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
634
+
635
+ # Building footprints and height information
570
636
  building_height_grid, building_min_height_grid, building_id_grid, building_gdf = get_building_height_grid(rectangle_vertices, meshsize, building_source, output_dir, **kwargs)
571
637
 
572
- # Save building data to GeoJSON
638
+ # Save building data to file for later analysis or visualization
573
639
  if not building_gdf.empty:
574
640
  save_path = f"{output_dir}/building.gpkg"
575
641
  building_gdf.to_file(save_path, driver='GPKG')
576
642
 
577
- # Get canopy height data
643
+ # STEP 2: Handle canopy height data
644
+ # Either use static values or fetch from satellite sources
578
645
  if canopy_height_source == "Static":
579
- # Create canopy height grid with same shape as land cover grid
646
+ # Create uniform canopy height for all tree-covered areas
580
647
  canopy_height_grid = np.zeros_like(land_cover_grid, dtype=float)
581
648
 
582
- # Set default static height for trees (20 meters is a typical average tree height)
649
+ # Apply static height to areas classified as trees
650
+ # Default height represents typical urban tree height
583
651
  static_tree_height = kwargs.get("static_tree_height", 10.0)
584
652
  tree_mask = (land_cover_grid == 4)
585
653
 
586
654
  # Set static height for tree cells
587
655
  canopy_height_grid[tree_mask] = static_tree_height
588
656
  else:
657
+ # Fetch canopy height from satellite/remote sensing sources
589
658
  canopy_height_grid = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
590
659
 
591
- # Handle DEM - either flat or from source
660
+ # STEP 3: Handle digital elevation model (terrain)
592
661
  if dem_source == "Flat":
662
+ # Create flat terrain for simplified modeling
593
663
  dem_grid = np.zeros_like(land_cover_grid)
594
664
  else:
665
+ # Fetch terrain elevation from various sources
595
666
  dem_grid = get_dem_grid(rectangle_vertices, meshsize, dem_source, output_dir, **kwargs)
596
667
 
597
- # Apply minimum canopy height threshold if specified
668
+ # STEP 4: Apply optional data filtering and cleaning
669
+
670
+ # Filter out low vegetation that may be noise in the data
598
671
  min_canopy_height = kwargs.get("min_canopy_height")
599
672
  if min_canopy_height is not None:
600
673
  canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
601
674
 
602
- # Remove objects near perimeter if specified
675
+ # Remove objects near the boundary to avoid edge effects
676
+ # This is useful when the area of interest is part of a larger urban area
603
677
  remove_perimeter_object = kwargs.get("remove_perimeter_object")
604
678
  if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
605
679
  # Calculate perimeter width based on grid dimensions
606
680
  w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
607
681
  h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
608
682
 
609
- # Clear canopy heights in perimeter
683
+ # Clear canopy heights in perimeter areas
610
684
  canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
611
685
 
612
- # Find building IDs in perimeter regions
686
+ # Identify buildings that intersect with perimeter areas
613
687
  ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
614
688
  ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
615
689
  ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
616
690
  ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
617
691
  remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
618
692
 
619
- # Remove buildings in perimeter
693
+ # Remove identified buildings from all grids
620
694
  for remove_id in remove_ids:
621
695
  positions = np.where(building_id_grid == remove_id)
622
696
  building_height_grid[positions] = 0
623
697
  building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
624
698
 
625
- # Visualize 2D grids on map if requested
699
+ # STEP 5: Generate optional 2D visualizations on interactive maps
626
700
  mapvis = kwargs.get("mapvis")
627
701
  if mapvis:
702
+ # Create map-based visualizations of all data layers
703
+ # These help users understand the input data before 3D modeling
704
+
628
705
  # Visualize land cover using the new function
629
706
  visualize_landcover_grid_on_basemap(
630
707
  land_cover_grid,
@@ -676,20 +753,22 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
676
753
  show_edge=False
677
754
  )
678
755
 
679
- # Generate 3D voxel grid
756
+ # STEP 6: Generate the final 3D voxel model
757
+ # This combines all 2D grids into a comprehensive 3D representation
680
758
  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)
681
759
 
682
- # Visualize 3D model if requested
760
+ # STEP 7: Generate optional 3D visualization
683
761
  voxelvis = kwargs.get("voxelvis")
684
762
  if voxelvis:
685
- # Create taller visualization grid with fixed height
763
+ # Create a taller grid for better visualization
764
+ # Fixed height ensures consistent camera positioning
686
765
  new_height = int(550/meshsize+0.5)
687
766
  voxcity_grid_vis = np.zeros((voxcity_grid.shape[0], voxcity_grid.shape[1], new_height))
688
767
  voxcity_grid_vis[:, :, :voxcity_grid.shape[2]] = voxcity_grid
689
768
  voxcity_grid_vis[-1, -1, -1] = -99 # Add marker to fix camera location and angle of view
690
769
  visualize_3d_voxel(voxcity_grid_vis, voxel_size=meshsize, save_path=kwargs["voxelvis_img_save_path"])
691
770
 
692
- # Save all data if a save path is provided
771
+ # STEP 8: Save all generated data for future use
693
772
  save_voxcity = kwargs.get("save_voxctiy_data", True)
694
773
  if save_voxcity:
695
774
  save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity_data.pkl")
@@ -845,18 +924,19 @@ def replace_nan_in_nested(arr, replace_value=10.0):
845
924
  Returns:
846
925
  Numpy array with NaN values replaced
847
926
  """
848
- # Convert array to list for easier manipulation
927
+ # Convert array to list for easier manipulation of nested structures
849
928
  arr = arr.tolist()
850
929
 
851
- # Iterate through all dimensions
930
+ # Iterate through all dimensions of the nested array
852
931
  for i in range(len(arr)):
853
932
  for j in range(len(arr[i])):
854
- # Check if the element is a list
933
+ # Check if the element is a list (building height segments)
855
934
  if arr[i][j]: # if not empty list
856
935
  for k in range(len(arr[i][j])):
857
- # For each innermost list
936
+ # For each innermost list (individual height segment)
858
937
  if isinstance(arr[i][j][k], list):
859
938
  for l in range(len(arr[i][j][k])):
939
+ # Replace NaN values with the specified replacement value
860
940
  if isinstance(arr[i][j][k][l], float) and np.isnan(arr[i][j][k][l]):
861
941
  arr[i][j][k][l] = replace_value
862
942
 
@@ -883,10 +963,11 @@ def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_
883
963
  import pickle
884
964
  import os
885
965
 
886
- # Create directory if it doesn't exist
966
+ # Ensure the output directory exists
887
967
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
888
968
 
889
- # Create a dictionary with all the data
969
+ # Create a comprehensive dictionary containing all voxcity data
970
+ # This preserves all components needed to reconstruct or analyze the model
890
971
  data_dict = {
891
972
  'voxcity_grid': voxcity_grid,
892
973
  'building_height_grid': building_height_grid,
@@ -900,7 +981,8 @@ def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_
900
981
  'rectangle_vertices': rectangle_vertices
901
982
  }
902
983
 
903
- # Save the data to a file using pickle
984
+ # Serialize and save the data using pickle for efficient storage
985
+ # Pickle preserves exact data types and structures
904
986
  with open(output_path, 'wb') as f:
905
987
  pickle.dump(data_dict, f)
906
988
 
@@ -927,13 +1009,14 @@ def load_voxcity_data(input_path):
927
1009
  """
928
1010
  import pickle
929
1011
 
930
- # Load the data from the file
1012
+ # Deserialize the data from the saved file
931
1013
  with open(input_path, 'rb') as f:
932
1014
  data_dict = pickle.load(f)
933
1015
 
934
1016
  print(f"Voxcity data loaded from {input_path}")
935
1017
 
936
- # Return all components as a tuple
1018
+ # Return all components as a tuple in the same order as the main function
1019
+ # This ensures compatibility with existing code that expects this structure
937
1020
  return (
938
1021
  data_dict['voxcity_grid'],
939
1022
  data_dict['building_height_grid'],