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

@@ -10,18 +10,19 @@ It includes functionality for:
10
10
  import numpy as np
11
11
  import pandas as pd
12
12
  import os
13
- from shapely.geometry import Polygon, box
13
+ from shapely.geometry import Polygon, Point, MultiPolygon, box, mapping
14
14
  from scipy.ndimage import label, generate_binary_structure
15
15
  from pyproj import Geod, Transformer, CRS
16
16
  import rasterio
17
+ from rasterio import features
18
+ from rasterio.transform import from_bounds
17
19
  from affine import Affine
18
- from shapely.geometry import box, Polygon, Point, MultiPolygon
19
- import warnings
20
-
20
+ import geopandas as gpd
21
+ from collections import defaultdict
21
22
  from scipy.interpolate import griddata
22
23
  from shapely.errors import GEOSException
23
- import geopandas as gpd
24
24
  from rtree import index
25
+ import warnings
25
26
 
26
27
  from .utils import (
27
28
  initialize_geod,
@@ -144,42 +145,114 @@ def group_and_label_cells(array):
144
145
 
145
146
  return result
146
147
 
147
- def process_grid(grid_bi, dem_grid):
148
+ def process_grid_optimized(grid_bi, dem_grid):
149
+ """
150
+ Optimized version using numpy's bincount for fast averaging.
151
+ This is much faster than iterating through unique IDs.
148
152
  """
149
- Process a binary grid and DEM grid to create averaged elevation values.
153
+ result = dem_grid.copy()
150
154
 
151
- This function takes a binary grid identifying regions and a corresponding DEM
152
- grid with elevation values. For each non-zero region in the binary grid, it
153
- calculates the mean elevation from the DEM grid and assigns this average to
154
- all cells in that region. The result is normalized by subtracting the minimum value.
155
+ # Only process if there are non-zero values
156
+ if np.any(grid_bi != 0):
157
+ # Handle float IDs by converting to integers
158
+ # First check if we have float values
159
+ if grid_bi.dtype.kind == 'f':
160
+ # Convert to int, handling NaN values
161
+ grid_bi_int = np.nan_to_num(grid_bi, nan=0).astype(np.int64)
162
+ else:
163
+ grid_bi_int = grid_bi.astype(np.int64)
164
+
165
+ # Flatten arrays for processing
166
+ flat_bi = grid_bi_int.ravel()
167
+ flat_dem = dem_grid.ravel()
168
+
169
+ # Filter out zero values
170
+ mask = flat_bi != 0
171
+ ids = flat_bi[mask]
172
+ values = flat_dem[mask]
173
+
174
+ # Check if we have any valid IDs
175
+ if len(ids) > 0:
176
+ # Use bincount to sum values for each ID
177
+ sums = np.bincount(ids, weights=values)
178
+ counts = np.bincount(ids)
179
+
180
+ # Avoid division by zero
181
+ counts[counts == 0] = 1
182
+
183
+ # Calculate means
184
+ means = sums / counts
185
+
186
+ # Apply means back to result
187
+ # Use the integer version for indexing
188
+ mask_nonzero = grid_bi_int != 0
189
+ # Ensure indices are within bounds
190
+ valid_ids = grid_bi_int[mask_nonzero]
191
+ valid_mask = valid_ids < len(means)
192
+
193
+ # Apply only to valid indices
194
+ result_mask = np.zeros_like(mask_nonzero)
195
+ result_mask[mask_nonzero] = valid_mask
196
+
197
+ # Set values
198
+ result[result_mask] = means[grid_bi_int[result_mask]]
155
199
 
156
- Args:
157
- grid_bi (numpy.ndarray): Binary grid indicating regions (0 for background,
158
- non-zero for different regions)
159
- dem_grid (numpy.ndarray): Grid of elevation values corresponding to the
160
- same spatial extent as grid_bi
200
+ return result - np.min(result)
201
+
202
+ def process_grid(grid_bi, dem_grid):
203
+ """
204
+ Safe version that tries optimization first, then falls back to original method.
205
+ """
206
+ try:
207
+ # Try the optimized version first
208
+ return process_grid_optimized(grid_bi, dem_grid)
209
+ except Exception as e:
210
+ print(f"Optimized process_grid failed: {e}, using original method")
211
+ # Fall back to original implementation
212
+ unique_ids = np.unique(grid_bi[grid_bi != 0])
213
+ result = dem_grid.copy()
161
214
 
162
- Returns:
163
- numpy.ndarray: Processed grid with averaged and normalized elevation values.
164
- Same shape as input grids.
215
+ for id_num in unique_ids:
216
+ mask = (grid_bi == id_num)
217
+ avg_value = np.mean(dem_grid[mask])
218
+ result[mask] = avg_value
165
219
 
166
- Example:
167
- >>> binary_grid = np.array([[1, 1, 0], [1, 1, 2], [0, 2, 2]])
168
- >>> elevation = np.array([[100, 110, 90], [105, 115, 120], [95, 125, 130]])
169
- >>> result = process_grid(binary_grid, elevation)
220
+ return result - np.min(result)
170
221
  """
171
- # Get unique non-zero region IDs
172
- unique_ids = np.unique(grid_bi[grid_bi != 0])
173
- result = dem_grid.copy()
222
+ Optimized version that avoids converting to Python lists.
223
+ Works directly with numpy arrays.
224
+ """
225
+ if not isinstance(arr, np.ndarray):
226
+ return arr
174
227
 
175
- # For each region, calculate mean elevation and assign to all cells in region
176
- for id_num in unique_ids:
177
- mask = (grid_bi == id_num)
178
- avg_value = np.mean(dem_grid[mask])
179
- result[mask] = avg_value
228
+ # Create output array
229
+ result = np.empty_like(arr, dtype=object)
180
230
 
181
- # Normalize by subtracting minimum value
182
- return result - np.min(result)
231
+ # Vectorized operation for empty cells
232
+ for i in range(arr.shape[0]):
233
+ for j in range(arr.shape[1]):
234
+ cell = arr[i, j]
235
+
236
+ if cell is None or (isinstance(cell, list) and len(cell) == 0):
237
+ result[i, j] = []
238
+ elif isinstance(cell, list):
239
+ # Process list without converting entire array
240
+ new_cell = []
241
+ for segment in cell:
242
+ if isinstance(segment, (list, np.ndarray)):
243
+ # Use numpy operations where possible
244
+ if isinstance(segment, np.ndarray):
245
+ new_segment = np.where(np.isnan(segment), replace_value, segment).tolist()
246
+ else:
247
+ new_segment = [replace_value if (isinstance(v, float) and np.isnan(v)) else v for v in segment]
248
+ new_cell.append(new_segment)
249
+ else:
250
+ new_cell.append(segment)
251
+ result[i, j] = new_cell
252
+ else:
253
+ result[i, j] = cell
254
+
255
+ return result
183
256
 
184
257
  def calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize):
