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/geoprocessor/grid.py
CHANGED
|
@@ -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
|
-
|
|
19
|
-
import
|
|
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
|
|
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
|
-
|
|
153
|
+
result = dem_grid.copy()
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
#
|
|
176
|
-
|
|
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
|
-
#
|
|
182
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
709
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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]
|
|
855
|
-
|
|
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):
|