voxcity 0.6.15__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.
Files changed (78) hide show
  1. voxcity/__init__.py +14 -8
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/citygml.py +32 -18
  4. voxcity/downloader/gba.py +210 -0
  5. voxcity/downloader/gee.py +5 -1
  6. voxcity/downloader/mbfp.py +1 -1
  7. voxcity/downloader/oemj.py +80 -8
  8. voxcity/downloader/osm.py +23 -7
  9. voxcity/downloader/overture.py +26 -1
  10. voxcity/downloader/utils.py +73 -73
  11. voxcity/errors.py +30 -0
  12. voxcity/exporter/__init__.py +13 -4
  13. voxcity/exporter/cityles.py +633 -535
  14. voxcity/exporter/envimet.py +728 -708
  15. voxcity/exporter/magicavoxel.py +334 -297
  16. voxcity/exporter/netcdf.py +238 -0
  17. voxcity/exporter/obj.py +1481 -655
  18. voxcity/generator/__init__.py +44 -0
  19. voxcity/generator/api.py +675 -0
  20. voxcity/generator/grids.py +379 -0
  21. voxcity/generator/io.py +94 -0
  22. voxcity/generator/pipeline.py +282 -0
  23. voxcity/generator/voxelizer.py +380 -0
  24. voxcity/geoprocessor/__init__.py +75 -6
  25. voxcity/geoprocessor/conversion.py +153 -0
  26. voxcity/geoprocessor/draw.py +62 -12
  27. voxcity/geoprocessor/heights.py +199 -0
  28. voxcity/geoprocessor/io.py +101 -0
  29. voxcity/geoprocessor/merge_utils.py +91 -0
  30. voxcity/geoprocessor/mesh.py +806 -790
  31. voxcity/geoprocessor/network.py +708 -679
  32. voxcity/geoprocessor/overlap.py +84 -0
  33. voxcity/geoprocessor/raster/__init__.py +82 -0
  34. voxcity/geoprocessor/raster/buildings.py +428 -0
  35. voxcity/geoprocessor/raster/canopy.py +258 -0
  36. voxcity/geoprocessor/raster/core.py +150 -0
  37. voxcity/geoprocessor/raster/export.py +93 -0
  38. voxcity/geoprocessor/raster/landcover.py +156 -0
  39. voxcity/geoprocessor/raster/raster.py +110 -0
  40. voxcity/geoprocessor/selection.py +85 -0
  41. voxcity/geoprocessor/utils.py +18 -14
  42. voxcity/models.py +113 -0
  43. voxcity/simulator/common/__init__.py +22 -0
  44. voxcity/simulator/common/geometry.py +98 -0
  45. voxcity/simulator/common/raytracing.py +450 -0
  46. voxcity/simulator/solar/__init__.py +43 -0
  47. voxcity/simulator/solar/integration.py +336 -0
  48. voxcity/simulator/solar/kernels.py +62 -0
  49. voxcity/simulator/solar/radiation.py +648 -0
  50. voxcity/simulator/solar/temporal.py +434 -0
  51. voxcity/simulator/view.py +36 -2286
  52. voxcity/simulator/visibility/__init__.py +29 -0
  53. voxcity/simulator/visibility/landmark.py +392 -0
  54. voxcity/simulator/visibility/view.py +508 -0
  55. voxcity/utils/logging.py +61 -0
  56. voxcity/utils/orientation.py +51 -0
  57. voxcity/utils/weather/__init__.py +26 -0
  58. voxcity/utils/weather/epw.py +146 -0
  59. voxcity/utils/weather/files.py +36 -0
  60. voxcity/utils/weather/onebuilding.py +486 -0
  61. voxcity/visualizer/__init__.py +24 -0
  62. voxcity/visualizer/builder.py +43 -0
  63. voxcity/visualizer/grids.py +141 -0
  64. voxcity/visualizer/maps.py +187 -0
  65. voxcity/visualizer/palette.py +228 -0
  66. voxcity/visualizer/renderer.py +928 -0
  67. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/METADATA +113 -36
  68. voxcity-0.7.0.dist-info/RECORD +77 -0
  69. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/WHEEL +1 -1
  70. voxcity/generator.py +0 -1137
  71. voxcity/geoprocessor/grid.py +0 -1568
  72. voxcity/geoprocessor/polygon.py +0 -1344
  73. voxcity/simulator/solar.py +0 -2329
  74. voxcity/utils/visualization.py +0 -2660
  75. voxcity/utils/weather.py +0 -817
  76. voxcity-0.6.15.dist-info/RECORD +0 -37
  77. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/AUTHORS.rst +0 -0
  78. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,2660 +0,0 @@