185
258
  """
@@ -587,6 +660,7 @@ def create_building_height_grid_from_gdf_polygon(
587
660
  gdf,
588
661
  meshsize,
589
662
  rectangle_vertices,
663
+ overlapping_footprint=False,
590
664
  gdf_comp=None,
591
665
  geotiff_path_comp=None,
592
666
  complement_building_footprints=None,
@@ -599,11 +673,13 @@ def create_building_height_grid_from_gdf_polygon(
599
673
  gdf (geopandas.GeoDataFrame): GeoDataFrame containing building information
600
674
  meshsize (float): Size of mesh cells
601
675
  rectangle_vertices (list): List of rectangle vertices defining the boundary
676
+ overlapping_footprint (bool): If True, use precise geometry-based processing for overlaps.
677
+ If False, use faster rasterio-based approach.
602
678
  gdf_comp (geopandas.GeoDataFrame, optional): Complementary GeoDataFrame
603
679
  geotiff_path_comp (str, optional): Path to complementary GeoTIFF file
604
680
  complement_building_footprints (bool, optional): Whether to complement footprints
605
681
  complement_height (float, optional): Height value to use for buildings with height=0
606
-
682
+
607
683
  Returns:
608
684
  tuple: (building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings)
609
685
  - building_height_grid (numpy.ndarray): Grid of building heights
@@ -612,34 +688,25 @@ def create_building_height_grid_from_gdf_polygon(
612
688
  - filtered_buildings (geopandas.GeoDataFrame): The buildings used (filtered_gdf)
613
689
  """
614
690
  # --------------------------------------------------------------------------
615
- # 1) INITIAL SETUP AND DATA FILTERING
691
+ # 1) COMMON INITIAL SETUP AND DATA FILTERING
616
692
  # --------------------------------------------------------------------------
617
693
  geod = initialize_geod()
618
694
  vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
619
-
695
+
620
696
  # Distances for each side
621
697
  dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
622
698
  dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
623
-
699
+
624
700
  # Normalized vectors
