voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
- voxcity/__init__.py +14 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
voxcity/utils/__init__.py
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
from .lc import *
|
|
2
2
|
from .weather import *
|
|
3
3
|
from .material import *
|
|
4
|
+
from .classes import (
|
|
5
|
+
VOXEL_CODES,
|
|
6
|
+
LAND_COVER_CLASSES,
|
|
7
|
+
print_voxel_codes,
|
|
8
|
+
print_land_cover_classes,
|
|
9
|
+
print_class_definitions,
|
|
10
|
+
get_land_cover_name,
|
|
11
|
+
get_voxel_code_name,
|
|
12
|
+
summarize_voxel_grid,
|
|
13
|
+
summarize_land_cover_grid,
|
|
14
|
+
)
|
voxcity/utils/classes.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VoxCity Class Definitions
|
|
3
|
+
|
|
4
|
+
This module provides standard class definitions for voxel grid semantics
|
|
5
|
+
and land cover classification used throughout VoxCity.
|
|
6
|
+
|
|
7
|
+
Voxel Grid Semantics:
|
|
8
|
+
The 3D voxel grid uses integer codes to represent different urban elements.
|
|
9
|
+
Negative values represent structural elements, while positive values
|
|
10
|
+
represent land cover classes at the ground surface layer.
|
|
11
|
+
|
|
12
|
+
Land Cover Classes:
|
|
13
|
+
Land cover is standardized to a 1-based indexing system (1-14) for
|
|
14
|
+
consistency across different data sources. This standard classification
|
|
15
|
+
is used in the voxel representation and exports.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from typing import Dict, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Voxel Semantic Codes
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
VOXEL_CODES = {
|
|
27
|
+
-3: "Building",
|
|
28
|
+
-2: "Tree Canopy",
|
|
29
|
+
-1: "Ground/Subsurface",
|
|
30
|
+
# Positive values (>=1) represent land cover classes at ground surface
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
VOXEL_CODE_DESCRIPTIONS = """
|
|
34
|
+
Voxel Grid Semantic Codes:
|
|
35
|
+
-3 : Building volume
|
|
36
|
+
-2 : Tree canopy (vegetation)
|
|
37
|
+
-1 : Ground/Subsurface
|
|
38
|
+
>=1: Land cover class at ground surface (see Land Cover Classes)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Standard Land Cover Classes (1-based indices)
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
LAND_COVER_CLASSES = {
|
|
47
|
+
1: "Bareland",
|
|
48
|
+
2: "Rangeland",
|
|
49
|
+
3: "Shrub",
|
|
50
|
+
4: "Agriculture land",
|
|
51
|
+
5: "Tree",
|
|
52
|
+
6: "Moss and lichen",
|
|
53
|
+
7: "Wet land",
|
|
54
|
+
8: "Mangrove",
|
|
55
|
+
9: "Water",
|
|
56
|
+
10: "Snow and ice",
|
|
57
|
+
11: "Developed space",
|
|
58
|
+
12: "Road",
|
|
59
|
+
13: "Building",
|
|
60
|
+
14: "No Data",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
LAND_COVER_DESCRIPTIONS = """
|
|
64
|
+
VoxCity Standard Land Cover Classes (1-based indices, used in voxel grids):
|
|
65
|
+
--------------------------------------------------
|
|
66
|
+
1: Bareland - Bare soil, rocks, desert
|
|
67
|
+
2: Rangeland - Grassland, pasture
|
|
68
|
+
3: Shrub - Shrubland, bushes
|
|
69
|
+
4: Agriculture land - Cropland, farmland
|
|
70
|
+
5: Tree - Forest, tree cover
|
|
71
|
+
6: Moss and lichen - Moss, lichen cover
|
|
72
|
+
7: Wet land - Wetland, marsh
|
|
73
|
+
8: Mangrove - Mangrove forest
|
|
74
|
+
9: Water - Water bodies
|
|
75
|
+
10: Snow and ice - Snow, ice, glaciers
|
|
76
|
+
11: Developed space - Urban areas, parking
|
|
77
|
+
12: Road - Roads, paved surfaces
|
|
78
|
+
13: Building - Building footprints
|
|
79
|
+
14: No Data - Missing or invalid data
|
|
80
|
+
--------------------------------------------------
|
|
81
|
+
Note: Source-specific land cover classes are converted to these
|
|
82
|
+
standard classes during voxelization.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Print Helper Functions
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
def print_voxel_codes() -> None:
|
|
91
|
+
"""Print voxel semantic codes to console."""
|
|
92
|
+
print(VOXEL_CODE_DESCRIPTIONS)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def print_land_cover_classes() -> None:
|
|
96
|
+
"""Print standard land cover class definitions to console."""
|
|
97
|
+
print(LAND_COVER_DESCRIPTIONS)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_class_definitions() -> None:
|
|
101
|
+
"""Print both voxel codes and land cover class definitions."""
|
|
102
|
+
print("\n" + "=" * 60)
|
|
103
|
+
print("VoxCity Class Definitions")
|
|
104
|
+
print("=" * 60)
|
|
105
|
+
print(VOXEL_CODE_DESCRIPTIONS)
|
|
106
|
+
print(LAND_COVER_DESCRIPTIONS)
|
|
107
|
+
print("=" * 60 + "\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_land_cover_name(index: int) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Get the land cover class name for a given index.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
index: Land cover class index (1-14)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Class name string, or "Unknown" if index is invalid
|
|
119
|
+
"""
|
|
120
|
+
return LAND_COVER_CLASSES.get(index, "Unknown")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_voxel_code_name(code: int) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Get the semantic name for a voxel code.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
code: Voxel code (negative for structures, positive for land cover)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Semantic name string
|
|
132
|
+
"""
|
|
133
|
+
if code in VOXEL_CODES:
|
|
134
|
+
return VOXEL_CODES[code]
|
|
135
|
+
elif code >= 1:
|
|
136
|
+
return f"Land Cover: {get_land_cover_name(code)}"
|
|
137
|
+
elif code == 0:
|
|
138
|
+
return "Empty/Air"
|
|
139
|
+
else:
|
|
140
|
+
return "Unknown"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def summarize_voxel_grid(voxel_grid: np.ndarray, print_output: bool = True) -> Dict[int, int]:
|
|
144
|
+
"""
|
|
145
|
+
Summarize the contents of a voxel grid.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
voxel_grid: 3D numpy array of voxel codes
|
|
149
|
+
print_output: Whether to print the summary
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary mapping voxel codes to counts
|
|
153
|
+
"""
|
|
154
|
+
unique, counts = np.unique(voxel_grid, return_counts=True)
|
|
155
|
+
summary = dict(zip(unique.tolist(), counts.tolist()))
|
|
156
|
+
|
|
157
|
+
if print_output:
|
|
158
|
+
print("\nVoxel Grid Summary:")
|
|
159
|
+
print("-" * 40)
|
|
160
|
+
for code in sorted(summary.keys()):
|
|
161
|
+
name = get_voxel_code_name(code)
|
|
162
|
+
count = summary[code]
|
|
163
|
+
percentage = 100.0 * count / voxel_grid.size
|
|
164
|
+
print(f" {code:4d}: {name:25s} - {count:,} voxels ({percentage:.2f}%)")
|
|
165
|
+
print("-" * 40)
|
|
166
|
+
|
|
167
|
+
return summary
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def summarize_land_cover_grid(land_cover_grid: np.ndarray, print_output: bool = True) -> Dict[int, int]:
|
|
171
|
+
"""
|
|
172
|
+
Summarize the contents of a land cover grid.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
land_cover_grid: 2D numpy array of land cover class indices
|
|
176
|
+
print_output: Whether to print the summary
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dictionary mapping class indices to counts
|
|
180
|
+
"""
|
|
181
|
+
unique, counts = np.unique(land_cover_grid, return_counts=True)
|
|
182
|
+
summary = dict(zip(unique.tolist(), counts.tolist()))
|
|
183
|
+
|
|
184
|
+
if print_output:
|
|
185
|
+
print("\nLand Cover Grid Summary:")
|
|
186
|
+
print("-" * 40)
|
|
187
|
+
for idx in sorted(summary.keys()):
|
|
188
|
+
name = get_land_cover_name(idx) if idx >= 1 else f"Source-specific ({idx})"
|
|
189
|
+
count = summary[idx]
|
|
190
|
+
percentage = 100.0 * count / land_cover_grid.size
|
|
191
|
+
print(f" {idx:4d}: {name:25s} - {count:,} cells ({percentage:.2f}%)")
|
|
192
|
+
print("-" * 40)
|
|
193
|
+
|
|
194
|
+
return summary
|
voxcity/utils/lc.py
CHANGED
|
@@ -34,21 +34,22 @@ def rgb_distance(color1, color2):
|
|
|
34
34
|
return np.sqrt(np.sum((np.array(color1) - np.array(color2))**2))
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# Standard land cover classes mapping (1-based indices for voxel representation)
|
|
38
38
|
# land_cover_classes = {
|
|
39
|
-
# (128, 0, 0): 'Bareland',
|
|
40
|
-
# (0, 255, 36): 'Rangeland',
|
|
41
|
-
# (97, 140, 86): 'Shrub',
|
|
42
|
-
# (75, 181, 73): 'Agriculture land',
|
|
43
|
-
# (34, 97, 38): 'Tree',
|
|
44
|
-
# (
|
|
45
|
-
# (
|
|
46
|
-
# (
|
|
47
|
-
# (
|
|
48
|
-
# (
|
|
49
|
-
# (
|
|
50
|
-
# (
|
|
51
|
-
# (
|
|
39
|
+
# (128, 0, 0): 'Bareland', 1
|
|
40
|
+
# (0, 255, 36): 'Rangeland', 2
|
|
41
|
+
# (97, 140, 86): 'Shrub', 3
|
|
42
|
+
# (75, 181, 73): 'Agriculture land', 4
|
|
43
|
+
# (34, 97, 38): 'Tree', 5
|
|
44
|
+
# (255, 255, 0): 'Moss and lichen', 6
|
|
45
|
+
# (77, 118, 99): 'Wet land', 7
|
|
46
|
+
# (22, 61, 51): 'Mangrove', 8
|
|
47
|
+
# (0, 69, 255): 'Water', 9
|
|
48
|
+
# (205, 215, 224): 'Snow and ice', 10
|
|
49
|
+
# (148, 148, 148): 'Developed space', 11
|
|
50
|
+
# (255, 255, 255): 'Road', 12
|
|
51
|
+
# (222, 31, 7): 'Building', 13
|
|
52
|
+
# (128, 0, 0): 'No Data', 14
|
|
52
53
|
# }
|
|
53
54
|
|
|
54
55
|
def get_land_cover_classes(source):
|
|
@@ -161,22 +162,58 @@ def get_land_cover_classes(source):
|
|
|
161
162
|
}
|
|
162
163
|
return land_cover_classes
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
|
|
166
|
+
def get_source_class_descriptions(source):
|
|
167
|
+
"""
|
|
168
|
+
Get a formatted string describing land cover classes for a specific source.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
source (str): Name of the land cover data source.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
str: Formatted string describing the source's land cover classes.
|
|
175
|
+
"""
|
|
176
|
+
land_cover_classes = get_land_cover_classes(source)
|
|
177
|
+
# Get unique class names (values from the dict)
|
|
178
|
+
class_names = list(dict.fromkeys(land_cover_classes.values()))
|
|
179
|
+
|
|
180
|
+
lines = [f"\n{source} Land Cover Classes (source-specific, 0-based indices):"]
|
|
181
|
+
lines.append("-" * 55)
|
|
182
|
+
for idx, name in enumerate(class_names):
|
|
183
|
+
lines.append(f" {idx:2d}: {name}")
|
|
184
|
+
lines.append("-" * 55)
|
|
185
|
+
|
|
186
|
+
# Special note for OpenStreetMap/Standard which uses same class names
|
|
187
|
+
if source in ("OpenStreetMap", "Standard"):
|
|
188
|
+
lines.append("Note: OpenStreetMap uses VoxCity Standard class names.")
|
|
189
|
+
lines.append(" Indices shift from 0-based to 1-based during voxelization.")
|
|
190
|
+
else:
|
|
191
|
+
lines.append("Note: These source-specific classes will be converted to")
|
|
192
|
+
lines.append(" VoxCity Standard Classes (1-14) during voxelization.")
|
|
193
|
+
|
|
194
|
+
lines.append("")
|
|
195
|
+
lines.append("Access 2D land cover grid from VoxCity object:")
|
|
196
|
+
lines.append(" land_cover_grid = voxcity.land_cover.classes")
|
|
197
|
+
lines.append("")
|
|
198
|
+
return "\n".join(lines)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Standard land cover classes with numeric indices (1-based for voxel representation)
|
|
165
202
|
# land_cover_classes = {
|
|
166
|
-
# (128, 0, 0): 'Bareland',
|
|
167
|
-
# (0, 255, 36): 'Rangeland',
|
|
168
|
-
# (97, 140, 86): 'Shrub',
|
|
169
|
-
# (75, 181, 73): 'Agriculture land',
|
|
170
|
-
# (34, 97, 38): 'Tree',
|
|
171
|
-
# (
|
|
172
|
-
# (77, 118, 99): 'Wet land',
|
|
173
|
-
# (22, 61, 51): 'Mangrove',
|
|
174
|
-
# (0, 69, 255): 'Water',
|
|
175
|
-
# (205, 215, 224): 'Snow and ice',
|
|
176
|
-
# (148, 148, 148): 'Developed space',
|
|
177
|
-
# (255, 255, 255): 'Road',
|
|
178
|
-
# (222, 31, 7): 'Building',
|
|
179
|
-
# (128, 0, 0): 'No Data',
|
|
203
|
+
# (128, 0, 0): 'Bareland', 1
|
|
204
|
+
# (0, 255, 36): 'Rangeland', 2
|
|
205
|
+
# (97, 140, 86): 'Shrub', 3
|
|
206
|
+
# (75, 181, 73): 'Agriculture land', 4
|
|
207
|
+
# (34, 97, 38): 'Tree', 5
|
|
208
|
+
# (255, 255, 0): 'Moss and lichen', 6
|
|
209
|
+
# (77, 118, 99): 'Wet land', 7
|
|
210
|
+
# (22, 61, 51): 'Mangrove', 8
|
|
211
|
+
# (0, 69, 255): 'Water', 9
|
|
212
|
+
# (205, 215, 224): 'Snow and ice', 10
|
|
213
|
+
# (148, 148, 148): 'Developed space', 11
|
|
214
|
+
# (255, 255, 255): 'Road', 12
|
|
215
|
+
# (222, 31, 7): 'Building', 13
|
|
216
|
+
# (128, 0, 0): 'No Data', 14
|
|
180
217
|
# }
|
|
181
218
|
|
|
182
219
|
|
|
@@ -185,21 +222,23 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
|
|
|
185
222
|
"""
|
|
186
223
|
Optimized version using direct numpy array indexing instead of np.vectorize.
|
|
187
224
|
This is 10-100x faster than the original.
|
|
225
|
+
|
|
226
|
+
Returns 1-based class indices (1-14) for consistency with voxel representations.
|
|
188
227
|
"""
|
|
189
|
-
# Define mappings
|
|
228
|
+
# Define mappings (1-based indices: 1=Bareland, 2=Rangeland, ..., 14=No Data)
|
|
190
229
|
if land_cover_source == 'Urbanwatch':
|
|
191
|
-
mapping = {0:
|
|
230
|
+
mapping = {0: 13, 1: 12, 2: 11, 3: 5, 4: 2, 5: 4, 6: 9, 7: 1, 8: 14, 9: 9}
|
|
192
231
|
elif land_cover_source == 'ESA WorldCover':
|
|
193
|
-
mapping = {0:
|
|
232
|
+
mapping = {0: 5, 1: 3, 2: 2, 3: 4, 4: 11, 5: 1, 6: 10, 7: 9, 8: 7, 9: 8, 10: 6}
|
|
194
233
|
elif land_cover_source == "ESRI 10m Annual Land Cover":
|
|
195
|
-
mapping = {0:
|
|
234
|
+
mapping = {0: 14, 1: 9, 2: 5, 3: 2, 4: 7, 5: 4, 6: 3, 7: 11, 8: 1, 9: 10, 10: 14}
|
|
196
235
|
elif land_cover_source == "Dynamic World V1":
|
|
197
|
-
mapping = {0:
|
|
236
|
+
mapping = {0: 9, 1: 5, 2: 2, 3: 7, 4: 4, 5: 3, 6: 11, 7: 1, 8: 10}
|
|
198
237
|
elif land_cover_source == "OpenEarthMapJapan":
|
|
199
|
-
mapping = {0:
|
|
238
|
+
mapping = {0: 1, 1: 2, 2: 11, 3: 12, 4: 5, 5: 9, 6: 4, 7: 13}
|
|
200
239
|
else:
|
|
201
|
-
# If unknown source, return as-is
|
|
202
|
-
return input_array.copy()
|
|
240
|
+
# If unknown source, return as-is with +1 offset for consistency
|
|
241
|
+
return input_array.copy() + 1
|
|
203
242
|
|
|
204
243
|
# Create a full mapping array for all possible values (0-255 for uint8)
|
|
205
244
|
max_val = max(max(mapping.keys()), input_array.max()) + 1
|
|
@@ -374,12 +413,14 @@ def convert_land_cover_array(input_array, land_cover_classes):
|
|
|
374
413
|
land_cover_classes (dict): Dictionary mapping RGB tuples to class names
|
|
375
414
|
|
|
376
415
|
Returns:
|
|
377
|
-
numpy.ndarray: Array with integer indices corresponding to land cover classes
|
|
416
|
+
numpy.ndarray: Array with 0-based integer indices corresponding to land cover classes
|
|
378
417
|
|
|
379
418
|
Note:
|
|
380
419
|
Classes not found in the mapping are assigned index -1
|
|
420
|
+
Indices are 0-based as source-specific indices. Use convert_land_cover() to
|
|
421
|
+
remap to standard 1-based indices for voxel representation.
|
|
381
422
|
"""
|
|
382
|
-
# Create a mapping of class names to integers
|
|
423
|
+
# Create a mapping of class names to integers (0-based, source-specific order)
|
|
383
424
|
class_to_int = {name: i for i, name in enumerate(land_cover_classes.values())}
|
|
384
425
|
|
|
385
426
|
# Create a vectorized function to map string values to integers
|
voxcity/utils/shape.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Tuple, Optional, Dict, Any, Callable
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..models import VoxCity, VoxelGrid, BuildingGrid, LandCoverGrid, DemGrid, CanopyGrid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _compute_center_crop_indices(size: int, target: int) -> Tuple[int, int]:
|
|
11
|
+
if size <= target:
|
|
12
|
+
return 0, size
|
|
13
|
+
start = max(0, (size - target) // 2)
|
|
14
|
+
end = start + target
|
|
15
|
+
return start, end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pad_split(total_pad: int) -> Tuple[int, int]:
|
|
19
|
+
# Split padding for centering; put extra on the bottom/right side
|
|
20
|
+
a = total_pad // 2
|
|
21
|
+
b = total_pad - a
|
|
22
|
+
return a, b
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _pad_crop_2d(
|
|
26
|
+
arr: np.ndarray,
|
|
27
|
+
target_xy: Tuple[int, int],
|
|
28
|
+
pad_value: Any,
|
|
29
|
+
align_xy: str = "center",
|
|
30
|
+
allow_crop_xy: bool = True,
|
|
31
|
+
) -> np.ndarray:
|
|
32
|
+
x, y = arr.shape[:2]
|
|
33
|
+
tx, ty = int(target_xy[0]), int(target_xy[1])
|
|
34
|
+
|
|
35
|
+
# Crop (center) if needed and allowed
|
|
36
|
+
if allow_crop_xy and (x > tx or y > ty):
|
|
37
|
+
if align_xy == "center":
|
|
38
|
+
xs, xe = _compute_center_crop_indices(x, tx) if x > tx else (0, x)
|
|
39
|
+
ys, ye = _compute_center_crop_indices(y, ty) if y > ty else (0, y)
|
|
40
|
+
else:
|
|
41
|
+
# top-left alignment: crop from bottom/right only
|
|
42
|
+
xs, xe = (0, tx) if x > tx else (0, x)
|
|
43
|
+
ys, ye = (0, ty) if y > ty else (0, y)
|
|
44
|
+
arr = arr[xs:xe, ys:ye]
|
|
45
|
+
x, y = arr.shape[:2]
|
|
46
|
+
|
|
47
|
+
# Pad to target
|
|
48
|
+
px = max(0, tx - x)
|
|
49
|
+
py = max(0, ty - y)
|
|
50
|
+
|
|
51
|
+
if px == 0 and py == 0:
|
|
52
|
+
return arr
|
|
53
|
+
|
|
54
|
+
if align_xy == "center":
|
|
55
|
+
px0, px1 = _pad_split(px)
|
|
56
|
+
py0, py1 = _pad_split(py)
|
|
57
|
+
else:
|
|
58
|
+
# top-left: pad only on bottom/right
|
|
59
|
+
px0, px1 = 0, px
|
|
60
|
+
py0, py1 = 0, py
|
|
61
|
+
|
|
62
|
+
if arr.ndim == 2:
|
|
63
|
+
pad_width = ((px0, px1), (py0, py1))
|
|
64
|
+
else:
|
|
65
|
+
# Preserve trailing dims (e.g., channels)
|
|
66
|
+
pad_width = ((px0, px1), (py0, py1)) + tuple((0, 0) for _ in range(arr.ndim - 2))
|
|
67
|
+
|
|
68
|
+
return np.pad(arr, pad_width, mode="constant", constant_values=pad_value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _pad_crop_3d_zbottom(
|
|
72
|
+
arr: np.ndarray,
|
|
73
|
+
target_shape: Tuple[int, int, int],
|
|
74
|
+
pad_value: Any,
|
|
75
|
+
align_xy: str = "center",
|
|
76
|
+
allow_crop_xy: bool = True,
|
|
77
|
+
allow_crop_z: bool = False,
|
|
78
|
+
) -> np.ndarray:
|
|
79
|
+
nx, ny, nz = arr.shape
|
|
80
|
+
tx, ty, tz = int(target_shape[0]), int(target_shape[1]), int(target_shape[2])
|
|
81
|
+
|
|
82
|
+
# XY crop/pad
|
|
83
|
+
arr_xy = _pad_crop_2d(arr, (tx, ty), pad_value, align_xy=align_xy, allow_crop_xy=allow_crop_xy)
|
|
84
|
+
nx, ny, nz = arr_xy.shape
|
|
85
|
+
|
|
86
|
+
# Z handling: keep ground at z=0; pad only at the top by default
|
|
87
|
+
if nz > tz:
|
|
88
|
+
if allow_crop_z:
|
|
89
|
+
arr_xy = arr_xy[:, :, :tz]
|
|
90
|
+
else:
|
|
91
|
+
tz = nz # expand target to avoid cropping
|
|
92
|
+
elif nz < tz:
|
|
93
|
+
pad_top = tz - nz # add empty air above
|
|
94
|
+
arr_xy = np.pad(arr_xy, ((0, 0), (0, 0), (0, pad_top)), mode="constant", constant_values=pad_value)
|
|
95
|
+
|
|
96
|
+
return arr_xy
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_voxcity_shape(
|
|
100
|
+
city: VoxCity,
|
|
101
|
+
target_shape: Tuple[int, int, int],
|
|
102
|
+
*,
|
|
103
|
+
align_xy: str = "center",
|
|
104
|
+
pad_values: Optional[Dict[str, Any]] = None,
|
|
105
|
+
allow_crop_xy: bool = True,
|
|
106
|
+
allow_crop_z: bool = False,
|
|
107
|
+
) -> VoxCity:
|
|
108
|
+
"""
|
|
109
|
+
Return a new VoxCity with arrays padded/cropped to target (x, y, z).
|
|
110
|
+
|
|
111
|
+
- XY alignment can be 'center' (default) or 'top-left'.
|
|
112
|
+
- Z padding is added at the TOP to preserve ground level at z=0.
|
|
113
|
+
- By default, Z is never cropped (allow_crop_z=False). If target z is smaller than current,
|
|
114
|
+
target z is expanded to current to avoid losing data.
|
|
115
|
+
"""
|
|
116
|
+
if pad_values is None:
|
|
117
|
+
pad_values = {}
|
|
118
|
+
|
|
119
|
+
# Resolve pad values for each layer
|
|
120
|
+
pv_vox = pad_values.get("voxels", 0)
|
|
121
|
+
pv_lc = pad_values.get("land_cover", 0)
|
|
122
|
+
pv_dem = pad_values.get("dem", 0.0)
|
|
123
|
+
pv_bh = pad_values.get("building_heights", 0.0)
|
|
124
|
+
pv_bid = pad_values.get("building_ids", 0)
|
|
125
|
+
pv_canopy = pad_values.get("canopy", 0.0)
|
|
126
|
+
pv_bmin = pad_values.get("building_min_heights_factory", None) # callable creating empty cell, default []
|
|
127
|
+
if pv_bmin is None:
|
|
128
|
+
def _empty_list() -> list:
|
|
129
|
+
return []
|
|
130
|
+
pv_bmin = _empty_list
|
|
131
|
+
elif not callable(pv_bmin):
|
|
132
|
+
const_val = pv_bmin
|
|
133
|
+
pv_bmin = (lambda v=const_val: v)
|
|
134
|
+
|
|
135
|
+
# Source arrays
|
|
136
|
+
vox = city.voxels.classes
|
|
137
|
+
bh = city.buildings.heights
|
|
138
|
+
bmin = city.buildings.min_heights
|
|
139
|
+
bid = city.buildings.ids
|
|
140
|
+
lc = city.land_cover.classes
|
|
141
|
+
dem = city.dem.elevation
|
|
142
|
+
can_top = city.tree_canopy.top
|
|
143
|
+
can_bot = city.tree_canopy.bottom if city.tree_canopy.bottom is not None else None
|
|
144
|
+
|
|
145
|
+
# Normalize shapes
|
|
146
|
+
vox_n = _pad_crop_3d_zbottom(
|
|
147
|
+
vox.astype(vox.dtype, copy=False),
|
|
148
|
+
target_shape,
|
|
149
|
+
pad_value=np.array(pv_vox, dtype=vox.dtype),
|
|
150
|
+
align_xy=align_xy,
|
|
151
|
+
allow_crop_xy=allow_crop_xy,
|
|
152
|
+
allow_crop_z=allow_crop_z,
|
|
153
|
+
)
|
|
154
|
+
tx, ty, tz = vox_n.shape
|
|
155
|
+
|
|
156
|
+
def _pad2d(a: np.ndarray, pad_val: Any) -> np.ndarray:
|
|
157
|
+
return _pad_crop_2d(
|
|
158
|
+
a.astype(a.dtype, copy=False),
|
|
159
|
+
(tx, ty),
|
|
160
|
+
pad_value=np.array(pad_val, dtype=a.dtype) if not isinstance(pad_val, (list, tuple, dict)) else pad_val,
|
|
161
|
+
align_xy=align_xy,
|
|
162
|
+
allow_crop_xy=allow_crop_xy,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
bh_n = _pad2d(bh, pv_bh)
|
|
166
|
+
bid_n = _pad2d(bid, pv_bid)
|
|
167
|
+
lc_n = _pad2d(lc, pv_lc)
|
|
168
|
+
dem_n = _pad2d(dem, pv_dem)
|
|
169
|
+
can_top_n = _pad2d(can_top, pv_canopy)
|
|
170
|
+
can_bot_n = _pad2d(can_bot, pv_canopy) if can_bot is not None else None # type: ignore
|
|
171
|
+
|
|
172
|
+
# Object-dtype 2D padding/cropping for min_heights
|
|
173
|
+
# Center-crop if needed, then pad with empty lists
|
|
174
|
+
bx, by = bmin.shape
|
|
175
|
+
if bx > tx or by > ty:
|
|
176
|
+
if align_xy == "center":
|
|
177
|
+
xs, xe = _compute_center_crop_indices(bx, tx) if bx > tx else (0, bx)
|
|
178
|
+
ys, ye = _compute_center_crop_indices(by, ty) if by > ty else (0, by)
|
|
179
|
+
else:
|
|
180
|
+
xs, xe = (0, tx) if bx > tx else (0, bx)
|
|
181
|
+
ys, ye = (0, ty) if by > ty else (0, by)
|
|
182
|
+
bmin_c = bmin[xs:xe, ys:ye]
|
|
183
|
+
else:
|
|
184
|
+
bmin_c = bmin
|
|
185
|
+
bx, by = bmin_c.shape
|
|
186
|
+
px = max(0, tx - bx)
|
|
187
|
+
py = max(0, ty - by)
|
|
188
|
+
if px or py:
|
|
189
|
+
out = np.empty((tx, ty), dtype=object)
|
|
190
|
+
# Fill with empty factory values
|
|
191
|
+
for i in range(tx):
|
|
192
|
+
for j in range(ty):
|
|
193
|
+
out[i, j] = pv_bmin()
|
|
194
|
+
if align_xy == "center":
|
|
195
|
+
px0, px1 = _pad_split(px)
|
|
196
|
+
py0, py1 = _pad_split(py)
|
|
197
|
+
else:
|
|
198
|
+
px0, py0 = 0, 0
|
|
199
|
+
px1, py1 = px, py
|
|
200
|
+
out[px0:px0 + bx, py0:py0 + by] = bmin_c
|
|
201
|
+
bmin_n = out
|
|
202
|
+
else:
|
|
203
|
+
bmin_n = bmin_c
|
|
204
|
+
|
|
205
|
+
# Rebuild VoxCity with normalized arrays and same metadata (meshsize/bounds)
|
|
206
|
+
meta = city.voxels.meta
|
|
207
|
+
voxels_new = VoxelGrid(classes=vox_n, meta=meta)
|
|
208
|
+
buildings_new = BuildingGrid(heights=bh_n, min_heights=bmin_n, ids=bid_n, meta=meta)
|
|
209
|
+
land_new = LandCoverGrid(classes=lc_n, meta=meta)
|
|
210
|
+
dem_new = DemGrid(elevation=dem_n, meta=meta)
|
|
211
|
+
canopy_new = CanopyGrid(top=can_top_n, bottom=can_bot_n, meta=meta)
|
|
212
|
+
|
|
213
|
+
city_new = VoxCity(
|
|
214
|
+
voxels=voxels_new,
|
|
215
|
+
buildings=buildings_new,
|
|
216
|
+
land_cover=land_new,
|
|
217
|
+
dem=dem_new,
|
|
218
|
+
tree_canopy=canopy_new,
|
|
219
|
+
extras=dict(city.extras) if city.extras is not None else {},
|
|
220
|
+
)
|
|
221
|
+
# Keep extras canopy mirrors in sync if present
|
|
222
|
+
try:
|
|
223
|
+
city_new.extras["canopy_top"] = can_top_n
|
|
224
|
+
if can_bot_n is not None:
|
|
225
|
+
city_new.extras["canopy_bottom"] = can_bot_n
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
return city_new
|
|
229
|
+
|
|
230
|
+
|
voxcity/visualizer/__init__.py
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
from .builder import MeshBuilder
|
|
2
|
-
from .renderer import PyVistaRenderer, create_multi_view_scene, visualize_voxcity_plotly, visualize_voxcity
|
|
3
|
-
from .palette import get_voxel_color_map
|
|
4
|
-
from .grids import visualize_landcover_grid_on_basemap, visualize_numerical_grid_on_basemap, visualize_numerical_gdf_on_basemap, visualize_point_gdf_on_basemap
|
|
5
|
-
from .maps import plot_grid, visualize_land_cover_grid_on_map, visualize_building_height_grid_on_map, visualize_numerical_grid_on_map
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"MeshBuilder",
|
|
9
|
-
"PyVistaRenderer",
|
|
10
|
-
"create_multi_view_scene",
|
|
11
|
-
"visualize_voxcity_plotly",
|
|
12
|
-
"visualize_voxcity",
|
|
13
|
-
"get_voxel_color_map",
|
|
14
|
-
"visualize_landcover_grid_on_basemap",
|
|
15
|
-
"visualize_numerical_grid_on_basemap",
|
|
16
|
-
"visualize_numerical_gdf_on_basemap",
|
|
17
|
-
"visualize_point_gdf_on_basemap",
|
|
18
|
-
"plot_grid",
|
|
19
|
-
"visualize_land_cover_grid_on_map",
|
|
20
|
-
"visualize_building_height_grid_on_map",
|
|
21
|
-
"visualize_numerical_grid_on_map",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
from .builder import MeshBuilder
|
|
2
|
+
from .renderer import PyVistaRenderer, create_multi_view_scene, visualize_voxcity_plotly, visualize_voxcity
|
|
3
|
+
from .palette import get_voxel_color_map
|
|
4
|
+
from .grids import visualize_landcover_grid_on_basemap, visualize_numerical_grid_on_basemap, visualize_numerical_gdf_on_basemap, visualize_point_gdf_on_basemap
|
|
5
|
+
from .maps import plot_grid, visualize_land_cover_grid_on_map, visualize_building_height_grid_on_map, visualize_numerical_grid_on_map
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"MeshBuilder",
|
|
9
|
+
"PyVistaRenderer",
|
|
10
|
+
"create_multi_view_scene",
|
|
11
|
+
"visualize_voxcity_plotly",
|
|
12
|
+
"visualize_voxcity",
|
|
13
|
+
"get_voxel_color_map",
|
|
14
|
+
"visualize_landcover_grid_on_basemap",
|
|
15
|
+
"visualize_numerical_grid_on_basemap",
|
|
16
|
+
"visualize_numerical_gdf_on_basemap",
|
|
17
|
+
"visualize_point_gdf_on_basemap",
|
|
18
|
+
"plot_grid",
|
|
19
|
+
"visualize_land_cover_grid_on_map",
|
|
20
|
+
"visualize_building_height_grid_on_map",
|
|
21
|
+
"visualize_numerical_grid_on_map",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|