1
- """
2
- VoxelCity Visualization Utilities
3
-
4
- This module provides comprehensive visualization tools for 3D voxel city data,
5
- including support for multiple color schemes, 3D plotting with matplotlib and plotly,
6
- grid visualization on basemaps, and mesh-based rendering with PyVista.
7
-
8
- The module handles various data types including:
9
- - Land cover classifications
10
- - Building heights and footprints
11
- - Digital elevation models (DEM)
12
- - Canopy heights
13
- - View indices (sky view factor, green view index)
14
- - Simulation results on building surfaces
15
-
16
- Key Features:
17
- - Multiple predefined color schemes for voxel visualization
18
- - 2D and 3D plotting capabilities
19
- - Interactive web maps with folium
20
- - Mesh export functionality (OBJ format)
21
- - Multi-view scene generation
22
- - Custom simulation result overlays
23
- """
24
-
25
- import numpy as np
26
- import matplotlib.pyplot as plt
27
- from mpl_toolkits.mplot3d import Axes3D
28
- from tqdm import tqdm
29
- import matplotlib.colors as mcolors
30
- from matplotlib.colors import ListedColormap, BoundaryNorm
31
- import matplotlib.cm as cm
32
- import contextily as ctx
33
- from shapely.geometry import Polygon
34
- import plotly.graph_objects as go
35
- from tqdm import tqdm
36
- import pyproj
37
- # import rasterio
38
- from pyproj import CRS
39
- # from shapely.geometry import box
40
- import seaborn as sns
41
- import random
42
- import folium
43
- import math
44
- import trimesh
45
- import pyvista as pv
46
- from IPython.display import display
47
- import os
48
- import sys
49
-
50
- # Import utility functions for land cover classification
51
- from .lc import get_land_cover_classes
52
- # from ..geo.geojson import filter_buildings
53
-
54
- # Import grid processing functions
55
- from ..geoprocessor.grid import (
56
- calculate_grid_size,
57
- create_coordinate_mesh,
58
- create_cell_polygon,
59
- grid_to_geodataframe
60
- )
61
-
62
- # Import geospatial utility functions
63
- from ..geoprocessor.utils import (
64
- initialize_geod,
65
- calculate_distance,
66
- normalize_to_one_meter,
67
- setup_transformer,
68
- transform_coords,
69
- )
70
-
71
- # Import mesh generation and export functions
72
- from ..geoprocessor.mesh import (
73
- create_voxel_mesh,
74
- create_sim_surface_mesh,
75
- create_city_meshes,
76
- export_meshes,
77
- save_obj_from_colored_mesh
78
- )
79
- # from ..exporter.obj import save_obj_from_colored_mesh
80
-
81
- # Import material property functions
82
- from .material import get_material_dict
83
-
84
- def get_voxel_color_map(color_scheme='default'):
85
- """
86
- Returns a color map for voxel visualization based on the specified color scheme.
87
-
88
- This function provides multiple predefined color schemes for visualizing voxel data.
89
- Each scheme maps voxel class IDs to RGB color values [0-255]. The class IDs follow
90
- a specific convention where negative values represent built environment elements
91
- and positive values represent natural/ground surface elements.
92
-
93
- Voxel Class ID Convention:
94
- -99: Void/empty space (black)
95
- -30: Landmark buildings (special highlighting)
96
- -17 to -11: Building materials (plaster, glass, stone, metal, concrete, wood, brick)
97
- -3: Generic building structures
98
- -2: Trees/vegetation (above ground)
99
- -1: Underground/subsurface
100
- 1-14: Ground surface land cover types (bareland, vegetation, water, etc.)
101
-
102
- Parameters:
103
- -----------
104
- color_scheme : str, optional
105
- The name of the color scheme to use. Available options:
106
-
107
- Basic Schemes:
108
- - 'default': Original balanced color scheme for general use
109
- - 'high_contrast': High contrast colors for better visibility
110
- - 'monochrome': Shades of blue for academic presentations
111
- - 'pastel': Softer, muted colors for aesthetic appeal
112
- - 'dark_mode': Darker colors for dark backgrounds
113
- - 'grayscale': Black and white gradient with color accents
114
-
115
- Thematic Schemes:
116
- - 'autumn': Warm reds, oranges, and browns
117
- - 'cool': Cool blues, purples, and cyans
118
- - 'earth_tones': Natural earth colors
119
- - 'vibrant': Very bright, saturated colors
120
-
121
- Stylistic Schemes:
122
- - 'cyberpunk': Neon-like purples, pinks, and blues
123
- - 'tropical': Vibrant greens, oranges, pinks (island vibes)
124
- - 'vintage': Muted, sepia-like tones
125
- - 'neon_dreams': Super-bright, nightclub neon palette
126
-
127
- Returns:
128
- --------
129
- dict
130
- A dictionary mapping voxel class IDs (int) to RGB color values (list of 3 ints [0-255])
131
-
132
- Examples:
133
- ---------
134
- >>> colors = get_voxel_color_map('default')
135
- >>> print(colors[-3]) # Building color
136
- [180, 187, 216]
137
-
138
- >>> colors = get_voxel_color_map('cyberpunk')
139
- >>> print(colors[9]) # Water color in cyberpunk scheme
140
- [51, 0, 102]
141
-
142
- Notes:
143
- ------
144
- - All color values are in RGB format with range [0, 255]
145
- - The 'default' scheme should not be modified to maintain consistency
146
- - Unknown color schemes will fall back to 'default' with a warning
147
- - Color schemes can be extended by adding new elif blocks
148
- """
149
- # ----------------------
150
- # DO NOT MODIFY DEFAULT
151
- # ----------------------
152
- if color_scheme == 'default':
153
- return {
154
- -99: [0, 0, 0], # void,
155
- -30: [255, 0, 102], # (Pink) 'Landmark',
156
- -17: [238, 242, 234], # (light gray) 'plaster',
157
- -16: [56, 78, 84], # (Dark blue) 'glass',
158
- -15: [147, 140, 114], # (Light brown) 'stone',
159
- -14: [139, 149, 159], # (Gray) 'metal',
160
- -13: [186, 187, 181], # (Gray) 'concrete',
161
- -12: [248, 166, 2], # (Orange) 'wood',
162
- -11: [81, 59, 56], # (Dark red) 'brick',
163
- -3: [180, 187, 216], # Building
164
- -2: [78, 99, 63], # Tree
165
- -1: [188, 143, 143], # Underground
166
- 1: [239, 228, 176], # 'Bareland (ground surface)',
167
- 2: [123, 130, 59], # 'Rangeland (ground surface)',
168
- 3: [97, 140, 86], # 'Shrub (ground surface)',
169
- 4: [112, 120, 56], # 'Agriculture land (ground surface)',
170
- 5: [116, 150, 66], # 'Tree (ground surface)',
171
- 6: [187, 204, 40], # 'Moss and lichen (ground surface)',
172
- 7: [77, 118, 99], # 'Wet land (ground surface)',
173
- 8: [22, 61, 51], # 'Mangrove (ground surface)',
174
- 9: [44, 66, 133], # 'Water (ground surface)',
175
- 10: [205, 215, 224], # 'Snow and ice (ground surface)',
176
- 11: [108, 119, 129], # 'Developed space (ground surface)',
177
- 12: [59, 62, 87], # 'Road (ground surface)',
178
- 13: [150, 166, 190], # 'Building (ground surface)'
179
- 14: [239, 228, 176], # 'No Data (ground surface)'
180
- }
181
-
182
- elif color_scheme == 'high_contrast':
183
- return {
184
- -99: [0, 0, 0], # void
185
- -30: [255, 0, 255], # (Bright Magenta) 'Landmark'
186
- -17: [255, 255, 255], # (Pure White) 'plaster'
187
- -16: [0, 0, 255], # (Bright Blue) 'glass'
188
- -15: [153, 76, 0], # (Dark Brown) 'stone'
189
- -14: [192, 192, 192], # (Silver) 'metal'
190
- -13: [128, 128, 128], # (Gray) 'concrete'
191
- -12: [255, 128, 0], # (Bright Orange) 'wood'
192
- -11: [153, 0, 0], # (Dark Red) 'brick'
193
- -3: [0, 255, 255], # (Cyan) Building
194
- -2: [0, 153, 0], # (Green) Tree
195
- -1: [204, 0, 102], # (Dark Pink) Underground
196
- 1: [255, 255, 153], # (Light Yellow) 'Bareland'
197
- 2: [102, 153, 0], # (Olive Green) 'Rangeland'
198
- 3: [0, 204, 0], # (Bright Green) 'Shrub'
199
- 4: [153, 204, 0], # (Yellowish Green) 'Agriculture land'
200
- 5: [0, 102, 0], # (Dark Green) 'Tree'
201
- 6: [204, 255, 51], # (Lime Green) 'Moss and lichen'
202
- 7: [0, 153, 153], # (Teal) 'Wet land'
203
- 8: [0, 51, 0], # (Very Dark Green) 'Mangrove'
204
- 9: [0, 102, 204], # (Bright Blue) 'Water'
205
- 10: [255, 255, 255], # (White) 'Snow and ice'
206
- 11: [76, 76, 76], # (Dark Gray) 'Developed space'
207
- 12: [0, 0, 0], # (Black) 'Road'
208
- 13: [102, 102, 255], # (Light Purple) 'Building'
209
- 14: [255, 204, 153], # (Light Orange) 'No Data'
210
- }
211
-
212
- elif color_scheme == 'monochrome':
213
- return {
214
- -99: [0, 0, 0], # void
215
- -30: [28, 28, 99], # 'Landmark'
216
- -17: [242, 242, 242], # 'plaster'
217
- -16: [51, 51, 153], # 'glass'
218
- -15: [102, 102, 204], # 'stone'
219
- -14: [153, 153, 204], # 'metal'
220
- -13: [204, 204, 230], # 'concrete'
221
- -12: [76, 76, 178], # 'wood'
222
- -11: [25, 25, 127], # 'brick'
223
- -3: [179, 179, 230], # Building
224
- -2: [51, 51, 153], # Tree
225
- -1: [102, 102, 178], # Underground
226
- 1: [230, 230, 255], # 'Bareland'
227
- 2: [128, 128, 204], # 'Rangeland'
228
- 3: [102, 102, 204], # 'Shrub'
229
- 4: [153, 153, 230], # 'Agriculture land'
230
- 5: [76, 76, 178], # 'Tree'
231
- 6: [204, 204, 255], # 'Moss and lichen'
232
- 7: [76, 76, 178], # 'Wet land'
233
- 8: [25, 25, 127], # 'Mangrove'
234
- 9: [51, 51, 204], # 'Water'
235
- 10: [242, 242, 255], # 'Snow and ice'
236
- 11: [128, 128, 178], # 'Developed space'
237
- 12: [51, 51, 127], # 'Road'
238
- 13: [153, 153, 204], # 'Building'
239
- 14: [230, 230, 255], # 'No Data'
240
- }
241
-
242
- elif color_scheme == 'pastel':
243
- return {
244
- -99: [0, 0, 0], # void
245
- -30: [255, 179, 217], # (Pastel Pink) 'Landmark'
246
- -17: [245, 245, 245], # (Off White) 'plaster'
247
- -16: [173, 196, 230], # (Pastel Blue) 'glass'
248
- -15: [222, 213, 196], # (Pastel Brown) 'stone'
249
- -14: [211, 219, 226], # (Pastel Gray) 'metal'
250
- -13: [226, 226, 226], # (Light Gray) 'concrete'
251
- -12: [255, 223, 179], # (Pastel Orange) 'wood'
252
- -11: [204, 168, 166], # (Pastel Red) 'brick'
253
- -3: [214, 217, 235], # (Pastel Purple) Building
254
- -2: [190, 207, 180], # (Pastel Green) Tree
255
- -1: [235, 204, 204], # (Pastel Pink) Underground
256
- 1: [250, 244, 227], # (Cream) 'Bareland'
257
- 2: [213, 217, 182], # (Pastel Olive) 'Rangeland'
258
- 3: [200, 226, 195], # (Pastel Green) 'Shrub'
259
- 4: [209, 214, 188], # (Pastel Yellow-Green) 'Agriculture land'
260
- 5: [195, 220, 168], # (Light Pastel Green) 'Tree'
261
- 6: [237, 241, 196], # (Pastel Yellow) 'Moss and lichen'
262
- 7: [180, 210, 205], # (Pastel Teal) 'Wet land'
263
- 8: [176, 196, 190], # (Darker Pastel Teal) 'Mangrove'
264
- 9: [188, 206, 235], # (Pastel Blue) 'Water'
265
- 10: [242, 245, 250], # (Light Blue-White) 'Snow and ice'
266
- 11: [209, 213, 219], # (Pastel Gray) 'Developed space'
267
- 12: [189, 190, 204], # (Pastel Blue-Gray) 'Road'
268
- 13: [215, 221, 232], # (Very Light Pastel Blue) 'Building'
269
- 14: [250, 244, 227], # (Cream) 'No Data'
270
- }
271
-
272
- elif color_scheme == 'dark_mode':
273
- return {
274
- -99: [0, 0, 0], # void
275
- -30: [153, 51, 102], # (Dark Pink) 'Landmark'
276
- -17: [76, 76, 76], # (Dark Gray) 'plaster'
277
- -16: [33, 46, 51], # (Very Dark Blue) 'glass'
278
- -15: [89, 84, 66], # (Very Dark Brown) 'stone'
279
- -14: [83, 89, 94], # (Dark Gray) 'metal'
280
- -13: [61, 61, 61], # (Dark Gray) 'concrete'
281
- -12: [153, 102, 0], # (Dark Orange) 'wood'
282
- -11: [51, 35, 33], # (Very Dark Red) 'brick'
283
- -3: [78, 82, 99], # (Dark Purple) Building
284
- -2: [46, 58, 37], # (Dark Green) Tree
285
- -1: [99, 68, 68], # (Dark Pink) Underground
286
- 1: [102, 97, 75], # (Dark Yellow) 'Bareland'
287
- 2: [61, 66, 31], # (Dark Olive) 'Rangeland'
288
- 3: [46, 77, 46], # (Dark Green) 'Shrub'
289
- 4: [56, 61, 28], # (Dark Yellow-Green) 'Agriculture land'
290
- 5: [54, 77, 31], # (Dark Green) 'Tree'
291
- 6: [89, 97, 20], # (Dark Yellow) 'Moss and lichen'
292
- 7: [38, 59, 49], # (Dark Teal) 'Wet land'
293
- 8: [16, 31, 26], # (Very Dark Green) 'Mangrove'
294
- 9: [22, 33, 66], # (Dark Blue) 'Water'
295
- 10: [82, 87, 92], # (Dark Blue-Gray) 'Snow and ice'
296
- 11: [46, 51, 56], # (Dark Gray) 'Developed space'
297
- 12: [25, 31, 43], # (Very Dark Blue) 'Road'
298
- 13: [56, 64, 82], # (Dark Blue-Gray) 'Building'
299
- 14: [102, 97, 75], # (Dark Yellow) 'No Data'
300
- }
301
-
302
- elif color_scheme == 'grayscale':
303
- return {
304
- -99: [0, 0, 0], # void (black)
305
- -30: [253, 231, 37], # (Pink) 'Landmark',
306
- -17: [240, 240, 240], # 'plaster'
307
- -16: [60, 60, 60], # 'glass'
308
- -15: [130, 130, 130], # 'stone'
309
- -14: [150, 150, 150], # 'metal'
310
- -13: [180, 180, 180], # 'concrete'
311
- -12: [170, 170, 170], # 'wood'
312
- -11: [70, 70, 70], # 'brick'
313
- -3: [190, 190, 190], # Building
314
- -2: [90, 90, 90], # Tree
315
- -1: [160, 160, 160], # Underground
316
- 1: [230, 230, 230], # 'Bareland'
317
- 2: [120, 120, 120], # 'Rangeland'
318
- 3: [110, 110, 110], # 'Shrub'
319
- 4: [115, 115, 115], # 'Agriculture land'
320
- 5: [100, 100, 100], # 'Tree'
321
- 6: [210, 210, 210], # 'Moss and lichen'
322
- 7: [95, 95, 95], # 'Wet land'
323
- 8: [40, 40, 40], # 'Mangrove'
324
- 9: [50, 50, 50], # 'Water'
325
- 10: [220, 220, 220], # 'Snow and ice'
326
- 11: [140, 140, 140], # 'Developed space'
327
- 12: [30, 30, 30], # 'Road'
328
- 13: [170, 170, 170], # 'Building'
329
- 14: [230, 230, 230], # 'No Data'
330
- }
331
-
332
- elif color_scheme == 'autumn':
333
- return {
334
- -99: [0, 0, 0], # void
335
- -30: [227, 66, 52], # (Red) 'Landmark'
336
- -17: [250, 240, 230], # (Antique White) 'plaster'
337
- -16: [94, 33, 41], # (Dark Red) 'glass'
338
- -15: [160, 120, 90], # (Medium Brown) 'stone'
339
- -14: [176, 141, 87], # (Bronze) 'metal'
340
- -13: [205, 186, 150], # (Tan) 'concrete'
341
- -12: [204, 85, 0], # (Dark Orange) 'wood'
342
- -11: [128, 55, 36], # (Rust) 'brick'
343
- -3: [222, 184, 135], # (Tan) Building
344
- -2: [107, 68, 35], # (Brown) Tree
345
- -1: [165, 105, 79], # (Copper) Underground
346
- 1: [255, 235, 205], # (Blanched Almond) 'Bareland'
347
- 2: [133, 99, 99], # (Brown) 'Rangeland'
348
- 3: [139, 69, 19], # (Saddle Brown) 'Shrub'
349
- 4: [160, 82, 45], # (Sienna) 'Agriculture land'
350
- 5: [101, 67, 33], # (Dark Brown) 'Tree'
351
- 6: [255, 228, 196], # (Bisque) 'Moss and lichen'
352
- 7: [138, 51, 36], # (Rust) 'Wet land'
353
- 8: [85, 45, 23], # (Deep Brown) 'Mangrove'
354
- 9: [175, 118, 70], # (Light Brown) 'Water'
355
- 10: [255, 250, 240], # (Floral White) 'Snow and ice'
356
- 11: [188, 143, 143], # (Rosy Brown) 'Developed space'
357
- 12: [69, 41, 33], # (Very Dark Brown) 'Road'
358
- 13: [210, 180, 140], # (Tan) 'Building'
359
- 14: [255, 235, 205], # (Blanched Almond) 'No Data'
360
- }
361
-
362
- elif color_scheme == 'cool':
363
- return {
364
- -99: [0, 0, 0], # void
365
- -30: [180, 82, 205], # (Purple) 'Landmark'
366
- -17: [240, 248, 255], # (Alice Blue) 'plaster'
367
- -16: [70, 130, 180], # (Steel Blue) 'glass'
368
- -15: [100, 149, 237], # (Cornflower Blue) 'stone'
369
- -14: [176, 196, 222], # (Light Steel Blue) 'metal'
370
- -13: [240, 255, 255], # (Azure) 'concrete'
371
- -12: [65, 105, 225], # (Royal Blue) 'wood'
372
- -11: [95, 158, 160], # (Cadet Blue) 'brick'
373
- -3: [135, 206, 235], # (Sky Blue) Building
374
- -2: [0, 128, 128], # (Teal) Tree
375
- -1: [127, 255, 212], # (Aquamarine) Underground
376
- 1: [220, 240, 250], # (Light Blue) 'Bareland'
377
- 2: [72, 209, 204], # (Medium Turquoise) 'Rangeland'
378
- 3: [0, 191, 255], # (Deep Sky Blue) 'Shrub'
379
- 4: [100, 149, 237], # (Cornflower Blue) 'Agriculture land'
380
- 5: [0, 128, 128], # (Teal) 'Tree'
381
- 6: [175, 238, 238], # (Pale Turquoise) 'Moss and lichen'
382
- 7: [32, 178, 170], # (Light Sea Green) 'Wet land'
383
- 8: [25, 25, 112], # (Midnight Blue) 'Mangrove'
384
- 9: [30, 144, 255], # (Dodger Blue) 'Water'
385
- 10: [240, 255, 255], # (Azure) 'Snow and ice'
386
- 11: [119, 136, 153], # (Light Slate Gray) 'Developed space'
387
- 12: [25, 25, 112], # (Midnight Blue) 'Road'
388
- 13: [173, 216, 230], # (Light Blue) 'Building'
389
- 14: [220, 240, 250], # (Light Blue) 'No Data'
390
- }
391
-
392
- elif color_scheme == 'earth_tones':
393
- return {
394
- -99: [0, 0, 0], # void
395
- -30: [210, 105, 30], # (Chocolate) 'Landmark'
396
- -17: [245, 245, 220], # (Beige) 'plaster'
397
- -16: [139, 137, 137], # (Gray) 'glass'
398
- -15: [160, 120, 90], # (Medium Brown) 'stone'
399
- -14: [169, 169, 169], # (Dark Gray) 'metal'
400
- -13: [190, 190, 180], # (Light Gray-Tan) 'concrete'
401
- -12: [160, 82, 45], # (Sienna) 'wood'
402
- -11: [139, 69, 19], # (Saddle Brown) 'brick'
403
- -3: [210, 180, 140], # (Tan) Building
404
- -2: [85, 107, 47], # (Dark Olive Green) Tree
405
- -1: [133, 94, 66], # (Beaver) Underground
406
- 1: [222, 184, 135], # (Burlywood) 'Bareland'
407
- 2: [107, 142, 35], # (Olive Drab) 'Rangeland'
408
- 3: [85, 107, 47], # (Dark Olive Green) 'Shrub'
409
- 4: [128, 128, 0], # (Olive) 'Agriculture land'
410
- 5: [34, 139, 34], # (Forest Green) 'Tree'
411
- 6: [189, 183, 107], # (Dark Khaki) 'Moss and lichen'
412
- 7: [143, 188, 143], # (Dark Sea Green) 'Wet land'
413
- 8: [46, 139, 87], # (Sea Green) 'Mangrove'
414
- 9: [95, 158, 160], # (Cadet Blue) 'Water'
415
- 10: [238, 232, 205], # (Light Tan) 'Snow and ice'
416
- 11: [169, 169, 169], # (Dark Gray) 'Developed space'
417
- 12: [90, 90, 90], # (Dark Gray) 'Road'
418
- 13: [188, 170, 152], # (Tan) 'Building'
419
- 14: [222, 184, 135], # (Burlywood) 'No Data'
420
- }
421
-
422
- elif color_scheme == 'vibrant':
423
- return {
424
- -99: [0, 0, 0], # void
425
- -30: [255, 0, 255], # (Magenta) 'Landmark'
426
- -17: [255, 255, 255], # (White) 'plaster'
427
- -16: [0, 191, 255], # (Deep Sky Blue) 'glass'
428
- -15: [255, 215, 0], # (Gold) 'stone'
429
- -14: [0, 250, 154], # (Medium Spring Green) 'metal'
430
- -13: [211, 211, 211], # (Light Gray) 'concrete'
431
- -12: [255, 69, 0], # (Orange Red) 'wood'
432
- -11: [178, 34, 34], # (Firebrick) 'brick'
433
- -3: [123, 104, 238], # (Medium Slate Blue) Building
434
- -2: [50, 205, 50], # (Lime Green) Tree
435
- -1: [255, 20, 147], # (Deep Pink) Underground
436
- 1: [255, 255, 0], # (Yellow) 'Bareland'
437
- 2: [0, 255, 0], # (Lime) 'Rangeland'
438
- 3: [0, 128, 0], # (Green) 'Shrub'
439
- 4: [154, 205, 50], # (Yellow Green) 'Agriculture land'
440
- 5: [34, 139, 34], # (Forest Green) 'Tree'
441
- 6: [127, 255, 0], # (Chartreuse) 'Moss and lichen'
442
- 7: [64, 224, 208], # (Turquoise) 'Wet land'
443
- 8: [0, 100, 0], # (Dark Green) 'Mangrove'
444
- 9: [0, 0, 255], # (Blue) 'Water'
445
- 10: [240, 248, 255], # (Alice Blue) 'Snow and ice'
446
- 11: [128, 128, 128], # (Gray) 'Developed space'
447
- 12: [47, 79, 79], # (Dark Slate Gray) 'Road'
448
- 13: [135, 206, 250], # (Light Sky Blue) 'Building'
449
- 14: [255, 255, 224], # (Light Yellow) 'No Data'
450
- }
451
-
452
- # ------------------------------------------------
453
- # NEWLY ADDED STYLISH COLOR SCHEMES BELOW:
454
- # ------------------------------------------------
455
- elif color_scheme == 'cyberpunk':
456
- """
457
- Vibrant neon purples, pinks, and blues with deep blacks.
458
- Think futuristic city vibes and bright neon signs.
459
- """
460
- return {
461
- -99: [0, 0, 0], # void (keep it pitch black)
462
- -30: [255, 0, 255], # (Neon Magenta) 'Landmark'
463
- -17: [255, 255, 255], # (Bright White) 'plaster'
464
- -16: [0, 255, 255], # (Neon Cyan) 'glass'
465
- -15: [128, 0, 128], # (Purple) 'stone'
466
- -14: [50, 50, 50], # (Dark Gray) 'metal'
467
- -13: [102, 0, 102], # (Dark Magenta) 'concrete'
468
- -12: [255, 20, 147], # (Deep Pink) 'wood'
469
- -11: [153, 0, 76], # (Deep Purple-Red) 'brick'
470
- -3: [124, 0, 255], # (Strong Neon Purple) Building
471
- -2: [0, 255, 153], # (Neon Greenish Cyan) Tree
472
- -1: [255, 0, 102], # (Hot Pink) Underground
473
- 1: [255, 255, 153], # (Pale Yellow) 'Bareland'
474
- 2: [0, 204, 204], # (Teal) 'Rangeland'
475
- 3: [153, 51, 255], # (Light Purple) 'Shrub'
476
- 4: [0, 153, 255], # (Bright Neon Blue) 'Agriculture land'
477
- 5: [0, 255, 153], # (Neon Greenish Cyan) 'Tree'
478
- 6: [204, 0, 255], # (Vivid Violet) 'Moss and lichen'
479
- 7: [0, 255, 255], # (Neon Cyan) 'Wet land'
480
- 8: [0, 102, 102], # (Dark Teal) 'Mangrove'
481
- 9: [51, 0, 102], # (Deep Indigo) 'Water'
482
- 10: [255, 255, 255], # (White) 'Snow and ice'
483
- 11: [102, 102, 102], # (Gray) 'Developed space'
484
- 12: [0, 0, 0], # (Black) 'Road'
485
- 13: [204, 51, 255], # (Bright Magenta) 'Building'
486
- 14: [255, 255, 153], # (Pale Yellow) 'No Data'
487
- }
488
-
489
- elif color_scheme == 'tropical':
490
- """
491
- Bold, bright 'tropical vacation' color palette.
492
- Lots of greens, oranges, pinks, reminiscent of island florals.
493
- """
494
- return {
495
- -99: [0, 0, 0], # void
496
- -30: [255, 99, 164], # (Bright Tropical Pink) 'Landmark'
497
- -17: [255, 248, 220], # (Cornsilk) 'plaster'
498
- -16: [0, 150, 136], # (Teal) 'glass'
499
- -15: [255, 140, 0], # (Dark Orange) 'stone'
500
- -14: [255, 215, 180], # (Light Peach) 'metal'
501
- -13: [210, 210, 210], # (Light Gray) 'concrete'
502
- -12: [255, 165, 0], # (Orange) 'wood'
503
- -11: [205, 92, 92], # (Indian Red) 'brick'
504
- -3: [255, 193, 37], # (Tropical Yellow) Building
505
- -2: [34, 139, 34], # (Forest Green) Tree
506
- -1: [255, 160, 122], # (Light Salmon) Underground
507
- 1: [240, 230, 140], # (Khaki) 'Bareland'
508
- 2: [60, 179, 113], # (Medium Sea Green) 'Rangeland'
509
- 3: [46, 139, 87], # (Sea Green) 'Shrub'
510
- 4: [255, 127, 80], # (Coral) 'Agriculture land'
511
- 5: [50, 205, 50], # (Lime Green) 'Tree'
512
- 6: [255, 239, 213], # (Papaya Whip) 'Moss and lichen'
513
- 7: [255, 99, 71], # (Tomato) 'Wet land'
514
- 8: [47, 79, 79], # (Dark Slate Gray) 'Mangrove'
515
- 9: [0, 128, 128], # (Teal) 'Water'
516
- 10: [224, 255, 255], # (Light Cyan) 'Snow and ice'
517
- 11: [218, 112, 214], # (Orchid) 'Developed space'
518
- 12: [85, 107, 47], # (Dark Olive Green) 'Road'
519
- 13: [253, 245, 230], # (Old Lace) 'Building'
520
- 14: [240, 230, 140], # (Khaki) 'No Data'
521
- }
522
-
523
- elif color_scheme == 'vintage':
524
- """
525
- A muted, old-photo or sepia-inspired palette
526
- for a nostalgic or antique look.
527
- """
528
- return {
529
- -99: [0, 0, 0], # void
530
- -30: [133, 94, 66], # (Beaver/Brownish) 'Landmark'
531
- -17: [250, 240, 230], # (Antique White) 'plaster'
532
- -16: [169, 157, 143], # (Muted Brown-Gray) 'glass'
533
- -15: [181, 166, 127], # (Khaki Tan) 'stone'
534
- -14: [120, 106, 93], # (Faded Gray-Brown) 'metal'
535
- -13: [190, 172, 145], # (Light Brown) 'concrete'
536
- -12: [146, 109, 83], # (Leather Brown) 'wood'
537
- -11: [125, 80, 70], # (Dusty Brick) 'brick'
538
- -3: [201, 174, 146], # (Tanned Beige) Building
539
- -2: [112, 98, 76], # (Faded Olive-Brown) Tree
540
- -1: [172, 140, 114], # (Light Saddle Brown) Underground
541
- 1: [222, 202, 166], # (Light Tan) 'Bareland'
542
- 2: [131, 114, 83], # (Brownish) 'Rangeland'
543
- 3: [105, 96, 74], # (Dark Olive Brown) 'Shrub'
544
- 4: [162, 141, 118], # (Beige Brown) 'Agriculture land'
545
- 5: [95, 85, 65], # (Muted Dark Brown) 'Tree'
546
- 6: [212, 200, 180], # (Off-White Tan) 'Moss and lichen'
547
- 7: [140, 108, 94], # (Dusky Mauve-Brown) 'Wet land'
548
- 8: [85, 73, 60], # (Dark Taupe) 'Mangrove'
549
- 9: [166, 152, 121], # (Pale Brown) 'Water'
550
- 10: [250, 245, 235], # (Light Antique White) 'Snow and ice'
551
- 11: [120, 106, 93], # (Faded Gray-Brown) 'Developed space'
552
- 12: [77, 66, 55], # (Dark Taupe) 'Road'
553
- 13: [203, 188, 162], # (Light Warm Gray) 'Building'
554
- 14: [222, 202, 166], # (Light Tan) 'No Data'
555
- }
556
-
557
- elif color_scheme == 'neon_dreams':
558
- """
559
- A super-bright, high-energy neon palette.
560
- Perfect if you want a 'nightclub in 2080' vibe.
561
- """
562
- return {
563
- -99: [0, 0, 0], # void
564
- -30: [255, 0, 255], # (Magenta) 'Landmark'
565
- -17: [255, 255, 255], # (White) 'plaster'
566
- -16: [0, 255, 255], # (Cyan) 'glass'
567
- -15: [255, 255, 0], # (Yellow) 'stone'
568
- -14: [0, 255, 0], # (Lime) 'metal'
569
- -13: [128, 128, 128], # (Gray) 'concrete'
570
- -12: [255, 165, 0], # (Neon Orange) 'wood'
571
- -11: [255, 20, 147], # (Deep Pink) 'brick'
572
- -3: [75, 0, 130], # (Indigo) Building
573
- -2: [102, 255, 0], # (Bright Lime Green) Tree
574
- -1: [255, 51, 153], # (Neon Pink) Underground
575
- 1: [255, 153, 0], # (Bright Orange) 'Bareland'
576
- 2: [153, 204, 0], # (Vivid Yellow-Green) 'Rangeland'
577
- 3: [102, 205, 170], # (Aquamarine-ish) 'Shrub'
578
- 4: [0, 250, 154], # (Medium Spring Green) 'Agriculture land'
579
- 5: [173, 255, 47], # (Green-Yellow) 'Tree'
580
- 6: [127, 255, 0], # (Chartreuse) 'Moss and lichen'
581
- 7: [64, 224, 208], # (Turquoise) 'Wet land'
582
- 8: [0, 128, 128], # (Teal) 'Mangrove'
583
- 9: [0, 0, 255], # (Blue) 'Water'
584
- 10: [224, 255, 255], # (Light Cyan) 'Snow and ice'
585
- 11: [192, 192, 192], # (Silver) 'Developed space'
586
- 12: [25, 25, 25], # (Near Black) 'Road'
587
- 13: [75, 0, 130], # (Indigo) 'Building'
588
- 14: [255, 153, 0], # (Bright Orange) 'No Data'
589
- }
590
-
591
- else:
592
- # If an unknown color scheme is specified, return the default
593
- print(f"Unknown color scheme '{color_scheme}'. Using default instead.")
594
- return get_voxel_color_map('default')
595
-
596
- def visualize_3d_voxel(voxel_grid, voxel_color_map = 'default', voxel_size=2.0, save_path=None):
597
- """
598
- Visualizes 3D voxel data using matplotlib's 3D plotting capabilities.
599
-
600
- This function creates a 3D visualization of voxel data where each non-zero voxel
601
- is rendered as a colored cube. The colors are determined by the voxel values
602
- and the specified color scheme. The visualization includes proper transparency
603
- handling and aspect ratio adjustment.
604
-
605
- Parameters:
606
- -----------
607
- voxel_grid : numpy.ndarray
608
- 3D numpy array containing voxel data. Shape should be (x, y, z) where
609
- each element represents a voxel class ID. Zero values are treated as empty space.
610
-
611
- voxel_color_map : str, optional
612
- Name of the color scheme to use for voxel coloring. Default is 'default'.
613
- See get_voxel_color_map() for available options.
614
-
615
- voxel_size : float, optional
616
- Physical size of each voxel in meters. Used for z-axis scaling and labels.
617
- Default is 2.0 meters.
618
-
619
- save_path : str, optional
620
- File path to save the generated plot. If None, the plot is only displayed.
621
- Default is None.
622
-
623
- Returns:
624
- --------
625
- None
626
- The function displays the plot and optionally saves it to file.
627
-
628
- Notes:
629
- ------
630
- - Void voxels (value -99) are rendered transparent
631
- - Underground voxels (value -1) and trees (value -2) have reduced transparency
632
- - Z-axis ticks are automatically scaled to show real-world heights
633
- - The plot aspect ratio is adjusted to maintain proper voxel proportions
634
- - For large voxel grids, this function may be slow due to matplotlib's 3D rendering
635
-
636
- Examples:
637
- ---------
638
- >>> # Basic visualization
639
- >>> visualize_3d_voxel(voxel_array)
640
-
641
- >>> # Use cyberpunk color scheme and save to file
642
- >>> visualize_3d_voxel(voxel_array, 'cyberpunk', save_path='city_view.png')
643
-
644
- >>> # Adjust voxel size for different scale
645
- >>> visualize_3d_voxel(voxel_array, voxel_size=1.0)
646
- """
647
- # Get the color mapping for the specified scheme
648
- color_map = get_voxel_color_map(voxel_color_map)
649
-
650
- print("\tVisualizing 3D voxel data")
651
- # Create a figure and a 3D axis
652
- fig = plt.figure(figsize=(12, 10))
653
- ax = fig.add_subplot(111, projection='3d')
654
-
655
- print("\tProcessing voxels...")
656
- # Create boolean mask for voxels that should be rendered (non-zero values)
657
- filled_voxels = voxel_grid != 0
658
-
659
- # Initialize color array with RGBA values (Red, Green, Blue, Alpha)
660
- colors = np.zeros(voxel_grid.shape + (4,)) # RGBA
661
-
662
- # Process each possible voxel value and assign colors
663
- for val in range(-99, 15): # Updated range to include -3 and -2
664
- # Create mask for voxels with this specific value
665
- mask = voxel_grid == val
666
-
667
- if val in color_map:
668
- # Convert RGB values from [0,255] to [0,1] range for matplotlib
669
- rgb = [x/255 for x in color_map[val]] # Normalize RGB values to [0, 1]
670
-
671
- # Set transparency based on voxel type
672
- # alpha = 0.7 if ((val == -1) or (val == -2)) else 0.9 # More transparent for underground and below
673
- alpha = 0.0 if (val == -99) else 1 # Void voxels are completely transparent
674
- # alpha = 1
675
-
676
- # Assign RGBA color to all voxels of this type
677
- colors[mask] = rgb + [alpha]
678
- else:
679
- # Default color for undefined voxel types
680
- colors[mask] = [0, 0, 0, 0.9] # Default color if not in color_map
681
-
682
- # Render voxels with progress bar
683
- with tqdm(total=np.prod(voxel_grid.shape)) as pbar:
684
- ax.voxels(filled_voxels, facecolors=colors, edgecolors=None)
685
- pbar.update(np.prod(voxel_grid.shape))
686
-
687
- # print("Finalizing plot...")
688
- # Set labels and title
689
- # ax.set_xlabel('X')
690
- # ax.set_ylabel('Y')
691
- # ax.set_zlabel('Z (meters)')
692
- # ax.set_title('3D Voxel Visualization')
693
-
694
- # Configure z-axis ticks to show meaningful height values
695
- # Adjust z-axis ticks to show every 10 cells or less
696
- z_max = voxel_grid.shape[2]
697
- if z_max <= 10:
698
- z_ticks = range(0, z_max + 1)
699
- else:
700
- z_ticks = range(0, z_max + 1, 10)
701
-
702
- # Remove axes for cleaner appearance
703
- ax.axis('off')
704
- # ax.set_zticks(z_ticks)
705
- # ax.set_zticklabels([f"{z * voxel_size:.1f}" for z in z_ticks])
706
-
707
- # Set aspect ratio to be equal for realistic proportions
708
- max_range = np.array([voxel_grid.shape[0], voxel_grid.shape[1], voxel_grid.shape[2]]).max()
709
- ax.set_box_aspect((voxel_grid.shape[0]/max_range, voxel_grid.shape[1]/max_range, voxel_grid.shape[2]/max_range))
710
-
711
- # Set z-axis limits to focus on the relevant height range
712
- ax.set_zlim(bottom=0)
713
- ax.set_zlim(top=150)
714
-
715
- # print("Visualization complete. Displaying plot...")
716
- plt.tight_layout()
717
-
718
- # Save plot if path is provided
719
- if save_path:
720
- plt.savefig(save_path, dpi=300, bbox_inches='tight')
721
- plt.show()
722
-
723
- def visualize_3d_voxel_plotly(voxel_grid, voxel_color_map = 'default', voxel_size=2.0):
724
- """
725
- Creates an interactive 3D visualization of voxel data using Plotly.
726
-
727
- This function generates an interactive 3D visualization using Plotly's Mesh3d
728
- and Scatter3d objects. Each voxel is rendered as a cube with proper lighting
729
- and edge visualization. The resulting plot supports interactive rotation,
730
- zooming, and panning.
731
-
732
- Parameters:
733
- -----------
734
- voxel_grid : numpy.ndarray
735
- 3D numpy array containing voxel data. Shape should be (x, y, z) where
736
- each element represents a voxel class ID. Zero values are treated as empty space.
737
-
738
- voxel_color_map : str, optional
739
- Name of the color scheme to use for voxel coloring. Default is 'default'.
740
- See get_voxel_color_map() for available options.
741
-
742
- voxel_size : float, optional
743
- Physical size of each voxel in meters. Used for z-axis scaling and labels.
744
- Default is 2.0 meters.
745
-
746
- Returns:
747
- --------
748
- None
749
- The function displays the interactive plot in the browser or notebook.
750
-
751
- Notes:
752
- ------
753
- - Creates individual cube geometries for each non-zero voxel
754
- - Includes edge lines for better visual definition
755
- - Uses orthographic projection for technical visualization
756
- - May be slow for very large voxel grids due to individual cube generation
757
- - Lighting is optimized for clear visualization of building structures
758
-
759
- Technical Details:
760
- ------------------
761
- - Each cube is defined by 8 vertices and 12 triangular faces
762
- - Edge lines are generated separately for visual clarity
763
- - Color mapping follows the same convention as matplotlib version
764
- - Camera is positioned for isometric view with orthographic projection
765
-
766
- Examples:
767
- ---------
768
- >>> # Basic interactive visualization
769
- >>> visualize_3d_voxel_plotly(voxel_array)
770
-
771
- >>> # Use high contrast colors for better visibility
772
- >>> visualize_3d_voxel_plotly(voxel_array, 'high_contrast')
773
-
774
- >>> # Adjust scale for different voxel sizes
775
- >>> visualize_3d_voxel_plotly(voxel_array, voxel_size=1.0)
776
- """
777
- # Get the color mapping for the specified scheme
778
- color_map = get_voxel_color_map(voxel_color_map)
779
-
780
- print("Preparing visualization...")
781
-
782
- print("Processing voxels...")
783
- # Initialize lists to store mesh data
784
- x, y, z = [], [], [] # Vertex coordinates
785
- i, j, k = [], [], [] # Face indices (triangles)
786
- colors = [] # Vertex colors
787
- edge_x, edge_y, edge_z = [], [], [] # Edge line coordinates
788
- vertex_index = 0 # Current vertex index counter
789
-
790
- # Define cube faces using vertex indices
791
- # Each cube has 12 triangular faces (2 per square face)
792
- cube_i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
793
- cube_j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
794
- cube_k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
795
-
796
- # Process each voxel in the grid
797
- with tqdm(total=np.prod(voxel_grid.shape)) as pbar:
798
- for xi in range(voxel_grid.shape[0]):
799
- for yi in range(voxel_grid.shape[1]):
800
- for zi in range(voxel_grid.shape[2]):
801
- # Only process non-zero voxels
802
- if voxel_grid[xi, yi, zi] != 0:
803
- # Define the 8 vertices of a unit cube at this position
804
- # Vertices are ordered: bottom face (z), then top face (z+1)
805
- cube_vertices = [
806
- [xi, yi, zi], [xi+1, yi, zi], [xi+1, yi+1, zi], [xi, yi+1, zi], # Bottom face
807
- [xi, yi, zi+1], [xi+1, yi, zi+1], [xi+1, yi+1, zi+1], [xi, yi+1, zi+1] # Top face
808
- ]
809
-
810
- # Add vertex coordinates to the mesh data
811
- x.extend([v[0] for v in cube_vertices])
812
- y.extend([v[1] for v in cube_vertices])
813
- z.extend([v[2] for v in cube_vertices])
814
-
815
- # Add face indices (offset by current vertex_index)
816
- i.extend([x + vertex_index for x in cube_i])
817
- j.extend([x + vertex_index for x in cube_j])
818
- k.extend([x + vertex_index for x in cube_k])
819
-
820
- # Get color for this voxel type and replicate for all 8 vertices
821
- color = color_map.get(voxel_grid[xi, yi, zi], [0, 0, 0])
822
- colors.extend([color] * 8)
823
-
824
- # Generate edge lines for visual clarity
825
- # Define the 12 edges of a cube (4 bottom + 4 top + 4 vertical)
826
- edges = [
827
- (0,1), (1,2), (2,3), (3,0), # Bottom face edges
828
- (4,5), (5,6), (6,7), (7,4), # Top face edges
829
- (0,4), (1,5), (2,6), (3,7) # Vertical edges
830
- ]
831
-
832
- # Add edge coordinates (None creates line breaks between edges)
833
- for start, end in edges:
834
- edge_x.extend([cube_vertices[start][0], cube_vertices[end][0], None])
835
- edge_y.extend([cube_vertices[start][1], cube_vertices[end][1], None])
836
- edge_z.extend([cube_vertices[start][2], cube_vertices[end][2], None])
837
-
838
- # Increment vertex index for next cube
839
- vertex_index += 8
840
- pbar.update(1)
841
-
842
- print("Creating Plotly figure...")
843
- # Create 3D mesh object with vertices, faces, and colors
844
- mesh = go.Mesh3d(
845
- x=x, y=y, z=z,
846
- i=i, j=j, k=k,
847
- vertexcolor=colors,
848
- opacity=1,
849
- flatshading=True,
850
- name='Voxel Grid'
851
- )
852
-
853
- # Configure lighting for better visualization
854
- # Add lighting to the mesh
855
- mesh.update(
856
- lighting=dict(ambient=0.7, # Ambient light (overall brightness)
857
- diffuse=1, # Diffuse light (surface shading)
858
- fresnel=0.1, # Fresnel effect (edge highlighting)
859
- specular=1, # Specular highlights
860
- roughness=0.05, # Surface roughness
861
- facenormalsepsilon=1e-15,
862
- vertexnormalsepsilon=1e-15),
863
- lightposition=dict(x=100, # Light source position
864
- y=200,
865
- z=0)
866
- )
867
-
868
- # Create edge lines for better visual definition
869
- edges = go.Scatter3d(
870
- x=edge_x, y=edge_y, z=edge_z,
871
- mode='lines',
872
- line=dict(color='lightgrey', width=1),
873
- name='Edges'
874
- )
875
-
876
- # Combine mesh and edges into a figure
877
- fig = go.Figure(data=[mesh, edges])
878
-
879
- # Configure plot layout and camera settings
880
- # Set labels, title, and use orthographic projection
881
- fig.update_layout(
882
- scene=dict(
883
- xaxis_title='X',
884
- yaxis_title='Y',
885
- zaxis_title='Z (meters)',
886
- aspectmode='data', # Maintain data aspect ratios
887
- camera=dict(
888
- projection=dict(type="orthographic") # Use orthographic projection
889
- )
890
- ),
891
- title='3D Voxel Visualization'
892
- )
893
-
894
- # Configure z-axis to show real-world heights
895
- # Adjust z-axis ticks to show every 10 cells or less
896
- z_max = voxel_grid.shape[2]
897
- if z_max <= 10:
898
- z_ticks = list(range(0, z_max + 1))
899
- else:
900
- z_ticks = list(range(0, z_max + 1, 10))
901
-
902
- # Update z-axis with meaningful height labels
903
- fig.update_layout(
904
- scene=dict(
905
- zaxis=dict(
906
- tickvals=z_ticks,
907
- ticktext=[f"{z * voxel_size:.1f}" for z in z_ticks]
908
- )
909
- )
910
- )
911
-
912
- print("Visualization complete. Displaying plot...")
913
- fig.show()
914
-
915
- def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light', **kwargs):
916
- """
917
- Core function for plotting 2D grid data overlaid on basemaps.
918
-
919
- This function handles the visualization of various types of grid data by creating
920
- colored polygons for each grid cell and overlaying them on a web basemap. It supports
921
- different data types with appropriate color schemes and handles special values like
922
- NaN and zero appropriately.
923
-
924
- Parameters:
925
- -----------
926
- grid : numpy.ndarray
927
- 2D array containing the grid data values to visualize.
928
-
929
- origin : numpy.ndarray
930
- Geographic coordinates [lon, lat] of the grid's origin point.
931
-
932
- adjusted_meshsize : float
933
- Size of each grid cell in meters after grid size adjustments.
934
-
935
- u_vec, v_vec : numpy.ndarray
936
- Unit vectors defining the grid orientation in geographic space.
937
-
938
- transformer : pyproj.Transformer
939
- Coordinate transformer for converting between geographic and projected coordinates.
940
-
941
- vertices : list
942
- List of [lon, lat] coordinates defining the grid boundary.
943
-
944
- data_type : str
945
- Type of data being visualized. Supported types:
946
- - 'land_cover': Land use/land cover classifications
947
- - 'building_height': Building height values
948
- - 'dem': Digital elevation model
949
- - 'canopy_height': Vegetation height
950
- - 'green_view_index': Green visibility index
951
- - 'sky_view_index': Sky visibility factor
952
-
953
- vmin, vmax : float, optional
954
- Min/max values for color scaling. Auto-calculated if not provided.
955
-
956
- color_map : str, optional
957
- Matplotlib colormap name to override default schemes.
958
-
959
- alpha : float, optional
960
- Transparency of grid overlay (0-1). Default is 0.5.
961
-
962
- buf : float, optional
963
- Buffer around grid for plot extent as fraction of grid size. Default is 0.2.
964
-
965
- edge : bool, optional
966
- Whether to draw cell edges. Default is True.
967
-
968
- basemap : str, optional
969
- Basemap style name. Default is 'CartoDB light'.
970
-
971
- **kwargs : dict
972
- Additional parameters specific to data types:
973
- - land_cover_classes: Dictionary for land cover data
974
- - buildings: List of building polygons for building_height data
975
-
976
- Returns:
977
- --------
978
- None
979
- Displays the plot with matplotlib.
980
-
981
- Notes:
982
- ------
983
- - Grid is transposed to match geographic orientation
984
- - Special handling for NaN, zero, and negative values depending on data type
985
- - Basemap is added using contextily for geographic context
986
- - Plot extent is automatically calculated from grid vertices
987
- """
988
- # Create matplotlib figure and axis
989
- fig, ax = plt.subplots(figsize=(12, 12))
990
-
991
- # Configure visualization parameters based on data type
992
- if data_type == 'land_cover':
993
- # Land cover uses discrete color categories
994
- land_cover_classes = kwargs.get('land_cover_classes')
995
- colors = [mcolors.to_rgb(f'#{r:02x}{g:02x}{b:02x}') for r, g, b in land_cover_classes.keys()]
996
- cmap = mcolors.ListedColormap(colors)
997
- norm = mcolors.BoundaryNorm(range(len(land_cover_classes)+1), cmap.N)
998
- title = 'Grid Cells with Dominant Land Cover Classes'
999
- label = 'Land Cover Class'
1000
- tick_labels = list(land_cover_classes.values())
1001
-
1002
- elif data_type == 'building_height':
1003
- # Building height uses continuous colormap with special handling for zero/NaN
1004
- # Create a masked array to handle special values
1005
- masked_grid = np.ma.masked_array(grid, mask=(np.isnan(grid) | (grid == 0)))
1006
-
1007
- # Set up colormap and normalization for positive values
1008
- cmap = plt.cm.viridis
1009
- if vmin is None:
1010
- vmin = np.nanmin(masked_grid[masked_grid > 0])
1011
- if vmax is None:
1012
- vmax = np.nanmax(masked_grid)
1013
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1014
-
1015
- title = 'Grid Cells with Building Heights'
1016
- label = 'Building Height (m)'
1017
- tick_labels = None
1018
-
1019
- elif data_type == 'dem':
1020
- # Digital elevation model uses terrain colormap
1021
- cmap = plt.cm.terrain
1022
- if vmin is None:
1023
- vmin = np.nanmin(grid)
1024
- if vmax is None:
1025
- vmax = np.nanmax(grid)
1026
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1027
- title = 'DEM Grid Overlaid on Map'
1028
- label = 'Elevation (m)'
1029
- tick_labels = None
1030
- elif data_type == 'canopy_height':
1031
- cmap = plt.cm.Greens
1032
- if vmin is None:
1033
- vmin = np.nanmin(grid)
1034
- if vmax is None:
1035
- vmax = np.nanmax(grid)
1036
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1037
- title = 'Canopy Height Grid Overlaid on Map'
1038
- label = 'Canopy Height (m)'
1039
- tick_labels = None
1040
- elif data_type == 'green_view_index':
1041
- cmap = plt.cm.Greens
1042
- if vmin is None:
1043
- vmin = np.nanmin(grid)
1044
- if vmax is None:
1045
- vmax = np.nanmax(grid)
1046
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1047
- title = 'Green View Index Grid Overlaid on Map'
1048
- label = 'Green View Index'
1049
- tick_labels = None
1050
- elif data_type == 'sky_view_index':
1051
- cmap = plt.cm.get_cmap('BuPu_r').copy()
1052
- if vmin is None:
1053
- vmin = np.nanmin(grid)
1054
- if vmax is None:
1055
- vmax = np.nanmax(grid)
1056
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1057
- title = 'Sky View Index Grid Overlaid on Map'
1058
- label = 'Sky View Index'
1059
- tick_labels = None
1060
- else:
1061
- cmap = plt.cm.viridis
1062
- if vmin is None:
1063
- vmin = np.nanmin(grid)
1064
- if vmax is None:
1065
- vmax = np.nanmax(grid)
1066
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1067
- tick_labels = None
1068
-
1069
- if color_map:
1070
- # cmap = plt.cm.get_cmap(color_map).copy()
1071
- cmap = sns.color_palette(color_map, as_cmap=True).copy()
1072
-
1073
- # Ensure grid is in the correct orientation
1074
- grid = grid.T
1075
-
1076
- for i in range(grid.shape[0]):
1077
- for j in range(grid.shape[1]):
1078
- cell = create_cell_polygon(origin, j, i, adjusted_meshsize, u_vec, v_vec) # Note the swap of i and j
1079
- x, y = cell.exterior.xy
1080
- x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
1081
-
1082
- value = grid[i, j]
1083
-
1084
- if data_type == 'building_height':
1085
- if np.isnan(value):
1086
- # White fill for NaN values
1087
- ax.fill(x, y, alpha=alpha, fc='gray', ec='black' if edge else None, linewidth=0.1)
1088
- elif value == 0:
1089
- # No fill for zero values, only edges if enabled
1090
- if edge:
1091
- ax.plot(x, y, color='black', linewidth=0.1)
1092
- elif value > 0:
1093
- # Viridis colormap for positive values
1094
- color = cmap(norm(value))
1095
- ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
1096
- elif data_type == 'canopy_height':
1097
- color = cmap(norm(value))
1098
- if value == 0:
1099
- # No fill for zero values, only edges if enabled
1100
- if edge:
1101
- ax.plot(x, y, color='black', linewidth=0.1)
1102
- else:
1103
- if edge:
1104
- ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
1105
- else:
1106
- ax.fill(x, y, alpha=alpha, fc=color, ec=None)
1107
- elif 'view' in data_type:
1108
- if np.isnan(value):
1109
- # No fill for zero values, only edges if enabled
1110
- if edge:
1111
- ax.plot(x, y, color='black', linewidth=0.1)
1112
- elif value >= 0:
1113
- # Viridis colormap for positive values
1114
- color = cmap(norm(value))
1115
- ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
1116
- else:
1117
- color = cmap(norm(value))
1118
- if edge:
1119
- ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
1120
- else:
1121
- ax.fill(x, y, alpha=alpha, fc=color, ec=None)
1122
-
1123
- crs_epsg_3857 = CRS.from_epsg(3857)
1124
-
1125
- basemaps = {
1126
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter, # Popular dark option
1127
- 'CartoDB light': ctx.providers.CartoDB.Positron, # Popular dark option
1128
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager, # Popular dark option
1129
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels, # Popular dark option
1130
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1131
- }
1132
- ctx.add_basemap(ax, crs=crs_epsg_3857, source=basemaps[basemap])
1133
- # if basemap == "dark":
1134
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.DarkMatter)
1135
- # elif basemap == 'light':
1136
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.Positron)
1137
- # elif basemap == 'voyager':
1138
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.Voyager)
1139
-
1140
- if data_type == 'building_height':
1141
- buildings = kwargs.get('buildings', [])
1142
- for building in buildings:
1143
- polygon = Polygon(building['geometry']['coordinates'][0])
1144
- x, y = polygon.exterior.xy
1145
- x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
1146
- ax.plot(x, y, color='red', linewidth=1.5)
1147
- # print(polygon)
1148
-
1149
- # Safe calculation of plot limits
1150
- all_coords = np.array(vertices)
1151
- x, y = zip(*[transformer.transform(lon, lat) for lat, lon in all_coords])
1152
-
1153
- # Calculate limits safely
1154
- x_min, x_max = min(x), max(x)
1155
- y_min, y_max = min(y), max(y)
1156
-
1157
- if x_min != x_max and y_min != y_max and buf != 0:
1158
- dist_x = x_max - x_min
1159
- dist_y = y_max - y_min
1160
- # Set limits with buffer
1161
- ax.set_xlim(x_min - buf * dist_x, x_max + buf * dist_x)
1162
- ax.set_ylim(y_min - buf * dist_y, y_max + buf * dist_y)
1163
- else:
1164
- # If coordinates are the same or buffer is 0, set limits without buffer
1165
- ax.set_xlim(x_min, x_max)
1166
- ax.set_ylim(y_min, y_max)
1167
-
1168
- plt.axis('off')
1169
- plt.tight_layout()
1170
- plt.show()
1171
-
1172
- def display_builing_ids_on_map(building_geojson, rectangle_vertices):
1173
- """
1174
- Creates an interactive folium map displaying building footprints with selectable IDs.
1175
-
1176
- This function generates a web map showing building polygons within a circular area
1177
- around the center of the specified rectangle. Each building is labeled with its
1178
- ID and additional information, making it easy to identify specific buildings
1179
- for analysis or selection.
1180
-
1181
- Parameters:
1182
- -----------
1183
- building_geojson : list
1184
- List of GeoJSON feature dictionaries representing building polygons.
1185
- Each feature should have:
1186
- - 'geometry': GeoJSON polygon geometry
1187
- - 'properties': Dictionary with 'id' and optional 'name' fields
1188
-
1189
- rectangle_vertices : list
1190
- List of [lat, lon] coordinate pairs defining the area of interest.
1191
- Used to calculate the map center and intersection area.
1192
-
1193
- Returns:
1194
- --------
1195
- folium.Map
1196
- Interactive folium map object with building polygons and labels.
1197
-
1198
- Notes:
1199
- ------
1200
- - Only buildings intersecting with a 200m radius circle are displayed
1201
- - Building IDs are displayed as selectable text labels
1202
- - Map is automatically centered on the rectangle vertices
1203
- - Popup information includes building ID and name (if available)
1204
- - Building polygons are styled with blue color and semi-transparent fill
1205
-
1206
- Examples:
1207
- ---------
1208
- >>> vertices = [[40.7580, -73.9855], [40.7590, -73.9855],
1209
- ... [40.7590, -73.9845], [40.7580, -73.9845]]
1210
- >>> buildings = [{'geometry': {...}, 'properties': {'id': '123', 'name': 'Building A'}}]
1211
- >>> map_obj = display_builing_ids_on_map(buildings, vertices)
1212
- >>> map_obj.save('buildings_map.html')
1213
- """
1214
- # Parse the GeoJSON data
1215
- geojson_data = building_geojson
1216
-
1217
- # Calculate the center point of the rectangle for map centering
1218
- # Extract all latitudes and longitudes
1219
- lats = [coord[0] for coord in rectangle_vertices]
1220
- lons = [coord[1] for coord in rectangle_vertices]
1221
-
1222
- # Calculate center by averaging min and max values
1223
- center_lat = (min(lats) + max(lats)) / 2
1224
- center_lon = (min(lons) + max(lons)) / 2
1225
-
1226
- # Create circle polygon for intersection testing (200m radius)
1227
- circle = create_circle_polygon(center_lat, center_lon, 200)
1228
-
1229
- # Create a map centered on the data
1230
- m = folium.Map(location=[center_lat, center_lon], zoom_start=17)
1231
-
1232
- # Process each building feature
1233
- # Add building footprints to the map
1234
- for feature in geojson_data:
1235
- # Convert coordinates if needed
1236
- coords = convert_coordinates(feature['geometry']['coordinates'][0])
1237
- building_polygon = Polygon(coords)
1238
-
1239
- # Only process buildings that intersect with the circular area
1240
- # Check if building intersects with circle
1241
- if building_polygon.intersects(circle):
1242
- # Extract building information from properties
1243
- # Get and format building properties
1244
- # building_id = format_building_id(feature['properties'].get('id', 0))
1245
- building_id = str(feature['properties'].get('id', 0))
1246
- building_name = feature['properties'].get('name:en',
1247
- feature['properties'].get('name', f'Building {building_id}'))
1248
-
1249
- # Create popup content with selectable ID
1250
- popup_content = f"""
1251
- <div>
1252
- Building ID: <span style="user-select: all">{building_id}</span><br>
1253
- Name: {building_name}
1254
- </div>
1255
- """
1256
-
1257
- # Add building polygon to map with popup information
1258
- # Add polygon to map
1259
- folium.Polygon(
1260
- locations=coords,
1261
- popup=folium.Popup(popup_content),
1262
- color='blue',
1263
- weight=2,
1264
- fill=True,
1265
- fill_color='blue',
1266
- fill_opacity=0.2
1267
- ).add_to(m)
1268
-
1269
- # Add building ID label at the polygon centroid
1270
- # Calculate centroid for label placement
1271
- centroid = calculate_centroid(coords)
1272
-
1273
- # Add building ID as a selectable label
1274
- folium.Marker(
1275
- centroid,
1276
- icon=folium.DivIcon(
1277
- html=f'''
1278
- <div style="
1279
- position: relative;
1280
- font-family: monospace;
1281
- font-size: 12px;
1282
- color: black;
1283
- background-color: rgba(255, 255, 255, 0.9);
1284
- padding: 5px 8px;
1285
- margin: -10px -15px;
1286
- border: 1px solid black;
1287
- border-radius: 4px;
1288
- user-select: all;
1289
- cursor: text;
1290
- white-space: nowrap;
1291
- display: inline-block;
1292
- box-shadow: 0 0 3px rgba(0,0,0,0.2);
1293
- ">{building_id}</div>
1294
- ''',
1295
- class_name="building-label"
1296
- )
1297
- ).add_to(m)
1298
-
1299
- # Save the map
1300
- return m
1301
-
1302
- def visualize_land_cover_grid_on_map(grid, rectangle_vertices, meshsize, source = 'Urbanwatch', vmin=None, vmax=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1303
- """
1304
- Visualizes land cover classification grid overlaid on a basemap.
1305
-
1306
- This function creates a map visualization of land cover data using predefined
1307
- color schemes for different land cover classes. Each grid cell is colored
1308
- according to its dominant land cover type and overlaid on a web basemap
1309
- for geographic context.
1310
-
1311
- Parameters:
1312
- -----------
1313
- grid : numpy.ndarray
1314
- 2D array containing land cover class indices. Values should correspond
1315
- to indices in the land cover classification system.
1316
-
1317
- rectangle_vertices : list
1318
- List of [lon, lat] coordinate pairs defining the grid boundary corners.
1319
- Should contain exactly 4 vertices in geographic coordinates.
1320
-
1321
- meshsize : float
1322
- Target size of each grid cell in meters. May be adjusted during processing.
1323
-
1324
- source : str, optional
1325
- Source of land cover classification system. Default is 'Urbanwatch'.
1326
- See get_land_cover_classes() for available options.
1327
-
1328
- vmin, vmax : float, optional
1329
- Not used for land cover (discrete categories). Included for API consistency.
1330
-
1331
- alpha : float, optional
1332
- Transparency of grid overlay (0-1). Default is 0.5.
1333
-
1334
- buf : float, optional
1335
- Buffer around grid for plot extent as fraction of grid size. Default is 0.2.
1336
-
1337
- edge : bool, optional
1338
- Whether to draw cell edges. Default is True.
1339
-
1340
- basemap : str, optional
1341
- Basemap style name. Options include:
1342
- - 'CartoDB light' (default)
1343
- - 'CartoDB dark'
1344
- - 'CartoDB voyager'
1345
- Default is 'CartoDB light'.
1346
-
1347
- Returns:
1348
- --------
1349
- None
1350
- Displays the plot and prints information about unique land cover classes.
1351
-
1352
- Notes:
1353
- ------
1354
- - Grid coordinates are calculated using geodetic calculations
1355
- - Land cover classes are mapped to predefined colors
1356
- - Unique classes present in the grid are printed for reference
1357
- - Uses Web Mercator projection (EPSG:3857) for basemap compatibility
1358
-
1359
- Examples:
1360
- ---------
1361
- >>> # Basic land cover visualization
1362
- >>> vertices = [[lon1, lat1], [lon2, lat2], [lon3, lat3], [lon4, lat4]]
1363
- >>> visualize_land_cover_grid_on_map(lc_grid, vertices, 10.0)
1364
-
1365
- >>> # With custom styling
1366
- >>> visualize_land_cover_grid_on_map(lc_grid, vertices, 10.0,
1367
- ... alpha=0.7, edge=False,
1368
- ... basemap='CartoDB dark')
1369
- """
1370
- # Initialize geodetic calculator for distance measurements
1371
- geod = initialize_geod()
1372
-
1373
- # Get land cover class definitions and colors
1374
- land_cover_classes = get_land_cover_classes(source)
1375
-
1376
- # Extract key vertices for grid calculations
1377
- vertex_0 = rectangle_vertices[0]
1378
- vertex_1 = rectangle_vertices[1]
1379
- vertex_3 = rectangle_vertices[3]
1380
-
1381
- # Calculate distances between vertices using geodetic calculations
1382
- dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1383
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1384
-
1385
- # Calculate side vectors in geographic coordinates
1386
- side_1 = np.array(vertex_1) - np.array(vertex_0)
1387
- side_2 = np.array(vertex_3) - np.array(vertex_0)
1388
-
1389
- # Create normalized unit vectors for grid orientation
1390
- u_vec = normalize_to_one_meter(side_1, dist_side_1)
1391
- v_vec = normalize_to_one_meter(side_2, dist_side_2)
1392
-
1393
- # Set grid origin and calculate optimal grid size
1394
- origin = np.array(rectangle_vertices[0])
1395
- grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1396
-
1397
- print(f"Calculated grid size: {grid_size}")
1398
- # print(f"Adjusted mesh size: {adjusted_meshsize}")
1399
-
1400
- # Set up coordinate transformation for basemap compatibility
1401
- geotiff_crs = CRS.from_epsg(3857)
1402
- transformer = setup_transformer(CRS.from_epsg(4326), geotiff_crs)
1403
-
1404
- # Generate grid cell coordinates (not currently used but available for advanced processing)
1405
- cell_coords = create_coordinate_mesh(origin, grid_size, adjusted_meshsize, u_vec, v_vec)
1406
- cell_coords_flat = cell_coords.reshape(2, -1).T
1407
- transformed_coords = np.array([transform_coords(transformer, lon, lat) for lat, lon in cell_coords_flat])
1408
- transformed_coords = transformed_coords.reshape(grid_size[::-1] + (2,))
1409
-
1410
- # print(f"Grid shape: {grid.shape}")
1411
-
1412
- # Create the visualization using the general plot_grid function
1413
- plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1414
- rectangle_vertices, 'land_cover', alpha=alpha, buf=buf, edge=edge, basemap=basemap, land_cover_classes=land_cover_classes)
1415
-
1416
- # Display information about the land cover classes present in the grid
1417
- unique_indices = np.unique(grid)
1418
- unique_classes = [list(land_cover_classes.values())[i] for i in unique_indices]
1419
- # print(f"Unique classes in the grid: {unique_classes}")
1420
-
1421
- def visualize_building_height_grid_on_map(building_height_grid, filtered_buildings, rectangle_vertices, meshsize, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1422
- # Calculate grid and normalize vectors
1423
- geod = initialize_geod()
1424
- vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
1425
-
1426
- dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1427
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1428
-
1429
- side_1 = np.array(vertex_1) - np.array(vertex_0)
1430
- side_2 = np.array(vertex_3) - np.array(vertex_0)
1431
-
1432
- u_vec = normalize_to_one_meter(side_1, dist_side_1)
1433
- v_vec = normalize_to_one_meter(side_2, dist_side_2)
1434
-
1435
- origin = np.array(rectangle_vertices[0])
1436
- _, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1437
-
1438
- # Setup transformer and plotting extent
1439
- transformer = setup_transformer(CRS.from_epsg(4326), CRS.from_epsg(3857))
1440
-
1441
- # Plot the results
1442
- plot_grid(building_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1443
- rectangle_vertices, 'building_height', vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap, buildings=filtered_buildings)
1444
-
1445
- def visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, meshsize, type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light'):
1446
- # Calculate grid and normalize vectors
1447
- geod = initialize_geod()
1448
- vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
1449
-
1450
- dist_side_1 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_1[1], vertex_1[0])
1451
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[0])
1452
-
1453
- side_1 = np.array(vertex_1) - np.array(vertex_0)
1454
- side_2 = np.array(vertex_3) - np.array(vertex_0)
1455
-
1456
- u_vec = normalize_to_one_meter(side_1, dist_side_1)
1457
- v_vec = normalize_to_one_meter(side_2, dist_side_2)
1458
-
1459
- origin = np.array(rectangle_vertices[0])
1460
- _, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
1461
-
1462
- # Setup transformer and plotting extent
1463
- transformer = setup_transformer(CRS.from_epsg(4326), CRS.from_epsg(3857))
1464
-
1465
- # Plot the results
1466
- plot_grid(canopy_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
1467
- rectangle_vertices, type, vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap)
1468
-
1469
- def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
1470
- all_classes = list(land_cover_classes.values())
1471
- unique_classes = list(dict.fromkeys(all_classes)) # Preserve order and remove duplicates
1472
-
1473
- colors = [color_map[cls] for cls in unique_classes]
1474
- cmap = mcolors.ListedColormap(colors)
1475
-
1476
- bounds = np.arange(len(unique_classes) + 1)
1477
- norm = mcolors.BoundaryNorm(bounds, cmap.N)
1478
-
1479
- class_to_num = {cls: i for i, cls in enumerate(unique_classes)}
1480
- numeric_grid = np.vectorize(class_to_num.get)(grid)
1481
-
1482
- plt.figure(figsize=(10, 10))
1483
- im = plt.imshow(numeric_grid, cmap=cmap, norm=norm, interpolation='nearest')
1484
- cbar = plt.colorbar(im, ticks=bounds[:-1] + 0.5)
1485
- cbar.set_ticklabels(unique_classes)
1486
- plt.title(f'Land Use/Land Cover Grid (Mesh Size: {mesh_size}m)')
1487
- plt.xlabel('Grid Cells (X)')
1488
- plt.ylabel('Grid Cells (Y)')
1489
- plt.show()
1490
-
1491
- def visualize_numerical_grid(grid, mesh_size, title, cmap='viridis', label='Value', vmin=None, vmax=None):
1492
- plt.figure(figsize=(10, 10))
1493
- plt.imshow(grid, cmap=cmap, vmin=vmin, vmax=vmax)
1494
- plt.colorbar(label=label)
1495
- plt.title(f'{title} (Mesh Size: {mesh_size}m)')
1496
- plt.xlabel('Grid Cells (X)')
1497
- plt.ylabel('Grid Cells (Y)')
1498
- plt.show()
1499
-
1500
- def convert_coordinates(coords):
1501
- return coords
1502
-
1503
- def calculate_centroid(coords):
1504
- lat_sum = sum(coord[0] for coord in coords)
1505
- lon_sum = sum(coord[1] for coord in coords)
1506
- return [lat_sum / len(coords), lon_sum / len(coords)]
1507
-
1508
- def calculate_center(features):
1509
- lats = []
1510
- lons = []
1511
- for feature in features:
1512
- coords = feature['geometry']['coordinates'][0]
1513
- for lat, lon in coords:
1514
- lats.append(lat)
1515
- lons.append(lon)
1516
- return sum(lats) / len(lats), sum(lons) / len(lons)
1517
-
1518
- def create_circle_polygon(center_lat, center_lon, radius_meters):
1519
- """Create a circular polygon with given center and radius"""
1520
- # Convert radius from meters to degrees (approximate)
1521
- radius_deg = radius_meters / 111000 # 1 degree ≈ 111km at equator
1522
-
1523
- # Create circle points
1524
- points = []
1525
- for angle in range(361): # 0 to 360 degrees
1526
- rad = math.radians(angle)
1527
- lat = center_lat + (radius_deg * math.cos(rad))
1528
- lon = center_lon + (radius_deg * math.sin(rad) / math.cos(math.radians(center_lat)))
1529
- points.append((lat, lon))
1530
- return Polygon(points)
1531
-
1532
- def visualize_landcover_grid_on_basemap(landcover_grid, rectangle_vertices, meshsize, source='Standard', alpha=0.6, figsize=(12, 8),
1533
- basemap='CartoDB light', show_edge=False, edge_color='black', edge_width=0.5):
1534
- """Visualizes a land cover grid GeoDataFrame using predefined color schemes.
1535
-
1536
- Args:
1537
- gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1538
- source: Source of land cover classification (e.g., 'Standard', 'Urbanwatch', etc.)
1539
- title: Title for the plot (default: None)
1540
- alpha: Transparency of the grid overlay (default: 0.6)
1541
- figsize: Figure size in inches (default: (12, 8))
1542
- basemap: Basemap style (default: 'CartoDB light')
1543
- show_edge: Whether to show cell edges (default: True)
1544
- edge_color: Color of cell edges (default: 'black')
1545
- edge_width: Width of cell edges (default: 0.5)
1546
- """
1547
- # Get land cover classes and colors
1548
- land_cover_classes = get_land_cover_classes(source)
1549
-
1550
- gdf = grid_to_geodataframe(landcover_grid, rectangle_vertices, meshsize)
1551
-
1552
- # Convert RGB tuples to normalized RGB values
1553
- colors = [(r/255, g/255, b/255) for (r,g,b) in land_cover_classes.keys()]
1554
-
1555
- # Create custom colormap
1556
- cmap = ListedColormap(colors)
1557
-
1558
- # Create bounds for discrete colorbar
1559
- bounds = np.arange(len(colors) + 1)
1560
- norm = BoundaryNorm(bounds, cmap.N)
1561
-
1562
- # Convert to Web Mercator
1563
- gdf_web = gdf.to_crs(epsg=3857)
1564
-
1565
- # Create figure and axis
1566
- fig, ax = plt.subplots(figsize=figsize)
1567
-
1568
- # Plot the GeoDataFrame
1569
- gdf_web.plot(column='value',
1570
- ax=ax,
1571
- alpha=alpha,
1572
- cmap=cmap,
1573
- norm=norm,
1574
- legend=True,
1575
- legend_kwds={
1576
- 'label': 'Land Cover Class',
1577
- 'ticks': bounds[:-1] + 0.5,
1578
- 'boundaries': bounds,
1579
- 'format': lambda x, p: list(land_cover_classes.values())[int(x)]
1580
- },
1581
- edgecolor=edge_color if show_edge else 'none',
1582
- linewidth=edge_width if show_edge else 0)
1583
-
1584
- # Add basemap
1585
- basemaps = {
1586
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1587
- 'CartoDB light': ctx.providers.CartoDB.Positron,
1588
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1589
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1590
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1591
- }
1592
- ctx.add_basemap(ax, source=basemaps[basemap])
1593
-
1594
- # Set title and remove axes
1595
- ax.set_axis_off()
1596
-
1597
- plt.tight_layout()
1598
- plt.show()
1599
-
1600
- def visualize_numerical_grid_on_basemap(grid, rectangle_vertices, meshsize, value_name="value", cmap='viridis', vmin=None, vmax=None,
1601
- alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1602
- show_edge=False, edge_color='black', edge_width=0.5):
1603
- """Visualizes a numerical grid GeoDataFrame (e.g., heights) on a basemap.
1604
-
1605
- Args:
1606
- gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1607
- title: Title for the plot (default: None)
1608
- cmap: Colormap to use (default: 'viridis')
1609
- vmin: Minimum value for colormap scaling (default: None)
1610
- vmax: Maximum value for colormap scaling (default: None)
1611
- alpha: Transparency of the grid overlay (default: 0.6)
1612
- figsize: Figure size in inches (default: (12, 8))
1613
- basemap: Basemap style (default: 'CartoDB light')
1614
- show_edge: Whether to show cell edges (default: True)
1615
- edge_color: Color of cell edges (default: 'black')
1616
- edge_width: Width of cell edges (default: 0.5)
1617
- """
1618
-
1619
- gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
1620
-
1621
- # Convert to Web Mercator
1622
- gdf_web = gdf.to_crs(epsg=3857)
1623
-
1624
- # Create figure and axis
1625
- fig, ax = plt.subplots(figsize=figsize)
1626
-
1627
- # Plot the GeoDataFrame
1628
- gdf_web.plot(column='value',
1629
- ax=ax,
1630
- alpha=alpha,
1631
- cmap=cmap,
1632
- vmin=vmin,
1633
- vmax=vmax,
1634
- legend=True,
1635
- legend_kwds={'label': value_name},
1636
- edgecolor=edge_color if show_edge else 'none',
1637
- linewidth=edge_width if show_edge else 0)
1638
-
1639
- # Add basemap
1640
- basemaps = {
1641
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1642
- 'CartoDB light': ctx.providers.CartoDB.Positron,
1643
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1644
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1645
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1646
- }
1647
- ctx.add_basemap(ax, source=basemaps[basemap])
1648
-
1649
- # Set title and remove axes
1650
- ax.set_axis_off()
1651
-
1652
- plt.tight_layout()
1653
- plt.show()
1654
-
1655
- def visualize_numerical_gdf_on_basemap(gdf, value_name="value", cmap='viridis', vmin=None, vmax=None,
1656
- alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1657
- show_edge=False, edge_color='black', edge_width=0.5,
1658
- input_crs=None):
1659
- """Visualizes a GeoDataFrame with numerical values on a basemap.
1660
-
1661
- Args:
1662
- gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
1663
- value_name: Name of the value column and legend label (default: "value")
1664
- cmap: Colormap to use (default: 'viridis')
1665
- vmin: Minimum value for colormap scaling (default: None)
1666
- vmax: Maximum value for colormap scaling (default: None)
1667
- alpha: Transparency of the grid overlay (default: 0.6)
1668
- figsize: Figure size in inches (default: (12, 8))
1669
- basemap: Basemap style (default: 'CartoDB light')
1670
- show_edge: Whether to show cell edges (default: False)
1671
- edge_color: Color of cell edges (default: 'black')
1672
- edge_width: Width of cell edges (default: 0.5)
1673
- input_crs: Optional CRS to assign if the GeoDataFrame has no CRS. If not provided
1674
- and CRS is missing, the function will attempt to infer WGS84 (EPSG:4326)
1675
- when coordinates look like lon/lat; otherwise it will raise a clear error.
1676
- """
1677
- # Ensure CRS is defined; if missing, assign or infer
1678
- if gdf.crs is None:
1679
- if input_crs is not None:
1680
- gdf = gdf.set_crs(input_crs, allow_override=True)
1681
- else:
1682
- # Try to infer WGS84 if bounds look like lon/lat
1683
- try:
1684
- minx, miny, maxx, maxy = gdf.total_bounds
1685
- looks_like_lonlat = (
1686
- -180.0 <= minx <= 180.0 and -180.0 <= maxx <= 180.0 and
1687
- -90.0 <= miny <= 90.0 and -90.0 <= maxy <= 90.0
1688
- )
1689
- except Exception:
1690
- looks_like_lonlat = False
1691
- if looks_like_lonlat:
1692
- gdf = gdf.set_crs("EPSG:4326", allow_override=True)
1693
- else:
1694
- raise ValueError(
1695
- "Input GeoDataFrame has no CRS. Provide 'input_crs' (e.g., 'EPSG:4326' or 'EPSG:XXXX') "
1696
- "or set gdf.crs before calling visualize_numerical_gdf_on_basemap."
1697
- )
1698
-
1699
- # Convert to Web Mercator if not already in that CRS
1700
- if str(gdf.crs) != 'EPSG:3857':
1701
- gdf_web = gdf.to_crs(epsg=3857)
1702
- else:
1703
- gdf_web = gdf
1704
-
1705
- # Create figure and axis
1706
- fig, ax = plt.subplots(figsize=figsize)
1707
-
1708
- # Plot the GeoDataFrame
1709
- gdf_web.plot(column=value_name,
1710
- ax=ax,
1711
- alpha=alpha,
1712
- cmap=cmap,
1713
- vmin=vmin,
1714
- vmax=vmax,
1715
- legend=True,
1716
- legend_kwds={'label': value_name},
1717
- edgecolor=edge_color if show_edge else 'none',
1718
- linewidth=edge_width if show_edge else 0)
1719
-
1720
- # Add basemap
1721
- basemaps = {
1722
- 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
1723
- 'CartoDB light': ctx.providers.CartoDB.Positron,
1724
- 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
1725
- 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
1726
- 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
1727
- }
1728
- ctx.add_basemap(ax, source=basemaps[basemap])
1729
-
1730
- # Set title and remove axes
1731
- ax.set_axis_off()
1732
-
1733
- plt.tight_layout()
1734
- plt.show()
1735
-
1736
- def visualize_point_gdf_on_basemap(point_gdf, value_name='value', **kwargs):
1737
- """Visualizes a point GeoDataFrame on a basemap with colors based on values.
1738
-
1739
- Args:
1740
- point_gdf: GeoDataFrame with point geometries and values
1741
- value_name: Name of the column containing values to visualize (default: 'value')
1742
- **kwargs: Optional visualization parameters including:
1743
- - figsize: Tuple for figure size (default: (12, 8))
1744
- - colormap: Matplotlib colormap name (default: 'viridis')
1745
- - markersize: Size of points (default: 20)
1746
- - alpha: Transparency of points (default: 0.7)
1747
- - vmin: Minimum value for colormap scaling (default: None)
1748
- - vmax: Maximum value for colormap scaling (default: None)
1749
- - title: Plot title (default: None)
1750
- - basemap_style: Contextily basemap style (default: CartoDB.Positron)
1751
- - zoom: Basemap zoom level (default: 15)
1752
-
1753
- Returns:
1754
- matplotlib figure and axis objects
1755
- """
1756
- import matplotlib.pyplot as plt
1757
- import contextily as ctx
1758
-
1759
- # Set default parameters
1760
- defaults = {
1761
- 'figsize': (12, 8),
1762
- 'colormap': 'viridis',
1763
- 'markersize': 20,
1764
- 'alpha': 0.7,
1765
- 'vmin': None,
1766
- 'vmax': None,
1767
- 'title': None,
1768
- 'basemap_style': ctx.providers.CartoDB.Positron,
1769
- 'zoom': 15
1770
- }
1771
-
1772
- # Update defaults with provided kwargs
1773
- settings = {**defaults, **kwargs}
1774
-
1775
- # Create figure and axis
1776
- fig, ax = plt.subplots(figsize=settings['figsize'])
1777
-
1778
- # Convert to Web Mercator for basemap compatibility
1779
- point_gdf_web = point_gdf.to_crs(epsg=3857)
1780
-
1781
- # Plot points
1782
- scatter = point_gdf_web.plot(
1783
- column=value_name,
1784
- ax=ax,
1785
- cmap=settings['colormap'],
1786
- markersize=settings['markersize'],
1787
- alpha=settings['alpha'],
1788
- vmin=settings['vmin'],
1789
- vmax=settings['vmax'],
1790
- legend=True,
1791
- legend_kwds={
1792
- 'label': value_name,
1793
- 'orientation': 'vertical',
1794
- 'shrink': 0.8
1795
- }
1796
- )
1797
-
1798
- # Add basemap
1799
- ctx.add_basemap(
1800
- ax,
1801
- source=settings['basemap_style'],
1802
- zoom=settings['zoom']
1803
- )
1804
-
1805
- # Set title if provided
1806
- if settings['title']:
1807
- plt.title(settings['title'])
1808
-
1809
- # Remove axes
1810
- ax.set_axis_off()
1811
-
1812
- # Adjust layout to prevent colorbar cutoff
1813
- plt.tight_layout()
1814
- plt.show()
1815
-
1816
- def create_multi_view_scene(meshes, output_directory="output", projection_type="perspective", distance_factor=1.0):
1817
- """
1818
- Creates multiple rendered views of 3D city meshes from different camera angles.
1819
-
1820
- This function generates a comprehensive set of views including isometric and
1821
- orthographic projections of the 3D city model. Each view is rendered as a
1822
- high-quality image and saved to the specified directory.
1823
-
1824
- Parameters:
1825
- -----------
1826
- meshes : dict
1827
- Dictionary mapping mesh names/IDs to trimesh.Trimesh objects.
1828
- Each mesh represents a different component of the city model.
1829
-
1830
- output_directory : str, optional
1831
- Directory path where rendered images will be saved. Default is "output".
1832
-
1833
- projection_type : str, optional
1834
- Camera projection type. Options:
1835
- - "perspective": Natural perspective projection (default)
1836
- - "orthographic": Technical orthographic projection
1837
-
1838
- distance_factor : float, optional
1839
- Multiplier for camera distance from the scene. Default is 1.0.
1840
- Higher values move camera further away, lower values bring it closer.
1841
-
1842
- Returns:
1843
- --------
1844
- list of tuple
1845
- List of (view_name, filename) pairs for each generated image.
1846
-
1847
- Notes:
1848
- ------
1849
- - Generates 9 different views: 4 isometric + 5 orthographic
1850
- - Isometric views: front-right, front-left, back-right, back-left
1851
- - Orthographic views: top, front, back, left, right
1852
- - Uses PyVista for high-quality rendering with proper lighting
1853
- - Camera positions are automatically calculated based on scene bounds
1854
- - Images are saved as PNG files with high DPI
1855
-
1856
- Technical Details:
1857
- ------------------
1858
- - Scene bounding box is computed from all mesh vertices
1859
- - Camera distance is scaled based on scene diagonal
1860
- - Orthographic projection uses parallel scaling for technical drawings
1861
- - Each view uses optimized lighting for clarity
1862
-
1863
- Examples:
1864
- ---------
1865
- >>> meshes = {'buildings': building_mesh, 'ground': ground_mesh}
1866
- >>> views = create_multi_view_scene(meshes, "renders/", "orthographic", 1.5)
1867
- >>> print(f"Generated {len(views)} views")
1868
- """
1869
- # Precompute PyVista meshes once to avoid repeated conversion per view
1870
- pv_meshes = {}
1871
- for class_id, mesh in meshes.items():
1872
- if mesh is None or len(mesh.vertices) == 0 or len(mesh.faces) == 0:
1873
- continue
1874
- # PyVista expects a faces array where each face is prefixed by its vertex count (3 for triangles)
1875
- faces = np.hstack([[3, *face] for face in mesh.faces])
1876
- pv_mesh = pv.PolyData(mesh.vertices, faces)
1877
- # Attach per-cell colors if provided
1878
- colors = getattr(mesh.visual, 'face_colors', None)
1879
- if colors is not None:
1880
- colors = np.asarray(colors)
1881
- if colors.size and colors.max() > 1:
1882
- colors = colors / 255.0
1883
- pv_mesh.cell_data['colors'] = colors
1884
- pv_meshes[class_id] = pv_mesh
1885
-
1886
- # Compute overall bounding box across all meshes without stacking
1887
- min_xyz = np.array([np.inf, np.inf, np.inf], dtype=float)
1888
- max_xyz = np.array([-np.inf, -np.inf, -np.inf], dtype=float)
1889
- for mesh in meshes.values():
1890
- if mesh is None or len(mesh.vertices) == 0:
1891
- continue
1892
- v = mesh.vertices
1893
- # update mins and maxs
1894
- min_xyz = np.minimum(min_xyz, v.min(axis=0))
1895
- max_xyz = np.maximum(max_xyz, v.max(axis=0))
1896
- bbox = np.vstack([min_xyz, max_xyz])
1897
-
1898
- # Compute the center and diagonal of the bounding box
1899
- center = (bbox[1] + bbox[0]) / 2
1900
- diagonal = np.linalg.norm(bbox[1] - bbox[0])
1901
-
1902
- # Adjust distance based on projection type
1903
- if projection_type.lower() == "orthographic":
1904
- distance = diagonal * 5 # Increase distance for orthographic to capture full scene
1905
- else:
1906
- distance = diagonal * 1.8 * distance_factor # Original distance for perspective
1907
-
1908
- # Define the isometric viewing angles
1909
- iso_angles = {
1910
- 'iso_front_right': (1, 1, 0.7),
1911
- 'iso_front_left': (-1, 1, 0.7),
1912
- 'iso_back_right': (1, -1, 0.7),
1913
- 'iso_back_left': (-1, -1, 0.7)
1914
- }
1915
-
1916
- # Compute camera positions for isometric views
1917
- camera_positions = {}
1918
- for name, direction in iso_angles.items():
1919
- direction = np.array(direction)
1920
- direction = direction / np.linalg.norm(direction)
1921
- camera_pos = center + direction * distance
1922
- camera_positions[name] = [camera_pos, center, (0, 0, 1)]
1923
-
1924
- # Add orthographic views
1925
- ortho_views = {
1926
- 'xy_top': [center + np.array([0, 0, distance]), center, (-1, 0, 0)],
1927
- 'yz_right': [center + np.array([distance, 0, 0]), center, (0, 0, 1)],
1928
- 'xz_front': [center + np.array([0, distance, 0]), center, (0, 0, 1)],
1929
- 'yz_left': [center + np.array([-distance, 0, 0]), center, (0, 0, 1)],
1930
- 'xz_back': [center + np.array([0, -distance, 0]), center, (0, 0, 1)]
1931
- }
1932
- camera_positions.update(ortho_views)
1933
-
1934
- images = []
1935
- for view_name, camera_pos in camera_positions.items():
1936
- # Create new plotter for each view (avoid notebook backend in headless/CI)
1937
- plotter = pv.Plotter(off_screen=True)
1938
-
1939
- # Set the projection type
1940
- if projection_type.lower() == "orthographic":
1941
- plotter.enable_parallel_projection()
1942
- # Set parallel scale to ensure the whole scene is visible
1943
- plotter.camera.parallel_scale = diagonal * 0.4 * distance_factor # Adjust this factor as needed
1944
-
1945
- elif projection_type.lower() != "perspective":
1946
- print(f"Warning: Unknown projection_type '{projection_type}'. Using perspective projection.")
1947
-
1948
- # Add each precomputed mesh to the scene
1949
- for class_id, pv_mesh in pv_meshes.items():
1950
- has_colors = 'colors' in pv_mesh.cell_data
1951
- plotter.add_mesh(
1952
- pv_mesh,
1953
- rgb=True,
1954
- scalars='colors' if has_colors else None
1955
- )
1956
-
1957
- # Set camera position for this view
1958
- plotter.camera_position = camera_pos
1959
-
1960
- # Save screenshot
1961
- filename = f'{output_directory}/city_view_{view_name}.png'
1962
- plotter.screenshot(filename)
1963
- images.append((view_name, filename))
1964
- plotter.close()
1965
-
1966
- return images
1967
-
1968
- def visualize_voxcity_multi_view(voxel_array, meshsize, **kwargs):
1969
- """
1970
- Creates comprehensive 3D visualizations of voxel city data with multiple viewing angles.
1971
-
1972
- This is the primary function for generating publication-quality renderings of voxel
1973
- city models. It converts voxel data to 3D meshes, optionally overlays simulation
1974
- results, and produces multiple rendered views from different camera positions.
1975
-
1976
- Parameters:
1977
- -----------
1978
- voxel_array : numpy.ndarray
1979
- 3D array containing voxel class IDs. Shape should be (x, y, z).
1980
-
1981
- meshsize : float
1982
- Physical size of each voxel in meters.
1983
-
1984
- **kwargs : dict
1985
- Optional visualization parameters:
1986
-
1987
- Color and Style:
1988
- - voxel_color_map (str): Color scheme name, default 'default'
1989
- - output_directory (str): Directory for output files, default 'output'
1990
- - output_file_name (str): Base name for exported files
1991
-
1992
- Simulation Overlay:
1993
- - sim_grid (numpy.ndarray): 2D simulation results to overlay
1994
- - dem_grid (numpy.ndarray): Digital elevation model for height reference
1995
- - view_point_height (float): Height offset for simulation surface, default 1.5m
1996
- - colormap (str): Matplotlib colormap for simulation data, default 'viridis'
1997
- - vmin, vmax (float): Color scale limits for simulation data
1998
-
1999
- Camera and Rendering:
2000
- - projection_type (str): 'perspective' or 'orthographic', default 'perspective'
2001
- - distance_factor (float): Camera distance multiplier, default 1.0
2002
- - window_width, window_height (int): Render resolution, default 1024x768
2003
-
2004
- Output Control:
2005
- - show_views (bool): Whether to display rendered views, default True
2006
- - save_obj (bool): Whether to export OBJ mesh files, default False
2007
-
2008
- Returns:
2009
- --------
2010
- None
2011
- Displays rendered views and optionally saves files to disk.
2012
-
2013
- Notes:
2014
- ------
2015
- - Automatically configures PyVista for headless rendering
2016
- - Generates meshes for each voxel class with appropriate colors
2017
- - Creates colorbar for simulation data if provided
2018
- - Produces 9 different camera views (4 isometric + 5 orthographic)
2019
- - Exports mesh files in OBJ format if requested
2020
-
2021
- Technical Requirements:
2022
- -----------------------
2023
- - Requires Xvfb for headless rendering on Linux systems
2024
- - Uses PyVista for high-quality 3D rendering
2025
- - Simulation data is interpolated onto elevated surface mesh
2026
-
2027
- Examples:
2028
- ---------
2029
- >>> # Basic visualization
2030
- >>> visualize_voxcity_multi_view(voxel_array, meshsize=2.0)
2031
-
2032
- >>> # With simulation results overlay
2033
- >>> visualize_voxcity_multi_view(
2034
- ... voxel_array, 2.0,
2035
- ... sim_grid=temperature_data,
2036
- ... dem_grid=elevation_data,
2037
- ... colormap='plasma',
2038
- ... output_file_name='temperature_analysis'
2039
- ... )
2040
-
2041
- >>> # High-resolution orthographic technical drawings
2042
- >>> visualize_voxcity_multi_view(
2043
- ... voxel_array, 2.0,
2044
- ... projection_type='orthographic',
2045
- ... window_width=2048,
2046
- ... window_height=1536,
2047
- ... save_obj=True
2048
- ... )
2049
- """
2050
- # Configure PyVista settings for high-quality rendering
2051
- pv.set_plot_theme('document')
2052
- pv.global_theme.background = 'white'
2053
- pv.global_theme.window_size = [1024, 768]
2054
- pv.global_theme.jupyter_backend = 'static'
2055
-
2056
- # Parse visualization parameters from kwargs
2057
- voxel_color_map = kwargs.get("voxel_color_map", 'default')
2058
- vox_dict = get_voxel_color_map(voxel_color_map)
2059
- output_directory = kwargs.get("output_directory", 'output')
2060
- base_filename = kwargs.get("output_file_name", None)
2061
- sim_grid = kwargs.get("sim_grid", None)
2062
- dem_grid_ori = kwargs.get("dem_grid", None)
2063
-
2064
- # Normalize DEM grid to start from zero elevation
2065
- if dem_grid_ori is not None:
2066
- dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2067
-
2068
- # Simulation overlay parameters
2069
- z_offset = kwargs.get("view_point_height", 1.5)
2070
- cmap_name = kwargs.get("colormap", "viridis")
2071
- vmin = kwargs.get("vmin", np.nanmin(sim_grid) if sim_grid is not None else None)
2072
- vmax = kwargs.get("vmax", np.nanmax(sim_grid) if sim_grid is not None else None)
2073
-
2074
- # Camera and rendering parameters
2075
- projection_type = kwargs.get("projection_type", "perspective")
2076
- distance_factor = kwargs.get("distance_factor", 1.0)
2077
-
2078
- # Output control parameters
2079
- save_obj = kwargs.get("save_obj", False)
2080
- show_views = kwargs.get("show_views", True)
2081
- # Disable rendering in CI/headless to avoid VTK segfaults
2082
- if os.getenv('GITHUB_ACTIONS', 'false').lower() == 'true' or os.getenv('CI', 'false').lower() == 'true':
2083
- show_views = False
2084
-
2085
- # Set up headless rendering environment for PyVista only if we will render
2086
- if show_views and sys.platform.startswith('linux'):
2087
- try:
2088
- import pyvista as _pv
2089
- _pv.start_xvfb()
2090
- except Exception:
2091
- os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2092
- os.environ['DISPLAY'] = ':99'
2093
-
2094
- # Create 3D meshes from voxel data
2095
- print("Creating voxel meshes...")
2096
- meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2097
-
2098
- # Add simulation results as elevated surface mesh if provided
2099
- if sim_grid is not None and dem_grid is not None:
2100
- print("Creating sim_grid surface mesh...")
2101
- sim_mesh = create_sim_surface_mesh(
2102
- sim_grid, dem_grid,
2103
- meshsize=meshsize,
2104
- z_offset=z_offset,
2105
- cmap_name=cmap_name,
2106
- vmin=vmin,
2107
- vmax=vmax
2108
- )
2109
- if sim_mesh is not None:
2110
- meshes["sim_surface"] = sim_mesh
2111
-
2112
- # Create and display colorbar for simulation data
2113
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2114
- scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2115
-
2116
- fig, ax = plt.subplots(figsize=(6, 1))
2117
- plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2118
- plt.tight_layout()
2119
- plt.show()
2120
-
2121
- # # Export mesh files if requested
2122
- # if base_filename is not None:
2123
- # print(f"Exporting files to '{base_filename}.*' ...")
2124
- # os.makedirs(output_directory, exist_ok=True)
2125
- # export_meshes(meshes, output_directory, base_filename)
2126
-
2127
- # Export OBJ mesh files if requested
2128
- if save_obj:
2129
- output_directory = kwargs.get('output_directory', 'output')
2130
- output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2131
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2132
- print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2133
-
2134
- # Generate and display multiple camera views
2135
- if show_views:
2136
- print("Creating multiple views...")
2137
- os.makedirs(output_directory, exist_ok=True)
2138
- image_files = create_multi_view_scene(meshes, output_directory=output_directory, projection_type=projection_type, distance_factor=distance_factor)
2139
-
2140
- # Display each rendered view
2141
- for view_name, img_file in image_files:
2142
- plt.figure(figsize=(24, 16))
2143
- img = plt.imread(img_file)
2144
- plt.imshow(img)
2145
- plt.title(view_name.replace('_', ' ').title(), pad=20)
2146
- plt.axis('off')
2147
- plt.show()
2148
- plt.close()
2149
-
2150
- def visualize_voxcity_multi_view_with_multiple_sim_grids(voxel_array, meshsize, sim_configs, **kwargs):
2151
- """
2152
- Create multiple views of the voxel city data with multiple simulation grids.
2153
-
2154
- Args:
2155
- voxel_array: 3D numpy array containing voxel data
2156
- meshsize: Size of each voxel/cell
2157
- sim_configs: List of dictionaries, each containing configuration for a simulation grid:
2158
- {
2159
- 'sim_grid': 2D numpy array of simulation values,
2160
- 'z_offset': height offset in meters (default: 1.5),
2161
- 'cmap_name': colormap name (default: 'viridis'),
2162
- 'vmin': minimum value for colormap (optional),
2163
- 'vmax': maximum value for colormap (optional),
2164
- 'label': label for the colorbar (optional)
2165
- }
2166
- **kwargs: Additional arguments including:
2167
- - vox_dict: Dictionary mapping voxel values to colors
2168
- - output_directory: Directory to save output images
2169
- - output_file_name: Base filename for exports
2170
- - dem_grid: DEM grid for height information
2171
- - projection_type: 'perspective' or 'orthographic'
2172
- - distance_factor: Factor to adjust camera distance
2173
- """
2174
- os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2175
- os.environ['DISPLAY'] = ':99'
2176
-
2177
- # Configure PyVista settings
2178
- pv.set_plot_theme('document')
2179
- pv.global_theme.background = 'white'
2180
- window_width = kwargs.get("window_width", 1024)
2181
- window_height = kwargs.get("window_height", 768)
2182
- pv.global_theme.window_size = [window_width, window_height]
2183
- pv.global_theme.jupyter_backend = 'static'
2184
-
2185
- # Parse general kwargs
2186
- voxel_color_map = kwargs.get("voxel_color_map", 'default')
2187
- vox_dict = get_voxel_color_map(voxel_color_map)
2188
- output_directory = kwargs.get("output_directory", 'output')
2189
- base_filename = kwargs.get("output_file_name", None)
2190
- dem_grid_ori = kwargs.get("dem_grid", None)
2191
- projection_type = kwargs.get("projection_type", "perspective")
2192
- distance_factor = kwargs.get("distance_factor", 1.0)
2193
- show_views = kwargs.get("show_views", True)
2194
- save_obj = kwargs.get("save_obj", False)
2195
-
2196
- if dem_grid_ori is not None:
2197
- dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2198
-
2199
- # Create meshes
2200
- print("Creating voxel meshes...")
2201
- meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
2202
-
2203
- # Process each simulation grid
2204
- for i, config in enumerate(sim_configs):
2205
- sim_grid = config['sim_grid']
2206
- if sim_grid is None or dem_grid is None:
2207
- continue
2208
-
2209
- z_offset = config.get('z_offset', 1.5)
2210
- cmap_name = config.get('cmap_name', 'viridis')
2211
- vmin = config.get('vmin', np.nanmin(sim_grid))
2212
- vmax = config.get('vmax', np.nanmax(sim_grid))
2213
- label = config.get('label', f'Simulation {i+1}')
2214
-
2215
- print(f"Creating sim_grid surface mesh for {label}...")
2216
- sim_mesh = create_sim_surface_mesh(
2217
- sim_grid, dem_grid,
2218
- meshsize=meshsize,
2219
- z_offset=z_offset,
2220
- cmap_name=cmap_name,
2221
- vmin=vmin,
2222
- vmax=vmax
2223
- )
2224
-
2225
- if sim_mesh is not None:
2226
- meshes[f"sim_surface_{i}"] = sim_mesh
2227
-
2228
- # Create colorbar for this simulation
2229
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2230
- scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2231
-
2232
- fig, ax = plt.subplots(figsize=(6, 1))
2233
- plt.colorbar(scalar_map, cax=ax, orientation='horizontal', label=label)
2234
- plt.tight_layout()
2235
- plt.show()
2236
-
2237
- # Export if filename provided
2238
- if base_filename is not None:
2239
- print(f"Exporting files to '{base_filename}.*' ...")
2240
- os.makedirs(output_directory, exist_ok=True)
2241
- export_meshes(meshes, output_directory, base_filename)
2242
-
2243
- if show_views:
2244
- # Create and save multiple views
2245
- print("Creating multiple views...")
2246
- os.makedirs(output_directory, exist_ok=True)
2247
- image_files = create_multi_view_scene(
2248
- meshes,
2249
- output_directory=output_directory,
2250
- projection_type=projection_type,
2251
- distance_factor=distance_factor
2252
- )
2253
-
2254
- # Display each view separately
2255
- for view_name, img_file in image_files:
2256
- plt.figure(figsize=(24, 16))
2257
- img = plt.imread(img_file)
2258
- plt.imshow(img)
2259
- plt.title(view_name.replace('_', ' ').title(), pad=20)
2260
- plt.axis('off')
2261
- plt.show()
2262
- plt.close()
2263
-
2264
- # After creating the meshes and before visualization
2265
- if save_obj:
2266
- output_directory = kwargs.get('output_directory', 'output')
2267
- output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2268
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2269
- print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2270
-
2271
- def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None, **kwargs):
2272
- """
2273
- Creates 3D visualizations of voxel city data with custom simulation mesh overlays.
2274
-
2275
- This advanced visualization function allows replacement of specific voxel classes
2276
- with custom simulation result meshes. It's particularly useful for overlaying
2277
- detailed simulation results (like computational fluid dynamics, thermal analysis,
2278
- or environmental factors) onto specific building or infrastructure components.
2279
-
2280
- The function supports simulation meshes with metadata containing numerical values
2281
- that can be visualized using color mapping, making it ideal for displaying
2282
- spatially-varying simulation results on building surfaces or other urban elements.
2283
-
2284
- Parameters:
2285
- -----------
2286
- voxel_array : np.ndarray
2287
- 3D array of voxel values representing the base city model.
2288
- Shape should be (x, y, z) where each element is a voxel class ID.
2289
-
2290
- meshsize : float
2291
- Size of each voxel in meters, used for coordinate scaling.
2292
-
2293
- custom_meshes : dict, optional
2294
- Dictionary mapping voxel class IDs to custom trimesh.Trimesh objects.
2295
- Example: {-3: building_simulation_mesh, -2: vegetation_mesh}
2296
- These meshes will replace the standard voxel representation for visualization.
2297
- Default is None.
2298
-
2299
- **kwargs:
2300
- Extensive configuration options organized by category:
2301
-
2302
- Base Visualization:
2303
- - vox_dict (dict): Dictionary mapping voxel class IDs to colors
2304
- - output_directory (str): Directory for saving output files
2305
- - output_file_name (str): Base filename for exported meshes
2306
-
2307
- Simulation Result Display:
2308
- - value_name (str): Name of metadata field containing simulation values
2309
- - colormap (str): Matplotlib colormap name for simulation results
2310
- - vmin, vmax (float): Color scale limits for simulation data
2311
- - colorbar_title (str): Title for the simulation result colorbar
2312
- - nan_color (str/tuple): Color for NaN/invalid simulation values, default 'gray'
2313
-
2314
- Ground Surface Overlay:
2315
- - sim_grid (np.ndarray): 2D array with ground-level simulation values
2316
- - dem_grid (np.ndarray): Digital elevation model for terrain height
2317
- - view_point_height (float): Height offset for ground simulation surface
2318
-
2319
- Camera and Rendering:
2320
- - projection_type (str): 'perspective' or 'orthographic', default 'perspective'
2321
- - distance_factor (float): Camera distance multiplier, default 1.0
2322
- - window_width, window_height (int): Render resolution
2323
-
2324
- Output Control:
2325
- - show_views (bool): Whether to display rendered views, default True
2326
- - save_obj (bool): Whether to export OBJ mesh files, default False
2327
-
2328
- Returns:
2329
- --------
2330
- list
2331
- List of (view_name, image_file_path) tuples for generated views.
2332
- Only returned if show_views=True.
2333
-
2334
- Notes:
2335
- ------
2336
- Simulation Mesh Requirements:
2337
- - Custom meshes should have simulation values stored in mesh.metadata[value_name]
2338
- - Values can include NaN for areas without valid simulation data
2339
- - Mesh geometry should align with the voxel grid coordinate system
2340
-
2341
- Color Mapping:
2342
- - Simulation values are mapped to colors using the specified colormap
2343
- - NaN values are rendered in the specified nan_color
2344
- - A colorbar is automatically generated and displayed
2345
-
2346
- Technical Implementation:
2347
- - Uses PyVista for high-quality 3D rendering
2348
- - Supports both individual mesh coloring and ground surface overlays
2349
- - Automatically handles coordinate system transformations
2350
- - Generates multiple camera views for comprehensive visualization
2351
-
2352
- Examples:
2353
- ---------
2354
- >>> # Basic usage with building simulation results
2355
- >>> building_mesh = trimesh.load('building_with_cfd_results.ply')
2356
- >>> building_mesh.metadata = {'temperature': temperature_values}
2357
- >>> custom_meshes = {-3: building_mesh} # -3 is building class ID
2358
- >>>
2359
- >>> visualize_voxcity_with_sim_meshes(
2360
- ... voxel_array, meshsize=2.0,
2361
- ... custom_meshes=custom_meshes,
2362
- ... value_name='temperature',
2363
- ... colormap='plasma',
2364
- ... colorbar_title='Temperature (°C)',
2365
- ... vmin=15, vmax=35
2366
- ... )
2367
-
2368
- >>> # With ground-level wind simulation overlay
2369
- >>> wind_mesh = create_wind_simulation_mesh(wind_data)
2370
- >>> visualize_voxcity_with_sim_meshes(
2371
- ... voxel_array, 2.0,
2372
- ... custom_meshes={-3: wind_mesh},
2373
- ... value_name='wind_speed',
2374
- ... sim_grid=ground_wind_grid,
2375
- ... dem_grid=elevation_grid,
2376
- ... colormap='viridis',
2377
- ... projection_type='orthographic',
2378
- ... save_obj=True
2379
- ... )
2380
-
2381
- >>> # Multiple simulation types with custom styling
2382
- >>> meshes = {
2383
- ... -3: building_thermal_mesh, # Buildings with thermal data
2384
- ... -2: vegetation_co2_mesh # Vegetation with CO2 absorption
2385
- ... }
2386
- >>> visualize_voxcity_with_sim_meshes(
2387
- ... voxel_array, 2.0,
2388
- ... custom_meshes=meshes,
2389
- ... value_name='co2_flux',
2390
- ... colormap='RdYlBu_r',
2391
- ... nan_color='lightgray',
2392
- ... distance_factor=1.5,
2393
- ... output_file_name='co2_analysis'
2394
- ... )
2395
-
2396
- See Also:
2397
- ---------
2398
- - visualize_building_sim_results(): Specialized function for building simulations
2399
- - visualize_voxcity_multi_view(): Basic voxel visualization without custom meshes
2400
- - create_multi_view_scene(): Lower-level rendering function
2401
- """
2402
- # Setup offscreen rendering only when needed and supported
2403
- if sys.platform.startswith('linux'):
2404
- if 'DISPLAY' not in os.environ:
2405
- os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
2406
- os.environ['DISPLAY'] = ':99'
2407
-
2408
- # Configure PyVista settings
2409
- pv.set_plot_theme('document')
2410
- pv.global_theme.background = 'white'
2411
- window_width = kwargs.get("window_width", 1024)
2412
- window_height = kwargs.get("window_height", 768)
2413
- pv.global_theme.window_size = [window_width, window_height]
2414
- pv.global_theme.jupyter_backend = 'static'
2415
-
2416
- # Parse kwargs
2417
- voxel_color_map = kwargs.get("voxel_color_map", 'default')
2418
- vox_dict = get_voxel_color_map(voxel_color_map)
2419
- output_directory = kwargs.get("output_directory", 'output')
2420
- base_filename = kwargs.get("output_file_name", None)
2421
- sim_grid = kwargs.get("sim_grid", None)
2422
- dem_grid_ori = kwargs.get("dem_grid", None)
2423
- if dem_grid_ori is not None:
2424
- dem_grid = dem_grid_ori - np.min(dem_grid_ori)
2425
- z_offset = kwargs.get("view_point_height", 1.5)
2426
- cmap_name = kwargs.get("colormap", "viridis")
2427
- vmin = kwargs.get("vmin", None)
2428
- vmax = kwargs.get("vmax", None)
2429
- projection_type = kwargs.get("projection_type", "perspective")
2430
- distance_factor = kwargs.get("distance_factor", 1.0)
2431
- colorbar_title = kwargs.get("colorbar_title", "")
2432
- value_name = kwargs.get("value_name", None)
2433
- nan_color = kwargs.get("nan_color", "gray")
2434
- show_views = kwargs.get("show_views", True)
2435
- save_obj = kwargs.get("save_obj", False)
2436
- include_classes = kwargs.get("include_classes", None)
2437
- exclude_classes = kwargs.get("exclude_classes", None)
2438
- copy_custom_mesh = kwargs.get("copy_custom_mesh", False)
2439
-
2440
- if value_name is None:
2441
- print("Set value_name")
2442
-
2443
- # Create meshes from voxel data
2444
- print("Creating voxel meshes...")
2445
- # Skip generating voxel meshes for classes that will be replaced by custom meshes
2446
- if exclude_classes is None and custom_meshes is not None:
2447
- exclude_classes = list(custom_meshes.keys())
2448
- meshes = create_city_meshes(
2449
- voxel_array,
2450
- vox_dict,
2451
- meshsize=meshsize,
2452
- include_classes=include_classes,
2453
- exclude_classes=exclude_classes,
2454
- )
2455
-
2456
- # Replace specific voxel class meshes with custom simulation meshes
2457
- if custom_meshes is not None:
2458
- for class_id, custom_mesh in custom_meshes.items():
2459
- # Apply coloring to custom meshes if they have metadata values
2460
- if hasattr(custom_mesh, 'metadata') and value_name in custom_mesh.metadata:
2461
- # Create a colored copy of the mesh for visualization
2462
- import matplotlib.cm as cm
2463
- import matplotlib.colors as mcolors
2464
-
2465
- # Get values from metadata
2466
- values = np.asarray(custom_mesh.metadata[value_name])
2467
-
2468
- # Set vmin/vmax if not provided
2469
- finite_mask = np.isfinite(values)
2470
- if not np.any(finite_mask):
2471
- local_vmin = 0.0 if vmin is None else vmin
2472
- local_vmax = 1.0 if vmax is None else vmax
2473
- else:
2474
- local_vmin = vmin if vmin is not None else float(np.nanmin(values[finite_mask]))
2475
- local_vmax = vmax if vmax is not None else float(np.nanmax(values[finite_mask]))
2476
-
2477
- # Create colors
2478
- cmap = cm.get_cmap(cmap_name)
2479
- norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
2480
-
2481
- # Handle NaN values with custom color
2482
- face_colors = np.zeros((len(values), 4), dtype=float)
2483
-
2484
- # Convert string color to RGBA if needed
2485
- if isinstance(nan_color, str):
2486
- import matplotlib.colors as mcolors
2487
- nan_rgba = np.array(mcolors.to_rgba(nan_color))
2488
- else:
2489
- # Assume it's already a tuple/list of RGBA values
2490
- nan_rgba = np.array(nan_color)
2491
-
2492
- # Apply colors: NaN values get nan_color, others get colormap colors
2493
- nan_mask = ~finite_mask
2494
- face_colors[~nan_mask] = cmap(norm(values[~nan_mask]))
2495
- face_colors[nan_mask] = nan_rgba
2496
-
2497
- # Create a copy with colors
2498
- vis_mesh = custom_mesh.copy() if copy_custom_mesh else custom_mesh
2499
- vis_mesh.visual.face_colors = face_colors
2500
-
2501
- # Prepare the colormap and create colorbar if views will be shown
2502
- if show_views:
2503
- norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
2504
- scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2505
- fig, ax = plt.subplots(figsize=(6, 1))
2506
- cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2507
- if colorbar_title:
2508
- cbar.set_label(colorbar_title)
2509
- plt.tight_layout()
2510
- plt.show()
2511
- plt.close(fig)
2512
-
2513
- if class_id in meshes:
2514
- print(f"Replacing voxel class {class_id} with colored custom simulation mesh")
2515
- meshes[class_id] = vis_mesh
2516
- else:
2517
- print(f"Adding colored custom simulation mesh for class {class_id}")
2518
- meshes[class_id] = vis_mesh
2519
- else:
2520
- # No metadata values, use the mesh as is
2521
- if class_id in meshes:
2522
- print(f"Replacing voxel class {class_id} with custom simulation mesh")
2523
- meshes[class_id] = custom_mesh
2524
- else:
2525
- print(f"Adding custom simulation mesh for class {class_id}")
2526
- meshes[class_id] = custom_mesh
2527
-
2528
- # Create sim_grid surface mesh if provided
2529
- if sim_grid is not None and dem_grid is not None:
2530
- print("Creating sim_grid surface mesh...")
2531
-
2532
- # If vmin/vmax not provided, use actual min/max of the valid sim data
2533
- if vmin is None:
2534
- vmin = np.nanmin(sim_grid)
2535
- if vmax is None:
2536
- vmax = np.nanmax(sim_grid)
2537
-
2538
- sim_mesh = create_sim_surface_mesh(
2539
- sim_grid, dem_grid,
2540
- meshsize=meshsize,
2541
- z_offset=z_offset,
2542
- cmap_name=cmap_name,
2543
- vmin=vmin,
2544
- vmax=vmax,
2545
- nan_color=nan_color # Pass nan_color to the mesh creation
2546
- )
2547
- if sim_mesh is not None:
2548
- meshes["sim_surface"] = sim_mesh
2549
-
2550
- # # Prepare the colormap and create colorbar
2551
- # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
2552
- # scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
2553
-
2554
- # # Create a figure and axis for the colorbar but don't display
2555
- # fig, ax = plt.subplots(figsize=(6, 1))
2556
- # cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
2557
- # if colorbar_title:
2558
- # cbar.set_label(colorbar_title)
2559
- # plt.tight_layout()
2560
- # plt.show()
2561
-
2562
- # # Export if filename provided
2563
- # if base_filename is not None:
2564
- # print(f"Exporting files to '{base_filename}.*' ...")
2565
- # # Create output directory if it doesn't exist
2566
- # os.makedirs(output_directory, exist_ok=True)
2567
- # export_meshes(meshes, output_directory, base_filename)
2568
-
2569
- # Create output directory if it doesn't exist
2570
- os.makedirs(output_directory, exist_ok=True)
2571
-
2572
- # After creating the meshes and before visualization
2573
- if save_obj:
2574
- output_directory = kwargs.get('output_directory', 'output')
2575
- output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
2576
- # Default: do NOT quantize to preserve exact face colors like in images
2577
- max_materials = kwargs.get('max_materials', None)
2578
- if max_materials is None:
2579
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
2580
- else:
2581
- obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name, max_materials=max_materials)
2582
- print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
2583
-
2584
- if show_views:
2585
- # Create and save multiple views
2586
- print("Creating multiple views...")
2587
- image_files = create_multi_view_scene(meshes, output_directory=output_directory,
2588
- projection_type=projection_type,
2589
- distance_factor=distance_factor)
2590
-
2591
- # Display each view separately
2592
- for view_name, img_file in image_files:
2593
- plt.figure(figsize=(24, 16))
2594
- img = plt.imread(img_file)
2595
- plt.imshow(img)
2596
- plt.title(view_name.replace('_', ' ').title(), pad=20)
2597
- plt.axis('off')
2598
- plt.show()
2599
- plt.close()
2600
-
2601
-
2602
- def visualize_building_sim_results(voxel_array, meshsize, building_sim_mesh, **kwargs):
2603
- """
2604
- Visualize building simulation results by replacing building meshes in the original model.
2605
-
2606
- This is a specialized wrapper around visualize_voxcity_with_sim_meshes that specifically
2607
- targets building simulation meshes (assuming building class ID is -3).
2608
-
2609
- Parameters
2610
- ----------
2611
- voxel_array : np.ndarray
2612
- 3D array of voxel values.
2613
- meshsize : float
2614
- Size of each voxel in meters.
2615
- building_sim_mesh : trimesh.Trimesh
2616
- Simulation result mesh for buildings with values stored in metadata.
2617
- **kwargs:
2618
- Same parameters as visualize_voxcity_with_sim_meshes.
2619
- Additional parameters:
2620
- value_name : str
2621
- Name of the field in metadata containing values to visualize (default: 'svf_values')
2622
- nan_color : str or tuple
2623
- Color for NaN values (default: 'gray')
2624
-
2625
- Returns
2626
- -------
2627
- list
2628
- List of (view_name, image_file_path) tuples for the generated views.
2629
- """
2630
- # Building class ID is typically -3 in voxcity
2631
- building_class_id = kwargs.get("building_class_id", -3)
2632
-
2633
- # Create custom meshes dictionary with the building simulation mesh
2634
- custom_meshes = {building_class_id: building_sim_mesh}
2635
-
2636
- # Performance: skip voxel mesh generation for building class by default
2637
- if "include_classes" not in kwargs and "exclude_classes" not in kwargs:
2638
- kwargs["exclude_classes"] = [building_class_id]
2639
-
2640
- # Memory-safety: do not duplicate the provided mesh unless requested
2641
- kwargs.setdefault("copy_custom_mesh", False)
2642
-
2643
- # Add colorbar title if not provided
2644
- if "colorbar_title" not in kwargs:
2645
- # Try to guess a title based on the mesh name/type
2646
- if hasattr(building_sim_mesh, 'name') and building_sim_mesh.name:
2647
- kwargs["colorbar_title"] = building_sim_mesh.name
2648
- else:
2649
- # Use value_field name as fallback
2650
- value_name = kwargs.get("value_name", "svf_values")
2651
- pretty_name = value_name.replace('_', ' ').title()
2652
- kwargs["colorbar_title"] = pretty_name
2653
-
2654
- # Call the more general visualization function
2655
- visualize_voxcity_with_sim_meshes(
2656
- voxel_array,
2657
- meshsize,
2658
- custom_meshes=custom_meshes,
2659
- **kwargs
2660
- )