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 +58 -49
- voxcity/geoprocessor/grid.py +305 -139
- voxcity/geoprocessor/mesh.py +790 -758
- voxcity/geoprocessor/polygon.py +1343 -1343
- voxcity/simulator/solar.py +2252 -1820
- voxcity/simulator/view.py +2239 -1336
- voxcity/utils/lc.py +21 -100
- voxcity/utils/visualization.py +91 -45
- {voxcity-0.5.30.dist-info → voxcity-0.6.0.dist-info}/METADATA +1 -1
- {voxcity-0.5.30.dist-info → voxcity-0.6.0.dist-info}/RECORD +14 -14
- {voxcity-0.5.30.dist-info → voxcity-0.6.0.dist-info}/WHEEL +0 -0
- {voxcity-0.5.30.dist-info → voxcity-0.6.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.30.dist-info → voxcity-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.30.dist-info → voxcity-0.6.0.dist-info}/top_level.txt +0 -0
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,
|
|
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
|
-
|
|
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
|
-
|
|
948
|
-
|
|
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
|
-
#
|
|
951
|
-
for i in range(
|
|
952
|
-
for j in range(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
|
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,
|