625
701
  side_1 = np.array(vertex_1) - np.array(vertex_0)
626
702
  side_2 = np.array(vertex_3) - np.array(vertex_0)
627
703
  u_vec = normalize_to_one_meter(side_1, dist_side_1)
628
704
  v_vec = normalize_to_one_meter(side_2, dist_side_2)
629
-
705
+
630
706
  # Grid parameters
631
707
  origin = np.array(rectangle_vertices[0])
632
708
  grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
633
-
634
- # Initialize output grids
635
- building_height_grid = np.zeros(grid_size)
636
- building_id_grid = np.zeros(grid_size)
637
- # Use a Python list-of-lists or object array for min_height tracking
638
- building_min_height_grid = np.empty(grid_size, dtype=object)
639
- for i in range(grid_size[0]):
640
- for j in range(grid_size[1]):
641
- building_min_height_grid[i, j] = []
642
-
709
+
643
710
  # Filter the input GeoDataFrame by bounding box
644
711
  extent = [
645
712
  min(coord[1] for coord in rectangle_vertices),
@@ -649,12 +716,12 @@ def create_building_height_grid_from_gdf_polygon(
649
716
  ]
650
717
  plotting_box = box(extent[2], extent[0], extent[3], extent[1])
651
718
  filtered_gdf = gdf[gdf.geometry.intersects(plotting_box)].copy()
652
-
719
+
653
720
  # Count buildings with height=0 or NaN
654
721
  zero_height_count = len(filtered_gdf[filtered_gdf['height'] == 0])
655
722
  nan_height_count = len(filtered_gdf[filtered_gdf['height'].isna()])
656
723
  print(f"{zero_height_count+nan_height_count} of the total {len(filtered_gdf)} building footprint from the base data source did not have height data.")
657
-
724
+
658
725
  # Optionally merge heights from complementary sources
659
726
  if gdf_comp is not None:
660
727
  filtered_gdf_comp = gdf_comp[gdf_comp.geometry.intersects(plotting_box)].copy()
@@ -667,11 +734,41 @@ def create_building_height_grid_from_gdf_polygon(
667
734
 
668
735
  # After filtering and complementing heights, process overlapping buildings
669
736
  filtered_gdf = process_building_footprints_by_overlap(filtered_gdf, overlap_threshold=0.5)
737
+
738
+ # --------------------------------------------------------------------------
739
+ # 2) BRANCH BASED ON OVERLAPPING_FOOTPRINT PARAMETER
740
+ # --------------------------------------------------------------------------
741
+
742
+ if overlapping_footprint:
743
+ # Use precise geometry-based approach for better overlap handling
744
+ return _process_with_geometry_intersection(
745
+ filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, complement_height
746
+ )
747
+ else:
748
+ # Use faster rasterio-based approach
749
+ return _process_with_rasterio(
750
+ filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec,
751
+ rectangle_vertices, complement_height
752
+ )
670
753
 
754
+
755
+ def _process_with_geometry_intersection(filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, complement_height):
756
+ """
757
+ Process buildings using precise geometry intersection approach.
758
+ Better for handling overlapping footprints but slower.
759
+ """
760
+ # Initialize output grids
761
+ building_height_grid = np.zeros(grid_size)
762
+ building_id_grid = np.zeros(grid_size)
763
+
764
+ # Use a Python list-of-lists or object array for min_height tracking
765
+ building_min_height_grid = np.empty(grid_size, dtype=object)
766
+ for i in range(grid_size[0]):
767
+ for j in range(grid_size[1]):
768
+ building_min_height_grid[i, j] = []
769
+
671
770
  # --------------------------------------------------------------------------
672
- # 2) PREPARE BUILDING POLYGONS & SPATIAL INDEX
673
- # - fix geometries if needed
674
- # - store bounding boxes
771
+ # PREPARE BUILDING POLYGONS & SPATIAL INDEX
675
772
  # --------------------------------------------------------------------------
676
773
  building_polygons = []
677
774
  for idx_b, row in filtered_gdf.iterrows():
@@ -684,177 +781,129 @@ def create_building_height_grid_from_gdf_polygon(
684
781
 
685
782
  min_height = row.get('min_height', 0)
686
783
  if pd.isna(min_height):
687
- min_height = 0
784
+ min_height = 0
785
+
688
786
  is_inner = row.get('is_inner', False)
689
787
  feature_id = row.get('id', idx_b)
690
-
691
- # Fix invalid geometry (buffer(0) or simplify if needed)
692
- # Doing this once per building avoids repeated overhead per cell.
788
+
789
+ # Fix invalid geometry
693
790
  if not polygon.is_valid:
694
791
  try:
695
- # Attempt simple fix
696
792
  polygon = polygon.buffer(0)
697
793
  if not polygon.is_valid:
698
- # Attempt a small simplify if buffer(0) didn't fix it
699
794
  polygon = polygon.simplify(1e-8)
700
795
  except Exception as e:
701
- # If still invalid, we keep it as-is; intersection attempts
702
- # will be caught in the main loop's try/except
703
796
  pass
704
-
797
+
705
798
  bounding_box = polygon.bounds # (minx, miny, maxx, maxy)
706
- building_polygons.append(
707
- (
708
- polygon,
709
- bounding_box,
710
- height,
711
- min_height,
712
- is_inner,
713
- feature_id
714
- )
715
- )
716
-
799
+ building_polygons.append((
800
+ polygon, bounding_box, height, min_height, is_inner, feature_id
801
+ ))
802
+
717
803
  # Build R-tree index using bounding boxes
718
804
  idx = index.Index()
719
805
  for i_b, (poly, bbox, _, _, _, _) in enumerate(building_polygons):
720
806
  idx.insert(i_b, bbox)
721
-
807
+
722
808
  # --------------------------------------------------------------------------
723
- # 3) MAIN GRID LOOP
809
+ # MAIN GRID LOOP WITH PRECISE INTERSECTION
724
810
  # --------------------------------------------------------------------------
725
811
  INTERSECTION_THRESHOLD = 0.3
726
-
812
+
727
813
  for i in range(grid_size[0]):
728
814
  for j in range(grid_size[1]):
729
815
  # Create the cell polygon once
730
816
  cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
731
817
  if not cell.is_valid:
732
818
  cell = cell.buffer(0)
733
-
734
- cell_area = cell.area # reused for intersection ratio checks
735
-
819
+ cell_area = cell.area
820
+
736
821
  # Find possible intersections from the index
737
822
  potential = list(idx.intersection(cell.bounds))
738
823
  if not potential:
739
824
  continue
740
-
825
+
741
826
  # Sort buildings by height descending
742
- # (height=None => treat as -inf so it sorts last)
743
827
  cell_buildings = []
744
828
  for k in potential:
745
829
  bpoly, bbox, height, minh, inr, fid = building_polygons[k]
746
- # We only sort by height for a stable layering
747
830
  sort_val = height if (height is not None) else -float('inf')
748
831
  cell_buildings.append((k, bpoly, bbox, height, minh, inr, fid, sort_val))
749
-
750
832
  cell_buildings.sort(key=lambda x: x[-1], reverse=True)
751
-
833
+
752
834
  found_intersection = False
753
835
  all_zero_or_nan = True
754
-
755
- for (
756
- k,
757
- polygon,
758
- bbox,
759
- height,
760
- min_height,
761
- is_inner,
762
- feature_id,
763
- _
764
- ) in cell_buildings:
836
+
837
+ for (k, polygon, bbox, height, min_height, is_inner, feature_id, _) in cell_buildings:
765
838
  try:
766
- # Quick bounding-box check to skip expensive geometry intersection
767
- # if bounding boxes cannot possibly meet the threshold
768
- # --------------------------------------------------------
839
+ # Quick bounding-box check
769
840
  minx_p, miny_p, maxx_p, maxy_p = bbox
770
841
  minx_c, miny_c, maxx_c, maxy_c = cell.bounds
771
-
842
+
772
843
  # Overlap bounding box
773
844
  overlap_minx = max(minx_p, minx_c)
774
845
  overlap_miny = max(miny_p, miny_c)
775
846
  overlap_maxx = min(maxx_p, maxx_c)
776
847
  overlap_maxy = min(maxy_p, maxy_c)
777
-
848
+
778
849
  if (overlap_maxx <= overlap_minx) or (overlap_maxy <= overlap_miny):
779
- # No bounding-box overlap => skip
780
850
  continue
781
-
851
+
782
852
  # Area of bounding-box intersection
783
853
  bbox_intersect_area = (overlap_maxx - overlap_minx) * (overlap_maxy - overlap_miny)
784
854
  if bbox_intersect_area < INTERSECTION_THRESHOLD * cell_area:
785
- # Even if the polygons overlap, they can't exceed threshold
786
855
  continue
787
- # --------------------------------------------------------
788
-
856
+
789
857
  # Ensure valid geometry
790
858
  if not polygon.is_valid:
791
859
  polygon = polygon.buffer(0)
792
- # If still invalid, let intersection attempt fail
793
-
860
+
794
861
  if cell.intersects(polygon):
795
862
  intersection = cell.intersection(polygon)
796
863
  inter_area = intersection.area
797
-
864
+
798
865
  # If the fraction of cell covered > threshold
799
866
  if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
800
867
  found_intersection = True
801
-
868
+
802
869
  # If not an inner courtyard
803
870
  if not is_inner:
804
871
  building_min_height_grid[i, j].append([min_height, height])
805
872
  building_id_grid[i, j] = feature_id
806
-
873
+
807
874
  # Update building height if valid
808
- if (
809
- height is not None
810
- and not np.isnan(height)
811
- and height > 0
812
- ):
875
+ if (height is not None and not np.isnan(height) and height > 0):
813
876
  all_zero_or_nan = False
814
877
  current_height = building_height_grid[i, j]
815
-
878
+
816
879
  # Replace if we had 0, nan, or smaller height
817
- if (
818
- current_height == 0
819
- or np.isnan(current_height)
820
- or current_height < height
821
- ):
880
+ if (current_height == 0 or np.isnan(current_height) or current_height < height):
822
881
  building_height_grid[i, j] = height
823
-
824
882
  else:
825
883
  # Inner courtyards => override with 0
826
884
  building_min_height_grid[i, j] = [[0, 0]]
827
885
  building_height_grid[i, j] = 0
828
886
  found_intersection = True
829
887
  all_zero_or_nan = False
830
- # Because it's an inner courtyard, we can break
831
888
  break
832
-
889
+
833
890
  except (GEOSException, ValueError) as e:
834
- # Attempt to fix in a fallback
891
+ # Attempt fallback fix
835
892
  try:
836
893
  simplified_polygon = polygon.simplify(1e-8)
837
894
  if simplified_polygon.is_valid:
838
895
  intersection = cell.intersection(simplified_polygon)
839
896
  inter_area = intersection.area
840
-
841
897
  if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
842
898
  found_intersection = True
843
899
  if not is_inner:
844
900
  building_min_height_grid[i, j].append([min_height, height])
845
901
  building_id_grid[i, j] = feature_id
846
-
847
- if (
848
- height is not None
849
- and not np.isnan(height)
850
- and height > 0
851
- ):
902
+ if (height is not None and not np.isnan(height) and height > 0):
852
903
  all_zero_or_nan = False
853
- if (
854
- building_height_grid[i, j] == 0
855
- or np.isnan(building_height_grid[i, j])
856
- or building_height_grid[i, j] < height
857
- ):
904
+ if (building_height_grid[i, j] == 0 or
905
+ np.isnan(building_height_grid[i, j]) or
906
+ building_height_grid[i, j] < height):
858
907
  building_height_grid[i, j] = height
859
908
  else:
860
909
  building_min_height_grid[i, j] = [[0, 0]]
@@ -863,14 +912,131 @@ def create_building_height_grid_from_gdf_polygon(
863
912
  all_zero_or_nan = False
864
913
  break
865
914
  except Exception as fix_error:
866
- # Log and skip
867
915
  print(f"Failed to process cell ({i}, {j}) - Building {k}: {str(fix_error)}")
868
916
  continue
869
-
917
+
870
918
  # If we found intersecting buildings but all were zero/NaN, mark as NaN
871
919
  if found_intersection and all_zero_or_nan:
872
920
  building_height_grid[i, j] = np.nan
921
+
922
+ return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
873
923
 
924
+
925
+ def _process_with_rasterio(filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, rectangle_vertices, complement_height):
926
+ """
927
+ Process buildings using fast rasterio-based approach.
928
+ Faster but less precise for overlapping footprints.
929
+ """
930
+ # Set up transform for rasterio
931
+ min_x, min_y = origin[0], origin[1]
932
+ max_corner = origin + grid_size[0] * adjusted_meshsize[0] * u_vec + grid_size[1] * adjusted_meshsize[1] * v_vec
933
+ max_x, max_y = max_corner[0], max_corner[1]
934
+
935
+ transform = from_bounds(min_x, min_y, max_x, max_y, grid_size[0], grid_size[1])
936
+
937
+ # Process buildings data
938
+ filtered_gdf = filtered_gdf.copy()
939
+ if complement_height is not None:
940
+ mask = (filtered_gdf['height'] == 0) | (filtered_gdf['height'].isna())
941
+ filtered_gdf.loc[mask, 'height'] = complement_height
942
+
943
+ # Add missing columns with defaults
944
+ filtered_gdf['min_height'] = 0
945
+
946
+ if 'is_inner' not in filtered_gdf.columns:
947
+ filtered_gdf['is_inner'] = False
948
+
949
+ if 'id' not in filtered_gdf.columns:
950
+ filtered_gdf['id'] = range(len(filtered_gdf))
951
+
952
+ # Sort by height for proper layering
953
+ regular_buildings = filtered_gdf[~filtered_gdf['is_inner']].copy()
954
+ regular_buildings = regular_buildings.sort_values('height', ascending=True, na_position='first')
955
+
956
+ # Initialize grids
957
+ building_height_grid = np.zeros(grid_size, dtype=np.float64)
958
+ building_id_grid = np.zeros(grid_size, dtype=np.float64)
959
+
960
+ # Vectorized rasterization
961
+ if len(regular_buildings) > 0:
962
+ valid_buildings = regular_buildings[regular_buildings.geometry.is_valid].copy()
963
+
964
+ if len(valid_buildings) > 0:
965
+ # Height grid
966
+ height_shapes = [(mapping(geom), height) for geom, height in
967
+ zip(valid_buildings.geometry, valid_buildings['height'])
968
+ if pd.notna(height) and height > 0]
969
+
970
+ if height_shapes:
971
+ building_height_grid = features.rasterize(
972
+ height_shapes,
973
+ out_shape=grid_size,
974
+ transform=transform,
975
+ fill=0,
976
+ dtype=np.float64
977
+ )
978
+
979
+ # ID grid
980
+ id_shapes = [(mapping(geom), id_val) for geom, id_val in
981
+ zip(valid_buildings.geometry, valid_buildings['id'])]
982
+
983
+ if id_shapes:
984
+ building_id_grid = features.rasterize(
985
+ id_shapes,
986
+ out_shape=grid_size,
987
+ transform=transform,
988
+ fill=0,
989
+ dtype=np.float64
990
+ )
991
+
992
+ # Handle inner courtyards
993
+ inner_buildings = filtered_gdf[filtered_gdf['is_inner']].copy()
994
+ if len(inner_buildings) > 0:
995
+ inner_shapes = [(mapping(geom), 1) for geom in inner_buildings.geometry if geom.is_valid]
996
+ if inner_shapes:
997
+ inner_mask = features.rasterize(
998
+ inner_shapes,
999
+ out_shape=grid_size,
1000
+ transform=transform,
1001
+ fill=0,
1002
+ dtype=np.uint8
1003
+ )
1004
+ building_height_grid[inner_mask > 0] = 0
1005
+ building_id_grid[inner_mask > 0] = 0
1006
+
1007
+ # Simplified min_height grid
1008
+ building_min_height_grid = np.empty(grid_size, dtype=object)
1009
+ min_heights = np.zeros(grid_size, dtype=np.float64)
1010
+
1011
+ if len(regular_buildings) > 0:
1012
+ valid_buildings = regular_buildings[regular_buildings.geometry.is_valid].copy()
1013
+ if len(valid_buildings) > 0:
1014
+ min_height_shapes = [(mapping(geom), min_h) for geom, min_h in
1015
+ zip(valid_buildings.geometry, valid_buildings['min_height'])
1016
+ if pd.notna(min_h)]
1017
+
1018
+ if min_height_shapes:
1019
+ min_heights = features.rasterize(
1020
+ min_height_shapes,
1021
+ out_shape=grid_size,
1022
+ transform=transform,
1023
+ fill=0,
1024
+ dtype=np.float64
1025
+ )
1026
+
1027
+ # Convert to list format (simplified)
1028
+ for i in range(grid_size[0]):
1029
+ for j in range(grid_size[1]):
1030
+ if building_height_grid[i, j] > 0:
1031
+ building_min_height_grid[i, j] = [[min_heights[i, j], building_height_grid[i, j]]]
1032
+ else:
1033
+ building_min_height_grid[i, j] = []
1034
+
1035
+ # Fix north-south orientation by flipping grids
1036
+ building_height_grid = np.flipud(building_height_grid)
1037
+ building_id_grid = np.flipud(building_id_grid)
1038
+ building_min_height_grid = np.flipud(building_min_height_grid)
1039
+
874
1040
  return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
875
1041
 
876
1042
  def create_building_height_grid_from_open_building_temporal_polygon(meshsize, rectangle_vertices, output_dir):