voxcity 0.3.25__py3-none-any.whl → 0.4.1__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/exporter/obj.py CHANGED
@@ -9,7 +9,7 @@ import numpy as np
9
9
  import os
10
10
  from numba import njit, prange
11
11
  import matplotlib.pyplot as plt
12
- from ..utils.visualization import get_default_voxel_color_map
12
+ from ..utils.visualization import get_voxel_color_map
13
13
 
14
14
  def convert_colormap_indices(original_map):
15
15
  """
@@ -209,7 +209,7 @@ def export_obj(array, output_dir, file_name, voxel_size, voxel_color_map=None):
209
209
  If None, uses default color map.
210
210
  """
211
211
  if voxel_color_map is None:
212
- voxel_color_map = get_default_voxel_color_map()
212
+ voxel_color_map = get_voxel_color_map()
213
213
 
214
214
  # Extract unique voxel values (excluding zero)
215
215
  unique_voxel_values = np.unique(array)
voxcity/generator.py CHANGED
@@ -82,7 +82,8 @@ def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwar
82
82
  print(f"Data source: {source}")
83
83
 
84
84
  # Initialize Earth Engine for accessing satellite data
85
- initialize_earth_engine()
85
+ if source is not "OpenStreetMap":
86
+ initialize_earth_engine()
86
87
 
87
88
  # Create output directory if it doesn't exist
88
89
  os.makedirs(output_dir, exist_ok=True)
@@ -157,7 +158,8 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
157
158
  """
158
159
 
159
160
  # Initialize Earth Engine for accessing satellite data
160
- initialize_earth_engine()
161
+ if source is not "OpenStreetMap":
162
+ initialize_earth_engine()
161
163
 
162
164
  print("Creating Building Height grid\n ")
163
165
  print(f"Data source: {source}")
@@ -320,7 +322,7 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
320
322
  image = get_dem_image(roi_buffered, source)
321
323
 
322
324
  # Save DEM data with appropriate resolution based on source
323
- if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM']:
325
+ if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM', 'Netherlands 0.5m DTM']:
324
326
  save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
325
327
  elif source == 'USGS 3DEP 1m':
326
328
  scale = max(meshsize, 1.25)
@@ -569,7 +571,18 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
569
571
  building_gdf.to_file(save_path, driver='GPKG')
570
572
 
571
573
  # Get canopy height data
572
- canopy_height_grid = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
574
+ if canopy_height_source == "Static":
575
+ # Create canopy height grid with same shape as land cover grid
576
+ canopy_height_grid = np.zeros_like(land_cover_grid, dtype=float)
577
+
578
+ # Set default static height for trees (20 meters is a typical average tree height)
579
+ static_tree_height = kwargs.get("static_tree_height", 10.0)
580
+ tree_mask = (land_cover_grid == 4)
581
+
582
+ # Set static height for tree cells
583
+ canopy_height_grid[tree_mask] = static_tree_height
584
+ else:
585
+ canopy_height_grid = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
573
586
 
574
587
  # Handle DEM - either flat or from source
575
588
  if dem_source == "Flat":
@@ -26,7 +26,8 @@ from ..geoprocessor.polygon import (
26
26
  filter_buildings,
27
27
  extract_building_heights_from_geotiff,
28
28
  extract_building_heights_from_gdf,
29
- complement_building_heights_from_gdf
29
+ complement_building_heights_from_gdf,
30
+ process_building_footprints_by_overlap
30
31
  )
31
32
  from ..utils.lc import (
32
33
  get_class_priority,
@@ -555,6 +556,9 @@ def create_building_height_grid_from_gdf_polygon(
555
556
  filtered_gdf = extract_building_heights_from_gdf(filtered_gdf, filtered_gdf_comp)
556
557
  elif geotiff_path_comp:
557
558
  filtered_gdf = extract_building_heights_from_geotiff(geotiff_path_comp, filtered_gdf)
559
+
560
+ # After filtering and complementing heights, process overlapping buildings
561
+ filtered_gdf = process_building_footprints_by_overlap(filtered_gdf, overlap_threshold=0.5)
558
562
 
559
563
  # --------------------------------------------------------------------------
560
564
  # 2) PREPARE BUILDING POLYGONS & SPATIAL INDEX
@@ -4,7 +4,7 @@ import matplotlib.colors as mcolors
4
4
  import matplotlib.cm as cm
5
5
  import matplotlib.pyplot as plt
6
6
 
7
- def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
7
+ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0, building_id_grid=None):
8
8
  """
9
9
  Create a mesh from voxels preserving sharp edges, scaled by meshsize.
10
10
 
@@ -16,15 +16,21 @@ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
16
16
  The ID of the class to extract.
17
17
  meshsize : float
18
18
  The real-world size of each voxel in meters, for x, y, and z.
19
+ building_id_grid : np.ndarray (2D), optional
20
+ 2D grid of building IDs, shape (X, Y). Used when class_id=-3 (buildings).
19
21
 
20
22
  Returns
21
23
  -------
22
24
  mesh : trimesh.Trimesh or None
23
25
  The resulting mesh for the given class_id (or None if no voxels).
