voxcity 0.3.17__tar.gz → 0.3.19__tar.gz
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-0.3.17 → voxcity-0.3.19}/PKG-INFO +4 -1
- {voxcity-0.3.17 → voxcity-0.3.19}/pyproject.toml +4 -1
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/geoprocessor/__init_.py +1 -0
- voxcity-0.3.19/src/voxcity/geoprocessor/mesh.py +269 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/geoprocessor/polygon.py +2 -2
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/utils/visualization.py +289 -2
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity.egg-info/PKG-INFO +4 -1
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity.egg-info/SOURCES.txt +1 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity.egg-info/requires.txt +3 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/AUTHORS.rst +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/CONTRIBUTING.rst +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/HISTORY.rst +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/LICENSE +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/MANIFEST.in +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/README.md +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/docs/Makefile +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/docs/archive/README.rst +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/docs/authors.rst +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/docs/conf.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/docs/index.rst +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/docs/make.bat +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/setup.cfg +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/__init__.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/__init__.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/eubucco.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/gee.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/mbfp.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/oemj.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/omt.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/osm.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/overture.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/downloader/utils.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/exporter/__init_.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/exporter/envimet.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/exporter/magicavoxel.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/exporter/obj.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/generator.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/geoprocessor/draw.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/geoprocessor/grid.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/geoprocessor/network.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/geoprocessor/utils.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/simulator/__init_.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/simulator/solar.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/simulator/utils.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/simulator/view.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/utils/__init_.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity/utils/weather.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity.egg-info/dependency_links.txt +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/src/voxcity.egg-info/top_level.txt +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/tests/__init__.py +0 -0
- {voxcity-0.3.17 → voxcity-0.3.19}/tests/voxelcity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.19
|
|
4
4
|
Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
|
|
5
5
|
Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
6
6
|
Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
@@ -50,6 +50,9 @@ Requires-Dist: timezonefinder
|
|
|
50
50
|
Requires-Dist: astral
|
|
51
51
|
Requires-Dist: osmnx
|
|
52
52
|
Requires-Dist: joblib
|
|
53
|
+
Requires-Dist: trimesh
|
|
54
|
+
Requires-Dist: pyvista
|
|
55
|
+
Requires-Dist: IPython
|
|
53
56
|
Provides-Extra: dev
|
|
54
57
|
Requires-Dist: coverage; extra == "dev"
|
|
55
58
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "voxcity"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.19"
|
|
4
4
|
requires-python = ">=3.10,<3.13"
|
|
5
5
|
classifiers = [
|
|
6
6
|
"Programming Language :: Python :: 3.10",
|
|
@@ -52,6 +52,9 @@ dependencies = [
|
|
|
52
52
|
"astral",
|
|
53
53
|
"osmnx",
|
|
54
54
|
"joblib",
|
|
55
|
+
"trimesh",
|
|
56
|
+
"pyvista",
|
|
57
|
+
"IPython"
|
|
55
58
|
]
|
|
56
59
|
|
|
57
60
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import trimesh
|
|
3
|
+
import matplotlib.colors as mcolors
|
|
4
|
+
import matplotlib.cm as cm
|
|
5
|
+
|
|
6
|
+
def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
|
|
7
|
+
"""
|
|
8
|
+
Create a mesh from voxels preserving sharp edges, scaled by meshsize.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
voxel_array : np.ndarray (3D)
|
|
13
|
+
The voxel array of shape (X, Y, Z).
|
|
14
|
+
class_id : int
|
|
15
|
+
The ID of the class to extract.
|
|
16
|
+
meshsize : float
|
|
17
|
+
The real-world size of each voxel in meters, for x, y, and z.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
mesh : trimesh.Trimesh or None
|
|
22
|
+
The resulting mesh for the given class_id (or None if no voxels).
|
|
23
|
+
"""
|
|
24
|
+
# Find voxels of the current class
|
|
25
|
+
voxel_coords = np.argwhere(voxel_array == class_id)
|
|
26
|
+
|
|
27
|
+
if len(voxel_coords) == 0:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
# Define the 6 faces of a unit cube (local coordinates 0..1)
|
|
31
|
+
unit_faces = np.array([
|
|
32
|
+
# Front
|
|
33
|
+
[[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]],
|
|
34
|
+
# Back
|
|
35
|
+
[[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]],
|
|
36
|
+
# Right
|
|
37
|
+
[[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]],
|
|
38
|
+
# Left
|
|
39
|
+
[[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]],
|
|
40
|
+
# Top
|
|
41
|
+
[[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]],
|
|
42
|
+
# Bottom
|
|
43
|
+
[[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
# Define face normals
|
|
47
|
+
face_normals = np.array([
|
|
48
|
+
[0, 0, 1], # Front
|
|
49
|
+
[0, 0, -1], # Back
|
|
50
|
+
[1, 0, 0], # Right
|
|
51
|
+
[-1, 0, 0], # Left
|
|
52
|
+
[0, 1, 0], # Top
|
|
53
|
+
[0, -1, 0] # Bottom
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
vertices = []
|
|
57
|
+
faces = []
|
|
58
|
+
face_normals_list = []
|
|
59
|
+
|
|
60
|
+
for x, y, z in voxel_coords:
|
|
61
|
+
# Check each face of the current voxel
|
|
62
|
+
adjacent_coords = [
|
|
63
|
+
(x, y, z+1), # Front
|
|
64
|
+
(x, y, z-1), # Back
|
|
65
|
+
(x+1, y, z), # Right
|
|
66
|
+
(x-1, y, z), # Left
|
|
67
|
+
(x, y+1, z), # Top
|
|
68
|
+
(x, y-1, z) # Bottom
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Only create faces where there's a transition between this class and "not this class"
|
|
72
|
+
for face_idx, adj_coord in enumerate(adjacent_coords):
|
|
73
|
+
try:
|
|
74
|
+
# If adj_coord is outside array bounds, it's a boundary => face is visible
|
|
75
|
+
if adj_coord[0] < 0 or adj_coord[1] < 0 or adj_coord[2] < 0:
|
|
76
|
+
is_boundary = True
|
|
77
|
+
else:
|
|
78
|
+
adj_value = voxel_array[adj_coord]
|
|
79
|
+
is_boundary = (adj_value == 0 or adj_value != class_id)
|
|
80
|
+
except IndexError:
|
|
81
|
+
# Out of range => boundary
|
|
82
|
+
is_boundary = True
|
|
83
|
+
|
|
84
|
+
if is_boundary:
|
|
85
|
+
# Local face in (0..1) for x,y,z, then shift by voxel coords
|
|
86
|
+
face_verts = (unit_faces[face_idx] + np.array([x, y, z])) * meshsize
|
|
87
|
+
current_vert_count = len(vertices)
|
|
88
|
+
|
|
89
|
+
vertices.extend(face_verts)
|
|
90
|
+
# Convert quad to two triangles
|
|
91
|
+
faces.extend([
|
|
92
|
+
[current_vert_count, current_vert_count + 1, current_vert_count + 2],
|
|
93
|
+
[current_vert_count, current_vert_count + 2, current_vert_count + 3]
|
|
94
|
+
])
|
|
95
|
+
# Add face normals for both triangles
|
|
96
|
+
face_normals_list.extend([face_normals[face_idx], face_normals[face_idx]])
|
|
97
|
+
|
|
98
|
+
if not vertices:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
vertices = np.array(vertices)
|
|
102
|
+
faces = np.array(faces)
|
|
103
|
+
face_normals_list = np.array(face_normals_list)
|
|
104
|
+
|
|
105
|
+
# Create mesh
|
|
106
|
+
mesh = trimesh.Trimesh(
|
|
107
|
+
vertices=vertices,
|
|
108
|
+
faces=faces,
|
|
109
|
+
face_normals=face_normals_list
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Merge vertices that are at the same position
|
|
113
|
+
mesh.merge_vertices()
|
|
114
|
+
|
|
115
|
+
return mesh
|
|
116
|
+
|
|
117
|
+
def create_sim_surface_mesh(sim_grid, dem_grid,
|
|
118
|
+
meshsize=1.0, z_offset=1.5,
|
|
119
|
+
cmap_name='viridis',
|
|
120
|
+
vmin=None, vmax=None):
|
|
121
|
+
"""
|
|
122
|
+
Create a planar surface mesh from sim_grid located at dem_grid + z_offset.
|
|
123
|
+
Skips any cells in sim_grid that are NaN, and flips both sim_grid and dem_grid
|
|
124
|
+
(up-down) to match voxel_array orientation. Applies meshsize scaling in x,y.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
sim_grid : 2D np.ndarray
|
|
129
|
+
2D array of simulation values (e.g., Green View Index).
|
|
130
|
+
dem_grid : 2D np.ndarray
|
|
131
|
+
2D array of ground elevations (same shape as sim_grid).
|
|
132
|
+
meshsize : float
|
|
133
|
+
Size of each cell in meters (same in x and y).
|
|
134
|
+
z_offset : float
|
|
135
|
+
Additional offset added to dem_grid for placing the mesh.
|
|
136
|
+
cmap_name : str
|
|
137
|
+
Matplotlib colormap name. Default is 'viridis'.
|
|
138
|
+
vmin : float or None
|
|
139
|
+
Minimum value for color mapping. If None, use min of sim_grid (non-NaN).
|
|
140
|
+
vmax : float or None
|
|
141
|
+
Maximum value for color mapping. If None, use max of sim_grid (non-NaN).
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
trimesh.Trimesh or None
|
|
146
|
+
A single mesh containing one square face per non-NaN cell.
|
|
147
|
+
Returns None if there are no valid cells.
|
|
148
|
+
"""
|
|
149
|
+
# Flip arrays vertically
|
|
150
|
+
sim_grid_flipped = np.flipud(sim_grid)
|
|
151
|
+
dem_grid_flipped = np.flipud(dem_grid)
|
|
152
|
+
|
|
153
|
+
# Identify valid (non-NaN) values
|
|
154
|
+
valid_mask = ~np.isnan(sim_grid_flipped)
|
|
155
|
+
valid_values = sim_grid_flipped[valid_mask]
|
|
156
|
+
if valid_values.size == 0:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# If vmin/vmax not provided, use actual min/max of the valid sim data
|
|
160
|
+
if vmin is None:
|
|
161
|
+
vmin = np.nanmin(valid_values)
|
|
162
|
+
if vmax is None:
|
|
163
|
+
vmax = np.nanmax(valid_values)
|
|
164
|
+
|
|
165
|
+
# Prepare the colormap
|
|
166
|
+
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
167
|
+
scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
|
|
168
|
+
|
|
169
|
+
vertices = []
|
|
170
|
+
faces = []
|
|
171
|
+
face_colors = []
|
|
172
|
+
|
|
173
|
+
vert_index = 0
|
|
174
|
+
nrows, ncols = sim_grid_flipped.shape
|
|
175
|
+
|
|
176
|
+
# Build a quad (two triangles) for each valid cell
|
|
177
|
+
for x in range(nrows):
|
|
178
|
+
for y in range(ncols):
|
|
179
|
+
val = sim_grid_flipped[x, y]
|
|
180
|
+
if np.isnan(val):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
z_base = dem_grid_flipped[x, y] + z_offset
|
|
184
|
+
|
|
185
|
+
# 4 corners in (x,y)*meshsize
|
|
186
|
+
v0 = [ x * meshsize, y * meshsize, z_base ]
|
|
187
|
+
v1 = [(x + 1) * meshsize, y * meshsize, z_base ]
|
|
188
|
+
v2 = [(x + 1) * meshsize, (y + 1) * meshsize, z_base ]
|
|
189
|
+
v3 = [ x * meshsize, (y + 1) * meshsize, z_base ]
|
|
190
|
+
|
|
191
|
+
vertices.extend([v0, v1, v2, v3])
|
|
192
|
+
faces.extend([
|
|
193
|
+
[vert_index, vert_index + 1, vert_index + 2],
|
|
194
|
+
[vert_index, vert_index + 2, vert_index + 3]
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
# Get color from colormap
|
|
198
|
+
color_rgba = np.array(scalar_map.to_rgba(val)) # shape (4,)
|
|
199
|
+
|
|
200
|
+
# Each cell has 2 faces => add the color twice
|
|
201
|
+
face_colors.append(color_rgba)
|
|
202
|
+
face_colors.append(color_rgba)
|
|
203
|
+
|
|
204
|
+
vert_index += 4
|
|
205
|
+
|
|
206
|
+
if len(vertices) == 0:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
vertices = np.array(vertices, dtype=float)
|
|
210
|
+
faces = np.array(faces, dtype=int)
|
|
211
|
+
face_colors = np.array(face_colors, dtype=float)
|
|
212
|
+
|
|
213
|
+
mesh = trimesh.Trimesh(
|
|
214
|
+
vertices=vertices,
|
|
215
|
+
faces=faces,
|
|
216
|
+
face_colors=face_colors,
|
|
217
|
+
process=False # skip auto merge if you want to preserve quads
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return mesh
|
|
221
|
+
|
|
222
|
+
def create_city_meshes(voxel_array, vox_dict, meshsize=1.0):
|
|
223
|
+
"""
|
|
224
|
+
Create meshes from voxel data with sharp edges preserved.
|
|
225
|
+
Applies meshsize for voxel scaling.
|
|
226
|
+
"""
|
|
227
|
+
meshes = {}
|
|
228
|
+
|
|
229
|
+
# Convert RGB colors to hex for material properties
|
|
230
|
+
color_dict = {k: mcolors.rgb2hex([v[0]/255, v[1]/255, v[2]/255])
|
|
231
|
+
for k, v in vox_dict.items() if k != 0} # Exclude air
|
|
232
|
+
|
|
233
|
+
# Create vertices and faces for each object class
|
|
234
|
+
for class_id in np.unique(voxel_array):
|
|
235
|
+
if class_id == 0: # Skip air
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
mesh = create_voxel_mesh(voxel_array, class_id, meshsize=meshsize)
|
|
240
|
+
|
|
241
|
+
if mesh is None:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Convert hex color to RGBA
|
|
245
|
+
rgb_color = np.array(mcolors.hex2color(color_dict[class_id]))
|
|
246
|
+
rgba_color = np.concatenate([rgb_color, [1.0]])
|
|
247
|
+
|
|
248
|
+
# Assign color to all faces
|
|
249
|
+
mesh.visual.face_colors = np.tile(rgba_color, (len(mesh.faces), 1))
|
|
250
|
+
|
|
251
|
+
meshes[class_id] = mesh
|
|
252
|
+
|
|
253
|
+
except ValueError as e:
|
|
254
|
+
print(f"Skipping class {class_id}: {e}")
|
|
255
|
+
|
|
256
|
+
return meshes
|
|
257
|
+
|
|
258
|
+
def export_meshes(meshes, output_directory, base_filename):
|
|
259
|
+
"""
|
|
260
|
+
Export meshes to OBJ (with MTL) and STL formats.
|
|
261
|
+
"""
|
|
262
|
+
# Export combined mesh as OBJ with materials
|
|
263
|
+
combined_mesh = trimesh.util.concatenate(list(meshes.values()))
|
|
264
|
+
combined_mesh.export(f"{output_directory}/{base_filename}.obj")
|
|
265
|
+
|
|
266
|
+
# Export individual meshes as STL
|
|
267
|
+
for class_id, mesh in meshes.items():
|
|
268
|
+
# Convert class_id to a string for filename
|
|
269
|
+
mesh.export(f"{output_directory}/{base_filename}_{class_id}.stl")
|
|
@@ -625,8 +625,8 @@ def extract_building_heights_from_geotiff(geotiff_path, gdf):
|
|
|
625
625
|
# Print statistics about height updates
|
|
626
626
|
if count_0 > 0:
|
|
627
627
|
print(f"{count_0} of the total {len(gdf)} building footprint from OSM did not have height data.")
|
|
628
|
-
print(f"For {count_1} of these building footprints without height, values from
|
|
629
|
-
print(f"For {count_2} of these building footprints without height, no data exist in
|
|
628
|
+
print(f"For {count_1} of these building footprints without height, values from complementary data were assigned.")
|
|
629
|
+
print(f"For {count_2} of these building footprints without height, no data exist in complementary data. Height values of 10m were set instead")
|
|
630
630
|
|
|
631
631
|
return gdf
|
|
632
632
|
|
|
@@ -16,6 +16,10 @@ import seaborn as sns
|
|
|
16
16
|
import random
|
|
17
17
|
import folium
|
|
18
18
|
import math
|
|
19
|
+
import trimesh
|
|
20
|
+
import pyvista as pv
|
|
21
|
+
from IPython.display import display
|
|
22
|
+
import os
|
|
19
23
|
|
|
20
24
|
from .lc import get_land_cover_classes
|
|
21
25
|
# from ..geo.geojson import filter_buildings
|
|
@@ -25,7 +29,6 @@ from ..geoprocessor.grid import (
|
|
|
25
29
|
create_cell_polygon,
|
|
26
30
|
grid_to_geodataframe
|
|
27
31
|
)
|
|
28
|
-
|
|
29
32
|
from ..geoprocessor.utils import (
|
|
30
33
|
initialize_geod,
|
|
31
34
|
calculate_distance,
|
|
@@ -33,6 +36,12 @@ from ..geoprocessor.utils import (
|
|
|
33
36
|
setup_transformer,
|
|
34
37
|
transform_coords,
|
|
35
38
|
)
|
|
39
|
+
from ..geoprocessor.mesh import (
|
|
40
|
+
create_voxel_mesh,
|
|
41
|
+
create_sim_surface_mesh,
|
|
42
|
+
create_city_meshes,
|
|
43
|
+
export_meshes
|
|
44
|
+
)
|
|
36
45
|
from .material import get_material_dict
|
|
37
46
|
|
|
38
47
|
def get_default_voxel_color_map():
|
|
@@ -911,4 +920,282 @@ def visualize_point_grid_on_basemap(point_gdf, value_name='value', **kwargs):
|
|
|
911
920
|
|
|
912
921
|
# Adjust layout to prevent colorbar cutoff
|
|
913
922
|
plt.tight_layout()
|
|
914
|
-
plt.show()
|
|
923
|
+
plt.show()
|
|
924
|
+
|
|
925
|
+
def create_multi_view_scene(meshes, output_directory="output"):
|
|
926
|
+
"""
|
|
927
|
+
Create multiple views of the scene from different angles.
|
|
928
|
+
"""
|
|
929
|
+
# Get the first mesh to compute bounding box
|
|
930
|
+
first_mesh = next(iter(meshes.values()))
|
|
931
|
+
vertices = first_mesh.vertices
|
|
932
|
+
bbox = np.array([
|
|
933
|
+
[vertices[:, 0].min(), vertices[:, 1].min(), vertices[:, 2].min()],
|
|
934
|
+
[vertices[:, 0].max(), vertices[:, 1].max(), vertices[:, 2].max()]
|
|
935
|
+
])
|
|
936
|
+
|
|
937
|
+
# Compute the center and diagonal of the bounding box
|
|
938
|
+
center = (bbox[1] + bbox[0]) / 2
|
|
939
|
+
diagonal = np.linalg.norm(bbox[1] - bbox[0])
|
|
940
|
+
|
|
941
|
+
# Use the diagonal to set the camera distance
|
|
942
|
+
distance = diagonal * 2
|
|
943
|
+
|
|
944
|
+
# Define the isometric viewing angles
|
|
945
|
+
iso_angles = {
|
|
946
|
+
'iso_front_right': (1, 1, 0.7),
|
|
947
|
+
'iso_front_left': (-1, 1, 0.7),
|
|
948
|
+
'iso_back_right': (1, -1, 0.7),
|
|
949
|
+
'iso_back_left': (-1, -1, 0.7)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
# Compute camera positions for isometric views
|
|
953
|
+
camera_positions = {}
|
|
954
|
+
for name, direction in iso_angles.items():
|
|
955
|
+
direction = np.array(direction)
|
|
956
|
+
direction = direction / np.linalg.norm(direction)
|
|
957
|
+
camera_pos = center + direction * distance
|
|
958
|
+
camera_positions[name] = [camera_pos, center, (0, 0, 1)]
|
|
959
|
+
|
|
960
|
+
# Add orthographic views
|
|
961
|
+
ortho_views = {
|
|
962
|
+
'xy_top': [center + np.array([0, 0, distance]), center, (0, 1, 0)],
|
|
963
|
+
'yz_right': [center + np.array([distance, 0, 0]), center, (0, 0, 1)],
|
|
964
|
+
'xz_front': [center + np.array([0, distance, 0]), center, (0, 0, 1)],
|
|
965
|
+
'yz_left': [center + np.array([-distance, 0, 0]), center, (0, 0, 1)],
|
|
966
|
+
'xz_back': [center + np.array([0, -distance, 0]), center, (0, 0, 1)]
|
|
967
|
+
}
|
|
968
|
+
camera_positions.update(ortho_views)
|
|
969
|
+
|
|
970
|
+
images = []
|
|
971
|
+
for view_name, camera_pos in camera_positions.items():
|
|
972
|
+
# Create new plotter for each view
|
|
973
|
+
plotter = pv.Plotter(notebook=True, off_screen=True)
|
|
974
|
+
|
|
975
|
+
# Add each mesh to the scene
|
|
976
|
+
for class_id, mesh in meshes.items():
|
|
977
|
+
vertices = mesh.vertices
|
|
978
|
+
faces = np.hstack([[3, *face] for face in mesh.faces])
|
|
979
|
+
pv_mesh = pv.PolyData(vertices, faces)
|
|
980
|
+
|
|
981
|
+
if hasattr(mesh.visual, 'face_colors'):
|
|
982
|
+
colors = mesh.visual.face_colors
|
|
983
|
+
if colors.max() > 1:
|
|
984
|
+
colors = colors / 255.0
|
|
985
|
+
pv_mesh.cell_data['colors'] = colors
|
|
986
|
+
|
|
987
|
+
plotter.add_mesh(pv_mesh,
|
|
988
|
+
rgb=True,
|
|
989
|
+
scalars='colors' if hasattr(mesh.visual, 'face_colors') else None)
|
|
990
|
+
|
|
991
|
+
# Set camera position for this view
|
|
992
|
+
plotter.camera_position = camera_pos
|
|
993
|
+
|
|
994
|
+
# Save screenshot
|
|
995
|
+
filename = f'{output_directory}/city_view_{view_name}.png'
|
|
996
|
+
plotter.screenshot(filename)
|
|
997
|
+
images.append((view_name, filename))
|
|
998
|
+
plotter.close()
|
|
999
|
+
|
|
1000
|
+
return images
|
|
1001
|
+
|
|
1002
|
+
def visualize_voxcity_multi_view(voxel_array, **kwargs):
|
|
1003
|
+
"""
|
|
1004
|
+
Create multiple views of the voxel city data.
|
|
1005
|
+
"""
|
|
1006
|
+
|
|
1007
|
+
os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
|
|
1008
|
+
os.environ['DISPLAY'] = ':99'
|
|
1009
|
+
|
|
1010
|
+
# Configure PyVista settings
|
|
1011
|
+
pv.set_plot_theme('document')
|
|
1012
|
+
pv.global_theme.background = 'white'
|
|
1013
|
+
pv.global_theme.window_size = [1024, 768]
|
|
1014
|
+
pv.global_theme.jupyter_backend = 'static'
|
|
1015
|
+
|
|
1016
|
+
# view_kwargs = {
|
|
1017
|
+
# "view_point_height": 1.5, # To set height of view point in meters. Default: 1.5 m.
|
|
1018
|
+
# "dem_grid": dem_grid,
|
|
1019
|
+
# "colormap": 'viridis', # Choose a colormap. Default: 'viridis'.
|
|
1020
|
+
# "obj_export": True, # Set "True" if you want to export the result in an OBJ file.
|
|
1021
|
+
# "output_directory": f'output/{key}/obj', # To set directory path for output files. Default: False.
|
|
1022
|
+
# "output_file_name": 'gvi', # To set file name excluding extension. Default: 'view_index'.
|
|
1023
|
+
# "num_colors": 10, # Number of discrete colors
|
|
1024
|
+
# "alpha": 1.0, # Set transparency (0.0 to 1.0)
|
|
1025
|
+
# "vmin": 0.0, # Minimum value for colormap normalization
|
|
1026
|
+
# "vmax": 1.0 # Maximum value for colormap normalization
|
|
1027
|
+
# }
|
|
1028
|
+
# Parse kwargs
|
|
1029
|
+
vox_dict = kwargs.get("vox_dict", get_default_voxel_color_map())
|
|
1030
|
+
output_directory = kwargs.get("output_directory", 'output')
|
|
1031
|
+
base_filename = kwargs.get("output_file_name", None)
|
|
1032
|
+
sim_grid = kwargs.get("sim_grid", None)
|
|
1033
|
+
dem_grid = kwargs.get("dem_grid", None)
|
|
1034
|
+
meshsize = kwargs.get("meshsize", 1.0) / 5
|
|
1035
|
+
z_offset = kwargs.get("view_point_height", 1.5)
|
|
1036
|
+
cmap_name = kwargs.get("colormap", "viridis")
|
|
1037
|
+
vmin = kwargs.get("vmin", 0)
|
|
1038
|
+
vmax = kwargs.get("vmax", 1)
|
|
1039
|
+
|
|
1040
|
+
# Create meshes
|
|
1041
|
+
print("Creating voxel meshes...")
|
|
1042
|
+
meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
|
|
1043
|
+
|
|
1044
|
+
# Create sim_grid surface mesh if provided
|
|
1045
|
+
if sim_grid is not None and dem_grid is not None:
|
|
1046
|
+
print("Creating sim_grid surface mesh...")
|
|
1047
|
+
sim_mesh = create_sim_surface_mesh(
|
|
1048
|
+
sim_grid, dem_grid,
|
|
1049
|
+
meshsize=meshsize,
|
|
1050
|
+
z_offset=z_offset,
|
|
1051
|
+
cmap_name=cmap_name,
|
|
1052
|
+
vmin=vmin,
|
|
1053
|
+
vmax=vmax
|
|
1054
|
+
)
|
|
1055
|
+
if sim_mesh is not None:
|
|
1056
|
+
meshes["sim_surface"] = sim_mesh
|
|
1057
|
+
|
|
1058
|
+
# Export if filename provided
|
|
1059
|
+
if base_filename is not None:
|
|
1060
|
+
print(f"Exporting files to '{base_filename}.*' ...")# Create output directory if it doesn't exist
|
|
1061
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
1062
|
+
export_meshes(meshes, output_directory, base_filename)
|
|
1063
|
+
|
|
1064
|
+
# Create and save multiple views
|
|
1065
|
+
print("Creating multiple views...")
|
|
1066
|
+
# Create output directory if it doesn't exist
|
|
1067
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
1068
|
+
image_files = create_multi_view_scene(meshes, output_directory=output_directory)
|
|
1069
|
+
|
|
1070
|
+
# Display each view separately
|
|
1071
|
+
for view_name, img_file in image_files:
|
|
1072
|
+
plt.figure(figsize=(12, 8))
|
|
1073
|
+
img = plt.imread(img_file)
|
|
1074
|
+
plt.imshow(img)
|
|
1075
|
+
plt.title(view_name.replace('_', ' ').title(), pad=20)
|
|
1076
|
+
plt.axis('off')
|
|
1077
|
+
plt.show()
|
|
1078
|
+
plt.close()
|
|
1079
|
+
|
|
1080
|
+
# def create_interactive_scene(meshes):
|
|
1081
|
+
# scene = trimesh.Scene()
|
|
1082
|
+
# scene.ambient_light = np.array([0.1, 0.1, 0.1, 1.0])
|
|
1083
|
+
# scene.directional_light = np.array([0.1, 0.1, 0.1, 1.0])
|
|
1084
|
+
|
|
1085
|
+
# for class_id, mesh in meshes.items():
|
|
1086
|
+
|
|
1087
|
+
# # If this is our sim_surface, do NOT override the per-face colors.
|
|
1088
|
+
# if class_id == "sim_surface":
|
|
1089
|
+
# # Just add the mesh as-is, retaining mesh.visual.face_colors
|
|
1090
|
+
# scene.add_geometry(mesh, node_name=f"class_{class_id}")
|
|
1091
|
+
# else:
|
|
1092
|
+
# # Existing code for single-color classes
|
|
1093
|
+
# material = trimesh.visual.material.PBRMaterial(
|
|
1094
|
+
# baseColorFactor=mesh.visual.face_colors[0],
|
|
1095
|
+
# metallicFactor=0.2,
|
|
1096
|
+
# roughnessFactor=0.8,
|
|
1097
|
+
# emissiveFactor=np.array([0.1, 0.1, 0.1]),
|
|
1098
|
+
# alphaMode='OPAQUE'
|
|
1099
|
+
# )
|
|
1100
|
+
# mesh.visual = trimesh.visual.TextureVisuals(
|
|
1101
|
+
# material=material,
|
|
1102
|
+
# uv=None
|
|
1103
|
+
# )
|
|
1104
|
+
# scene.add_geometry(mesh, node_name=f"class_{class_id}")
|
|
1105
|
+
|
|
1106
|
+
# # (Optional) add checkboxes if in Jupyter:
|
|
1107
|
+
# try:
|
|
1108
|
+
# import ipywidgets as widgets
|
|
1109
|
+
# from IPython.display import display
|
|
1110
|
+
|
|
1111
|
+
# def update_visibility(cid, visible):
|
|
1112
|
+
# scene.graph.nodes[f"class_{cid}"].visible = visible
|
|
1113
|
+
|
|
1114
|
+
# checkboxes = []
|
|
1115
|
+
# for cid in meshes.keys():
|
|
1116
|
+
# checkbox = widgets.Checkbox(value=True, description=f'Class {cid}')
|
|
1117
|
+
# checkbox.observe(
|
|
1118
|
+
# lambda change, _cid=cid: update_visibility(_cid, change['new']),
|
|
1119
|
+
# names='value'
|
|
1120
|
+
# )
|
|
1121
|
+
# checkboxes.append(checkbox)
|
|
1122
|
+
# display(widgets.VBox(checkboxes))
|
|
1123
|
+
# except ImportError:
|
|
1124
|
+
# pass # Not running in Jupyter
|
|
1125
|
+
|
|
1126
|
+
# return scene
|
|
1127
|
+
|
|
1128
|
+
# def visualize_voxcity_interactive(voxel_array, **kwargs):
|
|
1129
|
+
# """
|
|
1130
|
+
# Process voxel city data:
|
|
1131
|
+
# - create voxel meshes,
|
|
1132
|
+
# - optionally create a sim_grid surface mesh,
|
|
1133
|
+
# - optionally export,
|
|
1134
|
+
# - return a trimesh Scene for visualization.
|
|
1135
|
+
|
|
1136
|
+
# Optional arguments via **kwargs:
|
|
1137
|
+
# --------------------------------
|
|
1138
|
+
# base_filename : str, default "city_model"
|
|
1139
|
+
# Base name for exported files (OBJ, STL).
|
|
1140
|
+
# sim_grid : 2D np.ndarray or None, default None
|
|
1141
|
+
# Simulation array for creating a 2D surface mesh.
|
|
1142
|
+
# dem_grid : 2D np.ndarray or None, default None
|
|
1143
|
+
# DEM array for the surface mesh. Must match sim_grid shape.
|
|
1144
|
+
# meshsize : float, default 1.0
|
|
1145
|
+
# Real-world size (in meters) per voxel/cell in x,y,z.
|
|
1146
|
+
# z_offset : float, default 1.5
|
|
1147
|
+
# Offset added to dem_grid when placing sim_grid surface.
|
|
1148
|
+
# cmap_name : str, default 'viridis'
|
|
1149
|
+
# Matplotlib colormap name for sim_grid.
|
|
1150
|
+
# vmin : float or None, default 0
|
|
1151
|
+
# Minimum value for color mapping. If None, auto from data.
|
|
1152
|
+
# vmax : float or None, default 1
|
|
1153
|
+
# Maximum value for color mapping. If None, auto from data.
|
|
1154
|
+
# """
|
|
1155
|
+
|
|
1156
|
+
# # 1. Parse **kwargs
|
|
1157
|
+
# vox_dict = kwargs.get("vox_dict", get_default_voxel_color_map())
|
|
1158
|
+
# base_filename = kwargs.get("base_filename", None)
|
|
1159
|
+
# sim_grid = kwargs.get("sim_grid", None)
|
|
1160
|
+
# dem_grid = kwargs.get("dem_grid", None)
|
|
1161
|
+
# meshsize = kwargs.get("meshsize", 1.0) / 5
|
|
1162
|
+
# z_offset = kwargs.get("z_offset", 1.5)
|
|
1163
|
+
# cmap_name = kwargs.get("cmap_name", "viridis")
|
|
1164
|
+
# vmin = kwargs.get("vmin", 0)
|
|
1165
|
+
# vmax = kwargs.get("vmax", 1)
|
|
1166
|
+
|
|
1167
|
+
# # 2. Create voxel-based meshes (same logic as before)
|
|
1168
|
+
# print("Creating voxel meshes...")
|
|
1169
|
+
# meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
|
|
1170
|
+
|
|
1171
|
+
# # 3. Optionally create the sim_grid surface mesh
|
|
1172
|
+
# if sim_grid is not None and dem_grid is not None:
|
|
1173
|
+
# print("Creating sim_grid surface mesh...")
|
|
1174
|
+
# sim_mesh = create_sim_surface_mesh(
|
|
1175
|
+
# sim_grid,
|
|
1176
|
+
# dem_grid,
|
|
1177
|
+
# meshsize=meshsize,
|
|
1178
|
+
# z_offset=z_offset,
|
|
1179
|
+
# cmap_name=cmap_name,
|
|
1180
|
+
# vmin=vmin,
|
|
1181
|
+
# vmax=vmax
|
|
1182
|
+
# )
|
|
1183
|
+
# if sim_mesh is not None:
|
|
1184
|
+
# meshes["sim_surface"] = sim_mesh
|
|
1185
|
+
# else:
|
|
1186
|
+
# print("No valid cells in sim_grid (all NaN?). Skipping surface mesh.")
|
|
1187
|
+
|
|
1188
|
+
# # 4. Optionally export
|
|
1189
|
+
# if base_filename is not None:
|
|
1190
|
+
# print(f"Exporting files to '{base_filename}.*' ...")
|
|
1191
|
+
# export_meshes(meshes, base_filename)
|
|
1192
|
+
# else:
|
|
1193
|
+
# print("Skipping export step.")
|
|
1194
|
+
|
|
1195
|
+
# # 5. Create interactive visualization (voxel + optional sim_surface)
|
|
1196
|
+
# print("Creating interactive visualization...")
|
|
1197
|
+
# scene = create_interactive_scene(meshes)
|
|
1198
|
+
|
|
1199
|
+
# scene.show()
|
|
1200
|
+
|
|
1201
|
+
# return scene
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.19
|
|
4
4
|
Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
|
|
5
5
|
Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
6
6
|
Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
@@ -50,6 +50,9 @@ Requires-Dist: timezonefinder
|
|
|
50
50
|
Requires-Dist: astral
|
|
51
51
|
Requires-Dist: osmnx
|
|
52
52
|
Requires-Dist: joblib
|
|
53
|
+
Requires-Dist: trimesh
|
|
54
|
+
Requires-Dist: pyvista
|
|
55
|
+
Requires-Dist: IPython
|
|
53
56
|
Provides-Extra: dev
|
|
54
57
|
Requires-Dist: coverage; extra == "dev"
|
|
55
58
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -34,6 +34,7 @@ src/voxcity/exporter/obj.py
|
|
|
34
34
|
src/voxcity/geoprocessor/__init_.py
|
|
35
35
|
src/voxcity/geoprocessor/draw.py
|
|
36
36
|
src/voxcity/geoprocessor/grid.py
|
|
37
|
+
src/voxcity/geoprocessor/mesh.py
|
|
37
38
|
src/voxcity/geoprocessor/network.py
|
|
38
39
|
src/voxcity/geoprocessor/polygon.py
|
|
39
40
|
src/voxcity/geoprocessor/utils.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|