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/utils/lc.py
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Land Cover Classification Utilities for VoxelCity
|
|
3
|
+
|
|
4
|
+
This module provides utilities for handling land cover data from various sources,
|
|
5
|
+
including color-based classification, data conversion between different land cover
|
|
6
|
+
classification systems, and spatial analysis of land cover polygons.
|
|
7
|
+
|
|
8
|
+
Supported land cover data sources:
|
|
9
|
+
- Urbanwatch
|
|
10
|
+
- OpenEarthMapJapan
|
|
11
|
+
- ESRI 10m Annual Land Cover
|
|
12
|
+
- ESA WorldCover
|
|
13
|
+
- Dynamic World V1
|
|
14
|
+
- OpenStreetMap
|
|
15
|
+
- Standard classification
|
|
16
|
+
"""
|
|
17
|
+
|
|
1
18
|
import numpy as np
|
|
2
19
|
from shapely.geometry import Polygon
|
|
3
20
|
from rtree import index
|
|
4
21
|
from collections import Counter
|
|
5
22
|
|
|
6
23
|
def rgb_distance(color1, color2):
|
|
24
|
+
"""
|
|
25
|
+
Calculate the Euclidean distance between two RGB colors.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
color1 (tuple): RGB values as (R, G, B) tuple
|
|
29
|
+
color2 (tuple): RGB values as (R, G, B) tuple
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
float: Euclidean distance between the two colors
|
|
33
|
+
"""
|
|
7
34
|
return np.sqrt(np.sum((np.array(color1) - np.array(color2))**2))
|
|
8
35
|
|
|
9
36
|
|
|
37
|
+
# Legacy land cover classes mapping - kept for reference
|
|
10
38
|
# land_cover_classes = {
|
|
11
39
|
# (128, 0, 0): 'Bareland', 0
|
|
12
40
|
# (0, 255, 36): 'Rangeland', 1
|
|
@@ -24,7 +52,27 @@ def rgb_distance(color1, color2):
|
|
|
24
52
|
# }
|
|
25
53
|
|
|
26
54
|
def get_land_cover_classes(source):
|
|
55
|
+
"""
|
|
56
|
+
Get land cover classification mapping for a specific data source.
|
|
57
|
+
|
|
58
|
+
Each data source has its own color-to-class mapping system. This function
|
|
59
|
+
returns the appropriate RGB color to land cover class dictionary based on
|
|
60
|
+
the specified source.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
source (str): Name of the land cover data source. Supported sources:
|
|
64
|
+
"Urbanwatch", "OpenEarthMapJapan", "ESRI 10m Annual Land Cover",
|
|
65
|
+
"ESA WorldCover", "Dynamic World V1", "Standard", "OpenStreetMap"
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
dict: Dictionary mapping RGB tuples to land cover class names
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> classes = get_land_cover_classes("Urbanwatch")
|
|
72
|
+
>>> print(classes[(255, 0, 0)]) # Returns 'Building'
|
|
73
|
+
"""
|
|
27
74
|
if source == "Urbanwatch":
|
|
75
|
+
# Urbanwatch color scheme - focused on urban features
|
|
28
76
|
land_cover_classes = {
|
|
29
77
|
(255, 0, 0): 'Building',
|
|
30
78
|
(133, 133, 133): 'Road',
|
|
@@ -38,6 +86,7 @@ def get_land_cover_classes(source):
|
|
|
38
86
|
(0, 0, 0): 'Sea'
|
|
39
87
|
}
|
|
40
88
|
elif (source == "OpenEarthMapJapan"):
|
|
89
|
+
# OpenEarthMap Japan specific classification
|
|
41
90
|
land_cover_classes = {
|
|
42
91
|
(128, 0, 0): 'Bareland',
|
|
43
92
|
(0, 255, 36): 'Rangeland',
|
|
@@ -49,6 +98,7 @@ def get_land_cover_classes(source):
|
|
|
49
98
|
(222, 31, 7): 'Building'
|
|
50
99
|
}
|
|
51
100
|
elif source == "ESRI 10m Annual Land Cover":
|
|
101
|
+
# ESRI's global 10-meter resolution land cover classification
|
|
52
102
|
land_cover_classes = {
|
|
53
103
|
(255, 255, 255): 'No Data',
|
|
54
104
|
(26, 91, 171): 'Water',
|
|
@@ -63,6 +113,7 @@ def get_land_cover_classes(source):
|
|
|
63
113
|
(200, 200, 200): 'Clouds'
|
|
64
114
|
}
|
|
65
115
|
elif source == "ESA WorldCover":
|
|
116
|
+
# European Space Agency WorldCover 10m classification
|
|
66
117
|
land_cover_classes = {
|
|
67
118
|
(0, 112, 0): 'Trees',
|
|
68
119
|
(255, 224, 80): 'Shrubland',
|
|
@@ -77,6 +128,7 @@ def get_land_cover_classes(source):
|
|
|
77
128
|
(255, 255, 0): 'Moss and lichen'
|
|
78
129
|
}
|
|
79
130
|
elif source == "Dynamic World V1":
|
|
131
|
+
# Google's Dynamic World near real-time land cover
|
|
80
132
|
# Convert hex colors to RGB tuples
|
|
81
133
|
land_cover_classes = {
|
|
82
134
|
(65, 155, 223): 'Water', # #419bdf
|
|
@@ -90,6 +142,7 @@ def get_land_cover_classes(source):
|
|
|
90
142
|
(179, 159, 225): 'Snow and Ice' # #b39fe1
|
|
91
143
|
}
|
|
92
144
|
elif (source == 'Standard') or (source == "OpenStreetMap"):
|
|
145
|
+
# Standard/OpenStreetMap classification - comprehensive land cover types
|
|
93
146
|
land_cover_classes = {
|
|
94
147
|
(128, 0, 0): 'Bareland',
|
|
95
148
|
(0, 255, 36): 'Rangeland',
|
|
@@ -108,6 +161,7 @@ def get_land_cover_classes(source):
|
|
|
108
161
|
}
|
|
109
162
|
return land_cover_classes
|
|
110
163
|
|
|
164
|
+
# Legacy land cover classes with numeric indices - kept for reference
|
|
111
165
|
# land_cover_classes = {
|
|
112
166
|
# (128, 0, 0): 'Bareland', 0
|
|
113
167
|
# (0, 255, 36): 'Rangeland', 1
|
|
@@ -128,6 +182,37 @@ def get_land_cover_classes(source):
|
|
|
128
182
|
|
|
129
183
|
|
|
130
184
|
def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
185
|
+
"""
|
|
186
|
+
Convert land cover classification from source-specific indices to standardized indices.
|
|
187
|
+
|
|
188
|
+
This function maps land cover classes from various data sources to a standardized
|
|
189
|
+
classification system. Each source has different class definitions and indices,
|
|
190
|
+
so this conversion enables consistent processing across different data sources.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
input_array (numpy.ndarray): Input array with source-specific land cover indices
|
|
194
|
+
land_cover_source (str): Name of the source land cover classification system
|
|
195
|
+
Default is 'Urbanwatch'
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
numpy.ndarray: Array with standardized land cover indices
|
|
199
|
+
|
|
200
|
+
Standardized Classification System:
|
|
201
|
+
0: Bareland
|
|
202
|
+
1: Rangeland
|
|
203
|
+
2: Shrub
|
|
204
|
+
3: Agriculture land
|
|
205
|
+
4: Tree
|
|
206
|
+
5: Moss and lichen
|
|
207
|
+
6: Wet land
|
|
208
|
+
7: Mangrove
|
|
209
|
+
8: Water
|
|
210
|
+
9: Snow and ice
|
|
211
|
+
10: Developed space
|
|
212
|
+
11: Road
|
|
213
|
+
12: Building
|
|
214
|
+
13: No Data
|
|
215
|
+
"""
|
|
131
216
|
|
|
132
217
|
if land_cover_source == 'Urbanwatch':
|
|
133
218
|
# Define the mapping from Urbanwatch to new standardized classes
|
|
@@ -144,6 +229,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
144
229
|
9: 8 # Sea -> Water
|
|
145
230
|
}
|
|
146
231
|
elif land_cover_source == 'ESA WorldCover':
|
|
232
|
+
# ESA WorldCover to standardized mapping
|
|
147
233
|
convert_dict = {
|
|
148
234
|
0: 4, # Trees -> Tree
|
|
149
235
|
1: 2, # Shrubland -> Shrub
|
|
@@ -158,6 +244,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
158
244
|
10: 5 # Moss and lichen
|
|
159
245
|
}
|
|
160
246
|
elif land_cover_source == "ESRI 10m Annual Land Cover":
|
|
247
|
+
# ESRI 10m to standardized mapping
|
|
161
248
|
convert_dict = {
|
|
162
249
|
0: 13, # No Data
|
|
163
250
|
1: 8, # Water
|
|
@@ -172,6 +259,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
172
259
|
10: 13 # Clouds -> No Data
|
|
173
260
|
}
|
|
174
261
|
elif land_cover_source == "Dynamic World V1":
|
|
262
|
+
# Dynamic World to standardized mapping
|
|
175
263
|
convert_dict = {
|
|
176
264
|
0: 8, # Water
|
|
177
265
|
1: 4, # Trees -> Tree
|
|
@@ -184,6 +272,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
184
272
|
8: 9 # Snow and Ice
|
|
185
273
|
}
|
|
186
274
|
elif land_cover_source == "OpenEarthMapJapan":
|
|
275
|
+
# OpenEarthMapJapan to standardized mapping
|
|
187
276
|
convert_dict = {
|
|
188
277
|
0: 0, # Bareland
|
|
189
278
|
1: 1, # Rangeland
|
|
@@ -204,6 +293,26 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
204
293
|
return converted_array
|
|
205
294
|
|
|
206
295
|
def get_class_priority(source):
|
|
296
|
+
"""
|
|
297
|
+
Get priority rankings for land cover classes to resolve conflicts during classification.
|
|
298
|
+
|
|
299
|
+
When multiple land cover classes are present in the same area, this priority system
|
|
300
|
+
determines which class should take precedence. Higher priority values indicate
|
|
301
|
+
classes that should override lower priority classes.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
source (str): Name of the land cover data source
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
dict: Dictionary mapping class names to priority values (higher = more priority)
|
|
308
|
+
|
|
309
|
+
Priority Logic for OpenStreetMap:
|
|
310
|
+
- Built Environment: Highest priority (most definitive structures)
|
|
311
|
+
- Water Bodies: High priority (clearly defined features)
|
|
312
|
+
- Vegetation: Medium priority (managed vs natural)
|
|
313
|
+
- Natural Non-Vegetation: Lower priority (often default classifications)
|
|
314
|
+
- Uncertain/No Data: Lowest priority
|
|
315
|
+
"""
|
|
207
316
|
if source == "OpenStreetMap":
|
|
208
317
|
return {
|
|
209
318
|
# Built Environment (highest priority as they're most definitively mapped)
|
|
@@ -230,6 +339,7 @@ def get_class_priority(source):
|
|
|
230
339
|
# Uncertain
|
|
231
340
|
'No Data': 14 # Lowest priority as it represents uncertainty
|
|
232
341
|
}
|
|
342
|
+
# Legacy priority system - kept for reference
|
|
233
343
|
# return {
|
|
234
344
|
# 'Bareland': 4,
|
|
235
345
|
# 'Rangeland': 6,
|
|
@@ -242,6 +352,26 @@ def get_class_priority(source):
|
|
|
242
352
|
# }
|
|
243
353
|
|
|
244
354
|
def create_land_cover_polygons(land_cover_geojson):
|
|
355
|
+
"""
|
|
356
|
+
Create polygon geometries and spatial index from land cover GeoJSON data.
|
|
357
|
+
|
|
358
|
+
This function processes GeoJSON land cover data to create Shapely polygon
|
|
359
|
+
geometries and builds an R-tree spatial index for efficient spatial queries.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
land_cover_geojson (list): List of GeoJSON feature dictionaries containing
|
|
363
|
+
land cover polygons with geometry and properties
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
tuple: A tuple containing:
|
|
367
|
+
- land_cover_polygons (list): List of tuples (polygon, class_name)
|
|
368
|
+
- idx (rtree.index.Index): Spatial index for efficient polygon lookup
|
|
369
|
+
|
|
370
|
+
Note:
|
|
371
|
+
Each GeoJSON feature should have:
|
|
372
|
+
- geometry.coordinates[0]: List of coordinate pairs defining the polygon
|
|
373
|
+
- properties.class: String indicating the land cover class
|
|
374
|
+
"""
|
|
245
375
|
land_cover_polygons = []
|
|
246
376
|
idx = index.Index()
|
|
247
377
|
count = 0
|
|
@@ -262,19 +392,72 @@ def create_land_cover_polygons(land_cover_geojson):
|
|
|
262
392
|
return land_cover_polygons, idx
|
|
263
393
|
|
|
264
394
|
def get_nearest_class(pixel, land_cover_classes):
|
|
395
|
+
"""
|
|
396
|
+
Find the nearest land cover class for a given pixel color using RGB distance.
|
|
397
|
+
|
|
398
|
+
This function determines the most appropriate land cover class for a pixel
|
|
399
|
+
by finding the class with the minimum RGB color distance to the pixel's color.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
pixel (tuple): RGB color values as (R, G, B) tuple
|
|
403
|
+
land_cover_classes (dict): Dictionary mapping RGB tuples to class names
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
str: Name of the nearest land cover class
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> classes = {(255, 0, 0): 'Building', (0, 255, 0): 'Tree'}
|
|
410
|
+
>>> nearest = get_nearest_class((250, 5, 5), classes)
|
|
411
|
+
>>> print(nearest) # Returns 'Building'
|
|
412
|
+
"""
|
|
265
413
|
distances = {class_name: rgb_distance(pixel, color)
|
|
266
414
|
for color, class_name in land_cover_classes.items()}
|
|
267
415
|
return min(distances, key=distances.get)
|
|
268
416
|
|
|
269
417
|
def get_dominant_class(cell_data, land_cover_classes):
|
|
418
|
+
"""
|
|
419
|
+
Determine the dominant land cover class in a cell based on pixel majority.
|
|
420
|
+
|
|
421
|
+
This function analyzes all pixels within a cell, classifies each pixel to its
|
|
422
|
+
nearest land cover class, and returns the most frequently occurring class.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
cell_data (numpy.ndarray): 3D array of RGB pixel data for the cell
|
|
426
|
+
land_cover_classes (dict): Dictionary mapping RGB tuples to class names
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
str: Name of the dominant land cover class in the cell
|
|
430
|
+
|
|
431
|
+
Note:
|
|
432
|
+
If the cell contains no data, returns 'No Data'
|
|
433
|
+
"""
|
|
270
434
|
if cell_data.size == 0:
|
|
271
435
|
return 'No Data'
|
|
436
|
+
# Classify each pixel in the cell to its nearest land cover class
|
|
272
437
|
pixel_classes = [get_nearest_class(tuple(pixel), land_cover_classes)
|
|
273
438
|
for pixel in cell_data.reshape(-1, 3)]
|
|
439
|
+
# Count occurrences of each class
|
|
274
440
|
class_counts = Counter(pixel_classes)
|
|
441
|
+
# Return the most common class
|
|
275
442
|
return class_counts.most_common(1)[0][0]
|
|
276
443
|
|
|
277
444
|
def convert_land_cover_array(input_array, land_cover_classes):
|
|
445
|
+
"""
|
|
446
|
+
Convert an array of land cover class names to integer indices.
|
|
447
|
+
|
|
448
|
+
This function maps string-based land cover class names to integer indices
|
|
449
|
+
for numerical processing and storage efficiency.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
input_array (numpy.ndarray): Array containing land cover class names as strings
|
|
453
|
+
land_cover_classes (dict): Dictionary mapping RGB tuples to class names
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
numpy.ndarray: Array with integer indices corresponding to land cover classes
|
|
457
|
+
|
|
458
|
+
Note:
|
|
459
|
+
Classes not found in the mapping are assigned index -1
|
|
460
|
+
"""
|
|
278
461
|
# Create a mapping of class names to integers
|
|
279
462
|
class_to_int = {name: i for i, name in enumerate(land_cover_classes.values())}
|
|
280
463
|
|
voxcity/utils/material.py
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Material utilities for VoxelCity voxel grid processing.
|
|
3
|
+
|
|
4
|
+
This module provides functions for setting building materials and window patterns
|
|
5
|
+
in 3D voxel grids based on building IDs, material types, and window ratios.
|
|
6
|
+
The main functionality includes:
|
|
7
|
+
- Material ID mapping and retrieval
|
|
8
|
+
- Window pattern generation based on configurable ratios
|
|
9
|
+
- Building material assignment from GeoDataFrame data
|
|
10
|
+
"""
|
|
11
|
+
|
|
1
12
|
import numpy as np
|
|
2
13
|
|
|
3
14
|
def get_material_dict():
|
|
4
15
|
"""
|
|
5
16
|
Returns a dictionary mapping material names to their corresponding ID values.
|
|
17
|
+
|
|
18
|
+
The material IDs use negative values to distinguish them from other voxel types.
|
|
19
|
+
Each material has a unique negative ID that can be used for material-based
|
|
20
|
+
rendering and analysis.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict: Dictionary with material names as keys and negative integer IDs as values.
|
|
24
|
+
Available materials: unknown, brick, wood, concrete, metal, stone, glass, plaster
|
|
6
25
|
"""
|
|
7
26
|
return {
|
|
8
27
|
"unknown": -3,
|
|
@@ -19,23 +38,38 @@ def get_modulo_numbers(window_ratio):
|
|
|
19
38
|
"""
|
|
20
39
|
Determines the appropriate modulo numbers for x, y, z based on window_ratio.
|
|
21
40
|
|
|
41
|
+
This function creates different window patterns by returning modulo values that
|
|
42
|
+
control the spacing of windows in the x, y, and z dimensions. Lower window_ratio
|
|
43
|
+
values result in sparser window patterns (higher modulo values), while higher
|
|
44
|
+
ratios create denser patterns.
|
|
45
|
+
|
|
46
|
+
The function uses hash-based selection for certain ratios to introduce variety
|
|
47
|
+
in window patterns for buildings with similar window ratios.
|
|
48
|
+
|
|
22
49
|
Parameters:
|
|
23
|
-
|
|
50
|
+
window_ratio (float): Value between 0 and 1.0 representing window density
|
|
24
51
|
|
|
25
52
|
Returns:
|
|
26
|
-
|
|
53
|
+
tuple: (x_mod, y_mod, z_mod) - modulo numbers for each dimension
|
|
54
|
+
Higher values = sparser windows, lower values = denser windows
|
|
27
55
|
"""
|
|
56
|
+
# Very sparse windows - every 2nd position in all dimensions
|
|
28
57
|
if window_ratio <= 0.125 + 0.0625: # around 0.125
|
|
29
58
|
return (2, 2, 2)
|
|
59
|
+
# Medium-sparse windows - vary pattern across dimensions
|
|
30
60
|
elif window_ratio <= 0.25 + 0.125: # around 0.25
|
|
31
61
|
combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
|
|
62
|
+
# Use hash for consistent but varied selection
|
|
32
63
|
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
64
|
+
# Medium density windows - two dimensions sparse, one dense
|
|
33
65
|
elif window_ratio <= 0.5 + 0.125: # around 0.5
|
|
34
66
|
combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
|
|
35
67
|
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
68
|
+
# Dense windows - similar pattern to medium density
|
|
36
69
|
elif window_ratio <= 0.75 + 0.125: # around 0.75
|
|
37
70
|
combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
|
|
38
71
|
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
72
|
+
# Maximum density - windows at every position
|
|
39
73
|
else: # above 0.875
|
|
40
74
|
return (1, 1, 1)
|
|
41
75
|
|
|
@@ -44,64 +78,82 @@ def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark,
|
|
|
44
78
|
Marks cells in voxelcity_grid based on building IDs and window ratio.
|
|
45
79
|
Never sets glass_id to cells with maximum z index.
|
|
46
80
|
|
|
81
|
+
This function processes buildings by:
|
|
82
|
+
1. Finding all positions matching the specified building IDs
|
|
83
|
+
2. Setting the base material for all building voxels
|
|
84
|
+
3. Creating window patterns based on window_ratio and modulo calculations
|
|
85
|
+
4. Ensuring the top floor (maximum z) never gets windows (glass_id)
|
|
86
|
+
|
|
87
|
+
The window pattern is determined by the modulo values returned from get_modulo_numbers(),
|
|
88
|
+
which creates different densities and arrangements of windows based on the window_ratio.
|
|
89
|
+
|
|
47
90
|
Parameters:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
voxelcity_grid (numpy.ndarray): 3D numpy array representing the voxel grid
|
|
92
|
+
building_id_grid_ori (numpy.ndarray): 2D numpy array containing building IDs
|
|
93
|
+
ids (list/array): Building IDs to process
|
|
94
|
+
mark (int): Material ID value to set for building cells
|
|
95
|
+
window_ratio (float): Value between 0 and 1.0 determining window density:
|
|
96
|
+
~0.125: sparse windows (2,2,2)
|
|
97
|
+
~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
|
|
98
|
+
~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
|
|
99
|
+
~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
|
|
100
|
+
>0.875: maximum density (1,1,1)
|
|
101
|
+
glass_id (int): Material ID for glass/window cells (default: -16)
|
|
59
102
|
|
|
60
103
|
Returns:
|
|
61
|
-
|
|
104
|
+
numpy.ndarray: Modified voxelcity_grid with building materials and windows applied
|
|
62
105
|
"""
|
|
106
|
+
# Flip the building ID grid vertically to match coordinate system
|
|
63
107
|
building_id_grid = np.flipud(building_id_grid_ori.copy())
|
|
64
108
|
|
|
65
|
-
# Get modulo numbers based on window_ratio
|
|
109
|
+
# Get modulo numbers based on window_ratio for pattern generation
|
|
66
110
|
x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
|
|
67
111
|
|
|
68
|
-
#
|
|
112
|
+
# Find all positions where building IDs match the specified IDs
|
|
69
113
|
building_positions = np.where(np.isin(building_id_grid, ids))
|
|
70
114
|
|
|
71
|
-
#
|
|
115
|
+
# Process each building position
|
|
72
116
|
for i in range(len(building_positions[0])):
|
|
73
117
|
x, y = building_positions[0][i], building_positions[1][i]
|
|
118
|
+
|
|
119
|
+
# Set base building material for all voxels at this x,y position
|
|
120
|
+
# Only modify voxels that are currently marked as "unknown" (-3)
|
|
74
121
|
z_mask = voxelcity_grid[x, y, :] == -3
|
|
75
122
|
voxelcity_grid[x, y, z_mask] = mark
|
|
76
123
|
|
|
77
|
-
#
|
|
124
|
+
# Apply window pattern if position meets modulo conditions
|
|
78
125
|
if x % x_mod == 0 and y % y_mod == 0:
|
|
126
|
+
# Find all z positions with the building material
|
|
79
127
|
z_mask = voxelcity_grid[x, y, :] == mark
|
|
80
128
|
if np.any(z_mask):
|
|
81
|
-
#
|
|
129
|
+
# Get z indices and find the maximum (top floor)
|
|
82
130
|
z_indices = np.where(z_mask)[0]
|
|
83
131
|
max_z_index = np.max(z_indices)
|
|
84
132
|
|
|
85
|
-
# Create base mask excluding
|
|
133
|
+
# Create base mask excluding the top floor
|
|
134
|
+
# This ensures the roof never gets windows
|
|
86
135
|
base_mask = z_mask.copy()
|
|
87
136
|
base_mask[max_z_index] = False
|
|
88
137
|
|
|
89
|
-
# Create pattern
|
|
138
|
+
# Create window pattern based on z modulo
|
|
90
139
|
pattern_mask = np.zeros_like(z_mask)
|
|
91
140
|
valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
|
|
92
141
|
if len(valid_z_indices) > 0:
|
|
142
|
+
# Apply z modulo pattern to create vertical window spacing
|
|
93
143
|
pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
|
|
94
144
|
|
|
95
|
-
# For
|
|
145
|
+
# For higher window ratios, add additional window pattern
|
|
146
|
+
# This creates denser window arrangements for buildings with more windows
|
|
96
147
|
if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
|
|
97
148
|
additional_pattern = np.zeros_like(z_mask)
|
|
98
149
|
additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
|
|
150
|
+
# Combine patterns using logical OR to increase window density
|
|
99
151
|
pattern_mask = np.logical_or(pattern_mask, additional_pattern)
|
|
100
152
|
|
|
101
|
-
#
|
|
153
|
+
# Combine base mask (excluding top floor) with pattern mask
|
|
102
154
|
final_glass_mask = np.logical_and(base_mask, pattern_mask)
|
|
103
155
|
|
|
104
|
-
# Set
|
|
156
|
+
# Set glass material for all positions matching the final mask
|
|
105
157
|
voxelcity_grid[x, y, final_glass_mask] = glass_id
|
|
106
158
|
|
|
107
159
|
return voxelcity_grid
|
|
@@ -110,27 +162,42 @@ def set_building_material_by_gdf(voxelcity_grid_ori, building_id_grid, gdf_build
|
|
|
110
162
|
"""
|
|
111
163
|
Sets building materials based on a GeoDataFrame containing building information.
|
|
112
164
|
|
|
165
|
+
This function iterates through a GeoDataFrame of building data and applies
|
|
166
|
+
materials and window patterns to the corresponding buildings in the voxel grid.
|
|
167
|
+
It handles missing material information by defaulting to 'unknown' material.
|
|
168
|
+
|
|
113
169
|
Parameters:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
170
|
+
voxelcity_grid_ori (numpy.ndarray): 3D numpy array of the original voxel grid
|
|
171
|
+
building_id_grid (numpy.ndarray): 2D numpy array containing building IDs
|
|
172
|
+
gdf_buildings (GeoDataFrame): Building information with required columns:
|
|
173
|
+
'building_id': Unique identifier for each building
|
|
174
|
+
'surface_material': Material type (brick, wood, concrete, etc.)
|
|
175
|
+
'window_ratio': Float between 0-1 for window density
|
|
176
|
+
material_id_dict (dict, optional): Dictionary mapping material names to IDs.
|
|
177
|
+
If None, uses default from get_material_dict()
|
|
119
178
|
|
|
120
179
|
Returns:
|
|
121
|
-
|
|
180
|
+
numpy.ndarray: Modified voxelcity_grid with all building materials and windows applied
|
|
122
181
|
"""
|
|
182
|
+
# Create a copy to avoid modifying the original grid
|
|
123
183
|
voxelcity_grid = voxelcity_grid_ori.copy()
|
|
184
|
+
|
|
185
|
+
# Use default material dictionary if none provided
|
|
124
186
|
if material_id_dict == None:
|
|
125
187
|
material_id_dict = get_material_dict()
|
|
126
188
|
|
|
189
|
+
# Process each building in the GeoDataFrame
|
|
127
190
|
for index, row in gdf_buildings.iterrows():
|
|
128
|
-
#
|
|
191
|
+
# Extract building properties from the current row
|
|
129
192
|
osmid = row['building_id']
|
|
130
193
|
surface_material = row['surface_material']
|
|
131
194
|
window_ratio = row['window_ratio']
|
|
195
|
+
|
|
196
|
+
# Handle missing surface material data
|
|
132
197
|
if surface_material is None:
|
|
133
|
-
surface_material = 'unknown'
|
|
198
|
+
surface_material = 'unknown'
|
|
199
|
+
|
|
200
|
+
# Apply material and window pattern to this building
|
|
134
201
|
set_building_material_by_id(voxelcity_grid, building_id_grid, osmid,
|
|
135
202
|
material_id_dict[surface_material],
|
|
136
203
|
window_ratio=window_ratio,
|