voxcity 0.5.14__py3-none-any.whl → 0.5.15__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/downloader/citygml.py +202 -28
- voxcity/downloader/eubucco.py +91 -14
- voxcity/downloader/gee.py +164 -22
- voxcity/downloader/mbfp.py +55 -9
- voxcity/downloader/oemj.py +110 -24
- voxcity/downloader/omt.py +74 -7
- voxcity/downloader/osm.py +109 -23
- voxcity/downloader/overture.py +108 -23
- voxcity/downloader/utils.py +37 -7
- voxcity/exporter/envimet.py +180 -61
- voxcity/exporter/magicavoxel.py +138 -28
- voxcity/exporter/obj.py +159 -36
- voxcity/generator.py +159 -76
- voxcity/geoprocessor/draw.py +180 -27
- voxcity/geoprocessor/grid.py +178 -38
- voxcity/geoprocessor/mesh.py +347 -43
- voxcity/geoprocessor/network.py +196 -63
- voxcity/geoprocessor/polygon.py +365 -88
- voxcity/geoprocessor/utils.py +283 -72
- voxcity/simulator/solar.py +596 -201
- voxcity/simulator/view.py +278 -723
- voxcity/utils/lc.py +183 -0
- voxcity/utils/material.py +99 -32
- voxcity/utils/visualization.py +2578 -1988
- voxcity/utils/weather.py +816 -615
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/METADATA +10 -12
- voxcity-0.5.15.dist-info/RECORD +38 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/WHEEL +1 -1
- voxcity-0.5.14.dist-info/RECORD +0 -38
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/top_level.txt +0 -0
voxcity/generator.py
CHANGED
|
@@ -14,9 +14,13 @@ The main functions are:
|
|
|
14
14
|
- get_voxcity: Main function to generate a complete voxel city model
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
# Standard library imports
|
|
17
18
|
import numpy as np
|
|
18
19
|
import os
|
|
20
|
+
|
|
19
21
|
# Local application/library specific imports
|
|
22
|
+
|
|
23
|
+
# Data downloaders - modules for fetching geospatial data from various sources
|
|
20
24
|
from .downloader.mbfp import get_mbfp_gdf
|
|
21
25
|
from .downloader.osm import load_gdf_from_openstreetmap, load_land_cover_gdf_from_osm
|
|
22
26
|
from .downloader.oemj import save_oemj_as_geotiff
|
|
@@ -24,6 +28,8 @@ from .downloader.omt import load_gdf_from_openmaptiles
|
|
|
24
28
|
from .downloader.eubucco import load_gdf_from_eubucco
|
|
25
29
|
from .downloader.overture import load_gdf_from_overture
|
|
26
30
|
from .downloader.citygml import load_buid_dem_veg_from_citygml
|
|
31
|
+
|
|
32
|
+
# Google Earth Engine related imports - for satellite and elevation data
|
|
27
33
|
from .downloader.gee import (
|
|
28
34
|
initialize_earth_engine,
|
|
29
35
|
get_roi,
|
|
@@ -37,6 +43,8 @@ from .downloader.gee import (
|
|
|
37
43
|
save_geotiff_open_buildings_temporal,
|
|
38
44
|
save_geotiff_dsm_minus_dtm
|
|
39
45
|
)
|
|
46
|
+
|
|
47
|
+
# Grid processing functions - for converting geodata to raster grids
|
|
40
48
|
from .geoprocessor.grid import (
|
|
41
49
|
group_and_label_cells,
|
|
42
50
|
process_grid,
|
|
@@ -49,8 +57,12 @@ from .geoprocessor.grid import (
|
|
|
49
57
|
create_vegetation_height_grid_from_gdf_polygon,
|
|
50
58
|
create_dem_grid_from_gdf_polygon
|
|
51
59
|
)
|
|
60
|
+
|
|
61
|
+
# Utility functions
|
|
52
62
|
from .utils.lc import convert_land_cover, convert_land_cover_array
|
|
53
63
|
from .geoprocessor.polygon import get_gdf_from_gpkg, save_geojson
|
|
64
|
+
|
|
65
|
+
# Visualization functions - for creating plots and 3D visualizations
|
|
54
66
|
from .utils.visualization import (
|
|
55
67
|
get_land_cover_classes,
|
|
56
68
|
visualize_land_cover_grid,
|
|
@@ -83,55 +95,70 @@ def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwar
|
|
|
83
95
|
print("Creating Land Use Land Cover grid\n ")
|
|
84
96
|
print(f"Data source: {source}")
|
|
85
97
|
|
|
86
|
-
# Initialize Earth Engine for
|
|
98
|
+
# Initialize Earth Engine for satellite-based data sources
|
|
99
|
+
# Skip initialization for local/vector data sources
|
|
87
100
|
if source not in ["OpenStreetMap", "OpenEarthMapJapan"]:
|
|
88
101
|
initialize_earth_engine()
|
|
89
102
|
|
|
90
|
-
#
|
|
103
|
+
# Ensure output directory exists for saving intermediate files
|
|
91
104
|
os.makedirs(output_dir, exist_ok=True)
|
|
92
105
|
geotiff_path = os.path.join(output_dir, "land_cover.tif")
|
|
93
106
|
|
|
94
|
-
#
|
|
107
|
+
# Handle different data sources - each requires specific processing
|
|
108
|
+
# Satellite/raster-based sources are saved as GeoTIFF files
|
|
95
109
|
if source == 'Urbanwatch':
|
|
110
|
+
# Urban-focused land cover from satellite imagery
|
|
96
111
|
roi = get_roi(rectangle_vertices)
|
|
97
112
|
collection_name = "projects/sat-io/open-datasets/HRLC/urban-watch-cities"
|
|
98
113
|
image = get_ee_image_collection(collection_name, roi)
|
|
99
114
|
save_geotiff(image, geotiff_path)
|
|
100
115
|
elif source == 'ESA WorldCover':
|
|
116
|
+
# Global land cover from European Space Agency
|
|
101
117
|
roi = get_roi(rectangle_vertices)
|
|
102
118
|
save_geotiff_esa_land_cover(roi, geotiff_path)
|
|
103
119
|
elif source == 'ESRI 10m Annual Land Cover':
|
|
120
|
+
# High-resolution annual land cover from ESRI
|
|
104
121
|
esri_landcover_year = kwargs.get("esri_landcover_year")
|
|
105
122
|
roi = get_roi(rectangle_vertices)
|
|
106
123
|
save_geotiff_esri_landcover(roi, geotiff_path, year=esri_landcover_year)
|
|
107
124
|
elif source == 'Dynamic World V1':
|
|
125
|
+
# Near real-time land cover from Google's Dynamic World
|
|
108
126
|
dynamic_world_date = kwargs.get("dynamic_world_date")
|
|
109
127
|
roi = get_roi(rectangle_vertices)
|
|
110
128
|
save_geotiff_dynamic_world_v1(roi, geotiff_path, dynamic_world_date)
|
|
111
129
|
elif source == 'OpenEarthMapJapan':
|
|
130
|
+
# Japan-specific land cover dataset
|
|
112
131
|
save_oemj_as_geotiff(rectangle_vertices, geotiff_path)
|
|
113
132
|
elif source == 'OpenStreetMap':
|
|
114
|
-
#
|
|
133
|
+
# Vector-based land cover from OpenStreetMap
|
|
134
|
+
# This bypasses the GeoTIFF workflow and gets data directly as GeoJSON
|
|
115
135
|
land_cover_gdf = load_land_cover_gdf_from_osm(rectangle_vertices)
|
|
116
136
|
|
|
117
|
-
# Get
|
|
137
|
+
# Get the classification scheme for the selected data source
|
|
138
|
+
# Each source has its own land cover categories and color coding
|
|
118
139
|
land_cover_classes = get_land_cover_classes(source)
|
|
119
140
|
|
|
120
|
-
#
|
|
141
|
+
# Convert geospatial data to regular grid format
|
|
142
|
+
# Different processing for vector vs raster data sources
|
|
121
143
|
if source == 'OpenStreetMap':
|
|
144
|
+
# Process vector data directly from GeoDataFrame
|
|
122
145
|
land_cover_grid_str = create_land_cover_grid_from_gdf_polygon(land_cover_gdf, meshsize, source, rectangle_vertices)
|
|
123
146
|
else:
|
|
147
|
+
# Process raster data from GeoTIFF file
|
|
124
148
|
land_cover_grid_str = create_land_cover_grid_from_geotiff_polygon(geotiff_path, meshsize, land_cover_classes, rectangle_vertices)
|
|
125
149
|
|
|
126
|
-
#
|
|
150
|
+
# Prepare color mapping for visualization
|
|
151
|
+
# Convert RGB values from 0-255 range to 0-1 range for matplotlib
|
|
127
152
|
color_map = {cls: [r/255, g/255, b/255] for (r,g,b), cls in land_cover_classes.items()}
|
|
128
153
|
|
|
129
|
-
#
|
|
154
|
+
# Generate visualization if requested
|
|
130
155
|
grid_vis = kwargs.get("gridvis", True)
|
|
131
156
|
if grid_vis:
|
|
157
|
+
# Flip grid vertically for correct display orientation
|
|
132
158
|
visualize_land_cover_grid(np.flipud(land_cover_grid_str), meshsize, color_map, land_cover_classes)
|
|
133
159
|
|
|
134
|
-
# Convert string labels to integer codes
|
|
160
|
+
# Convert string-based land cover labels to integer codes for processing
|
|
161
|
+
# This enables efficient numerical operations on the grid
|
|
135
162
|
land_cover_grid_int = convert_land_cover_array(land_cover_grid_str, land_cover_classes)
|
|
136
163
|
|
|
137
164
|
return land_cover_grid_int
|
|
@@ -159,7 +186,7 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
|
|
|
159
186
|
- list: Filtered building features
|
|
160
187
|
"""
|
|
161
188
|
|
|
162
|
-
# Initialize Earth Engine for
|
|
189
|
+
# Initialize Earth Engine for satellite-based building data sources
|
|
163
190
|
if source not in ["OpenStreetMap", "Overture", "Local file"]:
|
|
164
191
|
initialize_earth_engine()
|
|
165
192
|
|
|
@@ -168,52 +195,60 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
|
|
|
168
195
|
|
|
169
196
|
os.makedirs(output_dir, exist_ok=True)
|
|
170
197
|
|
|
171
|
-
#
|
|
198
|
+
# Fetch building data from primary source
|
|
199
|
+
# Each source has different data formats and processing requirements
|
|
172
200
|
if source == 'Microsoft Building Footprints':
|
|
201
|
+
# Machine learning-derived building footprints from satellite imagery
|
|
173
202
|
gdf = get_mbfp_gdf(output_dir, rectangle_vertices)
|
|
174
203
|
elif source == 'OpenStreetMap':
|
|
204
|
+
# Crowd-sourced building data with varying completeness
|
|
175
205
|
gdf = load_gdf_from_openstreetmap(rectangle_vertices)
|
|
176
206
|
elif source == "Open Building 2.5D Temporal":
|
|
177
|
-
# Special case:
|
|
207
|
+
# Special case: this source provides both footprints and heights
|
|
208
|
+
# Skip GeoDataFrame processing and create grids directly
|
|
178
209
|
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_open_building_temporal_polygon(meshsize, rectangle_vertices, output_dir)
|
|
179
210
|
elif source == 'EUBUCCO v0.1':
|
|
211
|
+
# European building database with height information
|
|
180
212
|
gdf = load_gdf_from_eubucco(rectangle_vertices, output_dir)
|
|
181
213
|
elif source == "OpenMapTiles":
|
|
214
|
+
# Vector tiles service for building data
|
|
182
215
|
gdf = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
|
|
183
216
|
elif source == "Overture":
|
|
217
|
+
# Open building dataset from Overture Maps Foundation
|
|
184
218
|
gdf = load_gdf_from_overture(rectangle_vertices)
|
|
185
219
|
elif source == "Local file":
|
|
186
|
-
# Handle local
|
|
220
|
+
# Handle user-provided local building data files
|
|
187
221
|
_, extension = os.path.splitext(kwargs["building_path"])
|
|
188
222
|
if extension == ".gpkg":
|
|
189
223
|
gdf = get_gdf_from_gpkg(kwargs["building_path"], rectangle_vertices)
|
|
190
224
|
|
|
191
|
-
#
|
|
225
|
+
# Handle complementary data sources to fill gaps or provide additional information
|
|
226
|
+
# This allows combining multiple sources for better coverage or accuracy
|
|
192
227
|
building_complementary_source = kwargs.get("building_complementary_source")
|
|
193
228
|
building_complement_height = kwargs.get("building_complement_height")
|
|
194
229
|
|
|
195
230
|
if (building_complementary_source is None) or (building_complementary_source=='None'):
|
|
196
|
-
# Use only primary source
|
|
231
|
+
# Use only the primary data source
|
|
197
232
|
if source != "Open Building 2.5D Temporal":
|
|
198
233
|
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, complement_height=building_complement_height)
|
|
199
234
|
else:
|
|
200
|
-
#
|
|
235
|
+
# Combine primary source with complementary data
|
|
201
236
|
if building_complementary_source == "Open Building 2.5D Temporal":
|
|
202
|
-
#
|
|
237
|
+
# Use temporal height data to complement footprint data
|
|
203
238
|
roi = get_roi(rectangle_vertices)
|
|
204
239
|
os.makedirs(output_dir, exist_ok=True)
|
|
205
240
|
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
206
241
|
save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
|
|
207
242
|
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height)
|
|
208
243
|
elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
|
|
209
|
-
#
|
|
244
|
+
# Use digital surface model minus digital terrain model for height estimation
|
|
210
245
|
roi = get_roi(rectangle_vertices)
|
|
211
246
|
os.makedirs(output_dir, exist_ok=True)
|
|
212
247
|
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
213
248
|
save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
|
|
214
249
|
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height)
|
|
215
250
|
else:
|
|
216
|
-
#
|
|
251
|
+
# Fetch complementary data from another vector source
|
|
217
252
|
if building_complementary_source == 'Microsoft Building Footprints':
|
|
218
253
|
gdf_comp = get_mbfp_gdf(output_dir, rectangle_vertices)
|
|
219
254
|
elif building_complementary_source == 'OpenStreetMap':
|
|
@@ -229,15 +264,18 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
|
|
|
229
264
|
if extension == ".gpkg":
|
|
230
265
|
gdf_comp = get_gdf_from_gpkg(kwargs["building_complementary_path"], rectangle_vertices)
|
|
231
266
|
|
|
232
|
-
#
|
|
267
|
+
# Configure how to combine the complementary data
|
|
268
|
+
# Can complement footprints only or both footprints and heights
|
|
233
269
|
complement_building_footprints = kwargs.get("complement_building_footprints")
|
|
234
270
|
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, gdf_comp=gdf_comp, complement_building_footprints=complement_building_footprints, complement_height=building_complement_height)
|
|
235
271
|
|
|
236
|
-
#
|
|
272
|
+
# Generate visualization if requested
|
|
237
273
|
grid_vis = kwargs.get("gridvis", True)
|
|
238
274
|
if grid_vis:
|
|
275
|
+
# Replace zeros with NaN for better visualization (don't show empty areas)
|
|
239
276
|
building_height_grid_nan = building_height_grid.copy()
|
|
240
277
|
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
278
|
+
# Flip grid vertically for correct display orientation
|
|
241
279
|
visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
|
|
242
280
|
|
|
243
281
|
return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
|
|
@@ -260,32 +298,38 @@ def get_canopy_height_grid(rectangle_vertices, meshsize, source, output_dir, **k
|
|
|
260
298
|
print("Creating Canopy Height grid\n ")
|
|
261
299
|
print(f"Data source: High Resolution Canopy Height Maps by WRI and Meta")
|
|
262
300
|
|
|
263
|
-
# Initialize Earth Engine for
|
|
301
|
+
# Initialize Earth Engine for satellite-based canopy height data
|
|
264
302
|
initialize_earth_engine()
|
|
265
303
|
|
|
266
304
|
os.makedirs(output_dir, exist_ok=True)
|
|
267
305
|
geotiff_path = os.path.join(output_dir, "canopy_height.tif")
|
|
268
306
|
|
|
269
|
-
# Get region of interest and canopy height data
|
|
307
|
+
# Get region of interest and fetch canopy height data
|
|
270
308
|
roi = get_roi(rectangle_vertices)
|
|
271
309
|
if source == 'High Resolution 1m Global Canopy Height Maps':
|
|
310
|
+
# High-resolution (1m) global canopy height maps from Meta and WRI
|
|
311
|
+
# Based on satellite imagery and machine learning models
|
|
272
312
|
collection_name = "projects/meta-forest-monitoring-okw37/assets/CanopyHeight"
|
|
273
313
|
image = get_ee_image_collection(collection_name, roi)
|
|
274
314
|
elif source == 'ETH Global Sentinel-2 10m Canopy Height (2020)':
|
|
315
|
+
# Medium-resolution (10m) canopy height from ETH Zurich
|
|
316
|
+
# Derived from Sentinel-2 satellite data
|
|
275
317
|
collection_name = "users/nlang/ETH_GlobalCanopyHeight_2020_10m_v1"
|
|
276
318
|
image = get_ee_image(collection_name, roi)
|
|
277
319
|
|
|
278
|
-
# Save canopy height data as GeoTIFF
|
|
320
|
+
# Save canopy height data as GeoTIFF with specified resolution
|
|
279
321
|
save_geotiff(image, geotiff_path, resolution=meshsize)
|
|
280
322
|
|
|
281
|
-
#
|
|
323
|
+
# Convert GeoTIFF to regular grid format
|
|
282
324
|
canopy_height_grid = create_height_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices)
|
|
283
325
|
|
|
284
|
-
#
|
|
326
|
+
# Generate visualization if requested
|
|
285
327
|
grid_vis = kwargs.get("gridvis", True)
|
|
286
328
|
if grid_vis:
|
|
329
|
+
# Replace zeros with NaN for better visualization (show only areas with trees)
|
|
287
330
|
canopy_height_grid_nan = canopy_height_grid.copy()
|
|
288
331
|
canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
|
|
332
|
+
# Use green color scheme appropriate for vegetation
|
|
289
333
|
visualize_numerical_grid(np.flipud(canopy_height_grid_nan), meshsize, "Tree canopy height", cmap='Greens', label='Tree canopy height (m)')
|
|
290
334
|
|
|
291
335
|
return canopy_height_grid
|
|
@@ -310,38 +354,45 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
|
310
354
|
print(f"Data source: {source}")
|
|
311
355
|
|
|
312
356
|
if source == "Local file":
|
|
357
|
+
# Use user-provided local DEM file
|
|
313
358
|
geotiff_path = kwargs["dem_path"]
|
|
314
359
|
else:
|
|
315
|
-
#
|
|
360
|
+
# Fetch DEM data from various satellite/government sources
|
|
316
361
|
initialize_earth_engine()
|
|
317
362
|
|
|
318
363
|
geotiff_path = os.path.join(output_dir, "dem.tif")
|
|
319
364
|
|
|
320
|
-
# Add buffer around
|
|
365
|
+
# Add buffer around region of interest to ensure smooth interpolation at edges
|
|
366
|
+
# This prevents edge artifacts in the final grid
|
|
321
367
|
buffer_distance = 100
|
|
322
368
|
roi = get_roi(rectangle_vertices)
|
|
323
369
|
roi_buffered = roi.buffer(buffer_distance)
|
|
324
370
|
|
|
325
|
-
#
|
|
371
|
+
# Fetch elevation data from selected source
|
|
326
372
|
image = get_dem_image(roi_buffered, source)
|
|
327
373
|
|
|
328
|
-
# Save DEM data with appropriate resolution based on source
|
|
374
|
+
# Save DEM data with appropriate resolution based on source capabilities
|
|
329
375
|
if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM', 'Netherlands 0.5m DTM']:
|
|
376
|
+
# High-resolution elevation models - use specified mesh size
|
|
330
377
|
save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
|
|
331
378
|
elif source == 'USGS 3DEP 1m':
|
|
379
|
+
# US Geological Survey 3D Elevation Program
|
|
380
|
+
# Ensure minimum scale of 1.25m due to data limitations
|
|
332
381
|
scale = max(meshsize, 1.25)
|
|
333
382
|
save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
|
|
334
383
|
else:
|
|
335
|
-
# Default to 30m resolution for
|
|
384
|
+
# Default to 30m resolution for global/lower resolution sources
|
|
336
385
|
save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
|
|
337
386
|
|
|
338
|
-
#
|
|
387
|
+
# Convert GeoTIFF to regular grid with optional interpolation
|
|
388
|
+
# Interpolation helps fill gaps and smooth transitions
|
|
339
389
|
dem_interpolation = kwargs.get("dem_interpolation")
|
|
340
390
|
dem_grid = create_dem_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices, dem_interpolation=dem_interpolation)
|
|
341
391
|
|
|
342
|
-
#
|
|
392
|
+
# Generate visualization if requested
|
|
343
393
|
grid_vis = kwargs.get("gridvis", True)
|
|
344
394
|
if grid_vis:
|
|
395
|
+
# Use terrain color scheme appropriate for elevation data
|
|
345
396
|
visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
|
|
346
397
|
|
|
347
398
|
return dem_grid
|
|
@@ -367,13 +418,16 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
|
|
|
367
418
|
|
|
368
419
|
print("Generating 3D voxel data")
|
|
369
420
|
|
|
370
|
-
# Convert land cover values
|
|
421
|
+
# Convert land cover values to standardized format if needed
|
|
422
|
+
# OpenStreetMap data is already in the correct format
|
|
371
423
|
if (land_cover_source == 'OpenStreetMap'):
|
|
372
424
|
land_cover_grid_converted = land_cover_grid_ori
|
|
373
425
|
else:
|
|
374
426
|
land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
|
|
375
427
|
|
|
376
|
-
# Prepare
|
|
428
|
+
# Prepare all input grids for 3D processing
|
|
429
|
+
# Flip vertically to align with standard geographic orientation (north-up)
|
|
430
|
+
# Handle missing data appropriately for each grid type
|
|
377
431
|
building_height_grid = np.flipud(np.nan_to_num(building_height_grid_ori, nan=10.0)) # Replace NaN values with 10m height
|
|
378
432
|
building_min_height_grid = np.flipud(replace_nan_in_nested(building_min_height_grid_ori)) # Replace NaN in nested arrays
|
|
379
433
|
building_id_grid = np.flipud(building_id_grid_ori)
|
|
@@ -382,61 +436,68 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
|
|
|
382
436
|
dem_grid = process_grid(building_id_grid, dem_grid) # Process DEM based on building footprints
|
|
383
437
|
tree_grid = np.flipud(tree_grid_ori.copy())
|
|
384
438
|
|
|
385
|
-
# Validate input dimensions
|
|
439
|
+
# Validate that all input grids have consistent dimensions
|
|
386
440
|
assert building_height_grid.shape == land_cover_grid.shape == dem_grid.shape == tree_grid.shape, "Input grids must have the same shape"
|
|
387
441
|
|
|
388
442
|
rows, cols = building_height_grid.shape
|
|
389
443
|
|
|
390
|
-
# Calculate required height for 3D grid
|
|
444
|
+
# Calculate the required height for the 3D voxel grid
|
|
445
|
+
# Add 1 voxel layer to ensure sufficient vertical space
|
|
391
446
|
max_height = int(np.ceil(np.max(building_height_grid + dem_grid + tree_grid) / voxel_size))+1
|
|
392
447
|
|
|
393
|
-
# Initialize
|
|
448
|
+
# Initialize the 3D voxel grid with zeros
|
|
449
|
+
# Dimensions: (rows, columns, height_layers)
|
|
394
450
|
voxel_grid = np.zeros((rows, cols, max_height), dtype=np.int32)
|
|
395
451
|
|
|
396
|
-
#
|
|
452
|
+
# Configure tree trunk-to-crown ratio
|
|
453
|
+
# This determines how much of the tree is trunk vs canopy
|
|
397
454
|
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
398
455
|
if trunk_height_ratio is None:
|
|
399
456
|
trunk_height_ratio = 11.76 / 19.98 # Default ratio based on typical tree proportions
|
|
400
457
|
|
|
401
|
-
#
|
|
458
|
+
# Process each grid cell to build the 3D voxel representation
|
|
402
459
|
for i in range(rows):
|
|
403
460
|
for j in range(cols):
|
|
404
|
-
# Calculate ground level in voxel units
|
|
461
|
+
# Calculate ground level in voxel units
|
|
462
|
+
# Add 1 to ensure space for surface features
|
|
405
463
|
ground_level = int(dem_grid[i, j] / voxel_size + 0.5) + 1
|
|
406
464
|
|
|
465
|
+
# Extract current cell values
|
|
407
466
|
tree_height = tree_grid[i, j]
|
|
408
467
|
land_cover = land_cover_grid[i, j]
|
|
409
468
|
|
|
410
|
-
# Fill underground voxels with -1
|
|
469
|
+
# Fill underground voxels with -1 (represents subsurface)
|
|
411
470
|
voxel_grid[i, j, :ground_level] = -1
|
|
412
471
|
|
|
413
|
-
# Set surface land cover
|
|
472
|
+
# Set the ground surface to the land cover type
|
|
414
473
|
voxel_grid[i, j, ground_level-1] = land_cover
|
|
415
474
|
|
|
416
|
-
# Process
|
|
475
|
+
# Process tree canopy if trees are present
|
|
417
476
|
if tree_height > 0:
|
|
418
|
-
# Calculate crown base
|
|
477
|
+
# Calculate tree structure: trunk base to crown base to crown top
|
|
419
478
|
crown_base_height = (tree_height * trunk_height_ratio)
|
|
420
479
|
crown_base_height_level = int(crown_base_height / voxel_size + 0.5)
|
|
421
480
|
crown_top_height = tree_height
|
|
422
481
|
crown_top_height_level = int(crown_top_height / voxel_size + 0.5)
|
|
423
482
|
|
|
424
483
|
# Ensure minimum crown height of 1 voxel
|
|
484
|
+
# Prevent crown base and top from being at the same level
|
|
425
485
|
if (crown_top_height_level == crown_base_height_level) and (crown_base_height_level>0):
|
|
426
486
|
crown_base_height_level -= 1
|
|
427
487
|
|
|
428
|
-
# Calculate
|
|
488
|
+
# Calculate absolute positions relative to ground level
|
|
429
489
|
tree_start = ground_level + crown_base_height_level
|
|
430
490
|
tree_end = ground_level + crown_top_height_level
|
|
431
491
|
|
|
432
|
-
# Fill tree crown voxels with -2
|
|
492
|
+
# Fill tree crown voxels with -2 (represents vegetation canopy)
|
|
433
493
|
voxel_grid[i, j, tree_start:tree_end] = -2
|
|
434
494
|
|
|
435
|
-
# Process buildings - handle multiple height segments
|
|
495
|
+
# Process buildings - handle multiple height segments per building
|
|
496
|
+
# Some buildings may have multiple levels or complex height profiles
|
|
436
497
|
for k in building_min_height_grid[i, j]:
|
|
437
498
|
building_min_height = int(k[0] / voxel_size + 0.5) # Lower height of building segment
|
|
438
499
|
building_height = int(k[1] / voxel_size + 0.5) # Upper height of building segment
|
|
439
|
-
# Fill building voxels with -3
|
|
500
|
+
# Fill building voxels with -3 (represents built structures)
|
|
440
501
|
voxel_grid[i, j, ground_level+building_min_height:ground_level+building_height] = -3
|
|
441
502
|
|
|
442
503
|
return voxel_grid
|
|
@@ -558,73 +619,89 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
|
|
|
558
619
|
- dem_grid: 2D grid of ground elevation
|
|
559
620
|
- building_geojson: GeoJSON of building footprints and metadata
|
|
560
621
|
"""
|
|
561
|
-
#
|
|
622
|
+
# Set up output directory for intermediate and final files
|
|
562
623
|
output_dir = kwargs.get("output_dir", "output")
|
|
563
624
|
os.makedirs(output_dir, exist_ok=True)
|
|
564
625
|
|
|
565
|
-
# Remove 'output_dir' from kwargs to prevent duplication
|
|
626
|
+
# Remove 'output_dir' from kwargs to prevent duplication in function calls
|
|
566
627
|
kwargs.pop('output_dir', None)
|
|
567
628
|
|
|
568
|
-
# Generate all required 2D grids
|
|
629
|
+
# STEP 1: Generate all required 2D grids from various data sources
|
|
630
|
+
# These grids form the foundation for the 3D voxel model
|
|
631
|
+
|
|
632
|
+
# Land cover classification grid (e.g., urban, forest, water, agriculture)
|
|
569
633
|
land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
|
|
634
|
+
|
|
635
|
+
# Building footprints and height information
|
|
570
636
|
building_height_grid, building_min_height_grid, building_id_grid, building_gdf = get_building_height_grid(rectangle_vertices, meshsize, building_source, output_dir, **kwargs)
|
|
571
637
|
|
|
572
|
-
# Save building data to
|
|
638
|
+
# Save building data to file for later analysis or visualization
|
|
573
639
|
if not building_gdf.empty:
|
|
574
640
|
save_path = f"{output_dir}/building.gpkg"
|
|
575
641
|
building_gdf.to_file(save_path, driver='GPKG')
|
|
576
642
|
|
|
577
|
-
#
|
|
643
|
+
# STEP 2: Handle canopy height data
|
|
644
|
+
# Either use static values or fetch from satellite sources
|
|
578
645
|
if canopy_height_source == "Static":
|
|
579
|
-
# Create canopy height
|
|
646
|
+
# Create uniform canopy height for all tree-covered areas
|
|
580
647
|
canopy_height_grid = np.zeros_like(land_cover_grid, dtype=float)
|
|
581
648
|
|
|
582
|
-
#
|
|
649
|
+
# Apply static height to areas classified as trees
|
|
650
|
+
# Default height represents typical urban tree height
|
|
583
651
|
static_tree_height = kwargs.get("static_tree_height", 10.0)
|
|
584
652
|
tree_mask = (land_cover_grid == 4)
|
|
585
653
|
|
|
586
654
|
# Set static height for tree cells
|
|
587
655
|
canopy_height_grid[tree_mask] = static_tree_height
|
|
588
656
|
else:
|
|
657
|
+
# Fetch canopy height from satellite/remote sensing sources
|
|
589
658
|
canopy_height_grid = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
|
|
590
659
|
|
|
591
|
-
#
|
|
660
|
+
# STEP 3: Handle digital elevation model (terrain)
|
|
592
661
|
if dem_source == "Flat":
|
|
662
|
+
# Create flat terrain for simplified modeling
|
|
593
663
|
dem_grid = np.zeros_like(land_cover_grid)
|
|
594
664
|
else:
|
|
665
|
+
# Fetch terrain elevation from various sources
|
|
595
666
|
dem_grid = get_dem_grid(rectangle_vertices, meshsize, dem_source, output_dir, **kwargs)
|
|
596
667
|
|
|
597
|
-
# Apply
|
|
668
|
+
# STEP 4: Apply optional data filtering and cleaning
|
|
669
|
+
|
|
670
|
+
# Filter out low vegetation that may be noise in the data
|
|
598
671
|
min_canopy_height = kwargs.get("min_canopy_height")
|
|
599
672
|
if min_canopy_height is not None:
|
|
600
673
|
canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
|
|
601
674
|
|
|
602
|
-
# Remove objects near
|
|
675
|
+
# Remove objects near the boundary to avoid edge effects
|
|
676
|
+
# This is useful when the area of interest is part of a larger urban area
|
|
603
677
|
remove_perimeter_object = kwargs.get("remove_perimeter_object")
|
|
604
678
|
if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
|
|
605
679
|
# Calculate perimeter width based on grid dimensions
|
|
606
680
|
w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
|
|
607
681
|
h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
|
|
608
682
|
|
|
609
|
-
# Clear canopy heights in perimeter
|
|
683
|
+
# Clear canopy heights in perimeter areas
|
|
610
684
|
canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
|
|
611
685
|
|
|
612
|
-
#
|
|
686
|
+
# Identify buildings that intersect with perimeter areas
|
|
613
687
|
ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
|
|
614
688
|
ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
|
|
615
689
|
ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
|
|
616
690
|
ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
|
|
617
691
|
remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
|
|
618
692
|
|
|
619
|
-
# Remove buildings
|
|
693
|
+
# Remove identified buildings from all grids
|
|
620
694
|
for remove_id in remove_ids:
|
|
621
695
|
positions = np.where(building_id_grid == remove_id)
|
|
622
696
|
building_height_grid[positions] = 0
|
|
623
697
|
building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
|
|
624
698
|
|
|
625
|
-
#
|
|
699
|
+
# STEP 5: Generate optional 2D visualizations on interactive maps
|
|
626
700
|
mapvis = kwargs.get("mapvis")
|
|
627
701
|
if mapvis:
|
|
702
|
+
# Create map-based visualizations of all data layers
|
|
703
|
+
# These help users understand the input data before 3D modeling
|
|
704
|
+
|
|
628
705
|
# Visualize land cover using the new function
|
|
629
706
|
visualize_landcover_grid_on_basemap(
|
|
630
707
|
land_cover_grid,
|
|
@@ -676,20 +753,22 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
|
|
|
676
753
|
show_edge=False
|
|
677
754
|
)
|
|
678
755
|
|
|
679
|
-
# Generate 3D voxel
|
|
756
|
+
# STEP 6: Generate the final 3D voxel model
|
|
757
|
+
# This combines all 2D grids into a comprehensive 3D representation
|
|
680
758
|
voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source)
|
|
681
759
|
|
|
682
|
-
#
|
|
760
|
+
# STEP 7: Generate optional 3D visualization
|
|
683
761
|
voxelvis = kwargs.get("voxelvis")
|
|
684
762
|
if voxelvis:
|
|
685
|
-
# Create taller
|
|
763
|
+
# Create a taller grid for better visualization
|
|
764
|
+
# Fixed height ensures consistent camera positioning
|
|
686
765
|
new_height = int(550/meshsize+0.5)
|
|
687
766
|
voxcity_grid_vis = np.zeros((voxcity_grid.shape[0], voxcity_grid.shape[1], new_height))
|
|
688
767
|
voxcity_grid_vis[:, :, :voxcity_grid.shape[2]] = voxcity_grid
|
|
689
768
|
voxcity_grid_vis[-1, -1, -1] = -99 # Add marker to fix camera location and angle of view
|
|
690
769
|
visualize_3d_voxel(voxcity_grid_vis, voxel_size=meshsize, save_path=kwargs["voxelvis_img_save_path"])
|
|
691
770
|
|
|
692
|
-
# Save all data
|
|
771
|
+
# STEP 8: Save all generated data for future use
|
|
693
772
|
save_voxcity = kwargs.get("save_voxctiy_data", True)
|
|
694
773
|
if save_voxcity:
|
|
695
774
|
save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity_data.pkl")
|
|
@@ -845,18 +924,19 @@ def replace_nan_in_nested(arr, replace_value=10.0):
|
|
|
845
924
|
Returns:
|
|
846
925
|
Numpy array with NaN values replaced
|
|
847
926
|
"""
|
|
848
|
-
# Convert array to list for easier manipulation
|
|
927
|
+
# Convert array to list for easier manipulation of nested structures
|
|
849
928
|
arr = arr.tolist()
|
|
850
929
|
|
|
851
|
-
# Iterate through all dimensions
|
|
930
|
+
# Iterate through all dimensions of the nested array
|
|
852
931
|
for i in range(len(arr)):
|
|
853
932
|
for j in range(len(arr[i])):
|
|
854
|
-
# Check if the element is a list
|
|
933
|
+
# Check if the element is a list (building height segments)
|
|
855
934
|
if arr[i][j]: # if not empty list
|
|
856
935
|
for k in range(len(arr[i][j])):
|
|
857
|
-
# For each innermost list
|
|
936
|
+
# For each innermost list (individual height segment)
|
|
858
937
|
if isinstance(arr[i][j][k], list):
|
|
859
938
|
for l in range(len(arr[i][j][k])):
|
|
939
|
+
# Replace NaN values with the specified replacement value
|
|
860
940
|
if isinstance(arr[i][j][k][l], float) and np.isnan(arr[i][j][k][l]):
|
|
861
941
|
arr[i][j][k][l] = replace_value
|
|
862
942
|
|
|
@@ -883,10 +963,11 @@ def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_
|
|
|
883
963
|
import pickle
|
|
884
964
|
import os
|
|
885
965
|
|
|
886
|
-
#
|
|
966
|
+
# Ensure the output directory exists
|
|
887
967
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
888
968
|
|
|
889
|
-
# Create a dictionary
|
|
969
|
+
# Create a comprehensive dictionary containing all voxcity data
|
|
970
|
+
# This preserves all components needed to reconstruct or analyze the model
|
|
890
971
|
data_dict = {
|
|
891
972
|
'voxcity_grid': voxcity_grid,
|
|
892
973
|
'building_height_grid': building_height_grid,
|
|
@@ -900,7 +981,8 @@ def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_
|
|
|
900
981
|
'rectangle_vertices': rectangle_vertices
|
|
901
982
|
}
|
|
902
983
|
|
|
903
|
-
#
|
|
984
|
+
# Serialize and save the data using pickle for efficient storage
|
|
985
|
+
# Pickle preserves exact data types and structures
|
|
904
986
|
with open(output_path, 'wb') as f:
|
|
905
987
|
pickle.dump(data_dict, f)
|
|
906
988
|
|
|
@@ -927,13 +1009,14 @@ def load_voxcity_data(input_path):
|
|
|
927
1009
|
"""
|
|
928
1010
|
import pickle
|
|
929
1011
|
|
|
930
|
-
#
|
|
1012
|
+
# Deserialize the data from the saved file
|
|
931
1013
|
with open(input_path, 'rb') as f:
|
|
932
1014
|
data_dict = pickle.load(f)
|
|
933
1015
|
|
|
934
1016
|
print(f"Voxcity data loaded from {input_path}")
|
|
935
1017
|
|
|
936
|
-
# Return all components as a tuple
|
|
1018
|
+
# Return all components as a tuple in the same order as the main function
|
|
1019
|
+
# This ensures compatibility with existing code that expects this structure
|
|
937
1020
|
return (
|
|
938
1021
|
data_dict['voxcity_grid'],
|
|
939
1022
|
data_dict['building_height_grid'],
|