26
+ If class_id=-3, mesh.metadata['building_id'] contains building IDs.
24
27
  """
25
28
  # Find voxels of the current class
26
29
  voxel_coords = np.argwhere(voxel_array == class_id)
27
30
 
31
+ if building_id_grid is not None:
32
+ building_id_grid_flipud = np.flipud(building_id_grid)
33
+
28
34
  if len(voxel_coords) == 0:
29
35
  return None
30
36
 
@@ -57,8 +63,14 @@ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
57
63
  vertices = []
58
64
  faces = []
59
65
  face_normals_list = []
66
+ building_ids = [] # List to store building IDs for each face
60
67
 
61
68
  for x, y, z in voxel_coords:
69
+ # For buildings, get the building ID from the grid
70
+ building_id = None
71
+ if class_id == -3 and building_id_grid is not None:
72
+ building_id = building_id_grid_flipud[x, y]
73
+
62
74
  # Check each face of the current voxel
63
75
  adjacent_coords = [
64
76
  (x, y, z+1), # Front
@@ -95,6 +107,10 @@ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
95
107
  ])
96
108
  # Add face normals for both triangles
97
109
  face_normals_list.extend([face_normals[face_idx], face_normals[face_idx]])
110
+
111
+ # Store building ID for both triangles if this is a building
112
+ if class_id == -3 and building_id_grid is not None:
113
+ building_ids.extend([building_id, building_id])
98
114
 
99
115
  if not vertices:
100
116
  return None
@@ -112,6 +128,10 @@ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
112
128
 
113
129
  # Merge vertices that are at the same position
114
130
  mesh.merge_vertices()
131
+
132
+ # Add building IDs as metadata for buildings
133
+ if class_id == -3 and building_id_grid is not None and building_ids:
134
+ mesh.metadata = {'building_id': np.array(building_ids)}
115
135
 
116
136
  return mesh
117
137
 
@@ -19,6 +19,7 @@ from pyproj import Transformer, CRS
19
19
  import rasterio
20
20
  from rasterio.mask import mask
21
21
  import copy
22
+ from rtree import index
22
23
 
23
24
  from .utils import validate_polygon_coordinates
24
25
 
@@ -794,4 +795,97 @@ def get_buildings_in_drawn_polygon(building_gdf, drawn_polygon_vertices,
794
795
  else:
795
796
  raise ValueError("operation must be 'intersect' or 'within'")
796
797
 
797
- return included_building_ids
798
+ return included_building_ids
799
+
800
+ def process_building_footprints_by_overlap(filtered_gdf, overlap_threshold=0.5):
801
+ """
802
+ Process building footprints to merge overlapping buildings.
803
+
804
+ Args:
805
+ filtered_gdf (geopandas.GeoDataFrame): GeoDataFrame containing building footprints
806
+ overlap_threshold (float): Threshold for overlap ratio (0.0-1.0) to merge buildings
807
+
808
+ Returns:
809
+ geopandas.GeoDataFrame: Processed GeoDataFrame with updated IDs
810
+ """
811
+ # Make a copy to avoid modifying the original
812
+ gdf = filtered_gdf.copy()
813
+
814
+ # Ensure 'id' column exists
815
+ if 'id' not in gdf.columns:
816
+ gdf['id'] = gdf.index
817
+
818
+ # Calculate areas and sort by area (descending)
819
+ gdf['area'] = gdf.geometry.area
820
+ gdf = gdf.sort_values(by='area', ascending=False)
821
+ gdf = gdf.reset_index(drop=True)
822
+
823
+ # Create spatial index for efficient querying
824
+ spatial_idx = index.Index()
825
+ for i, geom in enumerate(gdf.geometry):
826
+ if geom.is_valid:
827
+ spatial_idx.insert(i, geom.bounds)
828
+ else:
829
+ # Fix invalid geometries
830
+ fixed_geom = geom.buffer(0)
831
+ if fixed_geom.is_valid:
832
+ spatial_idx.insert(i, fixed_geom.bounds)
833
+
834
+ # Track ID replacements to avoid repeated processing
835
+ id_mapping = {}
836
+
837
+ # Process each building (skip the largest one)
838
+ for i in range(1, len(gdf)):
839
+ current_poly = gdf.iloc[i].geometry
840
+ current_area = gdf.iloc[i].area
841
+ current_id = gdf.iloc[i]['id']
842
+
843
+ # Skip if already mapped
844
+ if current_id in id_mapping:
845
+ continue
846
+
847
+ # Ensure geometry is valid
848
+ if not current_poly.is_valid:
849
+ current_poly = current_poly.buffer(0)
850
+ if not current_poly.is_valid:
851
+ continue
852
+
853
+ # Find potential overlaps with larger polygons
854
+ potential_overlaps = [j for j in spatial_idx.intersection(current_poly.bounds) if j < i]
855
+
856
+ for j in potential_overlaps:
857
+ larger_poly = gdf.iloc[j].geometry
858
+ larger_id = gdf.iloc[j]['id']
859
+
860
+ # Skip if already processed
861
+ if larger_id in id_mapping:
862
+ larger_id = id_mapping[larger_id]
863
+
864
+ # Ensure geometry is valid
865
+ if not larger_poly.is_valid:
866
+ larger_poly = larger_poly.buffer(0)
867
+ if not larger_poly.is_valid:
868
+ continue
869
+
870
+ try:
871
+ # Calculate overlap
872
+ if current_poly.intersects(larger_poly):
873
+ overlap = current_poly.intersection(larger_poly)
874
+ overlap_ratio = overlap.area / current_area
875
+
876
+ # Replace ID if overlap exceeds threshold
877
+ if overlap_ratio > overlap_threshold:
878
+ id_mapping[current_id] = larger_id
879
+ gdf.at[i, 'id'] = larger_id
880
+ break # Stop at first significant overlap
881
+ except (GEOSException, ValueError) as e:
882
+ # Handle geometry errors gracefully
883
+ continue
884
+
885
+ # Propagate ID changes through the original DataFrame
886
+ for i, row in filtered_gdf.iterrows():
887
+ orig_id = row.get('id')
888
+ if orig_id in id_mapping:
889
+ filtered_gdf.at[i, 'id'] = id_mapping[orig_id]
890
+
891
+ return filtered_gdf