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,379 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from ..downloader.mbfp import get_mbfp_gdf
|
|
5
|
+
from ..downloader.osm import load_gdf_from_openstreetmap, load_land_cover_gdf_from_osm
|
|
6
|
+
from ..downloader.oemj import save_oemj_as_geotiff
|
|
7
|
+
from ..downloader.eubucco import load_gdf_from_eubucco
|
|
8
|
+
from ..downloader.overture import load_gdf_from_overture
|
|
9
|
+
from ..downloader.gba import load_gdf_from_gba
|
|
10
|
+
|
|
11
|
+
from ..downloader.gee import (
|
|
12
|
+
initialize_earth_engine,
|
|
13
|
+
get_roi,
|
|
14
|
+
get_ee_image_collection,
|
|
15
|
+
get_ee_image,
|
|
16
|
+
save_geotiff,
|
|
17
|
+
get_dem_image,
|
|
18
|
+
save_geotiff_esa_land_cover,
|
|
19
|
+
save_geotiff_esri_landcover,
|
|
20
|
+
save_geotiff_dynamic_world_v1,
|
|
21
|
+
save_geotiff_open_buildings_temporal,
|
|
22
|
+
save_geotiff_dsm_minus_dtm,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from ..geoprocessor.raster import (
|
|
26
|
+
process_grid,
|
|
27
|
+
create_land_cover_grid_from_geotiff_polygon,
|
|
28
|
+
create_height_grid_from_geotiff_polygon,
|
|
29
|
+
create_building_height_grid_from_gdf_polygon,
|
|
30
|
+
create_dem_grid_from_geotiff_polygon,
|
|
31
|
+
create_land_cover_grid_from_gdf_polygon,
|
|
32
|
+
create_building_height_grid_from_open_building_temporal_polygon,
|
|
33
|
+
create_canopy_grids_from_tree_gdf,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from ..utils.lc import convert_land_cover_array, get_land_cover_classes
|
|
37
|
+
from ..geoprocessor.io import get_gdf_from_gpkg
|
|
38
|
+
from ..visualizer.grids import visualize_land_cover_grid, visualize_numerical_grid
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Track last effective land cover source to help downstream components (e.g., voxelizer)
|
|
42
|
+
_LAST_EFFECTIVE_LC_SOURCE = None
|
|
43
|
+
|
|
44
|
+
def get_last_effective_land_cover_source():
|
|
45
|
+
return _LAST_EFFECTIVE_LC_SOURCE
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
49
|
+
print("Creating Land Use Land Cover grid\n ")
|
|
50
|
+
print(f"Data source: {source}")
|
|
51
|
+
|
|
52
|
+
if source not in ["OpenStreetMap", "OpenEarthMapJapan"]:
|
|
53
|
+
try:
|
|
54
|
+
initialize_earth_engine()
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print("Earth Engine unavailable (", str(e), ") — falling back to OpenStreetMap for land cover.")
|
|
57
|
+
source = 'OpenStreetMap'
|
|
58
|
+
|
|
59
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
60
|
+
geotiff_path = os.path.join(output_dir, "land_cover.tif")
|
|
61
|
+
|
|
62
|
+
# Track effective source to allow fallback behavior
|
|
63
|
+
effective_source = source
|
|
64
|
+
|
|
65
|
+
if source == 'Urbanwatch':
|
|
66
|
+
roi = get_roi(rectangle_vertices)
|
|
67
|
+
collection_name = "projects/sat-io/open-datasets/HRLC/urban-watch-cities"
|
|
68
|
+
try:
|
|
69
|
+
image = get_ee_image_collection(collection_name, roi)
|
|
70
|
+
# If collection is empty, image operations may fail; guard with try/except
|
|
71
|
+
save_geotiff(image, geotiff_path)
|
|
72
|
+
if (not os.path.exists(geotiff_path)) or (os.path.getsize(geotiff_path) == 0):
|
|
73
|
+
raise RuntimeError("Urbanwatch export produced no file")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print("Urbanwatch coverage not found for AOI; falling back to OpenStreetMap (reason:", str(e), ")")
|
|
76
|
+
effective_source = 'OpenStreetMap'
|
|
77
|
+
land_cover_gdf = load_land_cover_gdf_from_osm(rectangle_vertices)
|
|
78
|
+
elif source == 'ESA WorldCover':
|
|
79
|
+
roi = get_roi(rectangle_vertices)
|
|
80
|
+
save_geotiff_esa_land_cover(roi, geotiff_path)
|
|
81
|
+
elif source == 'ESRI 10m Annual Land Cover':
|
|
82
|
+
esri_landcover_year = kwargs.get("esri_landcover_year")
|
|
83
|
+
roi = get_roi(rectangle_vertices)
|
|
84
|
+
save_geotiff_esri_landcover(roi, geotiff_path, year=esri_landcover_year)
|
|
85
|
+
elif source == 'Dynamic World V1':
|
|
86
|
+
dynamic_world_date = kwargs.get("dynamic_world_date")
|
|
87
|
+
roi = get_roi(rectangle_vertices)
|
|
88
|
+
save_geotiff_dynamic_world_v1(roi, geotiff_path, dynamic_world_date)
|
|
89
|
+
elif source == 'OpenEarthMapJapan':
|
|
90
|
+
ssl_verify = kwargs.get('ssl_verify', kwargs.get('verify', True))
|
|
91
|
+
allow_insecure_ssl = kwargs.get('allow_insecure_ssl', False)
|
|
92
|
+
allow_http_fallback = kwargs.get('allow_http_fallback', False)
|
|
93
|
+
timeout_s = kwargs.get('timeout', 30)
|
|
94
|
+
|
|
95
|
+
save_oemj_as_geotiff(
|
|
96
|
+
rectangle_vertices,
|
|
97
|
+
geotiff_path,
|
|
98
|
+
ssl_verify=ssl_verify,
|
|
99
|
+
allow_insecure_ssl=allow_insecure_ssl,
|
|
100
|
+
allow_http_fallback=allow_http_fallback,
|
|
101
|
+
timeout_s=timeout_s,
|
|
102
|
+
)
|
|
103
|
+
if not os.path.exists(geotiff_path):
|
|
104
|
+
raise FileNotFoundError(
|
|
105
|
+
f"OEMJ download failed; expected GeoTIFF not found: {geotiff_path}. "
|
|
106
|
+
"You can try setting ssl_verify=False or allow_http_fallback=True in kwargs."
|
|
107
|
+
)
|
|
108
|
+
elif source == 'OpenStreetMap':
|
|
109
|
+
land_cover_gdf = load_land_cover_gdf_from_osm(rectangle_vertices)
|
|
110
|
+
|
|
111
|
+
land_cover_classes = get_land_cover_classes(effective_source)
|
|
112
|
+
|
|
113
|
+
if effective_source == 'OpenStreetMap':
|
|
114
|
+
default_class = kwargs.get('default_land_cover_class', 'Developed space')
|
|
115
|
+
land_cover_grid_str = create_land_cover_grid_from_gdf_polygon(land_cover_gdf, meshsize, effective_source, rectangle_vertices, default_class=default_class)
|
|
116
|
+
else:
|
|
117
|
+
land_cover_grid_str = create_land_cover_grid_from_geotiff_polygon(geotiff_path, meshsize, land_cover_classes, rectangle_vertices)
|
|
118
|
+
|
|
119
|
+
color_map = {cls: [r/255, g/255, b/255] for (r,g,b), cls in land_cover_classes.items()}
|
|
120
|
+
|
|
121
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
122
|
+
if grid_vis:
|
|
123
|
+
visualize_land_cover_grid(np.flipud(land_cover_grid_str), meshsize, color_map, land_cover_classes)
|
|
124
|
+
|
|
125
|
+
# Record effective source for downstream consumers
|
|
126
|
+
global _LAST_EFFECTIVE_LC_SOURCE
|
|
127
|
+
_LAST_EFFECTIVE_LC_SOURCE = effective_source
|
|
128
|
+
|
|
129
|
+
land_cover_grid_int = convert_land_cover_array(land_cover_grid_str, land_cover_classes)
|
|
130
|
+
return land_cover_grid_int
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, building_gdf=None, **kwargs):
|
|
134
|
+
ee_required_sources = {"Open Building 2.5D Temporal"}
|
|
135
|
+
if source in ee_required_sources:
|
|
136
|
+
initialize_earth_engine()
|
|
137
|
+
|
|
138
|
+
print("Creating Building Height grid\n ")
|
|
139
|
+
print(f"Base data source: {source}")
|
|
140
|
+
|
|
141
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
142
|
+
|
|
143
|
+
if building_gdf is not None:
|
|
144
|
+
gdf = building_gdf
|
|
145
|
+
print("Using provided GeoDataFrame for building data")
|
|
146
|
+
else:
|
|
147
|
+
floor_height = kwargs.get("floor_height", 3.0)
|
|
148
|
+
if source == 'Microsoft Building Footprints':
|
|
149
|
+
gdf = get_mbfp_gdf(output_dir, rectangle_vertices)
|
|
150
|
+
elif source == 'OpenStreetMap':
|
|
151
|
+
gdf = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
|
|
152
|
+
elif source == "Open Building 2.5D Temporal":
|
|
153
|
+
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)
|
|
154
|
+
elif source == 'EUBUCCO v0.1':
|
|
155
|
+
gdf = load_gdf_from_eubucco(rectangle_vertices, output_dir)
|
|
156
|
+
elif source == "Overture":
|
|
157
|
+
gdf = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
|
|
158
|
+
elif source in ("GBA", "Global Building Atlas"):
|
|
159
|
+
clip_gba = kwargs.get("gba_clip", False)
|
|
160
|
+
gba_download_dir = kwargs.get("gba_download_dir")
|
|
161
|
+
gdf = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
|
|
162
|
+
elif source == "Local file":
|
|
163
|
+
_, extension = os.path.splitext(kwargs["building_path"])
|
|
164
|
+
if extension == ".gpkg":
|
|
165
|
+
gdf = get_gdf_from_gpkg(kwargs["building_path"], rectangle_vertices)
|
|
166
|
+
elif source == "GeoDataFrame":
|
|
167
|
+
raise ValueError("When source is 'GeoDataFrame', building_gdf parameter must be provided")
|
|
168
|
+
|
|
169
|
+
building_complementary_source = kwargs.get("building_complementary_source")
|
|
170
|
+
try:
|
|
171
|
+
comp_label = building_complementary_source if building_complementary_source not in (None, "") else "None"
|
|
172
|
+
print(f"Complementary data source: {comp_label}")
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
building_complement_height = kwargs.get("building_complement_height")
|
|
176
|
+
overlapping_footprint = kwargs.get("overlapping_footprint", "auto")
|
|
177
|
+
|
|
178
|
+
if (building_complementary_source is None) or (building_complementary_source=='None'):
|
|
179
|
+
if source != "Open Building 2.5D Temporal":
|
|
180
|
+
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, overlapping_footprint=overlapping_footprint)
|
|
181
|
+
else:
|
|
182
|
+
if building_complementary_source == "Open Building 2.5D Temporal":
|
|
183
|
+
try:
|
|
184
|
+
roi = get_roi(rectangle_vertices)
|
|
185
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
186
|
+
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
187
|
+
save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
|
|
188
|
+
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, overlapping_footprint=overlapping_footprint)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print("Open Building 2.5D Temporal requires Earth Engine (", str(e), ") — proceeding without complementary raster.")
|
|
191
|
+
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, overlapping_footprint=overlapping_footprint)
|
|
192
|
+
elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
|
|
193
|
+
try:
|
|
194
|
+
roi = get_roi(rectangle_vertices)
|
|
195
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
196
|
+
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
197
|
+
save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
|
|
198
|
+
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, overlapping_footprint=overlapping_footprint)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print("DSM-DTM complementary raster requires Earth Engine (", str(e), ") — proceeding without complementary raster.")
|
|
201
|
+
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, overlapping_footprint=overlapping_footprint)
|
|
202
|
+
else:
|
|
203
|
+
if building_complementary_source == 'Microsoft Building Footprints':
|
|
204
|
+
gdf_comp = get_mbfp_gdf(output_dir, rectangle_vertices)
|
|
205
|
+
elif building_complementary_source == 'OpenStreetMap':
|
|
206
|
+
gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
|
|
207
|
+
elif building_complementary_source == 'EUBUCCO v0.1':
|
|
208
|
+
gdf_comp = load_gdf_from_eubucco(rectangle_vertices, output_dir)
|
|
209
|
+
elif building_complementary_source == "Overture":
|
|
210
|
+
gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
|
|
211
|
+
elif building_complementary_source in ("GBA", "Global Building Atlas"):
|
|
212
|
+
clip_gba = kwargs.get("gba_clip", False)
|
|
213
|
+
gba_download_dir = kwargs.get("gba_download_dir")
|
|
214
|
+
gdf_comp = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
|
|
215
|
+
elif building_complementary_source == "Local file":
|
|
216
|
+
_, extension = os.path.splitext(kwargs["building_complementary_path"])
|
|
217
|
+
if extension == ".gpkg":
|
|
218
|
+
gdf_comp = get_gdf_from_gpkg(kwargs["building_complementary_path"], rectangle_vertices)
|
|
219
|
+
|
|
220
|
+
complement_building_footprints = kwargs.get("complement_building_footprints")
|
|
221
|
+
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, overlapping_footprint=overlapping_footprint)
|
|
222
|
+
|
|
223
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
224
|
+
if grid_vis:
|
|
225
|
+
building_height_grid_nan = building_height_grid.copy()
|
|
226
|
+
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
227
|
+
visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
|
|
228
|
+
|
|
229
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_canopy_height_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
233
|
+
print("Creating Canopy Height grid\n ")
|
|
234
|
+
print(f"Data source: {source}")
|
|
235
|
+
|
|
236
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
237
|
+
|
|
238
|
+
# Explicit static path (no EE): use land cover mask with static height
|
|
239
|
+
if source == 'Static':
|
|
240
|
+
land_cover_grid = kwargs.get('land_cover_like')
|
|
241
|
+
if land_cover_grid is None:
|
|
242
|
+
# Minimal fallback if caller didn't provide land_cover_like
|
|
243
|
+
canopy_top = np.zeros((1, 1), dtype=float)
|
|
244
|
+
trunk_height_ratio = kwargs.get('trunk_height_ratio')
|
|
245
|
+
if trunk_height_ratio is None:
|
|
246
|
+
trunk_height_ratio = 11.76 / 19.98
|
|
247
|
+
canopy_bottom = canopy_top * float(trunk_height_ratio)
|
|
248
|
+
return canopy_top, canopy_bottom
|
|
249
|
+
|
|
250
|
+
from ..utils.lc import get_land_cover_classes
|
|
251
|
+
land_cover_source = kwargs.get('land_cover_source', 'OpenStreetMap')
|
|
252
|
+
classes_map = get_land_cover_classes(land_cover_source)
|
|
253
|
+
class_to_int = {name: i for i, name in enumerate(classes_map.values())}
|
|
254
|
+
tree_labels = ["Tree", "Trees", "Tree Canopy"]
|
|
255
|
+
tree_indices = [class_to_int[label] for label in tree_labels if label in class_to_int]
|
|
256
|
+
|
|
257
|
+
canopy_top = np.zeros_like(land_cover_grid, dtype=float)
|
|
258
|
+
static_tree_height = kwargs.get('static_tree_height', 10.0)
|
|
259
|
+
tree_mask = np.isin(land_cover_grid, tree_indices) if tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
|
|
260
|
+
canopy_top[tree_mask] = static_tree_height
|
|
261
|
+
|
|
262
|
+
trunk_height_ratio = kwargs.get('trunk_height_ratio')
|
|
263
|
+
if trunk_height_ratio is None:
|
|
264
|
+
trunk_height_ratio = 11.76 / 19.98
|
|
265
|
+
canopy_bottom = canopy_top * float(trunk_height_ratio)
|
|
266
|
+
|
|
267
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
268
|
+
if grid_vis:
|
|
269
|
+
vis = canopy_top.copy(); vis[vis == 0] = np.nan
|
|
270
|
+
visualize_numerical_grid(np.flipud(vis), meshsize, "Tree canopy height (top)", cmap='Greens', label='Tree canopy height (m)')
|
|
271
|
+
return canopy_top, canopy_bottom
|
|
272
|
+
|
|
273
|
+
if source in ('GeoDataFrame', 'tree_gdf', 'Tree_GeoDataFrame', 'GDF'):
|
|
274
|
+
tree_gdf = kwargs.get('tree_gdf')
|
|
275
|
+
tree_gdf_path = kwargs.get('tree_gdf_path')
|
|
276
|
+
if tree_gdf is None and tree_gdf_path is not None:
|
|
277
|
+
_, ext = os.path.splitext(tree_gdf_path)
|
|
278
|
+
if ext.lower() == '.gpkg':
|
|
279
|
+
tree_gdf = get_gdf_from_gpkg(tree_gdf_path, rectangle_vertices)
|
|
280
|
+
else:
|
|
281
|
+
raise ValueError("Unsupported tree file format. Use .gpkg or pass a GeoDataFrame.")
|
|
282
|
+
if tree_gdf is None:
|
|
283
|
+
raise ValueError("When source='GeoDataFrame', provide 'tree_gdf' or 'tree_gdf_path'.")
|
|
284
|
+
|
|
285
|
+
canopy_top, canopy_bottom = create_canopy_grids_from_tree_gdf(tree_gdf, meshsize, rectangle_vertices)
|
|
286
|
+
|
|
287
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
288
|
+
if grid_vis:
|
|
289
|
+
vis = canopy_top.copy()
|
|
290
|
+
vis[vis == 0] = np.nan
|
|
291
|
+
visualize_numerical_grid(np.flipud(vis), meshsize, "Tree canopy height (top)", cmap='Greens', label='Tree canopy height (m)')
|
|
292
|
+
|
|
293
|
+
return canopy_top, canopy_bottom
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
initialize_earth_engine()
|
|
297
|
+
except Exception as e:
|
|
298
|
+
print("Earth Engine unavailable (", str(e), ") — falling back to Static canopy heights.")
|
|
299
|
+
# Re-enter with explicit Static logic using land cover mask
|
|
300
|
+
return get_canopy_height_grid(rectangle_vertices, meshsize, 'Static', output_dir, **kwargs)
|
|
301
|
+
|
|
302
|
+
geotiff_path = os.path.join(output_dir, "canopy_height.tif")
|
|
303
|
+
|
|
304
|
+
roi = get_roi(rectangle_vertices)
|
|
305
|
+
if source == 'High Resolution 1m Global Canopy Height Maps':
|
|
306
|
+
collection_name = "projects/meta-forest-monitoring-okw37/assets/CanopyHeight"
|
|
307
|
+
image = get_ee_image_collection(collection_name, roi)
|
|
308
|
+
elif source == 'ETH Global Sentinel-2 10m Canopy Height (2020)':
|
|
309
|
+
collection_name = "users/nlang/ETH_GlobalCanopyHeight_2020_10m_v1"
|
|
310
|
+
image = get_ee_image(collection_name, roi)
|
|
311
|
+
else:
|
|
312
|
+
raise ValueError(f"Unsupported canopy source: {source}")
|
|
313
|
+
|
|
314
|
+
save_geotiff(image, geotiff_path, resolution=meshsize)
|
|
315
|
+
canopy_height_grid = create_height_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices)
|
|
316
|
+
|
|
317
|
+
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
318
|
+
if trunk_height_ratio is None:
|
|
319
|
+
trunk_height_ratio = 11.76 / 19.98
|
|
320
|
+
canopy_bottom_grid = canopy_height_grid * float(trunk_height_ratio)
|
|
321
|
+
|
|
322
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
323
|
+
if grid_vis:
|
|
324
|
+
canopy_height_grid_nan = canopy_height_grid.copy()
|
|
325
|
+
canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
|
|
326
|
+
visualize_numerical_grid(np.flipud(canopy_height_grid_nan), meshsize, "Tree canopy height", cmap='Greens', label='Tree canopy height (m)')
|
|
327
|
+
return canopy_height_grid, canopy_bottom_grid
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
331
|
+
print("Creating Digital Elevation Model (DEM) grid\n ")
|
|
332
|
+
print(f"Data source: {source}")
|
|
333
|
+
|
|
334
|
+
if source == "Local file":
|
|
335
|
+
geotiff_path = kwargs["dem_path"]
|
|
336
|
+
else:
|
|
337
|
+
try:
|
|
338
|
+
initialize_earth_engine()
|
|
339
|
+
except Exception as e:
|
|
340
|
+
print("Earth Engine unavailable (", str(e), ") — falling back to flat DEM.")
|
|
341
|
+
dem_interpolation = kwargs.get("dem_interpolation")
|
|
342
|
+
# Return flat DEM (zeros) with same shape as would be produced after rasterization
|
|
343
|
+
# We defer to downstream to handle zeros appropriately.
|
|
344
|
+
# To avoid shape inference here, we'll build after default path below.
|
|
345
|
+
geotiff_path = None
|
|
346
|
+
# Bypass EE path and produce zeros later
|
|
347
|
+
dem_grid = np.zeros((1, 1), dtype=float)
|
|
348
|
+
# Build shape using land cover grid shape if provided via kwargs for robustness
|
|
349
|
+
lc_like = kwargs.get("land_cover_like")
|
|
350
|
+
if lc_like is not None:
|
|
351
|
+
dem_grid = np.zeros_like(lc_like)
|
|
352
|
+
return dem_grid
|
|
353
|
+
|
|
354
|
+
geotiff_path = os.path.join(output_dir, "dem.tif")
|
|
355
|
+
|
|
356
|
+
buffer_distance = 100
|
|
357
|
+
roi = get_roi(rectangle_vertices)
|
|
358
|
+
roi_buffered = roi.buffer(buffer_distance)
|
|
359
|
+
|
|
360
|
+
image = get_dem_image(roi_buffered, source)
|
|
361
|
+
|
|
362
|
+
if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM', 'Netherlands 0.5m DTM']:
|
|
363
|
+
save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
|
|
364
|
+
elif source == 'USGS 3DEP 1m':
|
|
365
|
+
scale = max(meshsize, 1.25)
|
|
366
|
+
save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
|
|
367
|
+
else:
|
|
368
|
+
save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
|
|
369
|
+
|
|
370
|
+
dem_interpolation = kwargs.get("dem_interpolation")
|
|
371
|
+
dem_grid = create_dem_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices, dem_interpolation=dem_interpolation)
|
|
372
|
+
|
|
373
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
374
|
+
if grid_vis:
|
|
375
|
+
visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
|
|
376
|
+
|
|
377
|
+
return dem_grid
|
|
378
|
+
|
|
379
|
+
|
voxcity/generator/io.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
2
|
+
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
3
|
+
building_gdf, meshsize, rectangle_vertices):
|
|
4
|
+
import pickle
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
8
|
+
|
|
9
|
+
data_dict = {
|
|
10
|
+
'voxcity_grid': voxcity_grid,
|
|
11
|
+
'building_height_grid': building_height_grid,
|
|
12
|
+
'building_min_height_grid': building_min_height_grid,
|
|
13
|
+
'building_id_grid': building_id_grid,
|
|
14
|
+
'canopy_height_grid': canopy_height_grid,
|
|
15
|
+
'land_cover_grid': land_cover_grid,
|
|
16
|
+
'dem_grid': dem_grid,
|
|
17
|
+
'building_gdf': building_gdf,
|
|
18
|
+
'meshsize': meshsize,
|
|
19
|
+
'rectangle_vertices': rectangle_vertices
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
with open(output_path, 'wb') as f:
|
|
23
|
+
pickle.dump(data_dict, f)
|
|
24
|
+
|
|
25
|
+
print(f"Voxcity data saved to {output_path}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_voxcity(input_path):
|
|
29
|
+
import pickle
|
|
30
|
+
import numpy as np
|
|
31
|
+
from ..models import GridMetadata, VoxelGrid, BuildingGrid, LandCoverGrid, DemGrid, CanopyGrid, VoxCity
|
|
32
|
+
|
|
33
|
+
with open(input_path, 'rb') as f:
|
|
34
|
+
obj = pickle.load(f)
|
|
35
|
+
|
|
36
|
+
# New format: the entire VoxCity object (optionally wrapped)
|
|
37
|
+
if isinstance(obj, VoxCity):
|
|
38
|
+
return obj
|
|
39
|
+
if isinstance(obj, dict) and obj.get('__format__') == 'voxcity.v2' and isinstance(obj.get('voxcity'), VoxCity):
|
|
40
|
+
return obj['voxcity']
|
|
41
|
+
|
|
42
|
+
# Legacy dict format fallback
|
|
43
|
+
d = obj
|
|
44
|
+
rv = d.get('rectangle_vertices') or []
|
|
45
|
+
if rv:
|
|
46
|
+
xs = [p[0] for p in rv]
|
|
47
|
+
ys = [p[1] for p in rv]
|
|
48
|
+
bounds = (min(xs), min(ys), max(xs), max(ys))
|
|
49
|
+
else:
|
|
50
|
+
ny, nx = d['land_cover_grid'].shape
|
|
51
|
+
ms = float(d['meshsize'])
|
|
52
|
+
bounds = (0.0, 0.0, nx * ms, ny * ms)
|
|
53
|
+
|
|
54
|
+
meta = GridMetadata(crs='EPSG:4326', bounds=bounds, meshsize=float(d['meshsize']))
|
|
55
|
+
|
|
56
|
+
voxels = VoxelGrid(classes=d['voxcity_grid'], meta=meta)
|
|
57
|
+
buildings = BuildingGrid(
|
|
58
|
+
heights=d['building_height_grid'],
|
|
59
|
+
min_heights=d['building_min_height_grid'],
|
|
60
|
+
ids=d['building_id_grid'],
|
|
61
|
+
meta=meta,
|
|
62
|
+
)
|
|
63
|
+
land = LandCoverGrid(classes=d['land_cover_grid'], meta=meta)
|
|
64
|
+
dem = DemGrid(elevation=d['dem_grid'], meta=meta)
|
|
65
|
+
canopy = CanopyGrid(top=d.get('canopy_height_grid'), bottom=None, meta=meta)
|
|
66
|
+
|
|
67
|
+
extras = {
|
|
68
|
+
'rectangle_vertices': d.get('rectangle_vertices'),
|
|
69
|
+
'building_gdf': d.get('building_gdf'),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return VoxCity(voxels=voxels, buildings=buildings, land_cover=land, dem=dem, tree_canopy=canopy, extras=extras)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def save_voxcity(output_path, city):
|
|
77
|
+
"""Save a VoxCity instance to disk, preserving the entire object."""
|
|
78
|
+
import pickle
|
|
79
|
+
import os
|
|
80
|
+
from ..models import VoxCity as _VoxCity
|
|
81
|
+
|
|
82
|
+
if not isinstance(city, _VoxCity):
|
|
83
|
+
raise TypeError("save_voxcity expects a VoxCity instance")
|
|
84
|
+
|
|
85
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
86
|
+
|
|
87
|
+
with open(output_path, 'wb') as f:
|
|
88
|
+
payload = {
|
|
89
|
+
'__format__': 'voxcity.v2',
|
|
90
|
+
'voxcity': city,
|
|
91
|
+
}
|
|
92
|
+
pickle.dump(payload, f)
|
|
93
|
+
|
|
94
|
+
print(f"Voxcity data saved to {output_path}")
|