voxcity 0.6.26__py3-none-any.whl → 1.0.2__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 (81) hide show
  1. voxcity/__init__.py +10 -4
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/gba.py +210 -0
  4. voxcity/downloader/gee.py +5 -1
  5. voxcity/downloader/mbfp.py +1 -1
  6. voxcity/downloader/oemj.py +80 -8
  7. voxcity/downloader/utils.py +73 -73
  8. voxcity/errors.py +30 -0
  9. voxcity/exporter/__init__.py +9 -1
  10. voxcity/exporter/cityles.py +129 -34
  11. voxcity/exporter/envimet.py +51 -26
  12. voxcity/exporter/magicavoxel.py +42 -5
  13. voxcity/exporter/netcdf.py +27 -0
  14. voxcity/exporter/obj.py +103 -28
  15. voxcity/generator/__init__.py +47 -0
  16. voxcity/generator/api.py +721 -0
  17. voxcity/generator/grids.py +381 -0
  18. voxcity/generator/io.py +94 -0
  19. voxcity/generator/pipeline.py +282 -0
  20. voxcity/generator/update.py +429 -0
  21. voxcity/generator/voxelizer.py +392 -0
  22. voxcity/geoprocessor/__init__.py +75 -6
  23. voxcity/geoprocessor/conversion.py +153 -0
  24. voxcity/geoprocessor/draw.py +1488 -1169
  25. voxcity/geoprocessor/heights.py +199 -0
  26. voxcity/geoprocessor/io.py +101 -0
  27. voxcity/geoprocessor/merge_utils.py +91 -0
  28. voxcity/geoprocessor/mesh.py +26 -10
  29. voxcity/geoprocessor/network.py +35 -6
  30. voxcity/geoprocessor/overlap.py +84 -0
  31. voxcity/geoprocessor/raster/__init__.py +82 -0
  32. voxcity/geoprocessor/raster/buildings.py +435 -0
  33. voxcity/geoprocessor/raster/canopy.py +258 -0
  34. voxcity/geoprocessor/raster/core.py +150 -0
  35. voxcity/geoprocessor/raster/export.py +93 -0
  36. voxcity/geoprocessor/raster/landcover.py +159 -0
  37. voxcity/geoprocessor/raster/raster.py +110 -0
  38. voxcity/geoprocessor/selection.py +85 -0
  39. voxcity/geoprocessor/utils.py +824 -820
  40. voxcity/models.py +113 -0
  41. voxcity/simulator/common/__init__.py +22 -0
  42. voxcity/simulator/common/geometry.py +98 -0
  43. voxcity/simulator/common/raytracing.py +450 -0
  44. voxcity/simulator/solar/__init__.py +66 -0
  45. voxcity/simulator/solar/integration.py +336 -0
  46. voxcity/simulator/solar/kernels.py +62 -0
  47. voxcity/simulator/solar/radiation.py +648 -0
  48. voxcity/simulator/solar/sky.py +668 -0
  49. voxcity/simulator/solar/temporal.py +792 -0
  50. voxcity/simulator/view.py +36 -2286
  51. voxcity/simulator/visibility/__init__.py +29 -0
  52. voxcity/simulator/visibility/landmark.py +392 -0
  53. voxcity/simulator/visibility/view.py +508 -0
  54. voxcity/utils/__init__.py +11 -0
  55. voxcity/utils/classes.py +194 -0
  56. voxcity/utils/lc.py +80 -39
  57. voxcity/utils/logging.py +61 -0
  58. voxcity/utils/orientation.py +51 -0
  59. voxcity/utils/shape.py +230 -0
  60. voxcity/utils/weather/__init__.py +26 -0
  61. voxcity/utils/weather/epw.py +146 -0
  62. voxcity/utils/weather/files.py +36 -0
  63. voxcity/utils/weather/onebuilding.py +486 -0
  64. voxcity/visualizer/__init__.py +24 -0
  65. voxcity/visualizer/builder.py +43 -0
  66. voxcity/visualizer/grids.py +141 -0
  67. voxcity/visualizer/maps.py +187 -0
  68. voxcity/visualizer/palette.py +228 -0
  69. voxcity/visualizer/renderer.py +1145 -0
  70. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
  71. voxcity-1.0.2.dist-info/RECORD +81 -0
  72. voxcity/generator.py +0 -1302
  73. voxcity/geoprocessor/grid.py +0 -1739
  74. voxcity/geoprocessor/polygon.py +0 -1344
  75. voxcity/simulator/solar.py +0 -2339
  76. voxcity/utils/visualization.py +0 -2849
  77. voxcity/utils/weather.py +0 -1038
  78. voxcity-0.6.26.dist-info/RECORD +0 -38
  79. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
  80. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,29 @@
