voxcity 0.6.26__py3-none-any.whl → 0.7.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.
- voxcity/__init__.py +14 -8
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +13 -5
- voxcity/exporter/cityles.py +633 -538
- voxcity/exporter/envimet.py +728 -708
- voxcity/exporter/magicavoxel.py +334 -297
- voxcity/exporter/netcdf.py +238 -211
- voxcity/exporter/obj.py +1481 -1406
- voxcity/generator/__init__.py +44 -0
- voxcity/generator/api.py +675 -0
- voxcity/generator/grids.py +379 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/voxelizer.py +380 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +62 -12
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +806 -790
- voxcity/geoprocessor/network.py +708 -679
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +428 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +156 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +18 -14
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +43 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/temporal.py +434 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +928 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/METADATA +107 -34
- voxcity-0.7.0.dist-info/RECORD +77 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import geopandas as gpd
|
|
6
|
+
from shapely.geometry import Polygon, Point
|
|
7
|
+
from pyproj import CRS, Transformer, Geod
|
|
8
|
+
|
|
9
|
+
from ..utils import initialize_geod, calculate_distance, normalize_to_one_meter
|
|
10
|
+
from .core import calculate_grid_size
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_vegetation_height_grid_from_gdf_polygon(veg_gdf, mesh_size, polygon):
|
|
14
|
+
"""
|
|
15
|
+
Create a vegetation height grid from a GeoDataFrame of vegetation polygons/objects.
|
|
16
|
+
Cells with vegetation take the max height of intersecting features.
|
|
17
|
+
Returns north-up grid (row 0 = north).
|
|
18
|
+
"""
|
|
19
|
+
if veg_gdf.crs is None:
|
|
20
|
+
warnings.warn("veg_gdf has no CRS. Assuming EPSG:4326. ")
|
|
21
|
+
veg_gdf = veg_gdf.set_crs(epsg=4326)
|
|
22
|
+
else:
|
|
23
|
+
if veg_gdf.crs.to_epsg() != 4326:
|
|
24
|
+
veg_gdf = veg_gdf.to_crs(epsg=4326)
|
|
25
|
+
|
|
26
|
+
if 'height' not in veg_gdf.columns:
|
|
27
|
+
raise ValueError("Vegetation GeoDataFrame must have a 'height' column.")
|
|
28
|
+
|
|
29
|
+
if isinstance(polygon, list):
|
|
30
|
+
poly = Polygon(polygon)
|
|
31
|
+
elif isinstance(polygon, Polygon):
|
|
32
|
+
poly = polygon
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError("polygon must be a list of (lon, lat) or a shapely Polygon.")
|
|
35
|
+
|
|
36
|
+
left, bottom, right, top = poly.bounds
|
|
37
|
+
geod = Geod(ellps="WGS84")
|
|
38
|
+
_, _, width_m = geod.inv(left, bottom, right, bottom)
|
|
39
|
+
_, _, height_m = geod.inv(left, bottom, left, top)
|
|
40
|
+
|
|
41
|
+
num_cells_x = int(width_m / mesh_size + 0.5)
|
|
42
|
+
num_cells_y = int(height_m / mesh_size + 0.5)
|
|
43
|
+
|
|
44
|
+
if num_cells_x < 1 or num_cells_y < 1:
|
|
45
|
+
warnings.warn("Polygon bounding box is smaller than mesh_size; returning empty array.")
|
|
46
|
+
return np.array([])
|
|
47
|
+
|
|
48
|
+
xs = np.linspace(left, right, num_cells_x)
|
|
49
|
+
ys = np.linspace(top, bottom, num_cells_y)
|
|
50
|
+
X, Y = np.meshgrid(xs, ys)
|
|
51
|
+
xs_flat = X.ravel()
|
|
52
|
+
ys_flat = Y.ravel()
|
|
53
|
+
|
|
54
|
+
grid_points = gpd.GeoDataFrame(
|
|
55
|
+
geometry=[Point(lon, lat) for lon, lat in zip(xs_flat, ys_flat)],
|
|
56
|
+
crs="EPSG:4326"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
joined = gpd.sjoin(
|
|
60
|
+
grid_points,
|
|
61
|
+
veg_gdf[['height', 'geometry']],
|
|
62
|
+
how='left',
|
|
63
|
+
predicate='intersects'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
joined_agg = (
|
|
67
|
+
joined.groupby(joined.index).agg({'height': 'max'})
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
veg_grid = np.zeros((num_cells_y, num_cells_x), dtype=float)
|
|
71
|
+
for i, row_data in joined_agg.iterrows():
|
|
72
|
+
if not np.isnan(row_data['height']):
|
|
73
|
+
row_idx = i // num_cells_x
|
|
74
|
+
col_idx = i % num_cells_x
|
|
75
|
+
veg_grid[row_idx, col_idx] = row_data['height']
|
|
76
|
+
|
|
77
|
+
return np.flipud(veg_grid)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_dem_grid_from_gdf_polygon(terrain_gdf, mesh_size, polygon):
|
|
81
|
+
"""
|
|
82
|
+
Create a height grid from a terrain GeoDataFrame using nearest-neighbor sampling.
|
|
83
|
+
Returns north-up grid.
|
|
84
|
+
"""
|
|
85
|
+
if terrain_gdf.crs is None:
|
|
86
|
+
warnings.warn("terrain_gdf has no CRS. Assuming EPSG:4326. ")
|
|
87
|
+
terrain_gdf = terrain_gdf.set_crs(epsg=4326)
|
|
88
|
+
else:
|
|
89
|
+
if terrain_gdf.crs.to_epsg() != 4326:
|
|
90
|
+
terrain_gdf = terrain_gdf.to_crs(epsg=4326)
|
|
91
|
+
|
|
92
|
+
if isinstance(polygon, list):
|
|
93
|
+
poly = Polygon(polygon)
|
|
94
|
+
elif isinstance(polygon, Polygon):
|
|
95
|
+
poly = polygon
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError("`polygon` must be a list of (lon, lat) or a shapely Polygon.")
|
|
98
|
+
|
|
99
|
+
left, bottom, right, top = poly.bounds
|
|
100
|
+
geod = Geod(ellps="WGS84")
|
|
101
|
+
_, _, width_m = geod.inv(left, bottom, right, bottom)
|
|
102
|
+
_, _, height_m = geod.inv(left, bottom, left, top)
|
|
103
|
+
num_cells_x = int(width_m / mesh_size + 0.5)
|
|
104
|
+
num_cells_y = int(height_m / mesh_size + 0.5)
|
|
105
|
+
if num_cells_x < 1 or num_cells_y < 1:
|
|
106
|
+
warnings.warn("Polygon bounding box is smaller than mesh_size; returning empty array.")
|
|
107
|
+
return np.array([])
|
|
108
|
+
|
|
109
|
+
xs = np.linspace(left, right, num_cells_x)
|
|
110
|
+
ys = np.linspace(top, bottom, num_cells_y)
|
|
111
|
+
X, Y = np.meshgrid(xs, ys)
|
|
112
|
+
xs_flat = X.ravel()
|
|
113
|
+
ys_flat = Y.ravel()
|
|
114
|
+
|
|
115
|
+
grid_points = gpd.GeoDataFrame(
|
|
116
|
+
geometry=[Point(lon, lat) for lon, lat in zip(xs_flat, ys_flat)],
|
|
117
|
+
crs="EPSG:4326"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if 'elevation' not in terrain_gdf.columns:
|
|
121
|
+
raise ValueError("terrain_gdf must have an 'elevation' column.")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
centroid = poly.centroid
|
|
125
|
+
lon_c, lat_c = float(centroid.x), float(centroid.y)
|
|
126
|
+
zone = int((lon_c + 180.0) // 6) + 1
|
|
127
|
+
epsg_proj = 32600 + zone if lat_c >= 0 else 32700 + zone
|
|
128
|
+
terrain_proj = terrain_gdf.to_crs(epsg=epsg_proj)
|
|
129
|
+
grid_points_proj = grid_points.to_crs(epsg=epsg_proj)
|
|
130
|
+
|
|
131
|
+
grid_points_elev = gpd.sjoin_nearest(
|
|
132
|
+
grid_points_proj,
|
|
133
|
+
terrain_proj[['elevation', 'geometry']],
|
|
134
|
+
how="left",
|
|
135
|
+
distance_col="dist_to_terrain"
|
|
136
|
+
)
|
|
137
|
+
grid_points_elev.index = grid_points.index
|
|
138
|
+
except Exception:
|
|
139
|
+
grid_points_elev = gpd.sjoin_nearest(
|
|
140
|
+
grid_points,
|
|
141
|
+
terrain_gdf[['elevation', 'geometry']],
|
|
142
|
+
how="left",
|
|
143
|
+
distance_col="dist_to_terrain"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
dem_grid = np.full((num_cells_y, num_cells_x), np.nan, dtype=float)
|
|
147
|
+
for i, elevation_val in zip(grid_points_elev.index, grid_points_elev['elevation']):
|
|
148
|
+
row = i // num_cells_x
|
|
149
|
+
col = i % num_cells_x
|
|
150
|
+
dem_grid[row, col] = elevation_val
|
|
151
|
+
return np.flipud(dem_grid)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def create_canopy_grids_from_tree_gdf(tree_gdf, meshsize, rectangle_vertices):
|
|
155
|
+
"""
|
|
156
|
+
Create canopy top and bottom height grids from a tree GeoDataFrame.
|
|
157
|
+
Returns (canopy_height_grid, canopy_bottom_height_grid).
|
|
158
|
+
"""
|
|
159
|
+
if tree_gdf is None or len(tree_gdf) == 0:
|
|
160
|
+
return np.array([]), np.array([])
|
|
161
|
+
|
|
162
|
+
required_cols = ['top_height', 'bottom_height', 'crown_diameter', 'geometry']
|
|
163
|
+
for col in required_cols:
|
|
164
|
+
if col not in tree_gdf.columns:
|
|
165
|
+
raise ValueError(f"tree_gdf must contain '{col}' column.")
|
|
166
|
+
|
|
167
|
+
if tree_gdf.crs is None:
|
|
168
|
+
warnings.warn("tree_gdf has no CRS. Assuming EPSG:4326.")
|
|
169
|
+
tree_gdf = tree_gdf.set_crs(epsg=4326)
|
|
170
|
+
elif tree_gdf.crs.to_epsg() != 4326:
|
|
171
|
+
tree_gdf = tree_gdf.to_crs(epsg=4326)
|
|
172
|
+
|
|
173
|
+
geod = initialize_geod()
|
|
174
|
+
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
175
|
+
|
|
176
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
177
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
178
|
+
|
|
179
|
+
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
180
|
+
side_2 = np.array(vertex_3) - np.array(vertex_0)
|
|
181
|
+
u_vec = normalize_to_one_meter(side_1, dist_side_1)
|
|
182
|
+
v_vec = normalize_to_one_meter(side_2, dist_side_2)
|
|
183
|
+
|
|
184
|
+
origin = np.array(rectangle_vertices[0])
|
|
185
|
+
grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
|
|
186
|
+
nx, ny = grid_size[0], grid_size[1]
|
|
187
|
+
|
|
188
|
+
i_centers_m = (np.arange(nx) + 0.5) * adjusted_meshsize[0]
|
|
189
|
+
j_centers_m = (np.arange(ny) + 0.5) * adjusted_meshsize[1]
|
|
190
|
+
|
|
191
|
+
canopy_top = np.zeros((nx, ny), dtype=float)
|
|
192
|
+
canopy_bottom = np.zeros((nx, ny), dtype=float)
|
|
193
|
+
|
|
194
|
+
transform_mat = np.column_stack((u_vec, v_vec))
|
|
195
|
+
try:
|
|
196
|
+
transform_inv = np.linalg.inv(transform_mat)
|
|
197
|
+
except np.linalg.LinAlgError:
|
|
198
|
+
transform_inv = np.linalg.pinv(transform_mat)
|
|
199
|
+
|
|
200
|
+
for _, row in tree_gdf.iterrows():
|
|
201
|
+
geom = row['geometry']
|
|
202
|
+
if geom is None or not hasattr(geom, 'x'):
|
|
203
|
+
continue
|
|
204
|
+
top_h = float(row.get('top_height', 0.0) or 0.0)
|
|
205
|
+
bot_h = float(row.get('bottom_height', 0.0) or 0.0)
|
|
206
|
+
dia = float(row.get('crown_diameter', 0.0) or 0.0)
|
|
207
|
+
if dia <= 0 or top_h <= 0:
|
|
208
|
+
continue
|
|
209
|
+
if bot_h < 0:
|
|
210
|
+
bot_h = 0.0
|
|
211
|
+
if bot_h > top_h:
|
|
212
|
+
top_h, bot_h = bot_h, top_h
|
|
213
|
+
R = dia / 2.0
|
|
214
|
+
a = max((top_h - bot_h) / 2.0, 0.0)
|
|
215
|
+
z0 = (top_h + bot_h) / 2.0
|
|
216
|
+
tree_lon = float(geom.x)
|
|
217
|
+
tree_lat = float(geom.y)
|
|
218
|
+
delta = np.array([tree_lon, tree_lat]) - origin
|
|
219
|
+
alpha_beta = transform_inv @ delta
|
|
220
|
+
alpha_m = alpha_beta[0]
|
|
221
|
+
beta_m = alpha_beta[1]
|
|
222
|
+
du_cells = int(R / adjusted_meshsize[0] + 2)
|
|
223
|
+
dv_cells = int(R / adjusted_meshsize[1] + 2)
|
|
224
|
+
i_center_idx = int(alpha_m / adjusted_meshsize[0])
|
|
225
|
+
j_center_idx = int(beta_m / adjusted_meshsize[1])
|
|
226
|
+
i_min = max(0, i_center_idx - du_cells)
|
|
227
|
+
i_max = min(nx - 1, i_center_idx + du_cells)
|
|
228
|
+
j_min = max(0, j_center_idx - dv_cells)
|
|
229
|
+
j_max = min(ny - 1, j_center_idx + dv_cells)
|
|
230
|
+
if i_min > i_max or j_min > j_max:
|
|
231
|
+
continue
|
|
232
|
+
ic = i_centers_m[i_min:i_max + 1][:, None]
|
|
233
|
+
jc = j_centers_m[j_min:j_max + 1][None, :]
|
|
234
|
+
di = ic - alpha_m
|
|
235
|
+
dj = jc - beta_m
|
|
236
|
+
r = np.sqrt(di * di + dj * dj)
|
|
237
|
+
within = r <= R
|
|
238
|
+
if not np.any(within):
|
|
239
|
+
continue
|
|
240
|
+
ratio = np.clip(r / max(R, 1e-9), 0.0, 1.0)
|
|
241
|
+
factor = np.sqrt(1.0 - ratio * ratio)
|
|
242
|
+
local_top = z0 + a * factor
|
|
243
|
+
local_bot = z0 - a * factor
|
|
244
|
+
local_top_masked = np.where(within, local_top, 0.0)
|
|
245
|
+
local_bot_masked = np.where(within, local_bot, 0.0)
|
|
246
|
+
canopy_top[i_min:i_max + 1, j_min:j_max + 1] = np.maximum(
|
|
247
|
+
canopy_top[i_min:i_max + 1, j_min:j_max + 1], local_top_masked
|
|
248
|
+
)
|
|
249
|
+
canopy_bottom[i_min:i_max + 1, j_min:j_max + 1] = np.maximum(
|
|
250
|
+
canopy_bottom[i_min:i_max + 1, j_min:j_max + 1], local_bot_masked
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
canopy_bottom = np.minimum(canopy_bottom, canopy_top)
|
|
254
|
+
return canopy_top, canopy_bottom
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Tuple, Dict, Any
|
|
3
|
+
from shapely.geometry import Polygon
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def apply_operation(arr: np.ndarray, meshsize: float) -> np.ndarray:
|
|
7
|
+
"""
|
|
8
|
+
Applies a sequence of operations to an array based on a mesh size to normalize and discretize values.
|
|
9
|
+
|
|
10
|
+
1) Divide by meshsize, 2) +0.5, 3) floor, 4) rescale by meshsize
|
|
11
|
+
"""
|
|
12
|
+
step1 = arr / meshsize
|
|
13
|
+
step2 = step1 + 0.5
|
|
14
|
+
step3 = np.floor(step2)
|
|
15
|
+
return step3 * meshsize
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def translate_array(input_array: np.ndarray, translation_dict: Dict[Any, Any]) -> np.ndarray:
|
|
19
|
+
"""
|
|
20
|
+
Translate values in an array using a dictionary mapping (vectorized).
|
|
21
|
+
|
|
22
|
+
Any value not found in the mapping is replaced with an empty string.
|
|
23
|
+
Returns an object-dtype ndarray preserving the input shape.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(input_array, np.ndarray):
|
|
26
|
+
input_array = np.asarray(input_array)
|
|
27
|
+
|
|
28
|
+
getter = np.vectorize(lambda v: translation_dict.get(v, ''), otypes=[object])
|
|
29
|
+
return getter(input_array)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def group_and_label_cells(array: np.ndarray) -> np.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
Convert non-zero numbers in a 2D numpy array to sequential IDs starting from 1.
|
|
35
|
+
"""
|
|
36
|
+
result = array.copy()
|
|
37
|
+
unique_values = sorted(set(array.flatten()) - {0})
|
|
38
|
+
value_to_id = {value: idx + 1 for idx, value in enumerate(unique_values)}
|
|
39
|
+
for value in unique_values:
|
|
40
|
+
result[array == value] = value_to_id[value]
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def process_grid_optimized(grid_bi: np.ndarray, dem_grid: np.ndarray) -> np.ndarray:
|
|
45
|
+
"""
|
|
46
|
+
Optimized version that computes per-building averages without allocating huge arrays
|
|
47
|
+
when building IDs are large and sparse.
|
|
48
|
+
"""
|
|
49
|
+
result = dem_grid.copy()
|
|
50
|
+
if np.any(grid_bi != 0):
|
|
51
|
+
if grid_bi.dtype.kind == 'f':
|
|
52
|
+
grid_bi_int = np.nan_to_num(grid_bi, nan=0).astype(np.int64)
|
|
53
|
+
else:
|
|
54
|
+
grid_bi_int = grid_bi.astype(np.int64)
|
|
55
|
+
|
|
56
|
+
flat_ids = grid_bi_int.ravel()
|
|
57
|
+
flat_dem = dem_grid.ravel()
|
|
58
|
+
nz_mask = flat_ids != 0
|
|
59
|
+
if np.any(nz_mask):
|
|
60
|
+
ids_nz = flat_ids[nz_mask]
|
|
61
|
+
vals_nz = flat_dem[nz_mask]
|
|
62
|
+
unique_ids, inverse_idx = np.unique(ids_nz, return_inverse=True)
|
|
63
|
+
sums = np.bincount(inverse_idx, weights=vals_nz)
|
|
64
|
+
counts = np.bincount(inverse_idx)
|
|
65
|
+
counts[counts == 0] = 1
|
|
66
|
+
means = sums / counts
|
|
67
|
+
result.ravel()[nz_mask] = means[inverse_idx]
|
|
68
|
+
return result - np.min(result)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def process_grid(grid_bi: np.ndarray, dem_grid: np.ndarray) -> np.ndarray:
|
|
72
|
+
"""
|
|
73
|
+
Safe version that tries optimization first, then falls back to original method.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
return process_grid_optimized(grid_bi, dem_grid)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Optimized process_grid failed: {e}, using original method")
|
|
79
|
+
unique_ids = np.unique(grid_bi[grid_bi != 0])
|
|
80
|
+
result = dem_grid.copy()
|
|
81
|
+
for id_num in unique_ids:
|
|
82
|
+
mask = (grid_bi == id_num)
|
|
83
|
+
avg_value = np.mean(dem_grid[mask])
|
|
84
|
+
result[mask] = avg_value
|
|
85
|
+
return result - np.min(result)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def calculate_grid_size(
|
|
89
|
+
side_1: np.ndarray,
|
|
90
|
+
side_2: np.ndarray,
|
|
91
|
+
u_vec: np.ndarray,
|
|
92
|
+
v_vec: np.ndarray,
|
|
93
|
+
meshsize: float
|
|
94
|
+
) -> Tuple[Tuple[int, int], Tuple[float, float]]:
|
|
95
|
+
"""
|
|
96
|
+
Calculate grid size and adjusted mesh size based on input parameters.
|
|
97
|
+
Returns ((nx, ny), (dx, dy))
|
|
98
|
+
"""
|
|
99
|
+
dist_side_1_m = np.linalg.norm(side_1) / (np.linalg.norm(u_vec) + 1e-12)
|
|
100
|
+
dist_side_2_m = np.linalg.norm(side_2) / (np.linalg.norm(v_vec) + 1e-12)
|
|
101
|
+
|
|
102
|
+
grid_size_0 = max(1, int(dist_side_1_m / meshsize + 0.5))
|
|
103
|
+
grid_size_1 = max(1, int(dist_side_2_m / meshsize + 0.5))
|
|
104
|
+
|
|
105
|
+
adjusted_mesh_size_0 = dist_side_1_m / grid_size_0
|
|
106
|
+
adjusted_mesh_size_1 = dist_side_2_m / grid_size_1
|
|
107
|
+
|
|
108
|
+
return (grid_size_0, grid_size_1), (adjusted_mesh_size_0, adjusted_mesh_size_1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_coordinate_mesh(
|
|
112
|
+
origin: np.ndarray,
|
|
113
|
+
grid_size: Tuple[int, int],
|
|
114
|
+
adjusted_meshsize: Tuple[float, float],
|
|
115
|
+
u_vec: np.ndarray,
|
|
116
|
+
v_vec: np.ndarray
|
|
117
|
+
) -> np.ndarray:
|
|
118
|
+
"""
|
|
119
|
+
Create a coordinate mesh based on input parameters.
|
|
120
|
+
Returns array of shape (coord_dim, ny, nx)
|
|
121
|
+
"""
|
|
122
|
+
x = np.linspace(0, grid_size[0], grid_size[0])
|
|
123
|
+
y = np.linspace(0, grid_size[1], grid_size[1])
|
|
124
|
+
xx, yy = np.meshgrid(x, y)
|
|
125
|
+
cell_coords = origin[:, np.newaxis, np.newaxis] + \
|
|
126
|
+
xx[np.newaxis, :, :] * adjusted_meshsize[0] * u_vec[:, np.newaxis, np.newaxis] + \
|
|
127
|
+
yy[np.newaxis, :, :] * adjusted_meshsize[1] * v_vec[:, np.newaxis, np.newaxis]
|
|
128
|
+
return cell_coords
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_cell_polygon(
|
|
132
|
+
origin: np.ndarray,
|
|
133
|
+
i: int,
|
|
134
|
+
j: int,
|
|
135
|
+
adjusted_meshsize: Tuple[float, float],
|
|
136
|
+
u_vec: np.ndarray,
|
|
137
|
+
v_vec: np.ndarray
|
|
138
|
+
):
|
|
139
|
+
"""
|
|
140
|
+
Create a polygon representing a grid cell.
|
|
141
|
+
"""
|
|
142
|
+
bottom_left = origin + i * adjusted_meshsize[0] * u_vec + j * adjusted_meshsize[1] * v_vec
|
|
143
|
+
bottom_right = origin + (i + 1) * adjusted_meshsize[0] * u_vec + j * adjusted_meshsize[1] * v_vec
|
|
144
|
+
top_right = origin + (i + 1) * adjusted_meshsize[0] * u_vec + (j + 1) * adjusted_meshsize[1] * v_vec
|
|
145
|
+
top_left = origin + i * adjusted_meshsize[0] * u_vec + (j + 1) * adjusted_meshsize[1] * v_vec
|
|
146
|
+
return Polygon([bottom_left, bottom_right, top_right, top_left])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import geopandas as gpd
|
|
3
|
+
from shapely.geometry import box
|
|
4
|
+
from pyproj import CRS, Transformer
|
|
5
|
+
from ...utils.orientation import ensure_orientation, ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
9
|
+
"""
|
|
10
|
+
Converts a 2D grid to a GeoDataFrame with cell polygons and values.
|
|
11
|
+
Output CRS: EPSG:4326
|
|
12
|
+
"""
|
|
13
|
+
grid = ensure_orientation(grid_ori.copy(), ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP)
|
|
14
|
+
|
|
15
|
+
min_lon = min(v[0] for v in rectangle_vertices)
|
|
16
|
+
max_lon = max(v[0] for v in rectangle_vertices)
|
|
17
|
+
min_lat = min(v[1] for v in rectangle_vertices)
|
|
18
|
+
max_lat = max(v[1] for v in rectangle_vertices)
|
|
19
|
+
|
|
20
|
+
rows, cols = grid.shape
|
|
21
|
+
|
|
22
|
+
wgs84 = CRS.from_epsg(4326)
|
|
23
|
+
web_mercator = CRS.from_epsg(3857)
|
|
24
|
+
transformer_to_mercator = Transformer.from_crs(wgs84, web_mercator, always_xy=True)
|
|
25
|
+
transformer_to_wgs84 = Transformer.from_crs(web_mercator, wgs84, always_xy=True)
|
|
26
|
+
|
|
27
|
+
min_x, min_y = transformer_to_mercator.transform(min_lon, min_lat)
|
|
28
|
+
max_x, max_y = transformer_to_mercator.transform(max_lon, max_lat)
|
|
29
|
+
|
|
30
|
+
cell_size_x = (max_x - min_x) / cols
|
|
31
|
+
cell_size_y = (max_y - min_y) / rows
|
|
32
|
+
|
|
33
|
+
polygons = []
|
|
34
|
+
values = []
|
|
35
|
+
|
|
36
|
+
for i in range(rows):
|
|
37
|
+
for j in range(cols):
|
|
38
|
+
cell_min_x = min_x + j * cell_size_x
|
|
39
|
+
cell_max_x = min_x + (j + 1) * cell_size_x
|
|
40
|
+
cell_min_y = max_y - (i + 1) * cell_size_y
|
|
41
|
+
cell_max_y = max_y - i * cell_size_y
|
|
42
|
+
cell_min_lon, cell_min_lat = transformer_to_wgs84.transform(cell_min_x, cell_min_y)
|
|
43
|
+
cell_max_lon, cell_max_lat = transformer_to_wgs84.transform(cell_max_x, cell_max_y)
|
|
44
|
+
cell_poly = box(cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat)
|
|
45
|
+
polygons.append(cell_poly)
|
|
46
|
+
values.append(grid[i, j])
|
|
47
|
+
|
|
48
|
+
gdf = gpd.GeoDataFrame({'geometry': polygons, 'value': values}, crs=CRS.from_epsg(4326))
|
|
49
|
+
return gdf
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def grid_to_point_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
53
|
+
"""
|
|
54
|
+
Converts a 2D grid to a GeoDataFrame with point geometries at cell centers and values.
|
|
55
|
+
Output CRS: EPSG:4326
|
|
56
|
+
"""
|
|
57
|
+
import geopandas as gpd
|
|
58
|
+
from shapely.geometry import Point
|
|
59
|
+
|
|
60
|
+
grid = ensure_orientation(grid_ori.copy(), ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP)
|
|
61
|
+
|
|
62
|
+
min_lon = min(v[0] for v in rectangle_vertices)
|
|
63
|
+
max_lon = max(v[0] for v in rectangle_vertices)
|
|
64
|
+
min_lat = min(v[1] for v in rectangle_vertices)
|
|
65
|
+
max_lat = max(v[1] for v in rectangle_vertices)
|
|
66
|
+
|
|
67
|
+
rows, cols = grid.shape
|
|
68
|
+
|
|
69
|
+
wgs84 = CRS.from_epsg(4326)
|
|
70
|
+
web_mercator = CRS.from_epsg(3857)
|
|
71
|
+
transformer_to_mercator = Transformer.from_crs(wgs84, web_mercator, always_xy=True)
|
|
72
|
+
transformer_to_wgs84 = Transformer.from_crs(web_mercator, wgs84, always_xy=True)
|
|
73
|
+
|
|
74
|
+
min_x, min_y = transformer_to_mercator.transform(min_lon, min_lat)
|
|
75
|
+
max_x, max_y = transformer_to_mercator.transform(max_lon, max_lat)
|
|
76
|
+
|
|
77
|
+
cell_size_x = (max_x - min_x) / cols
|
|
78
|
+
cell_size_y = (max_y - min_y) / rows
|
|
79
|
+
|
|
80
|
+
points = []
|
|
81
|
+
values = []
|
|
82
|
+
for i in range(rows):
|
|
83
|
+
for j in range(cols):
|
|
84
|
+
cell_center_x = min_x + (j + 0.5) * cell_size_x
|
|
85
|
+
cell_center_y = max_y - (i + 0.5) * cell_size_y
|
|
86
|
+
center_lon, center_lat = transformer_to_wgs84.transform(cell_center_x, cell_center_y)
|
|
87
|
+
points.append(Point(center_lon, center_lat))
|
|
88
|
+
values.append(grid[i, j])
|
|
89
|
+
|
|
90
|
+
gdf = gpd.GeoDataFrame({'geometry': points, 'value': values}, crs=CRS.from_epsg(4326))
|
|
91
|
+
return gdf
|
|
92
|
+
|
|
93
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Tuple, Dict, Any
|
|
3
|
+
from shapely.geometry import Polygon
|
|
4
|
+
from affine import Affine
|
|
5
|
+
import rasterio
|
|
6
|
+
|
|
7
|
+
from pyproj import Geod
|
|
8
|
+
|
|
9
|
+
from ...utils.lc import (
|
|
10
|
+
get_class_priority,
|
|
11
|
+
create_land_cover_polygons,
|
|
12
|
+
get_dominant_class,
|
|
13
|
+
)
|
|
14
|
+
from .core import translate_array
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def tree_height_grid_from_land_cover(land_cover_grid_ori: np.ndarray) -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Convert a land cover grid to a tree height grid.
|
|
20
|
+
"""
|
|
21
|
+
land_cover_grid = np.flipud(land_cover_grid_ori) + 1
|
|
22
|
+
tree_translation_dict = {1: 0, 2: 0, 3: 0, 4: 10, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0}
|
|
23
|
+
tree_height_grid = translate_array(np.flipud(land_cover_grid), tree_translation_dict).astype(int)
|
|
24
|
+
return tree_height_grid
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_land_cover_grid_from_geotiff_polygon(
|
|
28
|
+
tiff_path: str,
|
|
29
|
+
mesh_size: float,
|
|
30
|
+
land_cover_classes: Dict[str, Any],
|
|
31
|
+
polygon: List[Tuple[float, float]]
|
|
32
|
+
) -> np.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
Create a land cover grid from a GeoTIFF file within a polygon boundary.
|
|
35
|
+
"""
|
|
36
|
+
with rasterio.open(tiff_path) as src:
|
|
37
|
+
img = src.read((1, 2, 3))
|
|
38
|
+
left, bottom, right, top = src.bounds
|
|
39
|
+
poly = Polygon(polygon)
|
|
40
|
+
left_wgs84, bottom_wgs84, right_wgs84, top_wgs84 = poly.bounds
|
|
41
|
+
|
|
42
|
+
geod = Geod(ellps="WGS84")
|
|
43
|
+
_, _, width = geod.inv(left_wgs84, bottom_wgs84, right_wgs84, bottom_wgs84)
|
|
44
|
+
_, _, height = geod.inv(left_wgs84, bottom_wgs84, left_wgs84, top_wgs84)
|
|
45
|
+
|
|
46
|
+
num_cells_x = int(width / mesh_size + 0.5)
|
|
47
|
+
num_cells_y = int(height / mesh_size + 0.5)
|
|
48
|
+
|
|
49
|
+
adjusted_mesh_size_x = (right - left) / num_cells_x
|
|
50
|
+
adjusted_mesh_size_y = (top - bottom) / num_cells_y
|
|
51
|
+
|
|
52
|
+
new_affine = Affine(adjusted_mesh_size_x, 0, left, 0, -adjusted_mesh_size_y, top)
|
|
53
|
+
|
|
54
|
+
cols, rows = np.meshgrid(np.arange(num_cells_x), np.arange(num_cells_y))
|
|
55
|
+
xs, ys = new_affine * (cols, rows)
|
|
56
|
+
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
57
|
+
|
|
58
|
+
row, col = src.index(xs_flat, ys_flat)
|
|
59
|
+
row, col = np.array(row), np.array(col)
|
|
60
|
+
|
|
61
|
+
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|
|
62
|
+
row, col = row[valid], col[valid]
|
|
63
|
+
|
|
64
|
+
grid = np.full((num_cells_y, num_cells_x), 'No Data', dtype=object)
|
|
65
|
+
for i, (r, c) in enumerate(zip(row, col)):
|
|
66
|
+
cell_data = img[:, r, c]
|
|
67
|
+
dominant_class = get_dominant_class(cell_data, land_cover_classes)
|
|
68
|
+
grid_row, grid_col = np.unravel_index(i, (num_cells_y, num_cells_x))
|
|
69
|
+
grid[grid_row, grid_col] = dominant_class
|
|
70
|
+
|
|
71
|
+
return np.flipud(grid)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_land_cover_grid_from_gdf_polygon(
|
|
75
|
+
gdf,
|
|
76
|
+
meshsize: float,
|
|
77
|
+
source: str,
|
|
78
|
+
rectangle_vertices: List[Tuple[float, float]],
|
|
79
|
+
default_class: str = 'Developed space'
|
|
80
|
+
) -> np.ndarray:
|
|
81
|
+
"""Create a grid of land cover classes from GeoDataFrame polygon data."""
|
|
82
|
+
import numpy as np
|
|
83
|
+
import geopandas as gpd
|
|
84
|
+
from shapely.geometry import box
|
|
85
|
+
from shapely.errors import GEOSException
|
|
86
|
+
from rtree import index
|
|
87
|
+
|
|
88
|
+
class_priority = get_class_priority(source)
|
|
89
|
+
|
|
90
|
+
from ..utils import (
|
|
91
|
+
initialize_geod,
|
|
92
|
+
calculate_distance,
|
|
93
|
+
normalize_to_one_meter,
|
|
94
|
+
)
|
|
95
|
+
from .core import calculate_grid_size, create_cell_polygon
|
|
96
|
+
|
|
97
|
+
geod = initialize_geod()
|
|
98
|
+
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
99
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
100
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
101
|
+
|
|
102
|
+
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
103
|
+
side_2 = np.array(vertex_3) - np.array(vertex_0)
|
|
104
|
+
u_vec = normalize_to_one_meter(side_1, dist_side_1)
|
|
105
|
+
v_vec = normalize_to_one_meter(side_2, dist_side_2)
|
|
106
|
+
|
|
107
|
+
origin = np.array(rectangle_vertices[0])
|
|
108
|
+
grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
|
|
109
|
+
|
|
110
|
+
grid = np.full(grid_size, default_class, dtype=object)
|
|
111
|
+
|
|
112
|
+
extent = [min(coord[1] for coord in rectangle_vertices), max(coord[1] for coord in rectangle_vertices),
|
|
113
|
+
min(coord[0] for coord in rectangle_vertices), max(coord[0] for coord in rectangle_vertices)]
|
|
114
|
+
plotting_box = box(extent[2], extent[0], extent[3], extent[1])
|
|
115
|
+
|
|
116
|
+
land_cover_polygons = []
|
|
117
|
+
idx = index.Index()
|
|
118
|
+
for i, row in gdf.iterrows():
|
|
119
|
+
polygon = row.geometry
|
|
120
|
+
land_cover_class = row['class']
|
|
121
|
+
land_cover_polygons.append((polygon, land_cover_class))
|
|
122
|
+
idx.insert(i, polygon.bounds)
|
|
123
|
+
|
|
124
|
+
for i in range(grid_size[0]):
|
|
125
|
+
for j in range(grid_size[1]):
|
|
126
|
+
land_cover_class = default_class
|
|
127
|
+
cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
|
|
128
|
+
for k in idx.intersection(cell.bounds):
|
|
129
|
+
polygon, land_cover_class_temp = land_cover_polygons[k]
|
|
130
|
+
try:
|
|
131
|
+
if cell.intersects(polygon):
|
|
132
|
+
intersection = cell.intersection(polygon)
|
|
133
|
+
if intersection.area > cell.area / 2:
|
|
134
|
+
rank = class_priority[land_cover_class]
|
|
135
|
+
rank_temp = class_priority[land_cover_class_temp]
|
|
136
|
+
if rank_temp < rank:
|
|
137
|
+
land_cover_class = land_cover_class_temp
|
|
138
|
+
grid[i, j] = land_cover_class
|
|
139
|
+
except GEOSException as e:
|
|
140
|
+
try:
|
|
141
|
+
fixed_polygon = polygon.buffer(0)
|
|
142
|
+
if cell.intersects(fixed_polygon):
|
|
143
|
+
intersection = cell.intersection(fixed_polygon)
|
|
144
|
+
if intersection.area > cell.area / 2:
|
|
145
|
+
rank = class_priority[land_cover_class]
|
|
146
|
+
rank_temp = class_priority[land_cover_class_temp]
|
|
147
|
+
if rank_temp < rank:
|
|
148
|
+
land_cover_class = land_cover_class_temp
|
|
149
|
+
grid[i, j] = land_cover_class
|
|
150
|
+
except Exception:
|
|
151
|
+
continue
|
|
152
|
+
return grid
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|