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,928 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
import trimesh
|
|
6
|
+
import matplotlib.cm as cm
|
|
7
|
+
import matplotlib.colors as mcolors
|
|
8
|
+
try:
|
|
9
|
+
import plotly.graph_objects as go
|
|
10
|
+
except ImportError: # optional dependency
|
|
11
|
+
go = None # type: ignore
|
|
12
|
+
|
|
13
|
+
from ..models import VoxCity
|
|
14
|
+
from .builder import MeshBuilder
|
|
15
|
+
from .palette import get_voxel_color_map
|
|
16
|
+
from ..geoprocessor.mesh import create_sim_surface_mesh
|
|
17
|
+
try:
|
|
18
|
+
import pyvista as pv
|
|
19
|
+
except ImportError: # optional dependency
|
|
20
|
+
pv = None # type: ignore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _rgb_tuple_to_plotly_color(rgb_tuple):
|
|
24
|
+
"""
|
|
25
|
+
Convert [R, G, B] or (R, G, B) with 0-255 range to plotly 'rgb(r,g,b)' string.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
r, g, b = rgb_tuple
|
|
29
|
+
r = int(max(0, min(255, r)))
|
|
30
|
+
g = int(max(0, min(255, g)))
|
|
31
|
+
b = int(max(0, min(255, b)))
|
|
32
|
+
return f"rgb({r},{g},{b})"
|
|
33
|
+
except Exception:
|
|
34
|
+
return "rgb(128,128,128)"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mpl_cmap_to_plotly_colorscale(cmap_name, n=256):
|
|
38
|
+
"""
|
|
39
|
+
Convert a matplotlib colormap name to a Plotly colorscale list.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
cmap = cm.get_cmap(cmap_name)
|
|
43
|
+
except Exception:
|
|
44
|
+
cmap = cm.get_cmap('viridis')
|
|
45
|
+
if n < 2:
|
|
46
|
+
n = 2
|
|
47
|
+
scale = []
|
|
48
|
+
for i in range(n):
|
|
49
|
+
x = i / (n - 1)
|
|
50
|
+
r, g, b, _ = cmap(x)
|
|
51
|
+
scale.append([x, f"rgb({int(255*r)},{int(255*g)},{int(255*b)})"])
|
|
52
|
+
return scale
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def visualize_voxcity_plotly(
|
|
56
|
+
voxel_array,
|
|
57
|
+
meshsize,
|
|
58
|
+
classes=None,
|
|
59
|
+
voxel_color_map='default',
|
|
60
|
+
opacity=1.0,
|
|
61
|
+
max_dimension=160,
|
|
62
|
+
downsample=None,
|
|
63
|
+
title=None,
|
|
64
|
+
width=1000,
|
|
65
|
+
height=800,
|
|
66
|
+
show=True,
|
|
67
|
+
return_fig=False,
|
|
68
|
+
# Building simulation overlay
|
|
69
|
+
building_sim_mesh=None,
|
|
70
|
+
building_value_name='svf_values',
|
|
71
|
+
building_colormap='viridis',
|
|
72
|
+
building_vmin=None,
|
|
73
|
+
building_vmax=None,
|
|
74
|
+
building_nan_color='gray',
|
|
75
|
+
building_opacity=1.0,
|
|
76
|
+
building_shaded=False,
|
|
77
|
+
render_voxel_buildings=False,
|
|
78
|
+
# Ground simulation surface overlay
|
|
79
|
+
ground_sim_grid=None,
|
|
80
|
+
ground_dem_grid=None,
|
|
81
|
+
ground_z_offset=None,
|
|
82
|
+
ground_view_point_height=None,
|
|
83
|
+
ground_colormap='viridis',
|
|
84
|
+
ground_vmin=None,
|
|
85
|
+
ground_vmax=None,
|
|
86
|
+
sim_surface_opacity=0.95,
|
|
87
|
+
ground_shaded=False,
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Interactive 3D visualization using Plotly Mesh3d of voxel faces and optional overlays.
|
|
91
|
+
"""
|
|
92
|
+
# Validate optional dependency
|
|
93
|
+
if go is None:
|
|
94
|
+
raise ImportError("Plotly is required for interactive visualization. Install with: pip install plotly")
|
|
95
|
+
# Validate/prepare voxels
|
|
96
|
+
if voxel_array is None or getattr(voxel_array, 'ndim', 0) != 3:
|
|
97
|
+
if building_sim_mesh is None and (ground_sim_grid is None or ground_dem_grid is None):
|
|
98
|
+
raise ValueError("voxel_array must be a 3D numpy array when no overlays are provided")
|
|
99
|
+
vox = None
|
|
100
|
+
else:
|
|
101
|
+
vox = voxel_array
|
|
102
|
+
|
|
103
|
+
# Downsample strategy
|
|
104
|
+
stride = 1
|
|
105
|
+
if vox is not None:
|
|
106
|
+
if downsample is not None:
|
|
107
|
+
stride = max(1, int(downsample))
|
|
108
|
+
else:
|
|
109
|
+
nx_tmp, ny_tmp, nz_tmp = vox.shape
|
|
110
|
+
max_dim = max(nx_tmp, ny_tmp, nz_tmp)
|
|
111
|
+
if max_dim > max_dimension:
|
|
112
|
+
stride = int(np.ceil(max_dim / max_dimension))
|
|
113
|
+
if stride > 1:
|
|
114
|
+
# Surface-aware downsampling: stride X/Y, pick topmost non-zero along Z in each window
|
|
115
|
+
orig = voxel_array
|
|
116
|
+
nx0, ny0, nz0 = orig.shape
|
|
117
|
+
xs = orig[::stride, ::stride, :]
|
|
118
|
+
nx_ds, ny_ds, _ = xs.shape
|
|
119
|
+
nz_ds = int(np.ceil(nz0 / float(stride)))
|
|
120
|
+
vox = np.zeros((nx_ds, ny_ds, nz_ds), dtype=orig.dtype)
|
|
121
|
+
for k in range(nz_ds):
|
|
122
|
+
z0w = k * stride
|
|
123
|
+
z1w = min(z0w + stride, nz0)
|
|
124
|
+
W = xs[:, :, z0w:z1w]
|
|
125
|
+
if W.size == 0:
|
|
126
|
+
continue
|
|
127
|
+
nz_mask = (W != 0)
|
|
128
|
+
has_any = nz_mask.any(axis=2)
|
|
129
|
+
rev_mask = nz_mask[:, :, ::-1]
|
|
130
|
+
idx_rev = rev_mask.argmax(axis=2)
|
|
131
|
+
real_idx = (W.shape[2] - 1) - idx_rev
|
|
132
|
+
gathered = np.take_along_axis(W, real_idx[..., None], axis=2).squeeze(-1)
|
|
133
|
+
vox[:, :, k] = np.where(has_any, gathered, 0)
|
|
134
|
+
|
|
135
|
+
nx, ny, nz = vox.shape
|
|
136
|
+
dx = meshsize * stride
|
|
137
|
+
dy = meshsize * stride
|
|
138
|
+
dz = meshsize * stride
|
|
139
|
+
x = np.arange(nx, dtype=float) * dx
|
|
140
|
+
y = np.arange(ny, dtype=float) * dy
|
|
141
|
+
z = np.arange(nz, dtype=float) * dz
|
|
142
|
+
|
|
143
|
+
# Choose classes
|
|
144
|
+
if classes is None:
|
|
145
|
+
classes_all = np.unique(vox[vox != 0]).tolist()
|
|
146
|
+
else:
|
|
147
|
+
classes_all = list(classes)
|
|
148
|
+
if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
|
|
149
|
+
classes_to_draw = classes_all if render_voxel_buildings else [c for c in classes_all if int(c) != -3]
|
|
150
|
+
else:
|
|
151
|
+
classes_to_draw = classes_all
|
|
152
|
+
|
|
153
|
+
# Resolve colors
|
|
154
|
+
if isinstance(voxel_color_map, dict):
|
|
155
|
+
vox_dict = voxel_color_map
|
|
156
|
+
else:
|
|
157
|
+
vox_dict = get_voxel_color_map(voxel_color_map)
|
|
158
|
+
|
|
159
|
+
# Occluder mask (any occupancy)
|
|
160
|
+
if stride > 1:
|
|
161
|
+
def _bool_max_pool_3d(arr_bool, sx):
|
|
162
|
+
if isinstance(sx, (tuple, list, np.ndarray)):
|
|
163
|
+
sx, sy, sz = int(sx[0]), int(sx[1]), int(sx[2])
|
|
164
|
+
else:
|
|
165
|
+
sy = sz = int(sx)
|
|
166
|
+
sx = int(sx)
|
|
167
|
+
a = np.asarray(arr_bool, dtype=bool)
|
|
168
|
+
nx_, ny_, nz_ = a.shape
|
|
169
|
+
px = (sx - (nx_ % sx)) % sx
|
|
170
|
+
py = (sy - (ny_ % sy)) % sy
|
|
171
|
+
pz = (sz - (nz_ % sz)) % sz
|
|
172
|
+
if px or py or pz:
|
|
173
|
+
a = np.pad(a, ((0, px), (0, py), (0, pz)), constant_values=False)
|
|
174
|
+
nxp, nyp, nzp = a.shape
|
|
175
|
+
a = a.reshape(nxp // sx, sx, nyp // sy, sy, nzp // sz, sz)
|
|
176
|
+
a = a.max(axis=1).max(axis=2).max(axis=4)
|
|
177
|
+
return a
|
|
178
|
+
occluder = _bool_max_pool_3d((voxel_array != 0), stride)
|
|
179
|
+
else:
|
|
180
|
+
occluder = (vox != 0)
|
|
181
|
+
|
|
182
|
+
def exposed_face_masks(occ, occ_any):
|
|
183
|
+
p = np.pad(occ_any, ((0,1),(0,0),(0,0)), constant_values=False)
|
|
184
|
+
posx = occ & (~p[1:,:,:])
|
|
185
|
+
p = np.pad(occ_any, ((1,0),(0,0),(0,0)), constant_values=False)
|
|
186
|
+
negx = occ & (~p[:-1,:,:])
|
|
187
|
+
p = np.pad(occ_any, ((0,0),(0,1),(0,0)), constant_values=False)
|
|
188
|
+
posy = occ & (~p[:,1:,:])
|
|
189
|
+
p = np.pad(occ_any, ((0,0),(1,0),(0,0)), constant_values=False)
|
|
190
|
+
negy = occ & (~p[:,:-1,:])
|
|
191
|
+
p = np.pad(occ_any, ((0,0),(0,0),(0,1)), constant_values=False)
|
|
192
|
+
posz = occ & (~p[:,:,1:])
|
|
193
|
+
p = np.pad(occ_any, ((0,0),(0,0),(1,0)), constant_values=False)
|
|
194
|
+
negz = occ & (~p[:,:,:-1])
|
|
195
|
+
return posx, negx, posy, negy, posz, negz
|
|
196
|
+
|
|
197
|
+
fig = go.Figure()
|
|
198
|
+
|
|
199
|
+
def add_faces(mask, plane, color_rgb):
|
|
200
|
+
idx = np.argwhere(mask)
|
|
201
|
+
if idx.size == 0:
|
|
202
|
+
return
|
|
203
|
+
xi, yi, zi = idx[:,0], idx[:,1], idx[:,2]
|
|
204
|
+
xc = x[xi]; yc = y[yi]; zc = z[zi]
|
|
205
|
+
x0, x1 = xc - dx/2.0, xc + dx/2.0
|
|
206
|
+
y0, y1 = yc - dy/2.0, yc + dy/2.0
|
|
207
|
+
z0, z1 = zc - dz/2.0, zc + dz/2.0
|
|
208
|
+
|
|
209
|
+
if plane == '+x':
|
|
210
|
+
vx = np.stack([x1, x1, x1, x1], axis=1)
|
|
211
|
+
vy = np.stack([y0, y1, y1, y0], axis=1)
|
|
212
|
+
vz = np.stack([z0, z0, z1, z1], axis=1)
|
|
213
|
+
elif plane == '-x':
|
|
214
|
+
vx = np.stack([x0, x0, x0, x0], axis=1)
|
|
215
|
+
vy = np.stack([y0, y1, y1, y0], axis=1)
|
|
216
|
+
vz = np.stack([z1, z1, z0, z0], axis=1)
|
|
217
|
+
elif plane == '+y':
|
|
218
|
+
vx = np.stack([x0, x1, x1, x0], axis=1)
|
|
219
|
+
vy = np.stack([y1, y1, y1, y1], axis=1)
|
|
220
|
+
vz = np.stack([z0, z0, z1, z1], axis=1)
|
|
221
|
+
elif plane == '-y':
|
|
222
|
+
vx = np.stack([x0, x1, x1, x0], axis=1)
|
|
223
|
+
vy = np.stack([y0, y0, y0, y0], axis=1)
|
|
224
|
+
vz = np.stack([z1, z1, z0, z0], axis=1)
|
|
225
|
+
elif plane == '+z':
|
|
226
|
+
vx = np.stack([x0, x1, x1, x0], axis=1)
|
|
227
|
+
vy = np.stack([y0, y0, y1, y1], axis=1)
|
|
228
|
+
vz = np.stack([z1, z1, z1, z1], axis=1)
|
|
229
|
+
elif plane == '-z':
|
|
230
|
+
vx = np.stack([x0, x1, x1, x0], axis=1)
|
|
231
|
+
vy = np.stack([y1, y1, y0, y0], axis=1)
|
|
232
|
+
vz = np.stack([z0, z0, z0, z0], axis=1)
|
|
233
|
+
else:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
V = np.column_stack([vx.reshape(-1), vy.reshape(-1), vz.reshape(-1)])
|
|
237
|
+
n = idx.shape[0]
|
|
238
|
+
starts = np.arange(0, 4*n, 4, dtype=np.int32)
|
|
239
|
+
tris = np.vstack([
|
|
240
|
+
np.stack([starts, starts+1, starts+2], axis=1),
|
|
241
|
+
np.stack([starts, starts+2, starts+3], axis=1)
|
|
242
|
+
])
|
|
243
|
+
|
|
244
|
+
lighting = dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
|
|
245
|
+
cx = (x.min() + x.max()) * 0.5 if len(x) > 0 else 0.0
|
|
246
|
+
cy = (y.min() + y.max()) * 0.5 if len(y) > 0 else 0.0
|
|
247
|
+
cz = (z.min() + z.max()) * 0.5 if len(z) > 0 else 0.0
|
|
248
|
+
lx = cx + (x.max() - x.min() + dx) * 0.9
|
|
249
|
+
ly = cy + (y.max() - y.min() + dy) * 0.6
|
|
250
|
+
lz = cz + (z.max() - z.min() + dz) * 1.4
|
|
251
|
+
|
|
252
|
+
fig.add_trace(
|
|
253
|
+
go.Mesh3d(
|
|
254
|
+
x=V[:,0], y=V[:,1], z=V[:,2],
|
|
255
|
+
i=tris[:,0], j=tris[:,1], k=tris[:,2],
|
|
256
|
+
color=_rgb_tuple_to_plotly_color(color_rgb),
|
|
257
|
+
opacity=float(opacity),
|
|
258
|
+
flatshading=False,
|
|
259
|
+
lighting=lighting,
|
|
260
|
+
lightposition=dict(x=lx, y=ly, z=lx),
|
|
261
|
+
name=f"{plane}"
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Draw voxel faces
|
|
266
|
+
if vox is not None and classes_to_draw:
|
|
267
|
+
for cls in classes_to_draw:
|
|
268
|
+
if not np.any(vox == cls):
|
|
269
|
+
continue
|
|
270
|
+
occ = (vox == cls)
|
|
271
|
+
p = np.pad(occluder, ((0,1),(0,0),(0,0)), constant_values=False); posx = occ & (~p[1:,:,:])
|
|
272
|
+
p = np.pad(occluder, ((1,0),(0,0),(0,0)), constant_values=False); negx = occ & (~p[:-1,:,:])
|
|
273
|
+
p = np.pad(occluder, ((0,0),(0,1),(0,0)), constant_values=False); posy = occ & (~p[:,1:,:])
|
|
274
|
+
p = np.pad(occluder, ((0,0),(1,0),(0,0)), constant_values=False); negy = occ & (~p[:,:-1,:])
|
|
275
|
+
p = np.pad(occluder, ((0,0),(0,0),(0,1)), constant_values=False); posz = occ & (~p[:,:,1:])
|
|
276
|
+
p = np.pad(occluder, ((0,0),(0,0),(1,0)), constant_values=False); negz = occ & (~p[:,:,:-1])
|
|
277
|
+
color_rgb = vox_dict.get(int(cls), [128,128,128])
|
|
278
|
+
add_faces(posx, '+x', color_rgb)
|
|
279
|
+
add_faces(negx, '-x', color_rgb)
|
|
280
|
+
add_faces(posy, '+y', color_rgb)
|
|
281
|
+
add_faces(negy, '-y', color_rgb)
|
|
282
|
+
add_faces(posz, '+z', color_rgb)
|
|
283
|
+
add_faces(negz, '-z', color_rgb)
|
|
284
|
+
|
|
285
|
+
# Building overlay
|
|
286
|
+
if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
|
|
287
|
+
Vb = np.asarray(building_sim_mesh.vertices)
|
|
288
|
+
Fb = np.asarray(building_sim_mesh.faces)
|
|
289
|
+
values = None
|
|
290
|
+
if hasattr(building_sim_mesh, 'metadata') and isinstance(building_sim_mesh.metadata, dict):
|
|
291
|
+
values = building_sim_mesh.metadata.get(building_value_name)
|
|
292
|
+
if values is not None:
|
|
293
|
+
values = np.asarray(values)
|
|
294
|
+
|
|
295
|
+
face_vals = None
|
|
296
|
+
if values is not None and len(values) == len(Fb):
|
|
297
|
+
face_vals = values.astype(float)
|
|
298
|
+
elif values is not None and len(values) == len(Vb):
|
|
299
|
+
vals_v = values.astype(float)
|
|
300
|
+
face_vals = np.nanmean(vals_v[Fb], axis=1)
|
|
301
|
+
|
|
302
|
+
facecolor = None
|
|
303
|
+
if face_vals is not None:
|
|
304
|
+
finite = np.isfinite(face_vals)
|
|
305
|
+
vmin_b = building_vmin if building_vmin is not None else (float(np.nanmin(face_vals[finite])) if np.any(finite) else 0.0)
|
|
306
|
+
vmax_b = building_vmax if building_vmax is not None else (float(np.nanmax(face_vals[finite])) if np.any(finite) else 1.0)
|
|
307
|
+
norm_b = mcolors.Normalize(vmin=vmin_b, vmax=vmax_b)
|
|
308
|
+
cmap_b = cm.get_cmap(building_colormap)
|
|
309
|
+
colors_rgba = np.zeros((len(Fb), 4), dtype=float)
|
|
310
|
+
colors_rgba[finite] = cmap_b(norm_b(face_vals[finite]))
|
|
311
|
+
nan_rgba = np.array(mcolors.to_rgba(building_nan_color))
|
|
312
|
+
colors_rgba[~finite] = nan_rgba
|
|
313
|
+
facecolor = [f"rgb({int(255*c[0])},{int(255*c[1])},{int(255*c[2])})" for c in colors_rgba]
|
|
314
|
+
|
|
315
|
+
lighting_b = (dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
|
|
316
|
+
if building_shaded else dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0))
|
|
317
|
+
|
|
318
|
+
cx = float((Vb[:,0].min() + Vb[:,0].max()) * 0.5)
|
|
319
|
+
cy = float((Vb[:,1].min() + Vb[:,1].max()) * 0.5)
|
|
320
|
+
lx = cx + (Vb[:,0].max() - Vb[:,0].min() + meshsize) * 0.9
|
|
321
|
+
ly = cy + (Vb[:,1].max() - Vb[:,1].min() + meshsize) * 0.6
|
|
322
|
+
lz = float((Vb[:,2].min() + Vb[:,2].max()) * 0.5) + (Vb[:,2].max() - Vb[:,2].min() + meshsize) * 1.4
|
|
323
|
+
|
|
324
|
+
fig.add_trace(
|
|
325
|
+
go.Mesh3d(
|
|
326
|
+
x=Vb[:,0], y=Vb[:,1], z=Vb[:,2],
|
|
327
|
+
i=Fb[:,0], j=Fb[:,1], k=Fb[:,2],
|
|
328
|
+
facecolor=facecolor if facecolor is not None else None,
|
|
329
|
+
color=None if facecolor is not None else 'rgb(200,200,200)',
|
|
330
|
+
opacity=float(building_opacity),
|
|
331
|
+
flatshading=False,
|
|
332
|
+
lighting=lighting_b,
|
|
333
|
+
lightposition=dict(x=lx, y=ly, z=lz),
|
|
334
|
+
name=building_value_name if facecolor is not None else 'building_mesh'
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if face_vals is not None:
|
|
339
|
+
colorscale_b = _mpl_cmap_to_plotly_colorscale(building_colormap)
|
|
340
|
+
fig.add_trace(
|
|
341
|
+
go.Scatter3d(
|
|
342
|
+
x=[None], y=[None], z=[None],
|
|
343
|
+
mode='markers',
|
|
344
|
+
marker=dict(size=0.1, color=[vmin_b, vmax_b], colorscale=colorscale_b, cmin=vmin_b, cmax=vmax_b,
|
|
345
|
+
colorbar=dict(title=building_value_name, len=0.5, y=0.8), showscale=True),
|
|
346
|
+
showlegend=False, hoverinfo='skip')
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Ground simulation surface overlay
|
|
350
|
+
if ground_sim_grid is not None and ground_dem_grid is not None:
|
|
351
|
+
sim_vals = np.asarray(ground_sim_grid, dtype=float)
|
|
352
|
+
finite = np.isfinite(sim_vals)
|
|
353
|
+
vmin_g = ground_vmin if ground_vmin is not None else (float(np.nanmin(sim_vals[finite])) if np.any(finite) else 0.0)
|
|
354
|
+
vmax_g = ground_vmax if ground_vmax is not None else (float(np.nanmax(sim_vals[finite])) if np.any(finite) else 1.0)
|
|
355
|
+
z_off = ground_z_offset if ground_z_offset is not None else ground_view_point_height
|
|
356
|
+
try:
|
|
357
|
+
z_off = float(z_off) if z_off is not None else 1.5
|
|
358
|
+
except Exception:
|
|
359
|
+
z_off = 1.5
|
|
360
|
+
try:
|
|
361
|
+
ms = float(meshsize)
|
|
362
|
+
z_off = (z_off // ms + 1.0) * ms
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
try:
|
|
366
|
+
dem_norm = np.asarray(ground_dem_grid, dtype=float)
|
|
367
|
+
dem_norm = dem_norm - np.nanmin(dem_norm)
|
|
368
|
+
except Exception:
|
|
369
|
+
dem_norm = ground_dem_grid
|
|
370
|
+
|
|
371
|
+
sim_mesh = create_sim_surface_mesh(
|
|
372
|
+
ground_sim_grid,
|
|
373
|
+
dem_norm,
|
|
374
|
+
meshsize=meshsize,
|
|
375
|
+
z_offset=z_off,
|
|
376
|
+
cmap_name=ground_colormap,
|
|
377
|
+
vmin=vmin_g,
|
|
378
|
+
vmax=vmax_g,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if sim_mesh is not None and getattr(sim_mesh, 'vertices', None) is not None:
|
|
382
|
+
V = np.asarray(sim_mesh.vertices)
|
|
383
|
+
F = np.asarray(sim_mesh.faces)
|
|
384
|
+
facecolor = None
|
|
385
|
+
try:
|
|
386
|
+
colors_rgba = np.asarray(sim_mesh.visual.face_colors)
|
|
387
|
+
if colors_rgba.ndim == 2 and colors_rgba.shape[0] == len(F):
|
|
388
|
+
facecolor = [f"rgb({int(c[0])},{int(c[1])},{int(c[2])})" for c in colors_rgba]
|
|
389
|
+
except Exception:
|
|
390
|
+
facecolor = None
|
|
391
|
+
|
|
392
|
+
lighting = (dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
|
|
393
|
+
if ground_shaded else dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0))
|
|
394
|
+
|
|
395
|
+
cx = float((V[:,0].min() + V[:,0].max()) * 0.5)
|
|
396
|
+
cy = float((V[:,1].min() + V[:,1].max()) * 0.5)
|
|
397
|
+
lx = cx + (V[:,0].max() - V[:,0].min() + meshsize) * 0.9
|
|
398
|
+
ly = cy + (V[:,1].max() - V[:,1].min() + meshsize) * 0.6
|
|
399
|
+
lz = float((V[:,2].min() + V[:,2].max()) * 0.5) + (V[:,2].max() - V[:,2].min() + meshsize) * 1.4
|
|
400
|
+
|
|
401
|
+
fig.add_trace(
|
|
402
|
+
go.Mesh3d(
|
|
403
|
+
x=V[:,0], y=V[:,1], z=V[:,2],
|
|
404
|
+
i=F[:,0], j=F[:,1], k=F[:,2],
|
|
405
|
+
facecolor=facecolor,
|
|
406
|
+
color=None if facecolor is not None else 'rgb(200,200,200)',
|
|
407
|
+
opacity=float(sim_surface_opacity),
|
|
408
|
+
flatshading=False,
|
|
409
|
+
lighting=lighting,
|
|
410
|
+
lightposition=dict(x=lx, y=ly, z=lz),
|
|
411
|
+
name='sim_surface'
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
colorscale_g = _mpl_cmap_to_plotly_colorscale(ground_colormap)
|
|
416
|
+
fig.add_trace(
|
|
417
|
+
go.Scatter3d(
|
|
418
|
+
x=[None], y=[None], z=[None],
|
|
419
|
+
mode='markers',
|
|
420
|
+
marker=dict(size=0.1, color=[vmin_g, vmax_g], colorscale=colorscale_g, cmin=vmin_g, cmax=vmax_g,
|
|
421
|
+
colorbar=dict(title='ground', len=0.5, y=0.2), showscale=True),
|
|
422
|
+
showlegend=False, hoverinfo='skip')
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
fig.update_layout(
|
|
426
|
+
title=title,
|
|
427
|
+
width=width,
|
|
428
|
+
height=height,
|
|
429
|
+
scene=dict(
|
|
430
|
+
xaxis_title="X (m)",
|
|
431
|
+
yaxis_title="Y (m)",
|
|
432
|
+
zaxis_title="Z (m)",
|
|
433
|
+
aspectmode="data",
|
|
434
|
+
camera=dict(eye=dict(x=1.6, y=1.6, z=1.0)),
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if show:
|
|
439
|
+
fig.show()
|
|
440
|
+
if return_fig:
|
|
441
|
+
return fig
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def create_multi_view_scene(meshes, output_directory="output", projection_type="perspective", distance_factor=1.0):
|
|
446
|
+
"""
|
|
447
|
+
Creates multiple rendered views of 3D city meshes from different camera angles.
|
|
448
|
+
"""
|
|
449
|
+
if pv is None:
|
|
450
|
+
raise ImportError("PyVista is required for static rendering. Install with: pip install pyvista")
|
|
451
|
+
pv_meshes = {}
|
|
452
|
+
for class_id, mesh in meshes.items():
|
|
453
|
+
if mesh is None or len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
|
454
|
+
continue
|
|
455
|
+
faces = np.hstack([[3, *face] for face in mesh.faces])
|
|
456
|
+
pv_mesh = pv.PolyData(mesh.vertices, faces)
|
|
457
|
+
colors = getattr(mesh.visual, 'face_colors', None)
|
|
458
|
+
if colors is not None:
|
|
459
|
+
colors = np.asarray(colors)
|
|
460
|
+
if colors.size and colors.max() > 1:
|
|
461
|
+
colors = colors / 255.0
|
|
462
|
+
pv_mesh.cell_data['colors'] = colors
|
|
463
|
+
pv_meshes[class_id] = pv_mesh
|
|
464
|
+
|
|
465
|
+
min_xyz = np.array([np.inf, np.inf, np.inf], dtype=float)
|
|
466
|
+
max_xyz = np.array([-np.inf, -np.inf, -np.inf], dtype=float)
|
|
467
|
+
for mesh in meshes.values():
|
|
468
|
+
if mesh is None or len(mesh.vertices) == 0:
|
|
469
|
+
continue
|
|
470
|
+
v = mesh.vertices
|
|
471
|
+
min_xyz = np.minimum(min_xyz, v.min(axis=0))
|
|
472
|
+
max_xyz = np.maximum(max_xyz, v.max(axis=0))
|
|
473
|
+
bbox = np.vstack([min_xyz, max_xyz])
|
|
474
|
+
|
|
475
|
+
center = (bbox[1] + bbox[0]) / 2
|
|
476
|
+
diagonal = np.linalg.norm(bbox[1] - bbox[0])
|
|
477
|
+
|
|
478
|
+
if projection_type.lower() == "orthographic":
|
|
479
|
+
distance = diagonal * 5
|
|
480
|
+
else:
|
|
481
|
+
distance = diagonal * 1.8 * distance_factor
|
|
482
|
+
|
|
483
|
+
iso_angles = {
|
|
484
|
+
'iso_front_right': (1, 1, 0.7),
|
|
485
|
+
'iso_front_left': (-1, 1, 0.7),
|
|
486
|
+
'iso_back_right': (1, -1, 0.7),
|
|
487
|
+
'iso_back_left': (-1, -1, 0.7)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
camera_positions = {}
|
|
491
|
+
for name, direction in iso_angles.items():
|
|
492
|
+
direction = np.array(direction)
|
|
493
|
+
direction = direction / np.linalg.norm(direction)
|
|
494
|
+
camera_pos = center + direction * distance
|
|
495
|
+
camera_positions[name] = [camera_pos, center, (0, 0, 1)]
|
|
496
|
+
|
|
497
|
+
ortho_views = {
|
|
498
|
+
'xy_top': [center + np.array([0, 0, distance]), center, (-1, 0, 0)],
|
|
499
|
+
'yz_right': [center + np.array([distance, 0, 0]), center, (0, 0, 1)],
|
|
500
|
+
'xz_front': [center + np.array([0, distance, 0]), center, (0, 0, 1)],
|
|
501
|
+
'yz_left': [center + np.array([-distance, 0, 0]), center, (0, 0, 1)],
|
|
502
|
+
'xz_back': [center + np.array([0, -distance, 0]), center, (0, 0, 1)]
|
|
503
|
+
}
|
|
504
|
+
camera_positions.update(ortho_views)
|
|
505
|
+
|
|
506
|
+
images = []
|
|
507
|
+
for view_name, camera_pos in camera_positions.items():
|
|
508
|
+
plotter = pv.Plotter(off_screen=True)
|
|
509
|
+
if projection_type.lower() == "orthographic":
|
|
510
|
+
plotter.enable_parallel_projection()
|
|
511
|
+
plotter.camera.parallel_scale = diagonal * 0.4 * distance_factor
|
|
512
|
+
elif projection_type.lower() != "perspective":
|
|
513
|
+
print(f"Warning: Unknown projection_type '{projection_type}'. Using perspective projection.")
|
|
514
|
+
for class_id, pv_mesh in pv_meshes.items():
|
|
515
|
+
has_colors = 'colors' in pv_mesh.cell_data
|
|
516
|
+
plotter.add_mesh(pv_mesh, rgb=True, scalars='colors' if has_colors else None)
|
|
517
|
+
plotter.camera_position = camera_pos
|
|
518
|
+
filename = f'{output_directory}/city_view_{view_name}.png'
|
|
519
|
+
plotter.screenshot(filename)
|
|
520
|
+
images.append((view_name, filename))
|
|
521
|
+
plotter.close()
|
|
522
|
+
return images
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class PyVistaRenderer:
|
|
526
|
+
"""Renderer that uses PyVista to produce multi-view images from meshes or VoxCity."""
|
|
527
|
+
|
|
528
|
+
def render_city(self, city: VoxCity, projection_type: str = "perspective", distance_factor: float = 1.0,
|
|
529
|
+
output_directory: str = "output", voxel_color_map: "str|dict" = "default",
|
|
530
|
+
building_sim_mesh=None, building_value_name: str = 'svf_values',
|
|
531
|
+
building_colormap: str = 'viridis', building_vmin=None, building_vmax=None,
|
|
532
|
+
building_nan_color: str = 'gray', building_opacity: float = 1.0,
|
|
533
|
+
render_voxel_buildings: bool = False,
|
|
534
|
+
ground_sim_grid=None, ground_dem_grid=None,
|
|
535
|
+
ground_z_offset: float | None = None, ground_view_point_height: float | None = None,
|
|
536
|
+
ground_colormap: str = 'viridis', ground_vmin=None, ground_vmax=None):
|
|
537
|
+
"""
|
|
538
|
+
Render city to static images with optional simulation overlays.
|
|
539
|
+
|
|
540
|
+
Parameters
|
|
541
|
+
----------
|
|
542
|
+
city : VoxCity
|
|
543
|
+
VoxCity object to render
|
|
544
|
+
projection_type : str
|
|
545
|
+
"perspective" or "orthographic"
|
|
546
|
+
distance_factor : float
|
|
547
|
+
Camera distance multiplier
|
|
548
|
+
output_directory : str
|
|
549
|
+
Directory to save rendered images
|
|
550
|
+
voxel_color_map : str or dict
|
|
551
|
+
Color mapping for voxel classes
|
|
552
|
+
building_sim_mesh : trimesh.Trimesh, optional
|
|
553
|
+
Building mesh with simulation results
|
|
554
|
+
building_value_name : str
|
|
555
|
+
Metadata key for building values
|
|
556
|
+
building_colormap : str
|
|
557
|
+
Colormap for building values
|
|
558
|
+
building_vmin, building_vmax : float, optional
|
|
559
|
+
Color scale limits for buildings
|
|
560
|
+
building_nan_color : str
|
|
561
|
+
Color for NaN values
|
|
562
|
+
building_opacity : float
|
|
563
|
+
Building mesh opacity
|
|
564
|
+
render_voxel_buildings : bool
|
|
565
|
+
Whether to render voxel buildings when building_sim_mesh is provided
|
|
566
|
+
ground_sim_grid : np.ndarray, optional
|
|
567
|
+
Ground-level simulation grid
|
|
568
|
+
ground_dem_grid : np.ndarray, optional
|
|
569
|
+
DEM grid for ground surface positioning
|
|
570
|
+
ground_z_offset : float, optional
|
|
571
|
+
Height offset for ground surface
|
|
572
|
+
ground_view_point_height : float, optional
|
|
573
|
+
Alternative height parameter
|
|
574
|
+
ground_colormap : str
|
|
575
|
+
Colormap for ground values
|
|
576
|
+
ground_vmin, ground_vmax : float, optional
|
|
577
|
+
Color scale limits for ground
|
|
578
|
+
"""
|
|
579
|
+
if pv is None:
|
|
580
|
+
raise ImportError("PyVista is required for static rendering. Install with: pip install pyvista")
|
|
581
|
+
|
|
582
|
+
meshsize = city.voxels.meta.meshsize
|
|
583
|
+
trimesh_dict = {}
|
|
584
|
+
|
|
585
|
+
# Build voxel meshes (always generate to show ground, trees, etc.)
|
|
586
|
+
collection = MeshBuilder.from_voxel_grid(city.voxels, meshsize=meshsize, voxel_color_map=voxel_color_map)
|
|
587
|
+
for key, mm in collection.items.items():
|
|
588
|
+
if mm.vertices.size == 0 or mm.faces.size == 0:
|
|
589
|
+
continue
|
|
590
|
+
# Skip building voxels if we have building_sim_mesh and don't want to render both
|
|
591
|
+
if not render_voxel_buildings and building_sim_mesh is not None and int(key) == -3:
|
|
592
|
+
continue
|
|
593
|
+
tri = trimesh.Trimesh(vertices=mm.vertices, faces=mm.faces, process=False)
|
|
594
|
+
if mm.colors is not None:
|
|
595
|
+
tri.visual.face_colors = mm.colors
|
|
596
|
+
trimesh_dict[key] = tri
|
|
597
|
+
|
|
598
|
+
# Add building simulation mesh overlay
|
|
599
|
+
if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
|
|
600
|
+
Vb = np.asarray(building_sim_mesh.vertices)
|
|
601
|
+
Fb = np.asarray(building_sim_mesh.faces)
|
|
602
|
+
|
|
603
|
+
# Get simulation values from metadata
|
|
604
|
+
values = None
|
|
605
|
+
if hasattr(building_sim_mesh, 'metadata') and isinstance(building_sim_mesh.metadata, dict):
|
|
606
|
+
values = building_sim_mesh.metadata.get(building_value_name)
|
|
607
|
+
|
|
608
|
+
if values is not None:
|
|
609
|
+
values = np.asarray(values)
|
|
610
|
+
|
|
611
|
+
# Determine if values are per-face or per-vertex
|
|
612
|
+
face_vals = None
|
|
613
|
+
if len(values) == len(Fb):
|
|
614
|
+
face_vals = values.astype(float)
|
|
615
|
+
elif len(values) == len(Vb):
|
|
616
|
+
vals_v = values.astype(float)
|
|
617
|
+
face_vals = np.nanmean(vals_v[Fb], axis=1)
|
|
618
|
+
|
|
619
|
+
if face_vals is not None:
|
|
620
|
+
# Apply colormap
|
|
621
|
+
finite = np.isfinite(face_vals)
|
|
622
|
+
vmin_b = building_vmin if building_vmin is not None else (float(np.nanmin(face_vals[finite])) if np.any(finite) else 0.0)
|
|
623
|
+
vmax_b = building_vmax if building_vmax is not None else (float(np.nanmax(face_vals[finite])) if np.any(finite) else 1.0)
|
|
624
|
+
norm_b = mcolors.Normalize(vmin=vmin_b, vmax=vmax_b)
|
|
625
|
+
cmap_b = cm.get_cmap(building_colormap)
|
|
626
|
+
|
|
627
|
+
colors_rgba = np.zeros((len(Fb), 4), dtype=np.uint8)
|
|
628
|
+
if np.any(finite):
|
|
629
|
+
colors_float = cmap_b(norm_b(face_vals[finite]))
|
|
630
|
+
colors_rgba[finite] = (colors_float * 255).astype(np.uint8)
|
|
631
|
+
|
|
632
|
+
# Handle NaN values
|
|
633
|
+
nan_rgba = np.array(mcolors.to_rgba(building_nan_color))
|
|
634
|
+
colors_rgba[~finite] = (nan_rgba * 255).astype(np.uint8)
|
|
635
|
+
|
|
636
|
+
# Create trimesh with colors
|
|
637
|
+
building_tri = trimesh.Trimesh(vertices=Vb, faces=Fb, process=False)
|
|
638
|
+
building_tri.visual.face_colors = colors_rgba
|
|
639
|
+
trimesh_dict['building_sim'] = building_tri
|
|
640
|
+
else:
|
|
641
|
+
# No values, just add the mesh with default color
|
|
642
|
+
building_tri = trimesh.Trimesh(vertices=Vb, faces=Fb, process=False)
|
|
643
|
+
trimesh_dict['building_sim'] = building_tri
|
|
644
|
+
|
|
645
|
+
# Add ground simulation surface overlay
|
|
646
|
+
if ground_sim_grid is not None and ground_dem_grid is not None:
|
|
647
|
+
z_off = ground_z_offset if ground_z_offset is not None else ground_view_point_height
|
|
648
|
+
try:
|
|
649
|
+
z_off = float(z_off) if z_off is not None else 1.5
|
|
650
|
+
except Exception:
|
|
651
|
+
z_off = 1.5
|
|
652
|
+
|
|
653
|
+
# Snap to grid
|
|
654
|
+
try:
|
|
655
|
+
z_off = (z_off // meshsize + 1.0) * meshsize
|
|
656
|
+
except Exception:
|
|
657
|
+
pass
|
|
658
|
+
|
|
659
|
+
# Normalize DEM
|
|
660
|
+
try:
|
|
661
|
+
dem_norm = np.asarray(ground_dem_grid, dtype=float)
|
|
662
|
+
dem_norm = dem_norm - np.nanmin(dem_norm)
|
|
663
|
+
except Exception:
|
|
664
|
+
dem_norm = ground_dem_grid
|
|
665
|
+
|
|
666
|
+
# Determine color range
|
|
667
|
+
sim_vals = np.asarray(ground_sim_grid, dtype=float)
|
|
668
|
+
finite = np.isfinite(sim_vals)
|
|
669
|
+
vmin_g = ground_vmin if ground_vmin is not None else (float(np.nanmin(sim_vals[finite])) if np.any(finite) else 0.0)
|
|
670
|
+
vmax_g = ground_vmax if ground_vmax is not None else (float(np.nanmax(sim_vals[finite])) if np.any(finite) else 1.0)
|
|
671
|
+
|
|
672
|
+
# Create ground simulation mesh
|
|
673
|
+
sim_mesh = create_sim_surface_mesh(
|
|
674
|
+
ground_sim_grid,
|
|
675
|
+
dem_norm,
|
|
676
|
+
meshsize=meshsize,
|
|
677
|
+
z_offset=z_off,
|
|
678
|
+
cmap_name=ground_colormap,
|
|
679
|
+
vmin=vmin_g,
|
|
680
|
+
vmax=vmax_g,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
if sim_mesh is not None and getattr(sim_mesh, 'vertices', None) is not None:
|
|
684
|
+
trimesh_dict['ground_sim'] = sim_mesh
|
|
685
|
+
|
|
686
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
687
|
+
return create_multi_view_scene(trimesh_dict, output_directory=output_directory,
|
|
688
|
+
projection_type=projection_type, distance_factor=distance_factor)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def visualize_voxcity(
|
|
693
|
+
city: VoxCity,
|
|
694
|
+
mode: str = "interactive",
|
|
695
|
+
*,
|
|
696
|
+
# Common options
|
|
697
|
+
voxel_color_map: "str|dict" = "default",
|
|
698
|
+
classes=None,
|
|
699
|
+
title: str | None = None,
|
|
700
|
+
# Interactive (Plotly) options
|
|
701
|
+
opacity: float = 1.0,
|
|
702
|
+
max_dimension: int = 160,
|
|
703
|
+
downsample: int | None = None,
|
|
704
|
+
width: int = 1000,
|
|
705
|
+
height: int = 800,
|
|
706
|
+
show: bool = True,
|
|
707
|
+
return_fig: bool = False,
|
|
708
|
+
# Static (PyVista) options
|
|
709
|
+
output_directory: str = "output",
|
|
710
|
+
projection_type: str = "perspective",
|
|
711
|
+
distance_factor: float = 1.0,
|
|
712
|
+
# Building simulation overlay options
|
|
713
|
+
building_sim_mesh=None,
|
|
714
|
+
building_value_name: str = 'svf_values',
|
|
715
|
+
building_colormap: str = 'viridis',
|
|
716
|
+
building_vmin: float | None = None,
|
|
717
|
+
building_vmax: float | None = None,
|
|
718
|
+
building_nan_color: str = 'gray',
|
|
719
|
+
building_opacity: float = 1.0,
|
|
720
|
+
building_shaded: bool = False,
|
|
721
|
+
render_voxel_buildings: bool = False,
|
|
722
|
+
# Ground simulation surface overlay options
|
|
723
|
+
ground_sim_grid=None,
|
|
724
|
+
ground_dem_grid=None,
|
|
725
|
+
ground_z_offset: float | None = None,
|
|
726
|
+
ground_view_point_height: float | None = None,
|
|
727
|
+
ground_colormap: str = 'viridis',
|
|
728
|
+
ground_vmin: float | None = None,
|
|
729
|
+
ground_vmax: float | None = None,
|
|
730
|
+
sim_surface_opacity: float = 0.95,
|
|
731
|
+
ground_shaded: bool = False,
|
|
732
|
+
):
|
|
733
|
+
"""
|
|
734
|
+
Visualize a VoxCity object with optional simulation result overlays.
|
|
735
|
+
|
|
736
|
+
Parameters
|
|
737
|
+
----------
|
|
738
|
+
city : VoxCity
|
|
739
|
+
VoxCity object to visualize
|
|
740
|
+
mode : str, default="interactive"
|
|
741
|
+
Visualization mode: "interactive" (Plotly) or "static" (PyVista)
|
|
742
|
+
|
|
743
|
+
Common Options
|
|
744
|
+
--------------
|
|
745
|
+
voxel_color_map : str or dict, default="default"
|
|
746
|
+
Color mapping for voxel classes
|
|
747
|
+
classes : list, optional
|
|
748
|
+
Specific voxel classes to render
|
|
749
|
+
title : str, optional
|
|
750
|
+
Plot title
|
|
751
|
+
|
|
752
|
+
Interactive Mode Options (Plotly)
|
|
753
|
+
----------------------------------
|
|
754
|
+
opacity : float, default=1.0
|
|
755
|
+
Voxel opacity (0-1)
|
|
756
|
+
max_dimension : int, default=160
|
|
757
|
+
Maximum grid dimension before downsampling
|
|
758
|
+
downsample : int, optional
|
|
759
|
+
Manual downsampling stride
|
|
760
|
+
width : int, default=1000
|
|
761
|
+
Plot width in pixels
|
|
762
|
+
height : int, default=800
|
|
763
|
+
Plot height in pixels
|
|
764
|
+
show : bool, default=True
|
|
765
|
+
Whether to display the plot
|
|
766
|
+
return_fig : bool, default=False
|
|
767
|
+
Whether to return the figure object
|
|
768
|
+
|
|
769
|
+
Static Mode Options (PyVista)
|
|
770
|
+
------------------------------
|
|
771
|
+
output_directory : str, default="output"
|
|
772
|
+
Directory for saving rendered images
|
|
773
|
+
projection_type : str, default="perspective"
|
|
774
|
+
Camera projection: "perspective" or "orthographic"
|
|
775
|
+
distance_factor : float, default=1.0
|
|
776
|
+
Camera distance multiplier
|
|
777
|
+
|
|
778
|
+
Building Simulation Overlay Options
|
|
779
|
+
------------------------------------
|
|
780
|
+
building_sim_mesh : trimesh.Trimesh, optional
|
|
781
|
+
Building mesh with simulation results in metadata.
|
|
782
|
+
Typically created by get_surface_view_factor() or get_building_solar_irradiance().
|
|
783
|
+
building_value_name : str, default='svf_values'
|
|
784
|
+
Metadata key to use for coloring (e.g., 'svf_values', 'global', 'direct', 'diffuse')
|
|
785
|
+
building_colormap : str, default='viridis'
|
|
786
|
+
Matplotlib colormap for building values
|
|
787
|
+
building_vmin : float, optional
|
|
788
|
+
Minimum value for color scale
|
|
789
|
+
building_vmax : float, optional
|
|
790
|
+
Maximum value for color scale
|
|
791
|
+
building_nan_color : str, default='gray'
|
|
792
|
+
Color for NaN/invalid values
|
|
793
|
+
building_opacity : float, default=1.0
|
|
794
|
+
Building mesh opacity (0-1)
|
|
795
|
+
building_shaded : bool, default=False
|
|
796
|
+
Whether to apply shading to building mesh
|
|
797
|
+
render_voxel_buildings : bool, default=False
|
|
798
|
+
Whether to render voxel buildings when building_sim_mesh is provided
|
|
799
|
+
|
|
800
|
+
Ground Simulation Surface Overlay Options
|
|
801
|
+
------------------------------------------
|
|
802
|
+
ground_sim_grid : np.ndarray, optional
|
|
803
|
+
2D array of ground-level simulation values (e.g., Green View Index, solar radiation).
|
|
804
|
+
Should have the same shape as the city's 2D grids.
|
|
805
|
+
ground_dem_grid : np.ndarray, optional
|
|
806
|
+
2D DEM array for positioning the ground simulation surface.
|
|
807
|
+
If None, uses city.dem.elevation when ground_sim_grid is provided.
|
|
808
|
+
ground_z_offset : float, optional
|
|
809
|
+
Height offset for ground simulation surface above DEM
|
|
810
|
+
ground_view_point_height : float, optional
|
|
811
|
+
Alternative parameter for ground surface height (used if ground_z_offset is None)
|
|
812
|
+
ground_colormap : str, default='viridis'
|
|
813
|
+
Matplotlib colormap for ground values
|
|
814
|
+
ground_vmin : float, optional
|
|
815
|
+
Minimum value for color scale
|
|
816
|
+
ground_vmax : float, optional
|
|
817
|
+
Maximum value for color scale
|
|
818
|
+
sim_surface_opacity : float, default=0.95
|
|
819
|
+
Ground simulation surface opacity (0-1)
|
|
820
|
+
ground_shaded : bool, default=False
|
|
821
|
+
Whether to apply shading to ground surface
|
|
822
|
+
|
|
823
|
+
Returns
|
|
824
|
+
-------
|
|
825
|
+
For mode="interactive":
|
|
826
|
+
plotly.graph_objects.Figure or None
|
|
827
|
+
Returns Figure if return_fig=True, otherwise None
|
|
828
|
+
|
|
829
|
+
For mode="static":
|
|
830
|
+
list of (view_name, filepath) tuples
|
|
831
|
+
List of rendered view names and their file paths
|
|
832
|
+
|
|
833
|
+
Examples
|
|
834
|
+
--------
|
|
835
|
+
Basic visualization:
|
|
836
|
+
>>> visualize_voxcity(city, mode="interactive")
|
|
837
|
+
|
|
838
|
+
With building solar irradiance results:
|
|
839
|
+
>>> building_mesh = get_building_solar_irradiance(city, ...)
|
|
840
|
+
>>> visualize_voxcity(city, mode="interactive",
|
|
841
|
+
... building_sim_mesh=building_mesh,
|
|
842
|
+
... building_value_name='global')
|
|
843
|
+
|
|
844
|
+
With ground-level Green View Index:
|
|
845
|
+
>>> visualize_voxcity(city, mode="interactive",
|
|
846
|
+
... ground_sim_grid=gvi_array,
|
|
847
|
+
... ground_colormap='YlGn')
|
|
848
|
+
|
|
849
|
+
Static rendering with simulation overlays:
|
|
850
|
+
>>> visualize_voxcity(city, mode="static",
|
|
851
|
+
... building_sim_mesh=svf_mesh,
|
|
852
|
+
... output_directory="renders")
|
|
853
|
+
"""
|
|
854
|
+
if not isinstance(mode, str):
|
|
855
|
+
raise ValueError("mode must be a string: 'interactive' or 'static'")
|
|
856
|
+
|
|
857
|
+
mode_l = mode.lower().strip()
|
|
858
|
+
meshsize = getattr(city.voxels.meta, "meshsize", None)
|
|
859
|
+
|
|
860
|
+
# Auto-fill ground_dem_grid from city if ground_sim_grid is provided but ground_dem_grid is not
|
|
861
|
+
if ground_sim_grid is not None and ground_dem_grid is None:
|
|
862
|
+
ground_dem_grid = getattr(city.dem, "elevation", None)
|
|
863
|
+
|
|
864
|
+
if mode_l == "interactive":
|
|
865
|
+
voxel_array = getattr(city.voxels, "classes", None)
|
|
866
|
+
return visualize_voxcity_plotly(
|
|
867
|
+
voxel_array=voxel_array,
|
|
868
|
+
meshsize=meshsize,
|
|
869
|
+
classes=classes,
|
|
870
|
+
voxel_color_map=voxel_color_map,
|
|
871
|
+
opacity=opacity,
|
|
872
|
+
max_dimension=max_dimension,
|
|
873
|
+
downsample=downsample,
|
|
874
|
+
title=title,
|
|
875
|
+
width=width,
|
|
876
|
+
height=height,
|
|
877
|
+
show=show,
|
|
878
|
+
return_fig=return_fig,
|
|
879
|
+
# Building simulation overlay
|
|
880
|
+
building_sim_mesh=building_sim_mesh,
|
|
881
|
+
building_value_name=building_value_name,
|
|
882
|
+
building_colormap=building_colormap,
|
|
883
|
+
building_vmin=building_vmin,
|
|
884
|
+
building_vmax=building_vmax,
|
|
885
|
+
building_nan_color=building_nan_color,
|
|
886
|
+
building_opacity=building_opacity,
|
|
887
|
+
building_shaded=building_shaded,
|
|
888
|
+
render_voxel_buildings=render_voxel_buildings,
|
|
889
|
+
# Ground simulation surface overlay
|
|
890
|
+
ground_sim_grid=ground_sim_grid,
|
|
891
|
+
ground_dem_grid=ground_dem_grid,
|
|
892
|
+
ground_z_offset=ground_z_offset,
|
|
893
|
+
ground_view_point_height=ground_view_point_height,
|
|
894
|
+
ground_colormap=ground_colormap,
|
|
895
|
+
ground_vmin=ground_vmin,
|
|
896
|
+
ground_vmax=ground_vmax,
|
|
897
|
+
sim_surface_opacity=sim_surface_opacity,
|
|
898
|
+
ground_shaded=ground_shaded,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
if mode_l == "static":
|
|
902
|
+
renderer = PyVistaRenderer()
|
|
903
|
+
return renderer.render_city(
|
|
904
|
+
city,
|
|
905
|
+
projection_type=projection_type,
|
|
906
|
+
distance_factor=distance_factor,
|
|
907
|
+
output_directory=output_directory,
|
|
908
|
+
voxel_color_map=voxel_color_map,
|
|
909
|
+
# Pass simulation overlay parameters
|
|
910
|
+
building_sim_mesh=building_sim_mesh,
|
|
911
|
+
building_value_name=building_value_name,
|
|
912
|
+
building_colormap=building_colormap,
|
|
913
|
+
building_vmin=building_vmin,
|
|
914
|
+
building_vmax=building_vmax,
|
|
915
|
+
building_nan_color=building_nan_color,
|
|
916
|
+
building_opacity=building_opacity,
|
|
917
|
+
render_voxel_buildings=render_voxel_buildings,
|
|
918
|
+
ground_sim_grid=ground_sim_grid,
|
|
919
|
+
ground_dem_grid=ground_dem_grid,
|
|
920
|
+
ground_z_offset=ground_z_offset,
|
|
921
|
+
ground_view_point_height=ground_view_point_height,
|
|
922
|
+
ground_colormap=ground_colormap,
|
|
923
|
+
ground_vmin=ground_vmin,
|
|
924
|
+
ground_vmax=ground_vmax,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
raise ValueError("Unknown mode. Use 'interactive' or 'static'.")
|
|
928
|
+
|