voxcity 0.5.30__py3-none-any.whl → 0.6.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.

Potentially problematic release.


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

voxcity/generator.py CHANGED
@@ -24,6 +24,19 @@ Key Features:
24
24
  # Standard library imports
25
25
  import numpy as np
26
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
27
40
 
28
41
  # Local application/library specific imports
29
42
 
@@ -244,11 +257,12 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
244
257
  # This allows combining multiple sources for better coverage or accuracy
245
258
  building_complementary_source = kwargs.get("building_complementary_source")
246
259
  building_complement_height = kwargs.get("building_complement_height")
260
+ overlapping_footprint = kwargs.get("overlapping_footprint")
247
261
 
248
262
  if (building_complementary_source is None) or (building_complementary_source=='None'):
249
263
  # Use only the primary data source
250
264
  if source != "Open Building 2.5D Temporal":
251
- 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)
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)
252
266
  else:
253
267
  # Combine primary source with complementary data
254
268
  if building_complementary_source == "Open Building 2.5D Temporal":
@@ -257,14 +271,14 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
257
271
  os.makedirs(output_dir, exist_ok=True)
258
272
  geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
259
273
  save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
260
- 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)
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)
261
275
  elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
262
276
  # Use digital surface model minus digital terrain model for height estimation
263
277
  roi = get_roi(rectangle_vertices)
264
278
  os.makedirs(output_dir, exist_ok=True)
265
279
  geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
266
280
  save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
267
- 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)
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)
268
282
  else:
269
283
  # Fetch complementary data from another vector source
270
284
  if building_complementary_source == 'Microsoft Building Footprints':
@@ -285,7 +299,7 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
285
299
  # Configure how to combine the complementary data
286
300
  # Can complement footprints only or both footprints and heights
287
301
  complement_building_footprints = kwargs.get("complement_building_footprints")
288
- 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)
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)
289
303
 
290
304
  # Generate visualization if requested
291
305
  grid_vis = kwargs.get("gridvis", True)
@@ -415,9 +429,10 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
415
429
 
416
430
  return dem_grid
417
431
 
418
- def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, building_id_grid_ori, land_cover_grid_ori, dem_grid_ori, tree_grid_ori, voxel_size, land_cover_source, **kwargs):
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):
419
435
  """Creates a 3D voxel representation combining all input grids.
420
-
421
436
  Args:
422
437
  building_height_grid_ori: Grid of building heights
423
438
  building_min_height_grid_ori: Grid of building minimum heights
@@ -427,22 +442,19 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
427
442
  tree_grid_ori: Grid of tree heights
428
443
  voxel_size: Size of each voxel in meters
429
444
  land_cover_source: Source of land cover data
430
- **kwargs: Additional arguments including:
445
+ kwargs: Additional arguments including:
431
446
  - trunk_height_ratio: Ratio of trunk height to total tree height
432
-
433
447
  Returns:
434
448
  numpy.ndarray: 3D voxel grid with encoded values for different features
435
449
  """
436
-
437
450
  print("Generating 3D voxel data")
438
-
451
+
439
452
  # Convert land cover values to standardized format if needed
440
453
  # OpenStreetMap data is already in the correct format
441
454
  if (land_cover_source == 'OpenStreetMap'):
442
455
  land_cover_grid_converted = land_cover_grid_ori
443
456
  else:
444
457
  land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
445
-
446
458
  # Prepare all input grids for 3D processing
447
459
  # Flip vertically to align with standard geographic orientation (north-up)
448
460
  # Handle missing data appropriately for each grid type
@@ -453,43 +465,33 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
453
465
  dem_grid = np.flipud(dem_grid_ori.copy()) - np.min(dem_grid_ori) # Normalize DEM to start at 0
454
466
  dem_grid = process_grid(building_id_grid, dem_grid) # Process DEM based on building footprints
455
467
  tree_grid = np.flipud(tree_grid_ori.copy())
456
-
457
468
  # Validate that all input grids have consistent dimensions
458
469
  assert building_height_grid.shape == land_cover_grid.shape == dem_grid.shape == tree_grid.shape, "Input grids must have the same shape"
459
-
460
470
  rows, cols = building_height_grid.shape
461
-
462
471
  # Calculate the required height for the 3D voxel grid
463
472
  # Add 1 voxel layer to ensure sufficient vertical space
464
473
  max_height = int(np.ceil(np.max(building_height_grid + dem_grid + tree_grid) / voxel_size))+1
465
-
466
474
  # Initialize the 3D voxel grid with zeros
467
475
  # Dimensions: (rows, columns, height_layers)
468
476
  voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
469
-
470
477
  # Configure tree trunk-to-crown ratio
471
478
  # This determines how much of the tree is trunk vs canopy
472
479
  trunk_height_ratio = kwargs.get("trunk_height_ratio")
473
480
  if trunk_height_ratio is None:
474
481
  trunk_height_ratio = 11.76 / 19.98 # Default ratio based on typical tree proportions
475
-
476
482
  # Process each grid cell to build the 3D voxel representation
477
483
  for i in range(rows):