1
+ from .view import (
2
+ get_view_index,
3
+ get_sky_view_factor_map,
4
+ get_surface_view_factor,
5
+ )
6
+ from .landmark import (
7
+ mark_building_by_id,
8
+ compute_landmark_visibility,
9
+ get_landmark_visibility_map,
10
+ get_surface_landmark_visibility,
11
+ )
12
+ from ..common.geometry import (
13
+ rotate_vector_axis_angle,
14
+ )
15
+
16
+ __all__ = [
17
+ # View
18
+ "get_view_index",
19
+ "get_sky_view_factor_map",
20
+ "get_surface_view_factor",
21
+ # Landmark
22
+ "mark_building_by_id",
23
+ "compute_landmark_visibility",
24
+ "get_landmark_visibility_map",
25
+ "get_surface_landmark_visibility",
26
+ # Geometry helpers
27
+ "rotate_vector_axis_angle",
28
+ ]
29
+
@@ -0,0 +1,392 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import matplotlib.patches as mpatches
4
+ from numba import njit, prange
5
+
6
+ from ...geoprocessor.selection import find_building_containing_point, get_buildings_in_drawn_polygon
7
+ from ...geoprocessor.mesh import create_voxel_mesh
8
+ from ...exporter.obj import grid_to_obj, export_obj
9
+
10
+
11
+ def mark_building_by_id(voxcity_grid_ori, building_id_grid_ori, ids, mark):
12
+ voxcity_grid = voxcity_grid_ori.copy()
13
+ building_id_grid = np.flipud(building_id_grid_ori.copy())
14
+ positions = np.where(np.isin(building_id_grid, ids))
15
+ for i in range(len(positions[0])):
16
+ x, y = positions[0][i], positions[1][i]
17
+ z_mask = voxcity_grid[x, y, :] == -3
18
+ voxcity_grid[x, y, z_mask] = mark
19
+ return voxcity_grid
20
+
21
+
22
+ @njit
23
+ def trace_ray_to_target(voxel_data, origin, target, opaque_values):
24
+ nx, ny, nz = voxel_data.shape
25
+ x0, y0, z0 = origin
26
+ x1, y1, z1 = target
27
+ dx = x1 - x0
28
+ dy = y1 - y0
29
+ dz = z1 - z0
30
+ length = np.sqrt(dx*dx + dy*dy + dz*dz)
31
+ if length == 0.0:
32
+ return True
33
+ dx /= length
34
+ dy /= length
35
+ dz /= length
36
+ x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
37
+ i, j, k = int(x0), int(y0), int(z0)
38
+ step_x = 1 if dx >= 0 else -1
39
+ step_y = 1 if dy >= 0 else -1
40
+ step_z = 1 if dz >= 0 else -1
41
+ if dx != 0:
42
+ t_max_x = ((i + (step_x > 0)) - x) / dx
43
+ t_delta_x = abs(1 / dx)
44
+ else:
45
+ t_max_x = np.inf
46
+ t_delta_x = np.inf
47
+ if dy != 0:
48
+ t_max_y = ((j + (step_y > 0)) - y) / dy
49
+ t_delta_y = abs(1 / dy)
50
+ else:
51
+ t_max_y = np.inf
52
+ t_delta_y = np.inf
53
+ if dz != 0:
54
+ t_max_z = ((k + (step_z > 0)) - z) / dz
55
+ t_delta_z = abs(1 / dz)
56
+ else:
57
+ t_max_z = np.inf
58
+ t_delta_z = np.inf
59
+ while True:
60
+ if (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
61
+ voxel_value = voxel_data[i, j, k]
62
+ if voxel_value in opaque_values:
63
+ return False
64
+ else:
65
+ return False
66
+ if i == int(x1) and j == int(y1) and k == int(z1):
67
+ return True
68
+ if t_max_x < t_max_y:
69
+ if t_max_x < t_max_z:
70
+ t_max = t_max_x
71
+ t_max_x += t_delta_x
72
+ i += step_x
73
+ else:
74
+ t_max = t_max_z
75
+ t_max_z += t_delta_z
76
+ k += step_z
77
+ else:
78
+ if t_max_y < t_max_z:
79
+ t_max = t_max_y
80
+ t_max_y += t_delta_y
81
+ j += step_y
82
+ else:
83
+ t_max = t_max_z
84
+ t_max_z += t_delta_z
85
+ k += step_z
86
+
87
+
88
+ @njit
89
+ def compute_visibility_to_all_landmarks(observer_location, landmark_positions, voxel_data, opaque_values):
90
+ for idx in range(landmark_positions.shape[0]):
91
+ target = landmark_positions[idx].astype(np.float64)
92
+ is_visible = trace_ray_to_target(voxel_data, observer_location, target, opaque_values)
93
+ if is_visible:
94
+ return 1
95
+ return 0
96
+
97
+
98
+ @njit(parallel=True)
99
+ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_height_voxel):
100
+ nx, ny, nz = voxel_data.shape
101
+ visibility_map = np.full((nx, ny), np.nan)
102
+ for x in prange(nx):
103
+ for y in range(ny):
104
+ found_observer = False
105
+ for z in range(1, nz):
106
+ if voxel_data[x, y, z] == 0 and voxel_data[x, y, z - 1] != 0:
107
+ if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
108
+ visibility_map[x, y] = np.nan
109
+ found_observer = True
110
+ break
111
+ else:
112
+ observer_location = np.array([x, y, z+view_height_voxel], dtype=np.float64)
113
+ visible = compute_visibility_to_all_landmarks(observer_location, landmark_positions, voxel_data, opaque_values)
114
+ visibility_map[x, y] = visible
115
+ found_observer = True
116
+ break
117
+ if not found_observer:
118
+ visibility_map[x, y] = np.nan
119
+ return visibility_map
120
+
121
+
122
+ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=0, colormap='viridis'):
123
+ landmark_positions = np.argwhere(voxel_data == target_value)
124
+ if landmark_positions.shape[0] == 0:
125
+ raise ValueError(f"No landmark with value {target_value} found in the voxel data.")
126
+ unique_values = np.unique(voxel_data)
127
+ opaque_values = np.array([v for v in unique_values if v != 0 and v != target_value], dtype=np.int32)
128
+ visibility_map = compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_height_voxel)
129
+ cmap = plt.cm.get_cmap(colormap, 2).copy()
130
+ cmap.set_bad(color='lightgray')
131
+ plt.figure(figsize=(10, 8))
132
+ plt.imshow(np.flipud(visibility_map), origin='lower', cmap=cmap, vmin=0, vmax=1)
133
+ visible_patch = mpatches.Patch(color=cmap(1.0), label='Visible (1)')
134
+ not_visible_patch = mpatches.Patch(color=cmap(0.0), label='Not Visible (0)')
135
+ plt.legend(handles=[visible_patch, not_visible_patch],
136
+ loc='center left',
137
+ bbox_to_anchor=(1.0, 0.5))
138
+ plt.axis('off')
139
+ plt.show()
140
+ return np.flipud(visibility_map)
141
+
142
+
143
+ def get_landmark_visibility_map(voxcity, building_gdf=None, **kwargs):
144
+ if building_gdf is None:
145
+ building_gdf = voxcity.extras.get('building_gdf', None)
146
+ if building_gdf is None:
147
+ raise ValueError("building_gdf not provided and not found in voxcity.extras['building_gdf']")
148
+ voxcity_grid_ori = voxcity.voxels.classes
149
+ building_id_grid = voxcity.buildings.ids
150
+ meshsize = voxcity.voxels.meta.meshsize
151
+ view_point_height = kwargs.get("view_point_height", 1.5)
152
+ view_height_voxel = int(view_point_height / meshsize)
153
+ colormap = kwargs.get("colormap", 'viridis')
154
+ landmark_ids = kwargs.get('landmark_building_ids', None)
155
+ landmark_polygon = kwargs.get('landmark_polygon', None)
156
+ if landmark_ids is None:
157
+ if landmark_polygon is not None:
158
+ landmark_ids = get_buildings_in_drawn_polygon(building_gdf, landmark_polygon, operation='within')
159
+ else:
160
+ rectangle_vertices = kwargs.get("rectangle_vertices", None)
161
+ if rectangle_vertices is None:
162
+ rectangle_vertices = voxcity.extras.get("rectangle_vertices", None)
163
+ if rectangle_vertices is None:
164
+ print("Cannot set landmark buildings. You need to input either of rectangle_vertices or landmark_ids.")
165
+ return None
166
+ lons = [coord[0] for coord in rectangle_vertices]
167
+ lats = [coord[1] for coord in rectangle_vertices]
168
+ center_lon = (min(lons) + max(lons)) / 2
169
+ center_lat = (min(lats) + max(lats)) / 2
170
+ target_point = (center_lon, center_lat)
171
+ landmark_ids = find_building_containing_point(building_gdf, target_point)
172
+ target_value = -30
173
+ voxcity_grid = mark_building_by_id(voxcity_grid_ori, building_id_grid, landmark_ids, target_value)
174
+ landmark_vis_map = compute_landmark_visibility(voxcity_grid, target_value=target_value, view_height_voxel=view_height_voxel, colormap=colormap)
175
+ obj_export = kwargs.get("obj_export")
176
+ if obj_export == True:
177
+ dem_grid = kwargs.get("dem_grid", voxcity.dem.elevation if voxcity.dem else np.zeros_like(landmark_vis_map))
178
+ output_dir = kwargs.get("output_directory", "output")
179
+ output_file_name = kwargs.get("output_file_name", "landmark_visibility")
180
+ num_colors = 2
181
+ alpha = kwargs.get("alpha", 1.0)
182
+ vmin = kwargs.get("vmin", 0.0)
183
+ vmax = kwargs.get("vmax", 1.0)
184
+ grid_to_obj(
185
+ landmark_vis_map,
186
+ dem_grid,
187
+ output_dir,
188
+ output_file_name,
189
+ meshsize,
190
+ view_point_height,
191
+ colormap_name=colormap,
192
+ num_colors=num_colors,
193
+ alpha=alpha,
194
+ vmin=vmin,
195
+ vmax=vmax
196
+ )
197
+ output_file_name_vox = 'voxcity_' + output_file_name
198
+ export_obj(voxcity_grid, output_dir, output_file_name_vox, meshsize)
199
+ return landmark_vis_map, voxcity_grid
200
+
201
+
202
+ # Surface landmark visibility (fast, chunked)
203
+ import math
204
+ from ..common.raytracing import _trace_ray
205
+
206
+
207
+ def _prepare_voxel_classes(voxel_data, landmark_value=-30):
208
+ is_tree = (voxel_data == -2)
209
+ is_opaque = (voxel_data != 0) & (voxel_data != landmark_value) & (~is_tree)
210
+ return is_tree, is_opaque
211
+
212
+
213
+ def _compute_all_faces_progress(face_centers, face_normals, landmark_positions_vox, vox_is_tree, vox_is_opaque, meshsize, att, att_cutoff, grid_bounds_real, boundary_epsilon, progress_report=False, chunks=10):
214
+ n_faces = face_centers.shape[0]
215
+ results = np.empty(n_faces, dtype=np.float64)
216
+ step = math.ceil(n_faces / chunks)
217
+ for start in range(0, n_faces, step):
218
+ end = min(start + step, n_faces)
219
+ results[start:end] = _compute_faces_chunk(
220
+ face_centers[start:end],
221
+ face_normals[start:end],
222
+ landmark_positions_vox,
223
+ vox_is_tree, vox_is_opaque,
224
+ meshsize, att, att_cutoff,
225
+ grid_bounds_real, boundary_epsilon
226
+ )
227
+ if progress_report:
228
+ pct = (end / n_faces) * 100
229
+ print(f" Processed {end}/{n_faces} faces ({pct:.1f}%)")
230
+ return results
231
+
232
+
233
+ @njit(parallel=True, cache=True, fastmath=True, nogil=True)
234
+ def _compute_faces_chunk(face_centers, face_normals, landmark_positions_vox, vox_is_tree, vox_is_opaque, meshsize, att, att_cutoff, grid_bounds_real, boundary_epsilon):
235
+ n_faces = face_centers.shape[0]
236
+ out = np.empty(n_faces, dtype=np.float64)
237
+ for f in prange(n_faces):
238
+ out[f] = _compute_face_visibility(
239
+ face_centers[f], face_normals[f],
240
+ landmark_positions_vox,
241
+ vox_is_tree, vox_is_opaque,
242
+ meshsize, att, att_cutoff,
243
+ grid_bounds_real, boundary_epsilon
244
+ )
245
+ return out
246
+
247
+
248
+ @njit(cache=True, fastmath=True, nogil=True)
249
+ def _compute_face_visibility(face_center, face_normal, landmark_positions_vox, vox_is_tree, vox_is_opaque, meshsize, att, att_cutoff, grid_bounds_real, boundary_epsilon):
250
+ is_vertical = (abs(face_normal[2]) < 0.01)
251
+ on_x_min = (abs(face_center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
252
+ on_y_min = (abs(face_center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
253
+ on_x_max = (abs(face_center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
254
+ on_y_max = (abs(face_center[1] - grid_bounds_real[1,1]) < boundary_epsilon)
255
+ if is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max):
256
+ return np.nan
257
+ nx = face_normal[0]; ny = face_normal[1]; nz = face_normal[2]
258
+ nrm = (nx*nx + ny*ny + nz*nz) ** 0.5
259
+ if nrm < 1e-12:
260
+ return 0.0
261
+ invn = 1.0 / nrm
262
+ nx *= invn; ny *= invn; nz *= invn
263
+ offset_vox = 0.1
264
+ ox = face_center[0] / meshsize + nx * offset_vox
265
+ oy = face_center[1] / meshsize + ny * offset_vox
266
+ oz = face_center[2] / meshsize + nz * offset_vox
267
+ for idx in range(landmark_positions_vox.shape[0]):
268
+ tx = landmark_positions_vox[idx, 0]
269
+ ty = landmark_positions_vox[idx, 1]
270
+ tz = landmark_positions_vox[idx, 2]
271
+ rx = tx - ox; ry = ty - oy; rz = tz - oz
272
+ rlen2 = rx*rx + ry*ry + rz*rz
273
+ if rlen2 == 0.0:
274
+ return 1.0
275
+ invr = 1.0 / (rlen2 ** 0.5)
276
+ rdx = rx * invr; rdy = ry * invr; rdz = rz * invr
277
+ if (rdx*nx + rdy*ny + rdz*nz) <= 0.0:
278
+ continue
279
+ if _trace_ray(vox_is_tree, vox_is_opaque, np.array((ox, oy, oz)), np.array((tx, ty, tz)), att, att_cutoff):
280
+ return 1.0
281
+ return 0.0
282
+
283
+
284
+ def get_surface_landmark_visibility(voxcity, building_gdf=None, **kwargs):
285
+ import os
286
+ if building_gdf is None:
287
+ building_gdf = voxcity.extras.get('building_gdf', None)
288
+ if building_gdf is None:
289
+ raise ValueError("building_gdf not provided and not found in voxcity.extras['building_gdf']")
290
+ voxel_data = voxcity.voxels.classes
291
+ building_id_grid = voxcity.buildings.ids
292
+ meshsize = voxcity.voxels.meta.meshsize
293
+ progress_report = kwargs.get("progress_report", False)
294
+ landmark_ids = kwargs.get('landmark_building_ids', None)
295
+ landmark_polygon = kwargs.get('landmark_polygon', None)
296
+ if landmark_ids is None:
297
+ if landmark_polygon is not None:
298
+ landmark_ids = get_buildings_in_drawn_polygon(building_gdf, landmark_polygon, operation='within')
299
+ else:
300
+ rectangle_vertices = kwargs.get("rectangle_vertices", None)
301
+ if rectangle_vertices is None:
302
+ rectangle_vertices = voxcity.extras.get("rectangle_vertices", None)
303
+ if rectangle_vertices is None:
304
+ print("Cannot set landmark buildings. You need to input either of rectangle_vertices or landmark_ids.")
305
+ return None, None
306
+ lons = [coord[0] for coord in rectangle_vertices]
307
+ lats = [coord[1] for coord in rectangle_vertices]
308
+ center_lon = (min(lons) + max(lons)) / 2
309
+ center_lat = (min(lats) + max(lats)) / 2
310
+ target_point = (center_lon, center_lat)
311
+ landmark_ids = find_building_containing_point(building_gdf, target_point)
312
+ building_class_id = kwargs.get("building_class_id", -3)
313
+ landmark_value = -30
314
+ tree_k = kwargs.get("tree_k", 0.6)
315
+ tree_lad = kwargs.get("tree_lad", 1.0)
316
+ colormap = kwargs.get("colormap", 'RdYlGn')
317
+ voxel_data_for_mesh = voxel_data.copy()
318
+ voxel_data_modified = voxel_data.copy()
319
+ voxel_data_modified = mark_building_by_id(voxel_data_modified, building_id_grid, landmark_ids, landmark_value)
320
+ voxel_data_for_mesh = mark_building_by_id(voxel_data_for_mesh, building_id_grid, landmark_ids, 0)
321
+ landmark_positions = np.argwhere(voxel_data_modified == landmark_value).astype(np.float64)
322
+ if landmark_positions.shape[0] == 0:
323
+ print(f"No landmarks found after marking buildings with IDs: {landmark_ids}")
324
+ return None, None
325
+ if progress_report:
326
+ print(f"Found {landmark_positions.shape[0]} landmark voxels")
327
+ print(f"Landmark building IDs: {landmark_ids}")
328
+ try:
329
+ building_mesh = create_voxel_mesh(
330
+ voxel_data_for_mesh,
331
+ building_class_id,
332
+ meshsize,
333
+ building_id_grid=building_id_grid,
334
+ mesh_type='open_air'
335
+ )
336
+ if building_mesh is None or len(building_mesh.faces) == 0:
337
+ print("No non-landmark building surfaces found in voxel data.")
338
+ return None, None
339
+ except Exception as e:
340
+ print(f"Error during mesh extraction: {e}")
341
+ return None, None
342
+ if progress_report:
343
+ print(f"Processing landmark visibility for {len(building_mesh.faces)} faces...")
344
+ face_centers = building_mesh.triangles_center.astype(np.float64)
345
+ face_normals = building_mesh.face_normals.astype(np.float64)
346
+ nx, ny, nz = voxel_data_modified.shape
347
+ grid_bounds_voxel = np.array([[0,0,0],[nx, ny, nz]], dtype=np.float64)
348
+ grid_bounds_real = grid_bounds_voxel * meshsize
349
+ boundary_epsilon = meshsize * 0.05
350
+ vox_is_tree, vox_is_opaque = _prepare_voxel_classes(voxel_data_modified, landmark_value)
351
+ att = float(np.exp(-tree_k * tree_lad * meshsize))
352
+ att_cutoff = 0.01
353
+ visibility_values = _compute_all_faces_progress(
354
+ face_centers,
355
+ face_normals,
356
+ landmark_positions,
357
+ vox_is_tree, vox_is_opaque,
358
+ float(meshsize), att, att_cutoff,
359
+ grid_bounds_real.astype(np.float64),
360
+ float(boundary_epsilon),
361
+ progress_report=progress_report
362
+ )
363
+ building_mesh.metadata = getattr(building_mesh, 'metadata', {})
364
+ building_mesh.metadata['landmark_visibility'] = visibility_values
365
+ valid_mask = ~np.isnan(visibility_values)
366
+ n_valid = np.sum(valid_mask)
367
+ n_visible = np.sum(visibility_values[valid_mask] > 0.5)
368
+ if progress_report:
369
+ print(f"Landmark visibility statistics:")
370
+ print(f" Total faces: {len(visibility_values)}")
371
+ print(f" Valid faces: {n_valid}")
372
+ print(f" Faces with landmark visibility: {n_visible} ({n_visible/n_valid*100:.1f}%)")
373
+ obj_export = kwargs.get("obj_export", False)
374
+ if obj_export:
375
+ output_dir = kwargs.get("output_directory", "output")
376
+ output_file_name = kwargs.get("output_file_name", "surface_landmark_visibility")
377
+ os.makedirs(output_dir, exist_ok=True)
378
+ try:
379
+ cmap = plt.cm.get_cmap(colormap)
380
+ face_colors = np.zeros((len(visibility_values), 4))
381
+ for i, val in enumerate(visibility_values):
382
+ if np.isnan(val):
383
+ face_colors[i] = [0.7, 0.7, 0.7, 1.0]
384
+ else:
385
+ face_colors[i] = cmap(val)
386
+ building_mesh.visual.face_colors = face_colors
387
+ building_mesh.export(f"{output_dir}/{output_file_name}.obj")
388
+ print(f"Exported surface mesh to {output_dir}/{output_file_name}.obj")
389
+ except Exception as e:
390
+ print(f"Error exporting mesh: {e}")
391
+ return building_mesh, voxel_data_modified
392
+