voxcity 0.5.14__py3-none-any.whl → 0.5.16__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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/downloader/citygml.py +202 -28
- voxcity/downloader/eubucco.py +91 -14
- voxcity/downloader/gee.py +164 -22
- voxcity/downloader/mbfp.py +55 -9
- voxcity/downloader/oemj.py +110 -24
- voxcity/downloader/omt.py +74 -7
- voxcity/downloader/osm.py +109 -23
- voxcity/downloader/overture.py +108 -23
- voxcity/downloader/utils.py +37 -7
- voxcity/exporter/envimet.py +180 -61
- voxcity/exporter/magicavoxel.py +138 -28
- voxcity/exporter/obj.py +159 -36
- voxcity/generator.py +159 -76
- voxcity/geoprocessor/draw.py +180 -27
- voxcity/geoprocessor/grid.py +178 -38
- voxcity/geoprocessor/mesh.py +347 -43
- voxcity/geoprocessor/network.py +196 -63
- voxcity/geoprocessor/polygon.py +365 -88
- voxcity/geoprocessor/utils.py +283 -72
- voxcity/simulator/solar.py +596 -201
- voxcity/simulator/view.py +278 -723
- voxcity/utils/lc.py +183 -0
- voxcity/utils/material.py +99 -32
- voxcity/utils/visualization.py +2578 -1988
- voxcity/utils/weather.py +816 -615
- {voxcity-0.5.14.dist-info → voxcity-0.5.16.dist-info}/METADATA +11 -13
- voxcity-0.5.16.dist-info/RECORD +38 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.16.dist-info}/WHEEL +1 -1
- voxcity-0.5.14.dist-info/RECORD +0 -38
- {voxcity-0.5.14.dist-info → voxcity-0.5.16.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.16.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.16.dist-info}/top_level.txt +0 -0
voxcity/simulator/view.py
CHANGED
|
@@ -114,7 +114,7 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
114
114
|
x0, y0, z0 = origin
|
|
115
115
|
dx, dy, dz = direction
|
|
116
116
|
|
|
117
|
-
# Normalize direction vector
|
|
117
|
+
# Normalize direction vector to ensure consistent step sizes
|
|
118
118
|
length = np.sqrt(dx*dx + dy*dy + dz*dz)
|
|
119
119
|
if length == 0.0:
|
|
120
120
|
return False, 1.0
|
|
@@ -122,18 +122,19 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
122
122
|
dy /= length
|
|
123
123
|
dz /= length
|
|
124
124
|
|
|
125
|
-
# Initialize ray position
|
|
125
|
+
# Initialize ray position at center of starting voxel
|
|
126
126
|
x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
|
|
127
127
|
i, j, k = int(x0), int(y0), int(z0)
|
|
128
128
|
|
|
129
|
-
#
|
|
129
|
+
# Determine step direction for each axis (-1 or +1)
|
|
130
130
|
step_x = 1 if dx >= 0 else -1
|
|
131
131
|
step_y = 1 if dy >= 0 else -1
|
|
132
132
|
step_z = 1 if dz >= 0 else -1
|
|
133
133
|
|
|
134
|
-
# Calculate DDA parameters with safety checks
|
|
134
|
+
# Calculate DDA parameters with safety checks to prevent division by zero
|
|
135
135
|
EPSILON = 1e-10 # Small value to prevent division by zero
|
|
136
136
|
|
|
137
|
+
# Calculate distances to next voxel boundaries and step sizes for X-axis
|
|
137
138
|
if abs(dx) > EPSILON:
|
|
138
139
|
t_max_x = ((i + (step_x > 0)) - x) / dx
|
|
139
140
|
t_delta_x = abs(1 / dx)
|
|
@@ -141,6 +142,7 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
141
142
|
t_max_x = np.inf
|
|
142
143
|
t_delta_x = np.inf
|
|
143
144
|
|
|
145
|
+
# Calculate distances to next voxel boundaries and step sizes for Y-axis
|
|
144
146
|
if abs(dy) > EPSILON:
|
|
145
147
|
t_max_y = ((j + (step_y > 0)) - y) / dy
|
|
146
148
|
t_delta_y = abs(1 / dy)
|
|
@@ -148,6 +150,7 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
148
150
|
t_max_y = np.inf
|
|
149
151
|
t_delta_y = np.inf
|
|
150
152
|
|
|
153
|
+
# Calculate distances to next voxel boundaries and step sizes for Z-axis
|
|
151
154
|
if abs(dz) > EPSILON:
|
|
152
155
|
t_max_z = ((k + (step_z > 0)) - z) / dz
|
|
153
156
|
t_delta_z = abs(1 / dz)
|
|
@@ -155,40 +158,39 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
155
158
|
t_max_z = np.inf
|
|
156
159
|
t_delta_z = np.inf
|
|
157
160
|
|
|
158
|
-
# Track cumulative values
|
|
161
|
+
# Track cumulative values for tree transmittance calculation
|
|
159
162
|
cumulative_transmittance = 1.0
|
|
160
163
|
cumulative_hit_contribution = 0.0
|
|
161
164
|
last_t = 0.0
|
|
162
165
|
|
|
163
|
-
# Main ray traversal loop
|
|
166
|
+
# Main ray traversal loop using DDA algorithm
|
|
164
167
|
while (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
|
|
165
168
|
voxel_value = voxel_data[i, j, k]
|
|
166
169
|
|
|
167
|
-
# Find next intersection
|
|
170
|
+
# Find next intersection point along the ray
|
|
168
171
|
t_next = min(t_max_x, t_max_y, t_max_z)
|
|
169
172
|
|
|
170
|
-
# Calculate segment length in current voxel
|
|
173
|
+
# Calculate segment length in current voxel (in real world units)
|
|
171
174
|
segment_length = (t_next - last_t) * meshsize
|
|
172
175
|
segment_length = max(0.0, segment_length)
|
|
173
176
|
|
|
174
|
-
# Handle tree voxels (value -2)
|
|
177
|
+
# Handle tree voxels (value -2) with Beer-Lambert law transmittance
|
|
175
178
|
if voxel_value == -2:
|
|
176
179
|
transmittance = calculate_transmittance(segment_length, tree_k, tree_lad)
|
|
177
180
|
cumulative_transmittance *= transmittance
|
|
178
181
|
|
|
179
|
-
#
|
|
180
|
-
# print(f"segment_length = {segment_length}, transmittance = {transmittance}, cumulative_transmittance = {cumulative_transmittance}")
|
|
181
|
-
|
|
182
|
-
# If transmittance becomes too low, consider it a hit
|
|
182
|
+
# If transmittance becomes too low, consider the ray blocked
|
|
183
183
|
if cumulative_transmittance < 0.01:
|
|
184
184
|
return True, cumulative_transmittance
|
|
185
185
|
|
|
186
|
-
# Check for hits with
|
|
186
|
+
# Check for hits with target objects based on inclusion/exclusion mode
|
|
187
187
|
if inclusion_mode:
|
|
188
|
+
# Inclusion mode: hit if voxel value is in the target set
|
|
188
189
|
for hv in hit_values:
|
|
189
190
|
if voxel_value == hv:
|
|
190
191
|
return True, cumulative_transmittance
|
|
191
192
|
else:
|
|
193
|
+
# Exclusion mode: hit if voxel value is NOT in the allowed set
|
|
192
194
|
in_set = False
|
|
193
195
|
for hv in hit_values:
|
|
194
196
|
if voxel_value == hv:
|
|
@@ -200,22 +202,27 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
200
202
|
# Update for next iteration
|
|
201
203
|
last_t = t_next
|
|
202
204
|
|
|
203
|
-
# Move to next voxel
|
|
205
|
+
# Move to next voxel using DDA step logic
|
|
204
206
|
if t_max_x < t_max_y:
|
|
205
207
|
if t_max_x < t_max_z:
|
|
208
|
+
# Step in X direction
|
|
206
209
|
t_max_x += t_delta_x
|
|
207
210
|
i += step_x
|
|
208
211
|
else:
|
|
212
|
+
# Step in Z direction
|
|
209
213
|
t_max_z += t_delta_z
|
|
210
214
|
k += step_z
|
|
211
215
|
else:
|
|
212
216
|
if t_max_y < t_max_z:
|
|
217
|
+
# Step in Y direction
|
|
213
218
|
t_max_y += t_delta_y
|
|
214
219
|
j += step_y
|
|
215
220
|
else:
|
|
221
|
+
# Step in Z direction
|
|
216
222
|
t_max_z += t_delta_z
|
|
217
223
|
k += step_z
|
|
218
224
|
|
|
225
|
+
# Ray exited the grid without hitting a target
|
|
219
226
|
return False, cumulative_transmittance
|
|
220
227
|
|
|
221
228
|
@njit
|
|
@@ -248,23 +255,28 @@ def compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values
|
|
|
248
255
|
total_rays = ray_directions.shape[0]
|
|
249
256
|
visibility_sum = 0.0
|
|
250
257
|
|
|
258
|
+
# Cast rays in all specified directions
|
|
251
259
|
for idx in range(total_rays):
|
|
252
260
|
direction = ray_directions[idx]
|
|
253
261
|
hit, value = trace_ray_generic(voxel_data, observer_location, direction,
|
|
254
262
|
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
255
263
|
|
|
264
|
+
# Accumulate visibility contributions based on mode
|
|
256
265
|
if inclusion_mode:
|
|
257
266
|
if hit:
|
|
267
|
+
# For trees in hit_values, use partial visibility based on transmittance
|
|
258
268
|
if -2 in hit_values:
|
|
259
|
-
#
|
|
269
|
+
# Use the hit contribution (1 - transmittance) for tree visibility
|
|
260
270
|
visibility_sum += value if value < 1.0 else 1.0
|
|
261
271
|
else:
|
|
272
|
+
# Full visibility for non-tree targets
|
|
262
273
|
visibility_sum += 1.0
|
|
263
274
|
else:
|
|
264
275
|
if not hit:
|
|
265
|
-
# For exclusion mode, use transmittance value directly
|
|
276
|
+
# For exclusion mode, use transmittance value directly as visibility
|
|
266
277
|
visibility_sum += value
|
|
267
278
|
|
|
279
|
+
# Return average visibility across all rays
|
|
268
280
|
return visibility_sum / total_rays
|
|
269
281
|
|
|
270
282
|
@njit(parallel=True)
|
|
@@ -298,28 +310,33 @@ def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_va
|
|
|
298
310
|
nx, ny, nz = voxel_data.shape
|
|
299
311
|
vi_map = np.full((nx, ny), np.nan)
|
|
300
312
|
|
|
313
|
+
# Process each horizontal position in parallel for efficiency
|
|
301
314
|
for x in prange(nx):
|
|
302
315
|
for y in range(ny):
|
|
303
316
|
found_observer = False
|
|
317
|
+
# Search from bottom to top for valid observer placement
|
|
304
318
|
for z in range(1, nz):
|
|
305
|
-
# Check for valid observer location
|
|
319
|
+
# Check for valid observer location: empty space above solid ground
|
|
306
320
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
307
|
-
# Skip invalid ground types
|
|
321
|
+
# Skip invalid ground types (water or negative values)
|
|
308
322
|
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
309
323
|
vi_map[x, y] = np.nan
|
|
310
324
|
found_observer = True
|
|
311
325
|
break
|
|
312
326
|
else:
|
|
313
|
-
# Place observer
|
|
327
|
+
# Place observer at specified height above ground level
|
|
314
328
|
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
329
|
+
# Compute view index for this location
|
|
315
330
|
vi_value = compute_vi_generic(observer_location, voxel_data, ray_directions,
|
|
316
331
|
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
317
332
|
vi_map[x, y] = vi_value
|
|
318
333
|
found_observer = True
|
|
319
334
|
break
|
|
335
|
+
# Mark locations where no valid observer position was found
|
|
320
336
|
if not found_observer:
|
|
321
337
|
vi_map[x, y] = np.nan
|
|
322
338
|
|
|
339
|
+
# Flip vertically to match display orientation
|
|
323
340
|
return np.flipud(vi_map)
|
|
324
341
|
|
|
325
342
|
def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_mode=True, **kwargs):
|
|
@@ -365,10 +382,10 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
365
382
|
Returns:
|
|
366
383
|
ndarray: 2D array of computed view index values.
|
|
367
384
|
"""
|
|
368
|
-
# Handle mode presets
|
|
385
|
+
# Handle predefined mode presets for common view indices
|
|
369
386
|
if mode == 'green':
|
|
370
387
|
# GVI defaults - detect vegetation and trees
|
|
371
|
-
hit_values = (-2, 2, 5, 7)
|
|
388
|
+
hit_values = (-2, 2, 5, 6, 7, 8)
|
|
372
389
|
inclusion_mode = True
|
|
373
390
|
elif mode == 'sky':
|
|
374
391
|
# SVI defaults - detect open sky
|
|
@@ -379,22 +396,25 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
379
396
|
if hit_values is None:
|
|
380
397
|
raise ValueError("For custom mode, you must provide hit_values.")
|
|
381
398
|
|
|
382
|
-
#
|
|
399
|
+
# Extract parameters from kwargs with sensible defaults
|
|
383
400
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
384
401
|
view_height_voxel = int(view_point_height / meshsize)
|
|
385
402
|
colormap = kwargs.get("colormap", 'viridis')
|
|
386
403
|
vmin = kwargs.get("vmin", 0.0)
|
|
387
404
|
vmax = kwargs.get("vmax", 1.0)
|
|
405
|
+
|
|
406
|
+
# Ray casting parameters for hemisphere sampling
|
|
388
407
|
N_azimuth = kwargs.get("N_azimuth", 60)
|
|
389
408
|
N_elevation = kwargs.get("N_elevation", 10)
|
|
390
409
|
elevation_min_degrees = kwargs.get("elevation_min_degrees", -30)
|
|
391
410
|
elevation_max_degrees = kwargs.get("elevation_max_degrees", 30)
|
|
392
411
|
|
|
393
|
-
# Tree transmittance parameters
|
|
412
|
+
# Tree transmittance parameters for Beer-Lambert law
|
|
394
413
|
tree_k = kwargs.get("tree_k", 0.5)
|
|
395
414
|
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
396
415
|
|
|
397
416
|
# Generate ray directions using spherical coordinates
|
|
417
|
+
# Create uniform sampling over specified azimuth and elevation ranges
|
|
398
418
|
azimuth_angles = np.linspace(0, 2 * np.pi, N_azimuth, endpoint=False)
|
|
399
419
|
elevation_angles = np.deg2rad(np.linspace(elevation_min_degrees, elevation_max_degrees, N_elevation))
|
|
400
420
|
|
|
@@ -403,6 +423,7 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
403
423
|
cos_elev = np.cos(elevation)
|
|
404
424
|
sin_elev = np.sin(elevation)
|
|
405
425
|
for azimuth in azimuth_angles:
|
|
426
|
+
# Convert spherical coordinates to Cartesian
|
|
406
427
|
dx = cos_elev * np.cos(azimuth)
|
|
407
428
|
dy = cos_elev * np.sin(azimuth)
|
|
408
429
|
dz = sin_elev
|
|
@@ -413,17 +434,17 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
413
434
|
vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel,
|
|
414
435
|
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
415
436
|
|
|
416
|
-
#
|
|
437
|
+
# Create visualization with custom colormap handling
|
|
417
438
|
import matplotlib.pyplot as plt
|
|
418
439
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
419
|
-
cmap.set_bad(color='lightgray')
|
|
440
|
+
cmap.set_bad(color='lightgray') # Color for NaN values (invalid locations)
|
|
420
441
|
plt.figure(figsize=(10, 8))
|
|
421
442
|
plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
422
443
|
plt.colorbar(label='View Index')
|
|
423
444
|
plt.axis('off')
|
|
424
445
|
plt.show()
|
|
425
446
|
|
|
426
|
-
# Optional OBJ export
|
|
447
|
+
# Optional OBJ export for 3D visualization
|
|
427
448
|
obj_export = kwargs.get("obj_export", False)
|
|
428
449
|
if obj_export:
|
|
429
450
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(vi_map))
|
|
@@ -450,28 +471,33 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
450
471
|
def mark_building_by_id(voxcity_grid_ori, building_id_grid_ori, ids, mark):
|
|
451
472
|
"""Mark specific buildings in the voxel grid with a given value.
|
|
452
473
|
|
|
453
|
-
|
|
454
|
-
|
|
474
|
+
This function is used to identify landmark buildings for visibility analysis
|
|
475
|
+
by replacing their voxel values with a special marker value. It handles
|
|
476
|
+
coordinate system alignment between the building ID grid and voxel grid.
|
|
455
477
|
|
|
456
478
|
Args:
|
|
457
|
-
|
|
458
|
-
building_id_grid_ori (ndarray): 2D array of building IDs
|
|
459
|
-
ids (list): List of building IDs to mark
|
|
460
|
-
mark (int): Value to mark the buildings with
|
|
461
|
-
"""
|
|
479
|
+
voxcity_grid_ori (ndarray): 3D array of voxel values (original, will be copied)
|
|
480
|
+
building_id_grid_ori (ndarray): 2D array of building IDs (original, will be copied)
|
|
481
|
+
ids (list): List of building IDs to mark as landmarks
|
|
482
|
+
mark (int): Value to mark the landmark buildings with (typically negative)
|
|
462
483
|
|
|
484
|
+
Returns:
|
|
485
|
+
ndarray: Modified 3D voxel grid with landmark buildings marked
|
|
486
|
+
"""
|
|
487
|
+
# Create working copies to avoid modifying original data
|
|
463
488
|
voxcity_grid = voxcity_grid_ori.copy()
|
|
464
489
|
|
|
465
490
|
# Flip building ID grid vertically to match voxel grid orientation
|
|
491
|
+
# This accounts for different coordinate system conventions
|
|
466
492
|
building_id_grid = np.flipud(building_id_grid_ori.copy())
|
|
467
493
|
|
|
468
|
-
#
|
|
494
|
+
# Find x,y positions where target building IDs are located
|
|
469
495
|
positions = np.where(np.isin(building_id_grid, ids))
|
|
470
496
|
|
|
471
|
-
#
|
|
497
|
+
# Process each location containing a target building
|
|
472
498
|
for i in range(len(positions[0])):
|
|
473
499
|
x, y = positions[0][i], positions[1][i]
|
|
474
|
-
#
|
|
500
|
+
# Find all building voxels (-3) at this x,y location and mark them
|
|
475
501
|
z_mask = voxcity_grid[x, y, :] == -3
|
|
476
502
|
voxcity_grid[x, y, z_mask] = mark
|
|
477
503
|
|
|
@@ -500,7 +526,7 @@ def trace_ray_to_target(voxel_data, origin, target, opaque_values):
|
|
|
500
526
|
dy = y1 - y0
|
|
501
527
|
dz = z1 - z0
|
|
502
528
|
|
|
503
|
-
# Normalize direction vector
|
|
529
|
+
# Normalize direction vector for consistent traversal
|
|
504
530
|
length = np.sqrt(dx*dx + dy*dy + dz*dz)
|
|
505
531
|
if length == 0.0:
|
|
506
532
|
return True # Origin and target are at the same location
|
|
@@ -518,7 +544,7 @@ def trace_ray_to_target(voxel_data, origin, target, opaque_values):
|
|
|
518
544
|
step_z = 1 if dz >= 0 else -1
|
|
519
545
|
|
|
520
546
|
# Calculate distances to next voxel boundaries and step sizes
|
|
521
|
-
# Handle cases where direction components are zero
|
|
547
|
+
# Handle cases where direction components are zero to avoid division by zero
|
|
522
548
|
if dx != 0:
|
|
523
549
|
t_max_x = ((i + (step_x > 0)) - x) / dx
|
|
524
550
|
t_delta_x = abs(1 / dx)
|
|
@@ -540,21 +566,22 @@ def trace_ray_to_target(voxel_data, origin, target, opaque_values):
|
|
|
540
566
|
t_max_z = np.inf
|
|
541
567
|
t_delta_z = np.inf
|
|
542
568
|
|
|
543
|
-
# Main ray traversal loop
|
|
569
|
+
# Main ray traversal loop using DDA algorithm
|
|
544
570
|
while True:
|
|
545
|
-
# Check if current voxel is within bounds and opaque
|
|
571
|
+
# Check if current voxel is within bounds and contains opaque material
|
|
546
572
|
if (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
|
|
547
573
|
voxel_value = voxel_data[i, j, k]
|
|
548
574
|
if voxel_value in opaque_values:
|
|
549
|
-
return False # Ray is blocked
|
|
575
|
+
return False # Ray is blocked by opaque voxel
|
|
550
576
|
else:
|
|
551
|
-
return False #
|
|
577
|
+
return False # Ray went out of bounds before reaching target
|
|
552
578
|
|
|
553
|
-
# Check if we've reached target voxel
|
|
579
|
+
# Check if we've reached the target voxel
|
|
554
580
|
if i == int(x1) and j == int(y1) and k == int(z1):
|
|
555
|
-
return True # Ray
|
|
581
|
+
return True # Ray successfully reached the target
|
|
556
582
|
|
|
557
583
|
# Move to next voxel using DDA algorithm
|
|
584
|
+
# Choose the axis with the smallest distance to next boundary
|
|
558
585
|
if t_max_x < t_max_y:
|
|
559
586
|
if t_max_x < t_max_z:
|
|
560
587
|
t_max = t_max_x
|
|
@@ -583,20 +610,21 @@ def compute_visibility_to_all_landmarks(observer_location, landmark_positions, v
|
|
|
583
610
|
|
|
584
611
|
Args:
|
|
585
612
|
observer_location (ndarray): Observer position (x,y,z) in voxel coordinates
|
|
586
|
-
landmark_positions (ndarray): Array of landmark positions
|
|
613
|
+
landmark_positions (ndarray): Array of landmark positions (n_landmarks, 3)
|
|
587
614
|
voxel_data (ndarray): 3D array of voxel values
|
|
588
615
|
opaque_values (ndarray): Array of voxel values that block visibility
|
|
589
616
|
|
|
590
617
|
Returns:
|
|
591
618
|
int: 1 if any landmark is visible, 0 if none are visible
|
|
592
619
|
"""
|
|
593
|
-
# Check visibility to each landmark
|
|
620
|
+
# Check visibility to each landmark sequentially
|
|
621
|
+
# Early exit strategy: return 1 as soon as any landmark is visible
|
|
594
622
|
for idx in range(landmark_positions.shape[0]):
|
|
595
623
|
target = landmark_positions[idx].astype(np.float64)
|
|
596
624
|
is_visible = trace_ray_to_target(voxel_data, observer_location, target, opaque_values)
|
|
597
625
|
if is_visible:
|
|
598
|
-
return 1 # Return
|
|
599
|
-
return 0 # No landmarks were visible
|
|
626
|
+
return 1 # Return immediately when first visible landmark is found
|
|
627
|
+
return 0 # No landmarks were visible from this location
|
|
600
628
|
|
|
601
629
|
@njit(parallel=True)
|
|
602
630
|
def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_height_voxel):
|
|
@@ -613,7 +641,7 @@ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_h
|
|
|
613
641
|
|
|
614
642
|
Args:
|
|
615
643
|
voxel_data (ndarray): 3D array of voxel values
|
|
616
|
-
landmark_positions (ndarray): Array of landmark positions
|
|
644
|
+
landmark_positions (ndarray): Array of landmark positions (n_landmarks, 3)
|
|
617
645
|
opaque_values (ndarray): Array of voxel values that block visibility
|
|
618
646
|
view_height_voxel (int): Height offset for observer in voxels
|
|
619
647
|
|
|
@@ -626,25 +654,28 @@ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_h
|
|
|
626
654
|
nx, ny, nz = voxel_data.shape
|
|
627
655
|
visibility_map = np.full((nx, ny), np.nan)
|
|
628
656
|
|
|
629
|
-
# Process each x,y position in parallel
|
|
657
|
+
# Process each x,y position in parallel for computational efficiency
|
|
630
658
|
for x in prange(nx):
|
|
631
659
|
for y in range(ny):
|
|
632
660
|
found_observer = False
|
|
633
|
-
# Find lowest
|
|
661
|
+
# Find the lowest valid observer location by searching from bottom up
|
|
634
662
|
for z in range(1, nz):
|
|
663
|
+
# Valid observer location: empty voxel above non-empty ground
|
|
635
664
|
if voxel_data[x, y, z] == 0 and voxel_data[x, y, z - 1] != 0:
|
|
636
|
-
# Skip
|
|
665
|
+
# Skip locations above building roofs or vegetation
|
|
637
666
|
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
638
667
|
visibility_map[x, y] = np.nan
|
|
639
668
|
found_observer = True
|
|
640
669
|
break
|
|
641
670
|
else:
|
|
642
|
-
# Place observer
|
|
671
|
+
# Place observer at specified height above ground level
|
|
643
672
|
observer_location = np.array([x, y, z+view_height_voxel], dtype=np.float64)
|
|
673
|
+
# Check visibility to any landmark from this location
|
|
644
674
|
visible = compute_visibility_to_all_landmarks(observer_location, landmark_positions, voxel_data, opaque_values)
|
|
645
675
|
visibility_map[x, y] = visible
|
|
646
676
|
found_observer = True
|
|
647
677
|
break
|
|
678
|
+
# Mark locations where no valid observer position exists
|
|
648
679
|
if not found_observer:
|
|
649
680
|
visibility_map[x, y] = np.nan
|
|
650
681
|
|
|
@@ -806,35 +837,55 @@ def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
|
|
|
806
837
|
"""
|
|
807
838
|
Compute and visualize the Sky View Factor (SVF) for each valid observer cell in the voxel grid.
|
|
808
839
|
|
|
840
|
+
Sky View Factor measures the proportion of the sky hemisphere that is visible from a given point.
|
|
841
|
+
It ranges from 0 (completely obstructed) to 1 (completely open sky). This implementation:
|
|
842
|
+
- Uses hemisphere ray casting to sample sky visibility
|
|
843
|
+
- Accounts for tree transmittance using Beer-Lambert law
|
|
844
|
+
- Places observers at valid street-level locations
|
|
845
|
+
- Provides optional visualization and OBJ export
|
|
846
|
+
|
|
809
847
|
Args:
|
|
810
848
|
voxel_data (ndarray): 3D array of voxel values.
|
|
811
849
|
meshsize (float): Size of each voxel in meters.
|
|
812
|
-
show_plot (bool): Whether to display the plot.
|
|
813
|
-
**kwargs: Additional parameters
|
|
850
|
+
show_plot (bool): Whether to display the SVF visualization plot.
|
|
851
|
+
**kwargs: Additional parameters including:
|
|
852
|
+
view_point_height (float): Observer height in meters (default: 1.5)
|
|
853
|
+
colormap (str): Matplotlib colormap name (default: 'BuPu_r')
|
|
854
|
+
vmin, vmax (float): Color scale limits (default: 0.0, 1.0)
|
|
855
|
+
N_azimuth (int): Number of azimuth angles for ray sampling (default: 60)
|
|
856
|
+
N_elevation (int): Number of elevation angles for ray sampling (default: 10)
|
|
857
|
+
elevation_min_degrees (float): Minimum elevation angle (default: 0)
|
|
858
|
+
elevation_max_degrees (float): Maximum elevation angle (default: 90)
|
|
859
|
+
tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
860
|
+
tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
861
|
+
obj_export (bool): Whether to export as OBJ file (default: False)
|
|
814
862
|
|
|
815
863
|
Returns:
|
|
816
|
-
ndarray: 2D array of SVF values at each
|
|
864
|
+
ndarray: 2D array of SVF values at each valid observer location (x, y).
|
|
865
|
+
NaN values indicate invalid observer positions.
|
|
817
866
|
"""
|
|
818
|
-
#
|
|
867
|
+
# Extract default parameters with sky-specific settings
|
|
819
868
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
820
869
|
view_height_voxel = int(view_point_height / meshsize)
|
|
821
|
-
colormap = kwargs.get("colormap", 'BuPu_r')
|
|
870
|
+
colormap = kwargs.get("colormap", 'BuPu_r') # Blue-purple colormap suitable for sky
|
|
822
871
|
vmin = kwargs.get("vmin", 0.0)
|
|
823
872
|
vmax = kwargs.get("vmax", 1.0)
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
873
|
+
|
|
874
|
+
# Ray sampling parameters optimized for sky view factor
|
|
875
|
+
N_azimuth = kwargs.get("N_azimuth", 60) # Full 360-degree azimuth sampling
|
|
876
|
+
N_elevation = kwargs.get("N_elevation", 10) # Hemisphere elevation sampling
|
|
877
|
+
elevation_min_degrees = kwargs.get("elevation_min_degrees", 0) # Horizon
|
|
878
|
+
elevation_max_degrees = kwargs.get("elevation_max_degrees", 90) # Zenith
|
|
828
879
|
|
|
829
|
-
#
|
|
830
|
-
tree_k = kwargs.get("tree_k", 0.6)
|
|
831
|
-
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
880
|
+
# Tree transmittance parameters for Beer-Lambert law
|
|
881
|
+
tree_k = kwargs.get("tree_k", 0.6) # Static extinction coefficient
|
|
882
|
+
tree_lad = kwargs.get("tree_lad", 1.0) # Leaf area density in m^-1
|
|
832
883
|
|
|
833
|
-
#
|
|
834
|
-
hit_values = (0,)
|
|
835
|
-
inclusion_mode = False
|
|
884
|
+
# Sky view factor configuration: detect open sky (value 0)
|
|
885
|
+
hit_values = (0,) # Sky voxels have value 0
|
|
886
|
+
inclusion_mode = False # Count rays that DON'T hit obstacles (exclusion mode)
|
|
836
887
|
|
|
837
|
-
# Generate ray directions over the
|
|
888
|
+
# Generate ray directions over the sky hemisphere (0 to 90 degrees elevation)
|
|
838
889
|
azimuth_angles = np.linspace(0, 2 * np.pi, N_azimuth, endpoint=False)
|
|
839
890
|
elevation_angles = np.deg2rad(np.linspace(elevation_min_degrees, elevation_max_degrees, N_elevation))
|
|
840
891
|
|
|
@@ -843,29 +894,29 @@ def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
|
|
|
843
894
|
cos_elev = np.cos(elevation)
|
|
844
895
|
sin_elev = np.sin(elevation)
|
|
845
896
|
for azimuth in azimuth_angles:
|
|
897
|
+
# Convert spherical to Cartesian coordinates
|
|
846
898
|
dx = cos_elev * np.cos(azimuth)
|
|
847
899
|
dy = cos_elev * np.sin(azimuth)
|
|
848
|
-
dz = sin_elev
|
|
900
|
+
dz = sin_elev # Always positive for sky hemisphere
|
|
849
901
|
ray_directions.append([dx, dy, dz])
|
|
850
902
|
ray_directions = np.array(ray_directions, dtype=np.float64)
|
|
851
903
|
|
|
852
|
-
# Compute the SVF map using the
|
|
904
|
+
# Compute the SVF map using the generic view index computation
|
|
853
905
|
vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel,
|
|
854
906
|
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
855
907
|
|
|
856
|
-
#
|
|
908
|
+
# Display visualization if requested
|
|
857
909
|
if show_plot:
|
|
858
910
|
import matplotlib.pyplot as plt
|
|
859
911
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
860
|
-
cmap.set_bad(color='lightgray')
|
|
912
|
+
cmap.set_bad(color='lightgray') # Gray for invalid observer locations
|
|
861
913
|
plt.figure(figsize=(10, 8))
|
|
862
|
-
# plt.title("Sky View Factor Map")
|
|
863
914
|
plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
864
915
|
plt.colorbar(label='Sky View Factor')
|
|
865
916
|
plt.axis('off')
|
|
866
917
|
plt.show()
|
|
867
918
|
|
|
868
|
-
# Optional OBJ export
|
|
919
|
+
# Optional OBJ export for 3D visualization
|
|
869
920
|
obj_export = kwargs.get("obj_export", False)
|
|
870
921
|
if obj_export:
|
|
871
922
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(vi_map))
|
|
@@ -889,341 +940,55 @@ def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
|
|
|
889
940
|
|
|
890
941
|
return vi_map
|
|
891
942
|
|
|
892
|
-
# def get_building_surface_svf(voxel_data, meshsize, **kwargs):
|
|
893
|
-
# """
|
|
894
|
-
# Compute and visualize the Sky View Factor (SVF) for building surface meshes.
|
|
895
|
-
|
|
896
|
-
# Args:
|
|
897
|
-
# voxel_data (ndarray): 3D array of voxel values.
|
|
898
|
-
# meshsize (float): Size of each voxel in meters.
|
|
899
|
-
# **kwargs: Additional parameters (colormap, ray counts, etc.)
|
|
900
|
-
|
|
901
|
-
# Returns:
|
|
902
|
-
# trimesh.Trimesh: Mesh of building surfaces with SVF values stored in metadata.
|
|
903
|
-
# """
|
|
904
|
-
# # Import required modules
|
|
905
|
-
# import trimesh
|
|
906
|
-
# import numpy as np
|
|
907
|
-
# import time
|
|
908
|
-
|
|
909
|
-
# # Default parameters
|
|
910
|
-
# colormap = kwargs.get("colormap", 'BuPu_r')
|
|
911
|
-
# vmin = kwargs.get("vmin", 0.0)
|
|
912
|
-
# vmax = kwargs.get("vmax", 1.0)
|
|
913
|
-
# N_azimuth = kwargs.get("N_azimuth", 60)
|
|
914
|
-
# N_elevation = kwargs.get("N_elevation", 10)
|
|
915
|
-
# debug = kwargs.get("debug", False)
|
|
916
|
-
# progress_report = kwargs.get("progress_report", False)
|
|
917
|
-
|
|
918
|
-
# # Tree transmittance parameters
|
|
919
|
-
# tree_k = kwargs.get("tree_k", 0.6)
|
|
920
|
-
# tree_lad = kwargs.get("tree_lad", 1.0)
|
|
921
|
-
|
|
922
|
-
# # Sky detection parameters
|
|
923
|
-
# hit_values = (0,) # Sky is typically represented by 0
|
|
924
|
-
# inclusion_mode = False # We want rays that DON'T hit obstacles
|
|
925
|
-
|
|
926
|
-
# # Extract building mesh (building voxels have value -3)
|
|
927
|
-
# building_class_id = kwargs.get("building_class_id", -3)
|
|
928
|
-
# start_time = time.time()
|
|
929
|
-
# # print(f"Extracting building mesh for class ID {building_class_id}...")
|
|
930
|
-
# try:
|
|
931
|
-
# building_mesh = create_voxel_mesh(voxel_data, building_class_id, meshsize)
|
|
932
|
-
# # print(f"Mesh extraction took {time.time() - start_time:.2f} seconds")
|
|
933
|
-
|
|
934
|
-
# if building_mesh is None or len(building_mesh.faces) == 0:
|
|
935
|
-
# print("No building surfaces found in voxel data.")
|
|
936
|
-
# return None
|
|
937
|
-
|
|
938
|
-
# # print(f"Successfully extracted mesh with {len(building_mesh.faces)} faces")
|
|
939
|
-
# except Exception as e:
|
|
940
|
-
# print(f"Error during mesh extraction: {e}")
|
|
941
|
-
# return None
|
|
942
|
-
|
|
943
|
-
# if progress_report:
|
|
944
|
-
# print(f"Processing SVF for {len(building_mesh.faces)} building faces...")
|
|
945
|
-
|
|
946
|
-
# try:
|
|
947
|
-
# # Calculate face centers and normals
|
|
948
|
-
# face_centers = building_mesh.triangles_center
|
|
949
|
-
# face_normals = building_mesh.face_normals
|
|
950
|
-
|
|
951
|
-
# # Initialize array to store SVF values for each face
|
|
952
|
-
# face_svf_values = np.zeros(len(building_mesh.faces))
|
|
953
|
-
|
|
954
|
-
# # Get voxel grid dimensions
|
|
955
|
-
# grid_shape = voxel_data.shape
|
|
956
|
-
# grid_bounds = np.array([
|
|
957
|
-
# [0, 0, 0], # Min bounds in voxel coordinates
|
|
958
|
-
# [grid_shape[0], grid_shape[1], grid_shape[2]] # Max bounds
|
|
959
|
-
# ])
|
|
960
|
-
|
|
961
|
-
# # Convert bounds to real-world coordinates
|
|
962
|
-
# grid_bounds_real = grid_bounds * meshsize
|
|
963
|
-
|
|
964
|
-
# # Small epsilon to detect boundary faces (within 0.5 voxel of boundary)
|
|
965
|
-
# boundary_epsilon = meshsize * 0.05
|
|
966
|
-
|
|
967
|
-
# # Create hemisphere directions for ray casting
|
|
968
|
-
# hemisphere_dirs = []
|
|
969
|
-
# azimuth_angles = np.linspace(0, 2 * np.pi, N_azimuth, endpoint=False)
|
|
970
|
-
# elevation_angles = np.linspace(0, np.pi/2, N_elevation) # 0 to 90 degrees
|
|
971
|
-
|
|
972
|
-
# for elevation in elevation_angles:
|
|
973
|
-
# sin_elev = np.sin(elevation)
|
|
974
|
-
# cos_elev = np.cos(elevation)
|
|
975
|
-
# for azimuth in azimuth_angles:
|
|
976
|
-
# x = cos_elev * np.cos(azimuth)
|
|
977
|
-
# y = cos_elev * np.sin(azimuth)
|
|
978
|
-
# z = sin_elev
|
|
979
|
-
# hemisphere_dirs.append([x, y, z])
|
|
980
|
-
|
|
981
|
-
# hemisphere_dirs = np.array(hemisphere_dirs)
|
|
982
|
-
|
|
983
|
-
# # Process each face
|
|
984
|
-
# from scipy.spatial.transform import Rotation
|
|
985
|
-
# processed_count = 0
|
|
986
|
-
# boundary_count = 0
|
|
987
|
-
# nan_boundary_count = 0
|
|
988
|
-
|
|
989
|
-
# start_time = time.time()
|
|
990
|
-
# for face_idx in range(len(building_mesh.faces)):
|
|
991
|
-
# try:
|
|
992
|
-
# center = face_centers[face_idx]
|
|
993
|
-
# normal = face_normals[face_idx]
|
|
994
|
-
|
|
995
|
-
# # Check if this is a vertical surface (normal has no Z component)
|
|
996
|
-
# is_vertical = abs(normal[2]) < 0.01
|
|
997
|
-
|
|
998
|
-
# # Check if this face is on the boundary of the voxel grid
|
|
999
|
-
# on_x_min = abs(center[0] - grid_bounds_real[0, 0]) < boundary_epsilon
|
|
1000
|
-
# on_y_min = abs(center[1] - grid_bounds_real[0, 1]) < boundary_epsilon
|
|
1001
|
-
# on_x_max = abs(center[0] - grid_bounds_real[1, 0]) < boundary_epsilon
|
|
1002
|
-
# on_y_max = abs(center[1] - grid_bounds_real[1, 1]) < boundary_epsilon
|
|
1003
|
-
|
|
1004
|
-
# # Check if this is a vertical surface on the boundary
|
|
1005
|
-
# is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
|
|
1006
|
-
|
|
1007
|
-
# # Set NaN for all vertical surfaces on domain boundaries
|
|
1008
|
-
# if is_boundary_vertical:
|
|
1009
|
-
# face_svf_values[face_idx] = np.nan
|
|
1010
|
-
# nan_boundary_count += 1
|
|
1011
|
-
# processed_count += 1
|
|
1012
|
-
# continue
|
|
1013
|
-
|
|
1014
|
-
# # For non-boundary surfaces, proceed with normal SVF calculation
|
|
1015
|
-
# # Convert center to voxel coordinates (for ray origin)
|
|
1016
|
-
# center_voxel = center / meshsize
|
|
1017
|
-
|
|
1018
|
-
# # IMPORTANT: Offset ray origin slightly to avoid self-intersection
|
|
1019
|
-
# ray_origin = center_voxel + normal * 0.1 # Offset by 0.1 voxel units in normal direction
|
|
1020
|
-
|
|
1021
|
-
# # Create rotation from z-axis to face normal
|
|
1022
|
-
# z_axis = np.array([0, 0, 1])
|
|
1023
|
-
|
|
1024
|
-
# # Handle special case where normal is parallel to z-axis
|
|
1025
|
-
# if np.isclose(np.abs(np.dot(normal, z_axis)), 1.0, atol=1e-6):
|
|
1026
|
-
# if np.dot(normal, z_axis) > 0: # Normal points up
|
|
1027
|
-
# rotation_matrix = np.eye(3) # Identity matrix
|
|
1028
|
-
# else: # Normal points down
|
|
1029
|
-
# rotation_matrix = np.array([
|
|
1030
|
-
# [1, 0, 0],
|
|
1031
|
-
# [0, -1, 0],
|
|
1032
|
-
# [0, 0, -1]
|
|
1033
|
-
# ])
|
|
1034
|
-
# rotation = Rotation.from_matrix(rotation_matrix)
|
|
1035
|
-
# else:
|
|
1036
|
-
# # For all other cases, find rotation that aligns z-axis with normal
|
|
1037
|
-
# rotation_axis = np.cross(z_axis, normal)
|
|
1038
|
-
# rotation_axis = rotation_axis / np.linalg.norm(rotation_axis)
|
|
1039
|
-
# angle = np.arccos(np.clip(np.dot(z_axis, normal), -1.0, 1.0))
|
|
1040
|
-
# rotation = Rotation.from_rotvec(rotation_axis * angle)
|
|
1041
|
-
|
|
1042
|
-
# # Transform hemisphere directions to align with face normal
|
|
1043
|
-
# local_dirs = rotation.apply(hemisphere_dirs)
|
|
1044
|
-
|
|
1045
|
-
# # Filter directions - keep only those that:
|
|
1046
|
-
# # 1. Are pointing outward from the face (dot product with normal > 0)
|
|
1047
|
-
# # 2. Have a positive z component (upward in world space)
|
|
1048
|
-
# valid_dirs = []
|
|
1049
|
-
# total_dirs = 0
|
|
1050
|
-
|
|
1051
|
-
# # Count total directions in the hemisphere (for normalization)
|
|
1052
|
-
# for dir_vector in local_dirs:
|
|
1053
|
-
# dot_product = np.dot(dir_vector, normal)
|
|
1054
|
-
# # Count this direction if it's pointing outward from the face
|
|
1055
|
-
# if dot_product > 0.01: # Small threshold to avoid precision issues
|
|
1056
|
-
# total_dirs += 1
|
|
1057
|
-
# # Only trace rays that have a positive z component (can reach sky)
|
|
1058
|
-
# if dir_vector[2] > 0:
|
|
1059
|
-
# valid_dirs.append(dir_vector)
|
|
1060
|
-
|
|
1061
|
-
# # If no valid directions, SVF is 0
|
|
1062
|
-
# if total_dirs == 0:
|
|
1063
|
-
# face_svf_values[face_idx] = 0
|
|
1064
|
-
# continue
|
|
1065
|
-
|
|
1066
|
-
# # If no upward directions, SVF is 0 (all rays are blocked by ground)
|
|
1067
|
-
# if len(valid_dirs) == 0:
|
|
1068
|
-
# face_svf_values[face_idx] = 0
|
|
1069
|
-
# continue
|
|
1070
|
-
|
|
1071
|
-
# # Convert to numpy array for compute_vi_generic
|
|
1072
|
-
# valid_dirs = np.array(valid_dirs, dtype=np.float64)
|
|
1073
|
-
|
|
1074
|
-
# # Calculate SVF using compute_vi_generic for the upward rays
|
|
1075
|
-
# # Then scale by the fraction of upward rays to total rays
|
|
1076
|
-
# upward_svf = compute_vi_generic(
|
|
1077
|
-
# ray_origin,
|
|
1078
|
-
# voxel_data,
|
|
1079
|
-
# valid_dirs,
|
|
1080
|
-
# hit_values,
|
|
1081
|
-
# meshsize,
|
|
1082
|
-
# tree_k,
|
|
1083
|
-
# tree_lad,
|
|
1084
|
-
# inclusion_mode
|
|
1085
|
-
# )
|
|
1086
|
-
|
|
1087
|
-
# # Scale SVF by the fraction of rays that could potentially reach the sky
|
|
1088
|
-
# # This accounts for downward rays that always have 0 SVF
|
|
1089
|
-
# face_svf_values[face_idx] = upward_svf * (len(valid_dirs) / total_dirs)
|
|
1090
|
-
|
|
1091
|
-
# except Exception as e:
|
|
1092
|
-
# print(f"Error processing face {face_idx}: {e}")
|
|
1093
|
-
# face_svf_values[face_idx] = 0
|
|
1094
|
-
|
|
1095
|
-
# # Progress reporting
|
|
1096
|
-
# processed_count += 1
|
|
1097
|
-
# if progress_report:
|
|
1098
|
-
# # Calculate frequency based on total number of faces, aiming for ~10 progress updates
|
|
1099
|
-
# progress_frequency = max(1, len(building_mesh.faces) // 10)
|
|
1100
|
-
# if processed_count % progress_frequency == 0 or processed_count == len(building_mesh.faces):
|
|
1101
|
-
# elapsed = time.time() - start_time
|
|
1102
|
-
# faces_per_second = processed_count / elapsed
|
|
1103
|
-
# remaining = (len(building_mesh.faces) - processed_count) / faces_per_second if processed_count < len(building_mesh.faces) else 0
|
|
1104
|
-
# print(f"Processed {processed_count}/{len(building_mesh.faces)} faces "
|
|
1105
|
-
# f"({processed_count/len(building_mesh.faces)*100:.1f}%) - "
|
|
1106
|
-
# f"{faces_per_second:.1f} faces/sec - "
|
|
1107
|
-
# f"Est. remaining: {remaining:.1f} sec")
|
|
1108
|
-
|
|
1109
|
-
# # print(f"Identified {nan_boundary_count} faces on domain vertical boundaries (set to NaN)")
|
|
1110
|
-
|
|
1111
|
-
# # Store SVF values directly in mesh metadata
|
|
1112
|
-
# if not hasattr(building_mesh, 'metadata'):
|
|
1113
|
-
# building_mesh.metadata = {}
|
|
1114
|
-
# building_mesh.metadata['svf_values'] = face_svf_values
|
|
1115
|
-
|
|
1116
|
-
# # Apply colors to the mesh based on SVF values (only for visualization)
|
|
1117
|
-
# if show_plot:
|
|
1118
|
-
# import matplotlib.cm as cm
|
|
1119
|
-
# import matplotlib.colors as mcolors
|
|
1120
|
-
# cmap = cm.get_cmap(colormap)
|
|
1121
|
-
# norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
1122
|
-
|
|
1123
|
-
# # Get a copy of face_svf_values with NaN replaced by a specific value outside the range
|
|
1124
|
-
# # This ensures NaN faces get a distinct color in the visualization
|
|
1125
|
-
# vis_values = face_svf_values.copy()
|
|
1126
|
-
# nan_mask = np.isnan(vis_values)
|
|
1127
|
-
# if np.any(nan_mask):
|
|
1128
|
-
# # Use a color below vmin for NaN values (they'll be clipped to vmin in the colormap)
|
|
1129
|
-
# # But we can see them as the minimum color
|
|
1130
|
-
# vis_values[nan_mask] = vmin - 0.1
|
|
1131
|
-
|
|
1132
|
-
# # Apply colors
|
|
1133
|
-
# face_colors = cmap(norm(vis_values))
|
|
1134
|
-
# building_mesh.visual.face_colors = face_colors
|
|
1135
|
-
|
|
1136
|
-
# # Create a scene with the colored mesh
|
|
1137
|
-
# scene = trimesh.Scene()
|
|
1138
|
-
# scene.add_geometry(building_mesh)
|
|
1139
|
-
# scene.show()
|
|
1140
|
-
|
|
1141
|
-
# # Also create a matplotlib figure with colorbar for reference
|
|
1142
|
-
# import matplotlib.pyplot as plt
|
|
1143
|
-
|
|
1144
|
-
# fig, ax = plt.subplots(figsize=(8, 3))
|
|
1145
|
-
# cb = plt.colorbar(
|
|
1146
|
-
# cm.ScalarMappable(norm=norm, cmap=cmap),
|
|
1147
|
-
# ax=ax,
|
|
1148
|
-
# orientation='horizontal',
|
|
1149
|
-
# label='Sky View Factor'
|
|
1150
|
-
# )
|
|
1151
|
-
# ax.remove() # Remove the axes, keep only colorbar
|
|
1152
|
-
# plt.tight_layout()
|
|
1153
|
-
# plt.show()
|
|
1154
|
-
|
|
1155
|
-
# # Plot histogram of SVF values (excluding NaN)
|
|
1156
|
-
# valid_svf = face_svf_values[~np.isnan(face_svf_values)]
|
|
1157
|
-
# plt.figure(figsize=(10, 6))
|
|
1158
|
-
# plt.hist(valid_svf, bins=50, color='skyblue', alpha=0.7)
|
|
1159
|
-
# plt.title('Distribution of Sky View Factor on Building Surfaces')
|
|
1160
|
-
# plt.xlabel('Sky View Factor')
|
|
1161
|
-
# plt.ylabel('Frequency')
|
|
1162
|
-
# plt.grid(True, alpha=0.3)
|
|
1163
|
-
# plt.tight_layout()
|
|
1164
|
-
# plt.show()
|
|
1165
|
-
|
|
1166
|
-
# # Handle optional OBJ export
|
|
1167
|
-
# obj_export = kwargs.get("obj_export", False)
|
|
1168
|
-
# if obj_export:
|
|
1169
|
-
# output_dir = kwargs.get("output_directory", "output")
|
|
1170
|
-
# output_file_name = kwargs.get("output_file_name", "building_surface_svf")
|
|
1171
|
-
|
|
1172
|
-
# # Ensure output directory exists
|
|
1173
|
-
# import os
|
|
1174
|
-
# os.makedirs(output_dir, exist_ok=True)
|
|
1175
|
-
|
|
1176
|
-
# # Export as OBJ with face colors
|
|
1177
|
-
# try:
|
|
1178
|
-
# building_mesh.export(f"{output_dir}/{output_file_name}.obj")
|
|
1179
|
-
# print(f"Exported building SVF mesh to {output_dir}/{output_file_name}.obj")
|
|
1180
|
-
# except Exception as e:
|
|
1181
|
-
# print(f"Error exporting mesh: {e}")
|
|
1182
|
-
|
|
1183
|
-
# return building_mesh
|
|
1184
|
-
|
|
1185
|
-
# except Exception as e:
|
|
1186
|
-
# print(f"Error during SVF calculation: {e}")
|
|
1187
|
-
# import traceback
|
|
1188
|
-
# traceback.print_exc()
|
|
1189
|
-
# return None
|
|
1190
|
-
|
|
1191
|
-
##############################################################################
|
|
1192
|
-
# 1) New Numba helper: Rodrigues’ rotation formula for rotating vectors
|
|
1193
|
-
##############################################################################
|
|
1194
943
|
@njit
|
|
1195
944
|
def rotate_vector_axis_angle(vec, axis, angle):
|
|
1196
945
|
"""
|
|
1197
|
-
Rotate a 3D vector
|
|
1198
|
-
|
|
946
|
+
Rotate a 3D vector around an arbitrary axis using Rodrigues' rotation formula.
|
|
947
|
+
|
|
948
|
+
This function implements the Rodrigues rotation formula:
|
|
949
|
+
v_rot = v*cos(θ) + (k × v)*sin(θ) + k*(k·v)*(1-cos(θ))
|
|
950
|
+
where k is the unit rotation axis, θ is the rotation angle, and v is the input vector.
|
|
951
|
+
|
|
952
|
+
Args:
|
|
953
|
+
vec (ndarray): 3D vector to rotate [x, y, z]
|
|
954
|
+
axis (ndarray): 3D rotation axis vector [x, y, z] (will be normalized)
|
|
955
|
+
angle (float): Rotation angle in radians
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
ndarray: Rotated 3D vector [x, y, z]
|
|
1199
959
|
"""
|
|
1200
|
-
# Normalize rotation axis
|
|
960
|
+
# Normalize rotation axis to unit length
|
|
1201
961
|
axis_len = np.sqrt(axis[0]**2 + axis[1]**2 + axis[2]**2)
|
|
1202
962
|
if axis_len < 1e-12:
|
|
1203
|
-
#
|
|
963
|
+
# Degenerate axis case: return original vector unchanged
|
|
1204
964
|
return vec
|
|
1205
965
|
|
|
1206
966
|
ux, uy, uz = axis / axis_len
|
|
1207
967
|
c = np.cos(angle)
|
|
1208
968
|
s = np.sin(angle)
|
|
969
|
+
|
|
970
|
+
# Calculate dot product: k·v
|
|
1209
971
|
dot = vec[0]*ux + vec[1]*uy + vec[2]*uz
|
|
1210
972
|
|
|
1211
|
-
# cross
|
|
973
|
+
# Calculate cross product: k × v
|
|
1212
974
|
cross_x = uy*vec[2] - uz*vec[1]
|
|
1213
975
|
cross_y = uz*vec[0] - ux*vec[2]
|
|
1214
976
|
cross_z = ux*vec[1] - uy*vec[0]
|
|
1215
977
|
|
|
1216
|
-
# Rodrigues formula: v_rot = v*c + (k
|
|
978
|
+
# Apply Rodrigues formula: v_rot = v*c + (k × v)*s + k*(k·v)*(1-c)
|
|
1217
979
|
v_rot = np.zeros(3, dtype=np.float64)
|
|
1218
|
-
|
|
980
|
+
|
|
981
|
+
# First term: v*cos(θ)
|
|
1219
982
|
v_rot[0] = vec[0] * c
|
|
1220
983
|
v_rot[1] = vec[1] * c
|
|
1221
984
|
v_rot[2] = vec[2] * c
|
|
1222
|
-
|
|
985
|
+
|
|
986
|
+
# Second term: (k × v)*sin(θ)
|
|
1223
987
|
v_rot[0] += cross_x * s
|
|
1224
988
|
v_rot[1] += cross_y * s
|
|
1225
989
|
v_rot[2] += cross_z * s
|
|
1226
|
-
|
|
990
|
+
|
|
991
|
+
# Third term: k*(k·v)*(1-cos(θ))
|
|
1227
992
|
tmp = dot * (1.0 - c)
|
|
1228
993
|
v_rot[0] += ux * tmp
|
|
1229
994
|
v_rot[1] += uy * tmp
|
|
@@ -1231,262 +996,6 @@ def rotate_vector_axis_angle(vec, axis, angle):
|
|
|
1231
996
|
|
|
1232
997
|
return v_rot
|
|
1233
998
|
|
|
1234
|
-
|
|
1235
|
-
# ##############################################################################
|
|
1236
|
-
# # 2) New Numba helper: vectorized SVF computation for each face
|
|
1237
|
-
# ##############################################################################
|
|
1238
|
-
# @njit
|
|
1239
|
-
# def compute_svf_for_all_faces(
|
|
1240
|
-
# face_centers,
|
|
1241
|
-
# face_normals,
|
|
1242
|
-
# hemisphere_dirs,
|
|
1243
|
-
# voxel_data,
|
|
1244
|
-
# meshsize,
|
|
1245
|
-
# tree_k,
|
|
1246
|
-
# tree_lad,
|
|
1247
|
-
# hit_values,
|
|
1248
|
-
# inclusion_mode,
|
|
1249
|
-
# grid_bounds_real,
|
|
1250
|
-
# boundary_epsilon
|
|
1251
|
-
# ):
|
|
1252
|
-
# """
|
|
1253
|
-
# Per-face SVF calculation in Numba:
|
|
1254
|
-
# - Checks boundary conditions & sets NaN for boundary-vertical faces
|
|
1255
|
-
# - Builds local hemisphere (rotates from +Z to face normal)
|
|
1256
|
-
# - Filters directions that actually face outward (+ dot>0) and have z>0
|
|
1257
|
-
# - Calls compute_vi_generic to get fraction that sees sky
|
|
1258
|
-
# - Returns array of SVF values (same length as face_centers)
|
|
1259
|
-
# """
|
|
1260
|
-
# n_faces = face_centers.shape[0]
|
|
1261
|
-
# face_svf_values = np.zeros(n_faces, dtype=np.float64)
|
|
1262
|
-
|
|
1263
|
-
# z_axis = np.array([0.0, 0.0, 1.0])
|
|
1264
|
-
|
|
1265
|
-
# for fidx in range(n_faces):
|
|
1266
|
-
# center = face_centers[fidx]
|
|
1267
|
-
# normal = face_normals[fidx]
|
|
1268
|
-
|
|
1269
|
-
# # -- 1) Check for boundary + vertical face => NaN
|
|
1270
|
-
# is_vertical = (abs(normal[2]) < 0.01)
|
|
1271
|
-
|
|
1272
|
-
# on_x_min = (abs(center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
|
|
1273
|
-
# on_y_min = (abs(center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
|
|
1274
|
-
# on_x_max = (abs(center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
|
|
1275
|
-
# on_y_max = (abs(center[1] - grid_bounds_real[1,1]) < boundary_epsilon)
|
|
1276
|
-
|
|
1277
|
-
# is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
|
|
1278
|
-
# if is_boundary_vertical:
|
|
1279
|
-
# face_svf_values[fidx] = np.nan
|
|
1280
|
-
# continue
|
|
1281
|
-
|
|
1282
|
-
# # -- 2) Compute rotation that aligns face normal -> +Z
|
|
1283
|
-
# norm_n = np.sqrt(normal[0]**2 + normal[1]**2 + normal[2]**2)
|
|
1284
|
-
# if norm_n < 1e-12:
|
|
1285
|
-
# # Degenerate normal
|
|
1286
|
-
# face_svf_values[fidx] = 0.0
|
|
1287
|
-
# continue
|
|
1288
|
-
|
|
1289
|
-
# dot_zn = z_axis[0]*normal[0] + z_axis[1]*normal[1] + z_axis[2]*normal[2]
|
|
1290
|
-
# cos_angle = dot_zn / (norm_n)
|
|
1291
|
-
# if cos_angle > 1.0: cos_angle = 1.0
|
|
1292
|
-
# if cos_angle < -1.0: cos_angle = -1.0
|
|
1293
|
-
# angle = np.arccos(cos_angle)
|
|
1294
|
-
|
|
1295
|
-
# # Distinguish near +Z vs near -Z vs general case
|
|
1296
|
-
# if abs(cos_angle - 1.0) < 1e-9:
|
|
1297
|
-
# # normal ~ +Z => no rotation
|
|
1298
|
-
# local_dirs = hemisphere_dirs
|
|
1299
|
-
# elif abs(cos_angle + 1.0) < 1e-9:
|
|
1300
|
-
# # normal ~ -Z => rotate 180 around X (or Y) axis
|
|
1301
|
-
# axis_180 = np.array([1.0, 0.0, 0.0])
|
|
1302
|
-
# local_dirs = np.empty_like(hemisphere_dirs)
|
|
1303
|
-
# for i in range(hemisphere_dirs.shape[0]):
|
|
1304
|
-
# local_dirs[i] = rotate_vector_axis_angle(hemisphere_dirs[i], axis_180, np.pi)
|
|
1305
|
-
# else:
|
|
1306
|
-
# # normal is neither up nor down -> do standard axis-angle
|
|
1307
|
-
# axis_x = z_axis[1]*normal[2] - z_axis[2]*normal[1]
|
|
1308
|
-
# axis_y = z_axis[2]*normal[0] - z_axis[0]*normal[2]
|
|
1309
|
-
# axis_z = z_axis[0]*normal[1] - z_axis[1]*normal[0]
|
|
1310
|
-
# rot_axis = np.array([axis_x, axis_y, axis_z], dtype=np.float64)
|
|
1311
|
-
|
|
1312
|
-
# local_dirs = np.empty_like(hemisphere_dirs)
|
|
1313
|
-
# for i in range(hemisphere_dirs.shape[0]):
|
|
1314
|
-
# local_dirs[i] = rotate_vector_axis_angle(
|
|
1315
|
-
# hemisphere_dirs[i],
|
|
1316
|
-
# rot_axis,
|
|
1317
|
-
# angle
|
|
1318
|
-
# )
|
|
1319
|
-
|
|
1320
|
-
# # -- 3) Count how many directions are outward & upward
|
|
1321
|
-
# total_outward = 0
|
|
1322
|
-
# num_upward = 0
|
|
1323
|
-
# for i in range(local_dirs.shape[0]):
|
|
1324
|
-
# dvec = local_dirs[i]
|
|
1325
|
-
# dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
|
|
1326
|
-
# if dp > 0.0:
|
|
1327
|
-
# total_outward += 1
|
|
1328
|
-
# if dvec[2] > 0.0:
|
|
1329
|
-
# num_upward += 1
|
|
1330
|
-
|
|
1331
|
-
# # If no outward directions at all => SVF=0
|
|
1332
|
-
# if total_outward == 0:
|
|
1333
|
-
# face_svf_values[fidx] = 0.0
|
|
1334
|
-
# continue
|
|
1335
|
-
|
|
1336
|
-
# # If no upward directions among them => SVF=0
|
|
1337
|
-
# if num_upward == 0:
|
|
1338
|
-
# face_svf_values[fidx] = 0.0
|
|
1339
|
-
# continue
|
|
1340
|
-
|
|
1341
|
-
# # -- 4) Create an array for only the upward directions
|
|
1342
|
-
# valid_dirs_arr = np.empty((num_upward, 3), dtype=np.float64)
|
|
1343
|
-
# out_idx = 0
|
|
1344
|
-
# for i in range(local_dirs.shape[0]):
|
|
1345
|
-
# dvec = local_dirs[i]
|
|
1346
|
-
# dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
|
|
1347
|
-
# if dp > 0.0 and dvec[2] > 0.0:
|
|
1348
|
-
# valid_dirs_arr[out_idx, 0] = dvec[0]
|
|
1349
|
-
# valid_dirs_arr[out_idx, 1] = dvec[1]
|
|
1350
|
-
# valid_dirs_arr[out_idx, 2] = dvec[2]
|
|
1351
|
-
# out_idx += 1
|
|
1352
|
-
|
|
1353
|
-
# # -- 5) Ray origin in voxel coords, offset along face normal
|
|
1354
|
-
# offset_vox = 0.1
|
|
1355
|
-
# ray_origin = (center / meshsize) + (normal / norm_n) * offset_vox
|
|
1356
|
-
|
|
1357
|
-
# # -- 6) Compute fraction of rays that see sky
|
|
1358
|
-
# upward_svf = compute_vi_generic(
|
|
1359
|
-
# ray_origin,
|
|
1360
|
-
# voxel_data,
|
|
1361
|
-
# valid_dirs_arr,
|
|
1362
|
-
# hit_values,
|
|
1363
|
-
# meshsize,
|
|
1364
|
-
# tree_k,
|
|
1365
|
-
# tree_lad,
|
|
1366
|
-
# inclusion_mode
|
|
1367
|
-
# )
|
|
1368
|
-
|
|
1369
|
-
# # Scale by fraction of directions that were outward
|
|
1370
|
-
# fraction_up = num_upward / total_outward
|
|
1371
|
-
# face_svf_values[fidx] = upward_svf * fraction_up
|
|
1372
|
-
|
|
1373
|
-
# return face_svf_values
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
# ##############################################################################
|
|
1377
|
-
# # 3) Modified get_building_surface_svf (only numeric loop changed)
|
|
1378
|
-
# ##############################################################################
|
|
1379
|
-
# def get_building_surface_svf(voxel_data, meshsize, **kwargs):
|
|
1380
|
-
# """
|
|
1381
|
-
# Compute and visualize the Sky View Factor (SVF) for building surface meshes.
|
|
1382
|
-
|
|
1383
|
-
# Args:
|
|
1384
|
-
# voxel_data (ndarray): 3D array of voxel values.
|
|
1385
|
-
# meshsize (float): Size of each voxel in meters.
|
|
1386
|
-
# **kwargs: Additional parameters (colormap, ray counts, etc.)
|
|
1387
|
-
|
|
1388
|
-
# Returns:
|
|
1389
|
-
# trimesh.Trimesh: Mesh of building surfaces with SVF values stored in metadata.
|
|
1390
|
-
# """
|
|
1391
|
-
# import matplotlib.pyplot as plt
|
|
1392
|
-
# import matplotlib.cm as cm
|
|
1393
|
-
# import matplotlib.colors as mcolors
|
|
1394
|
-
# import os
|
|
1395
|
-
|
|
1396
|
-
# # Default parameters
|
|
1397
|
-
# colormap = kwargs.get("colormap", 'BuPu_r')
|
|
1398
|
-
# vmin = kwargs.get("vmin", 0.0)
|
|
1399
|
-
# vmax = kwargs.get("vmax", 1.0)
|
|
1400
|
-
# N_azimuth = kwargs.get("N_azimuth", 60)
|
|
1401
|
-
# N_elevation = kwargs.get("N_elevation", 10)
|
|
1402
|
-
# debug = kwargs.get("debug", False)
|
|
1403
|
-
# progress_report = kwargs.get("progress_report", False)
|
|
1404
|
-
# building_id_grid = kwargs.get("building_id_grid", None)
|
|
1405
|
-
|
|
1406
|
-
# # Tree parameters
|
|
1407
|
-
# tree_k = kwargs.get("tree_k", 0.6)
|
|
1408
|
-
# tree_lad = kwargs.get("tree_lad", 1.0)
|
|
1409
|
-
|
|
1410
|
-
# # Sky detection parameters
|
|
1411
|
-
# hit_values = (0,) # '0' is sky
|
|
1412
|
-
# inclusion_mode = False # we want rays that DON'T hit obstacles (except sky)
|
|
1413
|
-
|
|
1414
|
-
# # Building ID in voxel data
|
|
1415
|
-
# building_class_id = kwargs.get("building_class_id", -3)
|
|
1416
|
-
|
|
1417
|
-
# start_time = time.time()
|
|
1418
|
-
# # 1) Extract building mesh from voxel_data
|
|
1419
|
-
# try:
|
|
1420
|
-
# # This function is presumably in your codebase (not shown):
|
|
1421
|
-
# building_mesh = create_voxel_mesh(voxel_data, building_class_id, meshsize, building_id_grid=building_id_grid)
|
|
1422
|
-
# if building_mesh is None or len(building_mesh.faces) == 0:
|
|
1423
|
-
# print("No building surfaces found in voxel data.")
|
|
1424
|
-
# return None
|
|
1425
|
-
# except Exception as e:
|
|
1426
|
-
# print(f"Error during mesh extraction: {e}")
|
|
1427
|
-
# return None
|
|
1428
|
-
|
|
1429
|
-
# if progress_report:
|
|
1430
|
-
# print(f"Processing SVF for {len(building_mesh.faces)} building faces...")
|
|
1431
|
-
|
|
1432
|
-
# # 2) Get face centers + normals as NumPy arrays
|
|
1433
|
-
# face_centers = building_mesh.triangles_center
|
|
1434
|
-
# face_normals = building_mesh.face_normals
|
|
1435
|
-
|
|
1436
|
-
# # 3) Precompute hemisphere directions (global, pointing up)
|
|
1437
|
-
# azimuth_angles = np.linspace(0, 2*np.pi, N_azimuth, endpoint=False)
|
|
1438
|
-
# elevation_angles = np.linspace(0, np.pi/2, N_elevation)
|
|
1439
|
-
# hemisphere_list = []
|
|
1440
|
-
# for elev in elevation_angles:
|
|
1441
|
-
# sin_elev = np.sin(elev)
|
|
1442
|
-
# cos_elev = np.cos(elev)
|
|
1443
|
-
# for az in azimuth_angles:
|
|
1444
|
-
# x = cos_elev * np.cos(az)
|
|
1445
|
-
# y = cos_elev * np.sin(az)
|
|
1446
|
-
# z = sin_elev
|
|
1447
|
-
# hemisphere_list.append([x, y, z])
|
|
1448
|
-
# hemisphere_dirs = np.array(hemisphere_list, dtype=np.float64)
|
|
1449
|
-
|
|
1450
|
-
# # 4) Domain bounds in real coordinates
|
|
1451
|
-
# grid_shape = voxel_data.shape
|
|
1452
|
-
# grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0],grid_shape[1],grid_shape[2]]], dtype=np.float64)
|
|
1453
|
-
# grid_bounds_real = grid_bounds_voxel * meshsize
|
|
1454
|
-
# boundary_epsilon = meshsize * 0.05
|
|
1455
|
-
|
|
1456
|
-
# # 5) Call Numba-accelerated routine
|
|
1457
|
-
# face_svf_values = compute_svf_for_all_faces(
|
|
1458
|
-
# face_centers,
|
|
1459
|
-
# face_normals,
|
|
1460
|
-
# hemisphere_dirs,
|
|
1461
|
-
# voxel_data,
|
|
1462
|
-
# meshsize,
|
|
1463
|
-
# tree_k,
|
|
1464
|
-
# tree_lad,
|
|
1465
|
-
# hit_values,
|
|
1466
|
-
# inclusion_mode,
|
|
1467
|
-
# grid_bounds_real,
|
|
1468
|
-
# boundary_epsilon
|
|
1469
|
-
# )
|
|
1470
|
-
|
|
1471
|
-
# # 6) Store SVF values in mesh metadata
|
|
1472
|
-
# if not hasattr(building_mesh, 'metadata'):
|
|
1473
|
-
# building_mesh.metadata = {}
|
|
1474
|
-
# building_mesh.metadata['svf_values'] = face_svf_values
|
|
1475
|
-
|
|
1476
|
-
# # OBJ export if desired
|
|
1477
|
-
# obj_export = kwargs.get("obj_export", False)
|
|
1478
|
-
# if obj_export:
|
|
1479
|
-
# output_dir = kwargs.get("output_directory", "output")
|
|
1480
|
-
# output_file_name = kwargs.get("output_file_name", "building_surface_svf")
|
|
1481
|
-
# os.makedirs(output_dir, exist_ok=True)
|
|
1482
|
-
# try:
|
|
1483
|
-
# building_mesh.export(f"{output_dir}/{output_file_name}.obj")
|
|
1484
|
-
# print(f"Exported building SVF mesh to {output_dir}/{output_file_name}.obj")
|
|
1485
|
-
# except Exception as e:
|
|
1486
|
-
# print(f"Error exporting mesh: {e}")
|
|
1487
|
-
|
|
1488
|
-
# return building_mesh
|
|
1489
|
-
|
|
1490
999
|
@njit
|
|
1491
1000
|
def compute_view_factor_for_all_faces(
|
|
1492
1001
|
face_centers,
|
|
@@ -1505,47 +1014,50 @@ def compute_view_factor_for_all_faces(
|
|
|
1505
1014
|
"""
|
|
1506
1015
|
Compute a per-face "view factor" for a specified set of target voxel classes.
|
|
1507
1016
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1017
|
+
This function computes view factors from building surface faces to target voxel types
|
|
1018
|
+
(e.g., sky, trees, other buildings). It uses hemisphere ray casting with rotation
|
|
1019
|
+
to align rays with each face's normal direction.
|
|
1020
|
+
|
|
1021
|
+
Typical usage examples:
|
|
1022
|
+
- Sky View Factor: target_values=(0,), inclusion_mode=False (sky voxels)
|
|
1023
|
+
- Tree View Factor: target_values=(-2,), inclusion_mode=True (tree voxels)
|
|
1024
|
+
- Building View Factor: target_values=(-3,), inclusion_mode=True (building voxels)
|
|
1511
1025
|
|
|
1512
|
-
But you can pass any other combination:
|
|
1513
|
-
- E.g. target_values = (-2,), inclusion_mode=True
|
|
1514
|
-
to measure fraction of directions that intersect 'trees' (-2).
|
|
1515
|
-
- E.g. target_values = (-3,), inclusion_mode=True
|
|
1516
|
-
to measure fraction of directions that intersect 'buildings' (-3).
|
|
1517
|
-
|
|
1518
1026
|
Args:
|
|
1519
|
-
face_centers (np.ndarray): (n_faces, 3) face centroid positions.
|
|
1520
|
-
face_normals (np.ndarray): (n_faces, 3) face
|
|
1521
|
-
hemisphere_dirs (np.ndarray): (N, 3) set of direction vectors in the hemisphere.
|
|
1027
|
+
face_centers (np.ndarray): (n_faces, 3) face centroid positions in real coordinates.
|
|
1028
|
+
face_normals (np.ndarray): (n_faces, 3) face normal vectors (outward pointing).
|
|
1029
|
+
hemisphere_dirs (np.ndarray): (N, 3) set of direction vectors in the upper hemisphere.
|
|
1522
1030
|
voxel_data (np.ndarray): 3D array of voxel values.
|
|
1523
1031
|
meshsize (float): Size of each voxel in meters.
|
|
1524
|
-
tree_k (float): Tree extinction coefficient.
|
|
1525
|
-
tree_lad (float): Leaf area density in m^-1.
|
|
1526
|
-
target_values (tuple[int]): Voxel classes that define a 'hit'.
|
|
1527
|
-
inclusion_mode (bool): If True, hitting
|
|
1528
|
-
If False, hitting anything
|
|
1032
|
+
tree_k (float): Tree extinction coefficient for Beer-Lambert law.
|
|
1033
|
+
tree_lad (float): Leaf area density in m^-1 for tree transmittance.
|
|
1034
|
+
target_values (tuple[int]): Voxel classes that define a 'hit' or target.
|
|
1035
|
+
inclusion_mode (bool): If True, hitting target_values counts as visibility.
|
|
1036
|
+
If False, hitting anything NOT in target_values blocks the ray.
|
|
1529
1037
|
grid_bounds_real (np.ndarray): [[x_min,y_min,z_min],[x_max,y_max,z_max]] in real coords.
|
|
1530
|
-
boundary_epsilon (float):
|
|
1038
|
+
boundary_epsilon (float): Tolerance for identifying boundary vertical faces.
|
|
1531
1039
|
ignore_downward (bool): If True, only consider upward rays. If False, consider all outward rays.
|
|
1532
1040
|
|
|
1533
1041
|
Returns:
|
|
1534
|
-
np.ndarray of shape (n_faces,):
|
|
1535
|
-
|
|
1042
|
+
np.ndarray of shape (n_faces,): Computed view factor for each face.
|
|
1043
|
+
NaN values indicate boundary vertical faces that should be excluded.
|
|
1536
1044
|
"""
|
|
1537
1045
|
n_faces = face_centers.shape[0]
|
|
1538
1046
|
face_vf_values = np.zeros(n_faces, dtype=np.float64)
|
|
1539
1047
|
|
|
1048
|
+
# Reference vector pointing upward (+Z direction)
|
|
1540
1049
|
z_axis = np.array([0.0, 0.0, 1.0])
|
|
1541
1050
|
|
|
1051
|
+
# Process each face individually
|
|
1542
1052
|
for fidx in range(n_faces):
|
|
1543
1053
|
center = face_centers[fidx]
|
|
1544
1054
|
normal = face_normals[fidx]
|
|
1545
1055
|
|
|
1546
|
-
#
|
|
1547
|
-
|
|
1056
|
+
# Check for boundary vertical faces and mark as NaN
|
|
1057
|
+
# This excludes faces on domain edges that may have artificial visibility
|
|
1058
|
+
is_vertical = (abs(normal[2]) < 0.01) # Face normal is nearly horizontal
|
|
1548
1059
|
|
|
1060
|
+
# Check if face is near domain boundaries
|
|
1549
1061
|
on_x_min = (abs(center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
|
|
1550
1062
|
on_y_min = (abs(center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
|
|
1551
1063
|
on_x_max = (abs(center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
|
|
@@ -1556,31 +1068,33 @@ def compute_view_factor_for_all_faces(
|
|
|
1556
1068
|
face_vf_values[fidx] = np.nan
|
|
1557
1069
|
continue
|
|
1558
1070
|
|
|
1559
|
-
#
|
|
1071
|
+
# Compute rotation to align face normal with +Z axis
|
|
1072
|
+
# This allows us to use the same hemisphere directions for all faces
|
|
1560
1073
|
norm_n = np.sqrt(normal[0]**2 + normal[1]**2 + normal[2]**2)
|
|
1561
1074
|
if norm_n < 1e-12:
|
|
1562
|
-
# Degenerate normal
|
|
1075
|
+
# Degenerate normal vector
|
|
1563
1076
|
face_vf_values[fidx] = 0.0
|
|
1564
1077
|
continue
|
|
1565
1078
|
|
|
1079
|
+
# Calculate angle between face normal and +Z axis
|
|
1566
1080
|
dot_zn = z_axis[0]*normal[0] + z_axis[1]*normal[1] + z_axis[2]*normal[2]
|
|
1567
1081
|
cos_angle = dot_zn / (norm_n)
|
|
1568
1082
|
if cos_angle > 1.0: cos_angle = 1.0
|
|
1569
1083
|
if cos_angle < -1.0: cos_angle = -1.0
|
|
1570
1084
|
angle = np.arccos(cos_angle)
|
|
1571
1085
|
|
|
1572
|
-
#
|
|
1086
|
+
# Handle special cases and general rotation
|
|
1573
1087
|
if abs(cos_angle - 1.0) < 1e-9:
|
|
1574
|
-
# normal
|
|
1088
|
+
# Face normal is already aligned with +Z => no rotation needed
|
|
1575
1089
|
local_dirs = hemisphere_dirs
|
|
1576
1090
|
elif abs(cos_angle + 1.0) < 1e-9:
|
|
1577
|
-
# normal
|
|
1091
|
+
# Face normal points in -Z direction => rotate 180 degrees around X axis
|
|
1578
1092
|
axis_180 = np.array([1.0, 0.0, 0.0])
|
|
1579
1093
|
local_dirs = np.empty_like(hemisphere_dirs)
|
|
1580
1094
|
for i in range(hemisphere_dirs.shape[0]):
|
|
1581
1095
|
local_dirs[i] = rotate_vector_axis_angle(hemisphere_dirs[i], axis_180, np.pi)
|
|
1582
1096
|
else:
|
|
1583
|
-
#
|
|
1097
|
+
# General case: rotate around axis perpendicular to both +Z and face normal
|
|
1584
1098
|
axis_x = z_axis[1]*normal[2] - z_axis[2]*normal[1]
|
|
1585
1099
|
axis_y = z_axis[2]*normal[0] - z_axis[0]*normal[2]
|
|
1586
1100
|
axis_z = z_axis[0]*normal[1] - z_axis[1]*normal[0]
|
|
@@ -1594,28 +1108,30 @@ def compute_view_factor_for_all_faces(
|
|
|
1594
1108
|
angle
|
|
1595
1109
|
)
|
|
1596
1110
|
|
|
1597
|
-
#
|
|
1598
|
-
total_outward = 0
|
|
1599
|
-
num_valid = 0
|
|
1111
|
+
# Count valid ray directions based on face orientation and downward filtering
|
|
1112
|
+
total_outward = 0 # Rays pointing away from face surface
|
|
1113
|
+
num_valid = 0 # Rays that meet all criteria (outward + optionally upward)
|
|
1114
|
+
|
|
1600
1115
|
for i in range(local_dirs.shape[0]):
|
|
1601
1116
|
dvec = local_dirs[i]
|
|
1117
|
+
# Check if ray points outward from face surface (positive dot product with normal)
|
|
1602
1118
|
dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
|
|
1603
1119
|
if dp > 0.0:
|
|
1604
1120
|
total_outward += 1
|
|
1121
|
+
# Apply downward filtering if requested
|
|
1605
1122
|
if not ignore_downward or dvec[2] > 0.0:
|
|
1606
1123
|
num_valid += 1
|
|
1607
1124
|
|
|
1608
|
-
#
|
|
1125
|
+
# Handle cases with no valid directions
|
|
1609
1126
|
if total_outward == 0:
|
|
1610
1127
|
face_vf_values[fidx] = 0.0
|
|
1611
1128
|
continue
|
|
1612
1129
|
|
|
1613
|
-
# If no valid directions => view factor = 0
|
|
1614
1130
|
if num_valid == 0:
|
|
1615
1131
|
face_vf_values[fidx] = 0.0
|
|
1616
1132
|
continue
|
|
1617
1133
|
|
|
1618
|
-
#
|
|
1134
|
+
# Create array containing only the valid ray directions
|
|
1619
1135
|
valid_dirs_arr = np.empty((num_valid, 3), dtype=np.float64)
|
|
1620
1136
|
out_idx = 0
|
|
1621
1137
|
for i in range(local_dirs.shape[0]):
|
|
@@ -1627,11 +1143,11 @@ def compute_view_factor_for_all_faces(
|
|
|
1627
1143
|
valid_dirs_arr[out_idx, 2] = dvec[2]
|
|
1628
1144
|
out_idx += 1
|
|
1629
1145
|
|
|
1630
|
-
#
|
|
1631
|
-
offset_vox = 0.1
|
|
1146
|
+
# Set ray origin slightly offset from face surface to avoid self-intersection
|
|
1147
|
+
offset_vox = 0.1 # Offset in voxel units
|
|
1632
1148
|
ray_origin = (center / meshsize) + (normal / norm_n) * offset_vox
|
|
1633
1149
|
|
|
1634
|
-
#
|
|
1150
|
+
# Compute fraction of valid rays that "see" the target using generic ray tracing
|
|
1635
1151
|
vf = compute_vi_generic(
|
|
1636
1152
|
ray_origin,
|
|
1637
1153
|
voxel_data,
|
|
@@ -1643,7 +1159,8 @@ def compute_view_factor_for_all_faces(
|
|
|
1643
1159
|
inclusion_mode
|
|
1644
1160
|
)
|
|
1645
1161
|
|
|
1646
|
-
# Scale by fraction of directions that were valid
|
|
1162
|
+
# Scale result by fraction of directions that were valid
|
|
1163
|
+
# This normalizes for the hemisphere portion that the face can actually "see"
|
|
1647
1164
|
fraction_valid = num_valid / total_outward
|
|
1648
1165
|
face_vf_values[fidx] = vf * fraction_valid
|
|
1649
1166
|
|
|
@@ -1651,34 +1168,73 @@ def compute_view_factor_for_all_faces(
|
|
|
1651
1168
|
|
|
1652
1169
|
def get_surface_view_factor(voxel_data, meshsize, **kwargs):
|
|
1653
1170
|
"""
|
|
1654
|
-
Compute and optionally visualize
|
|
1655
|
-
|
|
1171
|
+
Compute and optionally visualize view factors for surface meshes with respect to target voxel classes.
|
|
1172
|
+
|
|
1173
|
+
This function provides a flexible framework for computing various surface-based view factors:
|
|
1174
|
+
- Sky View Factor: Fraction of sky hemisphere visible from building surfaces
|
|
1175
|
+
- Tree View Factor: Fraction of directions that intersect vegetation
|
|
1176
|
+
- Building View Factor: Fraction of directions that intersect other buildings
|
|
1177
|
+
- Custom View Factors: User-defined target voxel classes
|
|
1656
1178
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
- target_values=(-2,), inclusion_mode=True => Tree view factor
|
|
1660
|
-
- target_values=(-3,), inclusion_mode=True => Building view factor
|
|
1661
|
-
etc.
|
|
1179
|
+
The function extracts surface meshes from the voxel data, then computes view factors
|
|
1180
|
+
for each face using hemisphere ray casting with proper geometric transformations.
|
|
1662
1181
|
|
|
1663
1182
|
Args:
|
|
1664
|
-
voxel_data (ndarray): 3D array of voxel values
|
|
1665
|
-
meshsize (float): Size of each voxel in meters
|
|
1666
|
-
**kwargs:
|
|
1667
|
-
|
|
1668
|
-
target_values (tuple[int]):
|
|
1669
|
-
inclusion_mode (bool):
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1183
|
+
voxel_data (ndarray): 3D array of voxel values representing the urban environment.
|
|
1184
|
+
meshsize (float): Size of each voxel in meters for coordinate scaling.
|
|
1185
|
+
**kwargs: Extensive configuration options including:
|
|
1186
|
+
# Target specification:
|
|
1187
|
+
target_values (tuple[int]): Voxel classes to measure visibility to (default: (0,) for sky)
|
|
1188
|
+
inclusion_mode (bool): Interpretation of target_values (default: False for sky)
|
|
1189
|
+
|
|
1190
|
+
# Surface extraction:
|
|
1191
|
+
building_class_id (int): Voxel class to extract surfaces from (default: -3 for buildings)
|
|
1192
|
+
building_id_grid (ndarray): Optional grid mapping voxels to building IDs
|
|
1193
|
+
|
|
1194
|
+
# Ray sampling:
|
|
1195
|
+
N_azimuth (int): Number of azimuth angles for hemisphere sampling (default: 60)
|
|
1196
|
+
N_elevation (int): Number of elevation angles for hemisphere sampling (default: 10)
|
|
1197
|
+
|
|
1198
|
+
# Tree transmittance (Beer-Lambert law):
|
|
1199
|
+
tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
1200
|
+
tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
1201
|
+
|
|
1202
|
+
# Visualization and export:
|
|
1203
|
+
colormap (str): Matplotlib colormap for visualization (default: 'BuPu_r')
|
|
1204
|
+
vmin, vmax (float): Color scale limits (default: 0.0, 1.0)
|
|
1205
|
+
obj_export (bool): Whether to export mesh as OBJ file (default: False)
|
|
1206
|
+
output_directory (str): Directory for OBJ export (default: "output")
|
|
1207
|
+
output_file_name (str): Base filename for OBJ export (default: "surface_view_factor")
|
|
1208
|
+
|
|
1209
|
+
# Other options:
|
|
1210
|
+
progress_report (bool): Whether to print computation progress (default: False)
|
|
1211
|
+
debug (bool): Enable debug output (default: False)
|
|
1212
|
+
|
|
1673
1213
|
Returns:
|
|
1674
|
-
trimesh.Trimesh:
|
|
1214
|
+
trimesh.Trimesh: Surface mesh with per-face view factor values stored in metadata.
|
|
1215
|
+
The view factor values can be accessed via mesh.metadata[value_name].
|
|
1216
|
+
Returns None if no surfaces are found or extraction fails.
|
|
1217
|
+
|
|
1218
|
+
Example Usage:
|
|
1219
|
+
# Sky View Factor for building surfaces
|
|
1220
|
+
mesh = get_surface_view_factor(voxel_data, meshsize,
|
|
1221
|
+
target_values=(0,), inclusion_mode=False)
|
|
1222
|
+
|
|
1223
|
+
# Tree View Factor for building surfaces
|
|
1224
|
+
mesh = get_surface_view_factor(voxel_data, meshsize,
|
|
1225
|
+
target_values=(-2,), inclusion_mode=True)
|
|
1226
|
+
|
|
1227
|
+
# Custom view factor with OBJ export
|
|
1228
|
+
mesh = get_surface_view_factor(voxel_data, meshsize,
|
|
1229
|
+
target_values=(-3,), inclusion_mode=True,
|
|
1230
|
+
obj_export=True, output_file_name="building_view_factor")
|
|
1675
1231
|
"""
|
|
1676
1232
|
import matplotlib.pyplot as plt
|
|
1677
1233
|
import matplotlib.cm as cm
|
|
1678
1234
|
import matplotlib.colors as mcolors
|
|
1679
1235
|
import os
|
|
1680
1236
|
|
|
1681
|
-
#
|
|
1237
|
+
# Extract configuration parameters with appropriate defaults
|
|
1682
1238
|
value_name = kwargs.get("value_name", 'view_factor_values')
|
|
1683
1239
|
colormap = kwargs.get("colormap", 'BuPu_r')
|
|
1684
1240
|
vmin = kwargs.get("vmin", 0.0)
|
|
@@ -1689,28 +1245,25 @@ def get_surface_view_factor(voxel_data, meshsize, **kwargs):
|
|
|
1689
1245
|
progress_report= kwargs.get("progress_report", False)
|
|
1690
1246
|
building_id_grid = kwargs.get("building_id_grid", None)
|
|
1691
1247
|
|
|
1692
|
-
# Tree
|
|
1248
|
+
# Tree transmittance parameters for Beer-Lambert law
|
|
1693
1249
|
tree_k = kwargs.get("tree_k", 0.6)
|
|
1694
1250
|
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
1695
1251
|
|
|
1696
|
-
#
|
|
1697
|
-
|
|
1698
|
-
#
|
|
1699
|
-
target_values = kwargs.get("target_values", (0,))
|
|
1700
|
-
inclusion_mode = kwargs.get("inclusion_mode", False)
|
|
1701
|
-
# ----------------------------------------
|
|
1252
|
+
# Target specification - defaults to sky view factor configuration
|
|
1253
|
+
target_values = kwargs.get("target_values", (0,)) # Sky voxels by default
|
|
1254
|
+
inclusion_mode = kwargs.get("inclusion_mode", False) # Exclusion mode for sky
|
|
1702
1255
|
|
|
1703
|
-
#
|
|
1704
|
-
building_class_id = kwargs.get("building_class_id", -3)
|
|
1256
|
+
# Surface extraction parameters
|
|
1257
|
+
building_class_id = kwargs.get("building_class_id", -3) # Building voxel class
|
|
1705
1258
|
|
|
1706
|
-
#
|
|
1259
|
+
# Extract surface mesh from the specified voxel class
|
|
1707
1260
|
try:
|
|
1708
1261
|
building_mesh = create_voxel_mesh(
|
|
1709
1262
|
voxel_data,
|
|
1710
1263
|
building_class_id,
|
|
1711
1264
|
meshsize,
|
|
1712
1265
|
building_id_grid=building_id_grid,
|
|
1713
|
-
mesh_type='open_air'
|
|
1266
|
+
mesh_type='open_air' # Extract surfaces exposed to air
|
|
1714
1267
|
)
|
|
1715
1268
|
if building_mesh is None or len(building_mesh.faces) == 0:
|
|
1716
1269
|
print("No surfaces found in voxel data for the specified class.")
|
|
@@ -1722,31 +1275,33 @@ def get_surface_view_factor(voxel_data, meshsize, **kwargs):
|
|
|
1722
1275
|
if progress_report:
|
|
1723
1276
|
print(f"Processing view factor for {len(building_mesh.faces)} faces...")
|
|
1724
1277
|
|
|
1725
|
-
#
|
|
1726
|
-
face_centers = building_mesh.triangles_center
|
|
1727
|
-
face_normals = building_mesh.face_normals
|
|
1278
|
+
# Extract geometric properties from the mesh
|
|
1279
|
+
face_centers = building_mesh.triangles_center # Centroid of each face
|
|
1280
|
+
face_normals = building_mesh.face_normals # Outward normal of each face
|
|
1728
1281
|
|
|
1729
|
-
#
|
|
1282
|
+
# Generate hemisphere ray directions using spherical coordinates
|
|
1283
|
+
# These directions will be rotated to align with each face's normal
|
|
1730
1284
|
azimuth_angles = np.linspace(0, 2*np.pi, N_azimuth, endpoint=False)
|
|
1731
|
-
elevation_angles = np.linspace(0, np.pi/2, N_elevation)
|
|
1285
|
+
elevation_angles = np.linspace(0, np.pi/2, N_elevation) # Upper hemisphere only
|
|
1732
1286
|
hemisphere_list = []
|
|
1733
1287
|
for elev in elevation_angles:
|
|
1734
1288
|
sin_elev = np.sin(elev)
|
|
1735
1289
|
cos_elev = np.cos(elev)
|
|
1736
1290
|
for az in azimuth_angles:
|
|
1291
|
+
# Convert spherical to Cartesian coordinates
|
|
1737
1292
|
x = cos_elev * np.cos(az)
|
|
1738
1293
|
y = cos_elev * np.sin(az)
|
|
1739
|
-
z = sin_elev
|
|
1294
|
+
z = sin_elev # Always positive (upper hemisphere)
|
|
1740
1295
|
hemisphere_list.append([x, y, z])
|
|
1741
1296
|
hemisphere_dirs = np.array(hemisphere_list, dtype=np.float64)
|
|
1742
1297
|
|
|
1743
|
-
#
|
|
1298
|
+
# Calculate domain bounds for boundary face detection
|
|
1744
1299
|
nx, ny, nz = voxel_data.shape
|
|
1745
1300
|
grid_bounds_voxel = np.array([[0,0,0],[nx, ny, nz]], dtype=np.float64)
|
|
1746
1301
|
grid_bounds_real = grid_bounds_voxel * meshsize
|
|
1747
|
-
boundary_epsilon = meshsize * 0.05
|
|
1302
|
+
boundary_epsilon = meshsize * 0.05 # Tolerance for boundary detection
|
|
1748
1303
|
|
|
1749
|
-
#
|
|
1304
|
+
# Compute view factors for all faces using optimized Numba implementation
|
|
1750
1305
|
face_vf_values = compute_view_factor_for_all_faces(
|
|
1751
1306
|
face_centers,
|
|
1752
1307
|
face_normals,
|
|
@@ -1755,18 +1310,18 @@ def get_surface_view_factor(voxel_data, meshsize, **kwargs):
|
|
|
1755
1310
|
meshsize,
|
|
1756
1311
|
tree_k,
|
|
1757
1312
|
tree_lad,
|
|
1758
|
-
target_values, #
|
|
1759
|
-
inclusion_mode, #
|
|
1313
|
+
target_values, # User-specified target voxel classes
|
|
1314
|
+
inclusion_mode, # User-specified hit interpretation
|
|
1760
1315
|
grid_bounds_real,
|
|
1761
1316
|
boundary_epsilon
|
|
1762
1317
|
)
|
|
1763
1318
|
|
|
1764
|
-
#
|
|
1319
|
+
# Store computed view factor values in mesh metadata for later access
|
|
1765
1320
|
if not hasattr(building_mesh, 'metadata'):
|
|
1766
1321
|
building_mesh.metadata = {}
|
|
1767
1322
|
building_mesh.metadata[value_name] = face_vf_values
|
|
1768
1323
|
|
|
1769
|
-
#
|
|
1324
|
+
# Optional OBJ file export for external visualization/analysis
|
|
1770
1325
|
obj_export = kwargs.get("obj_export", False)
|
|
1771
1326
|
if obj_export:
|
|
1772
1327
|
output_dir = kwargs.get("output_directory", "output")
|