voxcity 0.5.9__py3-none-any.whl → 0.5.11__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/generator.py +102 -1
- voxcity/geoprocessor/mesh.py +105 -1
- voxcity/utils/visualization.py +62 -116
- {voxcity-0.5.9.dist-info → voxcity-0.5.11.dist-info}/METADATA +5 -1
- {voxcity-0.5.9.dist-info → voxcity-0.5.11.dist-info}/RECORD +9 -9
- {voxcity-0.5.9.dist-info → voxcity-0.5.11.dist-info}/WHEEL +0 -0
- {voxcity-0.5.9.dist-info → voxcity-0.5.11.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.9.dist-info → voxcity-0.5.11.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.9.dist-info → voxcity-0.5.11.dist-info}/top_level.txt +0 -0
voxcity/generator.py
CHANGED
|
@@ -689,6 +689,14 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
|
|
|
689
689
|
voxcity_grid_vis[-1, -1, -1] = -99 # Add marker to fix camera location and angle of view
|
|
690
690
|
visualize_3d_voxel(voxcity_grid_vis, voxel_size=meshsize, save_path=kwargs["voxelvis_img_save_path"])
|
|
691
691
|
|
|
692
|
+
# Save all data if a save path is provided
|
|
693
|
+
save_voxcity = kwargs.get("save_voxctiy_data", True)
|
|
694
|
+
if save_voxcity:
|
|
695
|
+
save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity_data.pkl")
|
|
696
|
+
save_voxcity_data(save_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
697
|
+
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
698
|
+
building_gdf, meshsize, rectangle_vertices)
|
|
699
|
+
|
|
692
700
|
return voxcity_grid, building_height_grid, building_min_height_grid, building_id_grid, canopy_height_grid, land_cover_grid, dem_grid, building_gdf
|
|
693
701
|
|
|
694
702
|
def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_source, meshsize, url_citygml=None, citygml_path=None, **kwargs):
|
|
@@ -817,6 +825,14 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
|
|
|
817
825
|
# Generate 3D voxel grid
|
|
818
826
|
voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source)
|
|
819
827
|
|
|
828
|
+
# Save all data if a save path is provided
|
|
829
|
+
save_voxcity = kwargs.get("save_voxctiy_data", True)
|
|
830
|
+
if save_voxcity:
|
|
831
|
+
save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity_data.pkl")
|
|
832
|
+
save_voxcity_data(save_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
833
|
+
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
834
|
+
building_gdf, meshsize, rectangle_vertices)
|
|
835
|
+
|
|
820
836
|
return voxcity_grid, building_height_grid, building_min_height_grid, building_id_grid, canopy_height_grid, land_cover_grid, dem_grid, filtered_buildings
|
|
821
837
|
|
|
822
838
|
def replace_nan_in_nested(arr, replace_value=10.0):
|
|
@@ -844,4 +860,89 @@ def replace_nan_in_nested(arr, replace_value=10.0):
|
|
|
844
860
|
if isinstance(arr[i][j][k][l], float) and np.isnan(arr[i][j][k][l]):
|
|
845
861
|
arr[i][j][k][l] = replace_value
|
|
846
862
|
|
|
847
|
-
return np.array(arr, dtype=object)
|
|
863
|
+
return np.array(arr, dtype=object)
|
|
864
|
+
|
|
865
|
+
def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
866
|
+
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|
|
867
|
+
building_gdf, meshsize, rectangle_vertices):
|
|
868
|
+
"""Save voxcity data to a file for later loading.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
output_path: Path to save the data file
|
|
872
|
+
voxcity_grid: 3D voxel grid of the complete city model
|
|
873
|
+
building_height_grid: 2D grid of building heights
|
|
874
|
+
building_min_height_grid: 2D grid of minimum building heights
|
|
875
|
+
building_id_grid: 2D grid of building IDs
|
|
876
|
+
canopy_height_grid: 2D grid of tree canopy heights
|
|
877
|
+
land_cover_grid: 2D grid of land cover classifications
|
|
878
|
+
dem_grid: 2D grid of ground elevation
|
|
879
|
+
building_gdf: GeoDataFrame of building footprints and metadata
|
|
880
|
+
meshsize: Size of each grid cell in meters
|
|
881
|
+
rectangle_vertices: List of coordinates defining the area of interest
|
|
882
|
+
"""
|
|
883
|
+
import pickle
|
|
884
|
+
import os
|
|
885
|
+
|
|
886
|
+
# Create directory if it doesn't exist
|
|
887
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
888
|
+
|
|
889
|
+
# Create a dictionary with all the data
|
|
890
|
+
data_dict = {
|
|
891
|
+
'voxcity_grid': voxcity_grid,
|
|
892
|
+
'building_height_grid': building_height_grid,
|
|
893
|
+
'building_min_height_grid': building_min_height_grid,
|
|
894
|
+
'building_id_grid': building_id_grid,
|
|
895
|
+
'canopy_height_grid': canopy_height_grid,
|
|
896
|
+
'land_cover_grid': land_cover_grid,
|
|
897
|
+
'dem_grid': dem_grid,
|
|
898
|
+
'building_gdf': building_gdf,
|
|
899
|
+
'meshsize': meshsize,
|
|
900
|
+
'rectangle_vertices': rectangle_vertices
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
# Save the data to a file using pickle
|
|
904
|
+
with open(output_path, 'wb') as f:
|
|
905
|
+
pickle.dump(data_dict, f)
|
|
906
|
+
|
|
907
|
+
print(f"Voxcity data saved to {output_path}")
|
|
908
|
+
|
|
909
|
+
def load_voxcity_data(input_path):
|
|
910
|
+
"""Load voxcity data from a saved file.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
input_path: Path to the saved data file
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
tuple: All the voxcity data components including:
|
|
917
|
+
- voxcity_grid: 3D voxel grid of the complete city model
|
|
918
|
+
- building_height_grid: 2D grid of building heights
|
|
919
|
+
- building_min_height_grid: 2D grid of minimum building heights
|
|
920
|
+
- building_id_grid: 2D grid of building IDs
|
|
921
|
+
- canopy_height_grid: 2D grid of tree canopy heights
|
|
922
|
+
- land_cover_grid: 2D grid of land cover classifications
|
|
923
|
+
- dem_grid: 2D grid of ground elevation
|
|
924
|
+
- building_gdf: GeoDataFrame of building footprints and metadata
|
|
925
|
+
- meshsize: Size of each grid cell in meters
|
|
926
|
+
- rectangle_vertices: List of coordinates defining the area of interest
|
|
927
|
+
"""
|
|
928
|
+
import pickle
|
|
929
|
+
|
|
930
|
+
# Load the data from the file
|
|
931
|
+
with open(input_path, 'rb') as f:
|
|
932
|
+
data_dict = pickle.load(f)
|
|
933
|
+
|
|
934
|
+
print(f"Voxcity data loaded from {input_path}")
|
|
935
|
+
|
|
936
|
+
# Return all components as a tuple
|
|
937
|
+
return (
|
|
938
|
+
data_dict['voxcity_grid'],
|
|
939
|
+
data_dict['building_height_grid'],
|
|
940
|
+
data_dict['building_min_height_grid'],
|
|
941
|
+
data_dict['building_id_grid'],
|
|
942
|
+
data_dict['canopy_height_grid'],
|
|
943
|
+
data_dict['land_cover_grid'],
|
|
944
|
+
data_dict['dem_grid'],
|
|
945
|
+
data_dict['building_gdf'],
|
|
946
|
+
data_dict['meshsize'],
|
|
947
|
+
data_dict['rectangle_vertices']
|
|
948
|
+
)
|
voxcity/geoprocessor/mesh.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import os
|
|
2
3
|
import trimesh
|
|
3
4
|
import matplotlib.colors as mcolors
|
|
4
5
|
import matplotlib.cm as cm
|
|
@@ -304,4 +305,107 @@ def export_meshes(meshes, output_directory, base_filename):
|
|
|
304
305
|
# Export individual meshes as STL
|
|
305
306
|
for class_id, mesh in meshes.items():
|
|
306
307
|
# Convert class_id to a string for filename
|
|
307
|
-
mesh.export(f"{output_directory}/{base_filename}_{class_id}.stl")
|
|
308
|
+
mesh.export(f"{output_directory}/{base_filename}_{class_id}.stl")
|
|
309
|
+
|
|
310
|
+
def split_vertices_manual(mesh):
|
|
311
|
+
"""
|
|
312
|
+
Imitate trimesh's split_vertices() by giving each face its own copy of vertices.
|
|
313
|
+
This ensures every face is truly disconnected, preventing smooth shading in Rhino.
|
|
314
|
+
"""
|
|
315
|
+
new_meshes = []
|
|
316
|
+
|
|
317
|
+
# For each face, build a small, one-face mesh
|
|
318
|
+
for face_idx, face in enumerate(mesh.faces):
|
|
319
|
+
face_coords = mesh.vertices[face]
|
|
320
|
+
|
|
321
|
+
# Create mini-mesh without colors first
|
|
322
|
+
mini_mesh = trimesh.Trimesh(
|
|
323
|
+
vertices=face_coords,
|
|
324
|
+
faces=[[0, 1, 2]],
|
|
325
|
+
process=False # skip merging/cleaning
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# If the mesh has per-face colors, set the face color properly
|
|
329
|
+
if (mesh.visual.face_colors is not None
|
|
330
|
+
and len(mesh.visual.face_colors) == len(mesh.faces)):
|
|
331
|
+
# Create a visual object with the face color (for one face)
|
|
332
|
+
face_color = mesh.visual.face_colors[face_idx]
|
|
333
|
+
color_visual = trimesh.visual.ColorVisuals(
|
|
334
|
+
mesh=mini_mesh,
|
|
335
|
+
face_colors=np.array([face_color]), # One face, one color
|
|
336
|
+
vertex_colors=None
|
|
337
|
+
)
|
|
338
|
+
mini_mesh.visual = color_visual
|
|
339
|
+
|
|
340
|
+
new_meshes.append(mini_mesh)
|
|
341
|
+
|
|
342
|
+
# Concatenate all the single-face meshes
|
|
343
|
+
out_mesh = trimesh.util.concatenate(new_meshes)
|
|
344
|
+
return out_mesh
|
|
345
|
+
|
|
346
|
+
def save_obj_from_colored_mesh(meshes, output_path, base_filename):
|
|
347
|
+
"""
|
|
348
|
+
Save colored meshes as OBJ and MTL files.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
meshes : dict
|
|
353
|
+
Dictionary of trimesh.Trimesh objects with face colors.
|
|
354
|
+
output_path : str
|
|
355
|
+
Directory path where to save the files.
|
|
356
|
+
base_filename : str
|
|
357
|
+
Base name for the output files (without extension).
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
tuple
|
|
362
|
+
Paths to the saved (obj_file, mtl_file).
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
os.makedirs(output_path, exist_ok=True)
|
|
366
|
+
obj_path = os.path.join(output_path, f"{base_filename}.obj")
|
|
367
|
+
mtl_path = os.path.join(output_path, f"{base_filename}.mtl")
|
|
368
|
+
|
|
369
|
+
# Combine all meshes
|
|
370
|
+
combined_mesh = trimesh.util.concatenate(list(meshes.values()))
|
|
371
|
+
combined_mesh = split_vertices_manual(combined_mesh)
|
|
372
|
+
|
|
373
|
+
# Create unique materials for each unique face color
|
|
374
|
+
face_colors = combined_mesh.visual.face_colors
|
|
375
|
+
unique_colors = np.unique(face_colors, axis=0)
|
|
376
|
+
|
|
377
|
+
# Write MTL file
|
|
378
|
+
with open(mtl_path, 'w') as mtl_file:
|
|
379
|
+
for i, color in enumerate(unique_colors):
|
|
380
|
+
material_name = f'material_{i}'
|
|
381
|
+
mtl_file.write(f'newmtl {material_name}\n')
|
|
382
|
+
# Convert RGBA to RGB float values
|
|
383
|
+
rgb = color[:3].astype(float) / 255.0
|
|
384
|
+
mtl_file.write(f'Kd {rgb[0]:.6f} {rgb[1]:.6f} {rgb[2]:.6f}\n')
|
|
385
|
+
mtl_file.write(f'd {color[3]/255.0:.6f}\n\n') # Alpha value
|
|
386
|
+
|
|
387
|
+
# Create material groups based on face colors
|
|
388
|
+
color_to_material = {tuple(c): f'material_{i}' for i, c in enumerate(unique_colors)}
|
|
389
|
+
|
|
390
|
+
# Write OBJ file
|
|
391
|
+
with open(obj_path, 'w') as obj_file:
|
|
392
|
+
obj_file.write(f'mtllib {os.path.basename(mtl_path)}\n')
|
|
393
|
+
|
|
394
|
+
# Write vertices
|
|
395
|
+
for vertex in combined_mesh.vertices:
|
|
396
|
+
obj_file.write(f'v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n')
|
|
397
|
+
|
|
398
|
+
# Write faces grouped by material
|
|
399
|
+
current_material = None
|
|
400
|
+
for face_idx, face in enumerate(combined_mesh.faces):
|
|
401
|
+
face_color = tuple(face_colors[face_idx])
|
|
402
|
+
material_name = color_to_material[face_color]
|
|
403
|
+
|
|
404
|
+
if material_name != current_material:
|
|
405
|
+
obj_file.write(f'usemtl {material_name}\n')
|
|
406
|
+
current_material = material_name
|
|
407
|
+
|
|
408
|
+
# OBJ indices are 1-based
|
|
409
|
+
obj_file.write(f'f {face[0]+1} {face[1]+1} {face[2]+1}\n')
|
|
410
|
+
|
|
411
|
+
return obj_path, mtl_path
|
voxcity/utils/visualization.py
CHANGED
|
@@ -41,7 +41,8 @@ from ..geoprocessor.mesh import (
|
|
|
41
41
|
create_voxel_mesh,
|
|
42
42
|
create_sim_surface_mesh,
|
|
43
43
|
create_city_meshes,
|
|
44
|
-
export_meshes
|
|
44
|
+
export_meshes,
|
|
45
|
+
save_obj_from_colored_mesh
|
|
45
46
|
)
|
|
46
47
|
# from ..exporter.obj import save_obj_from_colored_mesh
|
|
47
48
|
from .material import get_material_dict
|
|
@@ -1531,6 +1532,7 @@ def visualize_voxcity_multi_view(voxel_array, meshsize, **kwargs):
|
|
|
1531
1532
|
projection_type = kwargs.get("projection_type", "perspective")
|
|
1532
1533
|
distance_factor = kwargs.get("distance_factor", 1.0)
|
|
1533
1534
|
save_obj = kwargs.get("save_obj", False)
|
|
1535
|
+
show_views = kwargs.get("show_views", True)
|
|
1534
1536
|
|
|
1535
1537
|
# Create meshes
|
|
1536
1538
|
print("Creating voxel meshes...")
|
|
@@ -1567,21 +1569,22 @@ def visualize_voxcity_multi_view(voxel_array, meshsize, **kwargs):
|
|
|
1567
1569
|
os.makedirs(output_directory, exist_ok=True)
|
|
1568
1570
|
export_meshes(meshes, output_directory, base_filename)
|
|
1569
1571
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1572
|
+
if show_views:
|
|
1573
|
+
# Create and save multiple views
|
|
1574
|
+
print("Creating multiple views...")
|
|
1575
|
+
# Create output directory if it doesn't exist
|
|
1576
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
1577
|
+
image_files = create_multi_view_scene(meshes, output_directory=output_directory, projection_type=projection_type, distance_factor=distance_factor)
|
|
1578
|
+
|
|
1579
|
+
# Display each view separately
|
|
1580
|
+
for view_name, img_file in image_files:
|
|
1581
|
+
plt.figure(figsize=(12, 8))
|
|
1582
|
+
img = plt.imread(img_file)
|
|
1583
|
+
plt.imshow(img)
|
|
1584
|
+
plt.title(view_name.replace('_', ' ').title(), pad=20)
|
|
1585
|
+
plt.axis('off')
|
|
1586
|
+
plt.show()
|
|
1587
|
+
plt.close()
|
|
1585
1588
|
|
|
1586
1589
|
# After creating the meshes and before visualization
|
|
1587
1590
|
if save_obj:
|
|
@@ -1633,6 +1636,8 @@ def visualize_voxcity_multi_view_with_multiple_sim_grids(voxel_array, meshsize,
|
|
|
1633
1636
|
dem_grid_ori = kwargs.get("dem_grid", None)
|
|
1634
1637
|
projection_type = kwargs.get("projection_type", "perspective")
|
|
1635
1638
|
distance_factor = kwargs.get("distance_factor", 1.0)
|
|
1639
|
+
show_views = kwargs.get("show_views", True)
|
|
1640
|
+
save_obj = kwargs.get("save_obj", False)
|
|
1636
1641
|
|
|
1637
1642
|
if dem_grid_ori is not None:
|
|
1638
1643
|
dem_grid = dem_grid_ori - np.min(dem_grid_ori)
|
|
@@ -1681,27 +1686,33 @@ def visualize_voxcity_multi_view_with_multiple_sim_grids(voxel_array, meshsize,
|
|
|
1681
1686
|
os.makedirs(output_directory, exist_ok=True)
|
|
1682
1687
|
export_meshes(meshes, output_directory, base_filename)
|
|
1683
1688
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1689
|
+
if show_views:
|
|
1690
|
+
# Create and save multiple views
|
|
1691
|
+
print("Creating multiple views...")
|
|
1692
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
1693
|
+
image_files = create_multi_view_scene(
|
|
1694
|
+
meshes,
|
|
1695
|
+
output_directory=output_directory,
|
|
1696
|
+
projection_type=projection_type,
|
|
1697
|
+
distance_factor=distance_factor
|
|
1698
|
+
)
|
|
1693
1699
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1700
|
+
# Display each view separately
|
|
1701
|
+
for view_name, img_file in image_files:
|
|
1702
|
+
plt.figure(figsize=(12, 8))
|
|
1703
|
+
img = plt.imread(img_file)
|
|
1704
|
+
plt.imshow(img)
|
|
1705
|
+
plt.title(view_name.replace('_', ' ').title(), pad=20)
|
|
1706
|
+
plt.axis('off')
|
|
1707
|
+
plt.show()
|
|
1708
|
+
plt.close()
|
|
1703
1709
|
|
|
1704
|
-
|
|
1710
|
+
# After creating the meshes and before visualization
|
|
1711
|
+
if save_obj:
|
|
1712
|
+
output_directory = kwargs.get('output_directory', 'output')
|
|
1713
|
+
output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
|
|
1714
|
+
obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
|
|
1715
|
+
print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
|
|
1705
1716
|
|
|
1706
1717
|
def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None, **kwargs):
|
|
1707
1718
|
"""
|
|
@@ -1778,6 +1789,7 @@ def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None,
|
|
|
1778
1789
|
colorbar_title = kwargs.get("colorbar_title", "")
|
|
1779
1790
|
value_name = kwargs.get("value_name", None)
|
|
1780
1791
|
nan_color = kwargs.get("nan_color", "gray")
|
|
1792
|
+
show_views = kwargs.get("show_views", True)
|
|
1781
1793
|
save_obj = kwargs.get("save_obj", False)
|
|
1782
1794
|
|
|
1783
1795
|
if value_name is None:
|
|
@@ -1900,28 +1912,28 @@ def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None,
|
|
|
1900
1912
|
print("Creating multiple views...")
|
|
1901
1913
|
# Create output directory if it doesn't exist
|
|
1902
1914
|
os.makedirs(output_directory, exist_ok=True)
|
|
1903
|
-
|
|
1915
|
+
|
|
1916
|
+
if show_views:
|
|
1917
|
+
image_files = create_multi_view_scene(meshes, output_directory=output_directory,
|
|
1904
1918
|
projection_type=projection_type,
|
|
1905
1919
|
distance_factor=distance_factor)
|
|
1906
1920
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1921
|
+
# Display each view separately
|
|
1922
|
+
for view_name, img_file in image_files:
|
|
1923
|
+
plt.figure(figsize=(24, 16))
|
|
1924
|
+
img = plt.imread(img_file)
|
|
1925
|
+
plt.imshow(img)
|
|
1926
|
+
plt.title(view_name.replace('_', ' ').title(), pad=20)
|
|
1927
|
+
plt.axis('off')
|
|
1928
|
+
plt.show()
|
|
1929
|
+
plt.close()
|
|
1916
1930
|
|
|
1917
1931
|
# After creating the meshes and before visualization
|
|
1918
1932
|
if save_obj:
|
|
1919
1933
|
output_directory = kwargs.get('output_directory', 'output')
|
|
1920
1934
|
output_file_name = kwargs.get('output_file_name', 'voxcity_mesh')
|
|
1921
1935
|
obj_path, mtl_path = save_obj_from_colored_mesh(meshes, output_directory, output_file_name)
|
|
1922
|
-
print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
|
|
1923
|
-
|
|
1924
|
-
return image_files
|
|
1936
|
+
print(f"Saved mesh files to:\n {obj_path}\n {mtl_path}")
|
|
1925
1937
|
|
|
1926
1938
|
def visualize_building_sim_results(voxel_array, meshsize, building_sim_mesh, **kwargs):
|
|
1927
1939
|
"""
|
|
@@ -1969,75 +1981,9 @@ def visualize_building_sim_results(voxel_array, meshsize, building_sim_mesh, **k
|
|
|
1969
1981
|
kwargs["colorbar_title"] = pretty_name
|
|
1970
1982
|
|
|
1971
1983
|
# Call the more general visualization function
|
|
1972
|
-
|
|
1984
|
+
visualize_voxcity_with_sim_meshes(
|
|
1973
1985
|
voxel_array,
|
|
1974
1986
|
meshsize,
|
|
1975
1987
|
custom_meshes=custom_meshes,
|
|
1976
1988
|
**kwargs
|
|
1977
|
-
)
|
|
1978
|
-
|
|
1979
|
-
def save_obj_from_colored_mesh(meshes, output_path, base_filename):
|
|
1980
|
-
"""
|
|
1981
|
-
Save colored meshes as OBJ and MTL files.
|
|
1982
|
-
|
|
1983
|
-
Parameters
|
|
1984
|
-
----------
|
|
1985
|
-
meshes : dict
|
|
1986
|
-
Dictionary of trimesh.Trimesh objects with face colors.
|
|
1987
|
-
output_path : str
|
|
1988
|
-
Directory path where to save the files.
|
|
1989
|
-
base_filename : str
|
|
1990
|
-
Base name for the output files (without extension).
|
|
1991
|
-
|
|
1992
|
-
Returns
|
|
1993
|
-
-------
|
|
1994
|
-
tuple
|
|
1995
|
-
Paths to the saved (obj_file, mtl_file).
|
|
1996
|
-
"""
|
|
1997
|
-
|
|
1998
|
-
os.makedirs(output_path, exist_ok=True)
|
|
1999
|
-
obj_path = os.path.join(output_path, f"{base_filename}.obj")
|
|
2000
|
-
mtl_path = os.path.join(output_path, f"{base_filename}.mtl")
|
|
2001
|
-
|
|
2002
|
-
# Combine all meshes
|
|
2003
|
-
combined_mesh = trimesh.util.concatenate(list(meshes.values()))
|
|
2004
|
-
|
|
2005
|
-
# Create unique materials for each unique face color
|
|
2006
|
-
face_colors = combined_mesh.visual.face_colors
|
|
2007
|
-
unique_colors = np.unique(face_colors, axis=0)
|
|
2008
|
-
|
|
2009
|
-
# Write MTL file
|
|
2010
|
-
with open(mtl_path, 'w') as mtl_file:
|
|
2011
|
-
for i, color in enumerate(unique_colors):
|
|
2012
|
-
material_name = f'material_{i}'
|
|
2013
|
-
mtl_file.write(f'newmtl {material_name}\n')
|
|
2014
|
-
# Convert RGBA to RGB float values
|
|
2015
|
-
rgb = color[:3].astype(float) / 255.0
|
|
2016
|
-
mtl_file.write(f'Kd {rgb[0]:.6f} {rgb[1]:.6f} {rgb[2]:.6f}\n')
|
|
2017
|
-
mtl_file.write(f'd {color[3]/255.0:.6f}\n\n') # Alpha value
|
|
2018
|
-
|
|
2019
|
-
# Create material groups based on face colors
|
|
2020
|
-
color_to_material = {tuple(c): f'material_{i}' for i, c in enumerate(unique_colors)}
|
|
2021
|
-
|
|
2022
|
-
# Write OBJ file
|
|
2023
|
-
with open(obj_path, 'w') as obj_file:
|
|
2024
|
-
obj_file.write(f'mtllib {os.path.basename(mtl_path)}\n')
|
|
2025
|
-
|
|
2026
|
-
# Write vertices
|
|
2027
|
-
for vertex in combined_mesh.vertices:
|
|
2028
|
-
obj_file.write(f'v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n')
|
|
2029
|
-
|
|
2030
|
-
# Write faces grouped by material
|
|
2031
|
-
current_material = None
|
|
2032
|
-
for face_idx, face in enumerate(combined_mesh.faces):
|
|
2033
|
-
face_color = tuple(face_colors[face_idx])
|
|
2034
|
-
material_name = color_to_material[face_color]
|
|
2035
|
-
|
|
2036
|
-
if material_name != current_material:
|
|
2037
|
-
obj_file.write(f'usemtl {material_name}\n')
|
|
2038
|
-
current_material = material_name
|
|
2039
|
-
|
|
2040
|
-
# OBJ indices are 1-based
|
|
2041
|
-
obj_file.write(f'f {face[0]+1} {face[1]+1} {face[2]+1}\n')
|
|
2042
|
-
|
|
2043
|
-
return obj_path, mtl_path
|
|
1989
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.11
|
|
4
4
|
Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
|
|
5
5
|
Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
6
6
|
Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
@@ -72,6 +72,10 @@ Dynamic: license-file
|
|
|
72
72
|
Tutorial preview: <a href="https://colab.research.google.com/drive/1Lofd3RawKMr6QuUsamGaF48u2MN0hfrP?usp=sharing">[Google Colab]</a>
|
|
73
73
|
</p>
|
|
74
74
|
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/logo.png" alt="Voxcity logo" width="800">
|
|
77
|
+
</p>
|
|
78
|
+
|
|
75
79
|
# VoxCity
|
|
76
80
|
|
|
77
81
|
**voxcity** is a Python package that provides a seamless solution for grid-based 3D city model generation and urban simulation for cities worldwide. VoxCity's generator module automatically downloads building heights, tree canopy heights, land cover, and terrain elevation within a specified target area, and voxelizes buildings, trees, land cover, and terrain to generate an integrated voxel city model. The simulator module enables users to conduct environmental simulations, including solar radiation and view index analyses. Users can export the generated models using several file formats compatible with external software, such as ENVI-met (INX), Blender, and Rhino (OBJ). Try it out using the [Google Colab Demo](https://colab.research.google.com/drive/1Lofd3RawKMr6QuUsamGaF48u2MN0hfrP?usp=sharing) or your local environment.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
voxcity/__init__.py,sha256=el9v3gfybHOF_GUYPeSOqN0-vCrTW0eU1mcvi0sEfeU,252
|
|
2
|
-
voxcity/generator.py,sha256=
|
|
2
|
+
voxcity/generator.py,sha256=qWe2LJsbY2_IYUuI99ZJ5_rRNJ8IQrEIdxND7azYd-M,47045
|
|
3
3
|
voxcity/downloader/__init__.py,sha256=OgGcGxOXF4tjcEL6DhOnt13DYPTvOigUelp5xIpTqM0,171
|
|
4
4
|
voxcity/downloader/citygml.py,sha256=R3DvsYJz_S5OPkeA71eEI2U7fBDLcpqdludV6gO1ihw,30305
|
|
5
5
|
voxcity/downloader/eubucco.py,sha256=XCkkdEPNuWdrnuxzL80Ext37WsgiCiZGueb-aQV5rvI,14476
|
|
@@ -17,7 +17,7 @@ voxcity/exporter/obj.py,sha256=M0MT9UZOVYsRJBEJea3qg1uu7NsMXagD24jQnmPaJLo,21629
|
|
|
17
17
|
voxcity/geoprocessor/__init_.py,sha256=JzPVhhttxBWvaZ0IGX2w7OWL5bCo_TIvpHefWeNXruA,133
|
|
18
18
|
voxcity/geoprocessor/draw.py,sha256=8Em2NvazFpYfFJUqG9LofNXaxdghKLL_rNuztmPwn8Q,13911
|
|
19
19
|
voxcity/geoprocessor/grid.py,sha256=NzUACYEtJ3Wc348ESo_N9VbaXPtSssuF_zad7BtDcmM,56389
|
|
20
|
-
voxcity/geoprocessor/mesh.py,sha256=
|
|
20
|
+
voxcity/geoprocessor/mesh.py,sha256=FC0rgL1b8qYDlAwYwVMS0qcFwBTdqv-RFMw89sCnGOk,15124
|
|
21
21
|
voxcity/geoprocessor/network.py,sha256=opb_kpUCAxDd1qtrWPStqR5reYZtVe96XxazNSen7Lk,18851
|
|
22
22
|
voxcity/geoprocessor/polygon.py,sha256=8Vb2AbkpKYhq1kk2hQMc-gitmUo9pFIe910v4p1vP2g,37772
|
|
23
23
|
voxcity/geoprocessor/utils.py,sha256=1BRHp-DDeOA8HG8jplY7Eo75G3oXkVGL6DGONL4BA8A,19815
|
|
@@ -28,11 +28,11 @@ voxcity/simulator/view.py,sha256=YufbLuDXrLg1d1dedM6pVyiJ7uHsqY8F2sLLnIoJvB4,749
|
|
|
28
28
|
voxcity/utils/__init_.py,sha256=nLYrj2huBbDBNMqfchCwexGP8Tlt9O_XluVDG7MoFkw,98
|
|
29
29
|
voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
|
|
30
30
|
voxcity/utils/material.py,sha256=Vt3IID5Ft54HNJcEC4zi31BCPqi_687X3CSp7rXaRVY,5907
|
|
31
|
-
voxcity/utils/visualization.py,sha256=
|
|
31
|
+
voxcity/utils/visualization.py,sha256=jsKfUoRRW0yRCmJ03I7ESK1Tic_Xk1tcliw-8syr3Y0,87228
|
|
32
32
|
voxcity/utils/weather.py,sha256=CFPtoqRTajwMRswswDChwQ3BW1cGsnA3orgWHgz7Ehg,26304
|
|
33
|
-
voxcity-0.5.
|
|
34
|
-
voxcity-0.5.
|
|
35
|
-
voxcity-0.5.
|
|
36
|
-
voxcity-0.5.
|
|
37
|
-
voxcity-0.5.
|
|
38
|
-
voxcity-0.5.
|
|
33
|
+
voxcity-0.5.11.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
|
|
34
|
+
voxcity-0.5.11.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
|
|
35
|
+
voxcity-0.5.11.dist-info/METADATA,sha256=hK1HErMdMRO_166kD7r3ZeGGT9pZq6Ok54PbcwpCYJA,25885
|
|
36
|
+
voxcity-0.5.11.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
37
|
+
voxcity-0.5.11.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
|
|
38
|
+
voxcity-0.5.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|