478
484
  for j in range(cols):
479
485
  # Calculate ground level in voxel units
480
486
  # Add 1 to ensure space for surface features
481
487
  ground_level = int(dem_grid[i, j] / voxel_size + 0.5) + 1
482
-
483
488
  # Extract current cell values
484
489
  tree_height = tree_grid[i, j]
485
490
  land_cover = land_cover_grid[i, j]
486
-
487
491
  # Fill underground voxels with -1 (represents subsurface)
488
492
  voxel_grid[i, j, :ground_level] = -1
489
-
490
493
  # Set the ground surface to the land cover type
491
494
  voxel_grid[i, j, ground_level-1] = land_cover
492
-
493
495
  # Process tree canopy if trees are present
494
496
  if tree_height > 0:
495
497
  # Calculate tree structure: trunk base to crown base to crown top
@@ -497,19 +499,18 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
497
499
  crown_base_height_level = int(crown_base_height / voxel_size + 0.5)
498
500
  crown_top_height = tree_height
499
501
  crown_top_height_level = int(crown_top_height / voxel_size + 0.5)
500
-
502
+
501
503
  # Ensure minimum crown height of 1 voxel
502
504
  # Prevent crown base and top from being at the same level
503
505
  if (crown_top_height_level == crown_base_height_level) and (crown_base_height_level>0):
504
506
  crown_base_height_level -= 1
505
-
507
+
506
508
  # Calculate absolute positions relative to ground level
507
509
  tree_start = ground_level + crown_base_height_level
508
510
  tree_end = ground_level + crown_top_height_level
509
-
511
+
510
512
  # Fill tree crown voxels with -2 (represents vegetation canopy)
511
513
  voxel_grid[i, j, tree_start:tree_end] = -2
512
-
513
514
  # Process buildings - handle multiple height segments per building
514
515
  # Some buildings may have multiple levels or complex height profiles
515
516
  for k in building_min_height_grid[i, j]:
@@ -517,7 +518,6 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
517
518
  building_height = int(k[1] / voxel_size + 0.5) # Upper height of building segment
518
519
  # Fill building voxels with -3 (represents built structures)
519
520
  voxel_grid[i, j, ground_level+building_min_height:ground_level+building_height] = -3
520
-
521
521
  return voxel_grid
522
522
 
523
523
  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):
@@ -935,32 +935,41 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
935
935
  return voxcity_grid, building_height_grid, building_min_height_grid, building_id_grid, canopy_height_grid, land_cover_grid, dem_grid, filtered_buildings
936
936
 
937
937
  def replace_nan_in_nested(arr, replace_value=10.0):
938
- """Replace NaN values in a nested array structure with a specified value.
939
-
940
- Args:
941
- arr: Numpy array containing nested lists and potentially NaN values
942
- replace_value: Value to replace NaN with (default: 10.0)
943
-
944
- Returns:
945
- Numpy array with NaN values replaced
946
938
  """
947
- # Convert array to list for easier manipulation of nested structures
948
- arr = arr.tolist()
939
+ Optimized version that avoids converting to Python lists.
940
+ Works directly with numpy arrays.
941
+ """
942
+ if not isinstance(arr, np.ndarray):
943
+ return arr
944
+
945
+ # Create output array
946
+ result = np.empty_like(arr, dtype=object)
949
947
 
950
- # Iterate through all dimensions of the nested array
951
- for i in range(len(arr)):
952
- for j in range(len(arr[i])):
953
- # Check if the element is a list (building height segments)
954
- if arr[i][j]: # if not empty list
955
- for k in range(len(arr[i][j])):
956
- # For each innermost list (individual height segment)
957
- if isinstance(arr[i][j][k], list):
958
- for l in range(len(arr[i][j][k])):
959
- # Replace NaN values with the specified replacement value
960
- if isinstance(arr[i][j][k][l], float) and np.isnan(arr[i][j][k][l]):
961
- arr[i][j][k][l] = replace_value
948
+ # Vectorized operation for empty cells
949
+ for i in range(arr.shape[0]):
950
+ for j in range(arr.shape[1]):
951
+ cell = arr[i, j]
952
+
953
+ if cell is None or (isinstance(cell, list) and len(cell) == 0):
954
+ result[i, j] = []
955
+ elif isinstance(cell, list):
956
+ # Process list without converting entire array
957
+ new_cell = []
958
+ for segment in cell:
959
+ if isinstance(segment, (list, np.ndarray)):
960
+ # Use numpy operations where possible
961
+ if isinstance(segment, np.ndarray):
962
+ new_segment = np.where(np.isnan(segment), replace_value, segment).tolist()
963
+ else:
964
+ new_segment = [replace_value if (isinstance(v, float) and np.isnan(v)) else v for v in segment]
965
+ new_cell.append(new_segment)
966
+ else:
967
+ new_cell.append(segment)
968
+ result[i, j] = new_cell
969
+ else:
970
+ result[i, j] = cell
962
971
 
963
- return np.array(arr, dtype=object)
972
+ return result
964
973
 
965
974
  def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_min_height_grid,
966
975
  building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,