maps4fs 2.8.9__py3-none-any.whl → 2.9.37__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.
- maps4fs/generator/component/background.py +118 -35
- maps4fs/generator/component/base/component_mesh.py +39 -13
- maps4fs/generator/component/building.py +706 -0
- maps4fs/generator/component/config.py +5 -0
- maps4fs/generator/component/dem.py +7 -0
- maps4fs/generator/component/grle.py +66 -0
- maps4fs/generator/component/i3d.py +63 -5
- maps4fs/generator/component/layer.py +19 -0
- maps4fs/generator/component/road.py +648 -0
- maps4fs/generator/component/satellite.py +3 -0
- maps4fs/generator/component/texture.py +89 -50
- maps4fs/generator/config.py +79 -11
- maps4fs/generator/game.py +64 -3
- maps4fs/generator/map.py +73 -39
- maps4fs/generator/monitor.py +118 -0
- maps4fs/generator/settings.py +18 -1
- maps4fs/generator/statistics.py +10 -0
- maps4fs/generator/utils.py +26 -1
- maps4fs/logger.py +2 -1
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.37.dist-info}/METADATA +6 -4
- maps4fs-2.9.37.dist-info/RECORD +32 -0
- maps4fs-2.9.37.dist-info/licenses/LICENSE.md +416 -0
- maps4fs-2.8.9.dist-info/RECORD +0 -29
- maps4fs-2.8.9.dist-info/licenses/LICENSE.md +0 -651
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.37.dist-info}/WHEEL +0 -0
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.37.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ from maps4fs.generator.component.base.component_image import ImageComponent
|
|
|
19
19
|
from maps4fs.generator.component.base.component_mesh import MeshComponent
|
|
20
20
|
from maps4fs.generator.component.dem import DEM
|
|
21
21
|
from maps4fs.generator.component.texture import Texture
|
|
22
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
22
23
|
from maps4fs.generator.settings import Parameters
|
|
23
24
|
|
|
24
25
|
SEGMENT_LENGTH = 2
|
|
@@ -38,6 +39,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
38
39
|
info, warning. If not provided, default logging will be used.
|
|
39
40
|
"""
|
|
40
41
|
|
|
42
|
+
@monitor_performance
|
|
41
43
|
def preprocess(self) -> None:
|
|
42
44
|
"""Registers the DEMs for the background terrain."""
|
|
43
45
|
self.stl_preview_path = os.path.join(self.previews_directory, "background_dem.stl")
|
|
@@ -76,6 +78,12 @@ class Background(MeshComponent, ImageComponent):
|
|
|
76
78
|
self.background_directory, "not_substracted.png"
|
|
77
79
|
)
|
|
78
80
|
self.not_resized_path: str = os.path.join(self.background_directory, "not_resized.png")
|
|
81
|
+
self.not_resized_with_foundations_path: str = os.path.join(
|
|
82
|
+
self.background_directory, "not_resized_with_foundations.png"
|
|
83
|
+
)
|
|
84
|
+
self.not_resized_with_flattened_roads_path: str = os.path.join(
|
|
85
|
+
self.background_directory, "not_resized_with_flattened_roads.png"
|
|
86
|
+
)
|
|
79
87
|
|
|
80
88
|
self.flatten_water_to: int | None = None
|
|
81
89
|
|
|
@@ -141,6 +149,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
141
149
|
"Mesh processing is disabled for the game, skipping water mesh processing."
|
|
142
150
|
)
|
|
143
151
|
|
|
152
|
+
@monitor_performance
|
|
144
153
|
def create_foundations(self, dem_image: np.ndarray) -> np.ndarray:
|
|
145
154
|
"""Creates foundations for buildings based on the DEM data.
|
|
146
155
|
|
|
@@ -246,6 +255,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
246
255
|
)
|
|
247
256
|
self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
|
|
248
257
|
|
|
258
|
+
@monitor_performance
|
|
249
259
|
def generate_obj_files(self) -> None:
|
|
250
260
|
"""Iterates over all dems and generates 3D obj files based on DEM data.
|
|
251
261
|
If at least one DEM file is missing, the generation will be stopped at all.
|
|
@@ -328,8 +338,9 @@ class Background(MeshComponent, ImageComponent):
|
|
|
328
338
|
return resolution
|
|
329
339
|
return Parameters.MAXIMUM_BACKGROUND_TEXTURE_SIZE
|
|
330
340
|
|
|
341
|
+
@monitor_performance
|
|
331
342
|
def decimate_background_mesh(self) -> None:
|
|
332
|
-
"""
|
|
343
|
+
"""Decimates the background mesh based on the map size."""
|
|
333
344
|
if not self.assets.background_mesh or not os.path.isfile(self.assets.background_mesh):
|
|
334
345
|
self.logger.warning("Background mesh not found, cannot generate i3d background.")
|
|
335
346
|
return
|
|
@@ -362,6 +373,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
362
373
|
|
|
363
374
|
self.assets.decimated_background_mesh = decimated_save_path
|
|
364
375
|
|
|
376
|
+
@monitor_performance
|
|
365
377
|
def texture_background_mesh(self) -> None:
|
|
366
378
|
"""Textures the background mesh using satellite imagery."""
|
|
367
379
|
satellite_component = self.map.get_satellite_component()
|
|
@@ -427,6 +439,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
427
439
|
self.logger.error("Could not texture background mesh: %s", e)
|
|
428
440
|
return
|
|
429
441
|
|
|
442
|
+
@monitor_performance
|
|
430
443
|
def convert_background_mesh_to_i3d(self) -> bool:
|
|
431
444
|
"""Converts the textured background mesh to i3d format.
|
|
432
445
|
|
|
@@ -498,6 +511,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
498
511
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
499
512
|
f.write(content + "\n\n" + note)
|
|
500
513
|
|
|
514
|
+
@monitor_performance
|
|
501
515
|
def convert_water_mesh_to_i3d(self) -> bool:
|
|
502
516
|
"""Converts the line-based water mesh to i3d format.
|
|
503
517
|
|
|
@@ -553,6 +567,10 @@ class Background(MeshComponent, ImageComponent):
|
|
|
553
567
|
|
|
554
568
|
if self.map.dem_settings.add_foundations:
|
|
555
569
|
dem_data = self.create_foundations(dem_data)
|
|
570
|
+
cv2.imwrite(self.not_resized_with_foundations_path, dem_data)
|
|
571
|
+
self.logger.debug(
|
|
572
|
+
"Not resized DEM with foundations saved: %s", self.not_resized_with_foundations_path
|
|
573
|
+
)
|
|
556
574
|
|
|
557
575
|
output_size = self.scaled_size + 1
|
|
558
576
|
|
|
@@ -730,6 +748,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
730
748
|
cv2.imwrite(colored_dem_path, dem_data_colored)
|
|
731
749
|
return colored_dem_path
|
|
732
750
|
|
|
751
|
+
@monitor_performance
|
|
733
752
|
def create_background_textures(self) -> None:
|
|
734
753
|
"""Creates background textures for the map."""
|
|
735
754
|
layers_schema = self.map.texture_schema
|
|
@@ -782,6 +801,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
782
801
|
|
|
783
802
|
cv2.imwrite(self.water_resources_path, background_image)
|
|
784
803
|
|
|
804
|
+
@monitor_performance
|
|
785
805
|
def subtraction(self) -> None:
|
|
786
806
|
"""Subtracts the water depth from the DEM data where the water resources are located."""
|
|
787
807
|
if not self.water_resources_path:
|
|
@@ -806,6 +826,10 @@ class Background(MeshComponent, ImageComponent):
|
|
|
806
826
|
|
|
807
827
|
if self.map.background_settings.flatten_water:
|
|
808
828
|
try:
|
|
829
|
+
# Check if there are any water pixels (255) in the water resources image.
|
|
830
|
+
if not np.any(water_resources_image == 255):
|
|
831
|
+
self.logger.warning("No water pixels found in water resources image.")
|
|
832
|
+
return
|
|
809
833
|
mask = water_resources_image == 255
|
|
810
834
|
flatten_to = int(np.mean(dem_image[mask]) - subtract_by) # type: ignore
|
|
811
835
|
self.flatten_water_to = flatten_to # type: ignore
|
|
@@ -845,13 +869,14 @@ class Background(MeshComponent, ImageComponent):
|
|
|
845
869
|
"""
|
|
846
870
|
self.logger.debug("Starting line-based water generation...")
|
|
847
871
|
water_polygons = self.get_infolayer_data(Parameters.BACKGROUND, Parameters.WATER)
|
|
848
|
-
self.logger.debug(
|
|
849
|
-
"Found %s water polygons in background info layer.", len(water_polygons) # type: ignore
|
|
850
|
-
)
|
|
851
872
|
if not water_polygons:
|
|
852
873
|
self.logger.warning("No water polygons found in background info layer.")
|
|
853
874
|
return
|
|
854
875
|
|
|
876
|
+
self.logger.debug(
|
|
877
|
+
"Found %s water polygons in background info layer.", len(water_polygons) # type: ignore
|
|
878
|
+
)
|
|
879
|
+
|
|
855
880
|
polygons: list[shapely.Polygon] = []
|
|
856
881
|
for polygon_points in water_polygons:
|
|
857
882
|
if not polygon_points or len(polygon_points) < 2:
|
|
@@ -967,6 +992,7 @@ class Background(MeshComponent, ImageComponent):
|
|
|
967
992
|
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
|
|
968
993
|
return mesh
|
|
969
994
|
|
|
995
|
+
@monitor_performance
|
|
970
996
|
def generate_water_resources_obj(self) -> None:
|
|
971
997
|
"""Generates 3D obj files based on water resources data."""
|
|
972
998
|
self.logger.debug("Starting water resources generation...")
|
|
@@ -1033,13 +1059,24 @@ class Background(MeshComponent, ImageComponent):
|
|
|
1033
1059
|
|
|
1034
1060
|
self.plane_from_np(elevated_water, elevated_save_path, include_zeros=False)
|
|
1035
1061
|
|
|
1062
|
+
@monitor_performance
|
|
1036
1063
|
def flatten_roads(self) -> None:
|
|
1037
1064
|
"""Flattens the roads in the DEM data by averaging the height values along the road polylines."""
|
|
1038
|
-
|
|
1065
|
+
supported_files = [self.not_resized_with_foundations_path, self.not_resized_path]
|
|
1066
|
+
|
|
1067
|
+
base_image_path = None
|
|
1068
|
+
for supported_file in supported_files:
|
|
1069
|
+
if not supported_file or not os.path.isfile(supported_file):
|
|
1070
|
+
continue
|
|
1071
|
+
|
|
1072
|
+
base_image_path = supported_file
|
|
1073
|
+
break
|
|
1074
|
+
|
|
1075
|
+
if not base_image_path:
|
|
1039
1076
|
self.logger.warning("No DEM data found for flattening roads.")
|
|
1040
1077
|
return
|
|
1041
1078
|
|
|
1042
|
-
dem_image = cv2.imread(
|
|
1079
|
+
dem_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED)
|
|
1043
1080
|
if dem_image is None:
|
|
1044
1081
|
self.logger.warning("Failed to read DEM data.")
|
|
1045
1082
|
return
|
|
@@ -1076,46 +1113,92 @@ class Background(MeshComponent, ImageComponent):
|
|
|
1076
1113
|
total_length = polyline.length
|
|
1077
1114
|
self.logger.debug("Total length of the road polyline: %s", total_length)
|
|
1078
1115
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1116
|
+
# Step 1: Create complete road mask once
|
|
1117
|
+
road_mask = np.zeros(dem_image.shape, dtype=np.uint8)
|
|
1118
|
+
# OpenCV thickness is total width, not radius like Shapely buffer
|
|
1119
|
+
# So we need width * 4 to match the old buffer(width * 2) behavior
|
|
1120
|
+
line_thickness = int(width * 4)
|
|
1121
|
+
|
|
1122
|
+
# Get densely sampled points for smooth road
|
|
1123
|
+
dense_sample_distance = min(SEGMENT_LENGTH, total_length / 100) # At least 100 samples
|
|
1124
|
+
num_dense_points = max(100, int(total_length / dense_sample_distance))
|
|
1125
|
+
dense_distances = np.linspace(0, total_length, num_dense_points)
|
|
1126
|
+
dense_points = [polyline.interpolate(d) for d in dense_distances]
|
|
1127
|
+
dense_coords = np.array([(int(p.x), int(p.y)) for p in dense_points], dtype=np.int32)
|
|
1128
|
+
|
|
1129
|
+
# Draw entire road at once
|
|
1130
|
+
if len(dense_coords) > 1:
|
|
1131
|
+
cv2.polylines(road_mask, [dense_coords], False, 255, thickness=line_thickness)
|
|
1132
|
+
|
|
1133
|
+
# Step 2: Get all road pixels that need processing
|
|
1134
|
+
road_pixels = np.where(road_mask == 255)
|
|
1135
|
+
if len(road_pixels[0]) == 0:
|
|
1136
|
+
continue
|
|
1085
1137
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1138
|
+
road_y, road_x = road_pixels
|
|
1139
|
+
self.logger.debug("Processing %s road pixels", len(road_y))
|
|
1140
|
+
|
|
1141
|
+
# Step 3: Efficient distance-based smooth gradation
|
|
1142
|
+
# Use much larger segments (10-20x SEGMENT_LENGTH) but interpolate between them
|
|
1143
|
+
large_segment_length = SEGMENT_LENGTH * 15 # 30 units instead of 2
|
|
1144
|
+
num_large_segments = max(1, int(np.ceil(total_length / large_segment_length)))
|
|
1145
|
+
large_distances = np.linspace(0, total_length, num_large_segments + 1)
|
|
1146
|
+
|
|
1147
|
+
# Calculate elevation values at large segment points
|
|
1148
|
+
segment_elevations = []
|
|
1149
|
+
for dist in large_distances:
|
|
1150
|
+
sample_point = polyline.interpolate(dist)
|
|
1151
|
+
sample_x, sample_y = int(sample_point.x), int(sample_point.y)
|
|
1152
|
+
|
|
1153
|
+
# Sample elevation in small area around this point
|
|
1154
|
+
sample_radius = max(5, line_thickness // 4)
|
|
1155
|
+
y_min = max(0, sample_y - sample_radius)
|
|
1156
|
+
y_max = min(dem_image.shape[0], sample_y + sample_radius)
|
|
1157
|
+
x_min = max(0, sample_x - sample_radius)
|
|
1158
|
+
x_max = min(dem_image.shape[1], sample_x + sample_radius)
|
|
1159
|
+
|
|
1160
|
+
if y_max > y_min and x_max > x_min:
|
|
1161
|
+
sample_elevation = np.mean(dem_image[y_min:y_max, x_min:x_max]) # type: ignore
|
|
1162
|
+
else:
|
|
1163
|
+
sample_elevation = (
|
|
1164
|
+
dem_image[sample_y, sample_x] # type: ignore
|
|
1165
|
+
if 0 <= sample_y < dem_image.shape[0] and 0 <= sample_x < dem_image.shape[1]
|
|
1166
|
+
else 0
|
|
1167
|
+
)
|
|
1090
1168
|
|
|
1091
|
-
|
|
1169
|
+
segment_elevations.append(sample_elevation)
|
|
1092
1170
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
)
|
|
1098
|
-
|
|
1171
|
+
# Step 4: Interpolate elevations smoothly along entire road
|
|
1172
|
+
road_distances_from_start = []
|
|
1173
|
+
for i in range(len(road_y)): # pylint: disable=consider-using-enumerate
|
|
1174
|
+
px, py = road_x[i], road_y[i]
|
|
1175
|
+
# Find closest point on polyline (approximation)
|
|
1176
|
+
closest_point = polyline.interpolate(polyline.project(shapely.Point(px, py)))
|
|
1177
|
+
distance_along_road = polyline.project(closest_point)
|
|
1178
|
+
road_distances_from_start.append(distance_along_road)
|
|
1099
1179
|
|
|
1100
|
-
|
|
1101
|
-
polygon_points = polygon.exterior.coords
|
|
1102
|
-
road_np = self.polygon_points_to_np(polygon_points)
|
|
1103
|
-
mask = np.zeros(dem_image.shape, dtype=np.uint8)
|
|
1180
|
+
road_distances_from_start = np.array(road_distances_from_start) # type: ignore
|
|
1104
1181
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
continue
|
|
1182
|
+
# Interpolate elevation values based on distance along road
|
|
1183
|
+
interpolated_elevations = np.interp(
|
|
1184
|
+
road_distances_from_start, large_distances, segment_elevations
|
|
1185
|
+
)
|
|
1110
1186
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1187
|
+
# Step 5: Apply interpolated elevations to road pixels
|
|
1188
|
+
dem_image[road_y, road_x] = interpolated_elevations
|
|
1189
|
+
full_mask[road_y, road_x] = 255
|
|
1114
1190
|
|
|
1115
1191
|
main_dem_path = self.game.dem_file_path(self.map_directory)
|
|
1116
1192
|
dem_image = self.blur_by_mask(dem_image, full_mask, blur_radius=5)
|
|
1117
1193
|
dem_image = self.blur_edges_by_mask(dem_image, full_mask)
|
|
1118
1194
|
|
|
1195
|
+
# Save the not resized DEM with flattened roads.
|
|
1196
|
+
cv2.imwrite(self.not_resized_with_flattened_roads_path, dem_image)
|
|
1197
|
+
self.logger.debug(
|
|
1198
|
+
"Not resized DEM with flattened roads saved to: %s",
|
|
1199
|
+
self.not_resized_with_flattened_roads_path,
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1119
1202
|
output_size = dem_image.shape[0] + 1
|
|
1120
1203
|
resized_dem = cv2.resize(
|
|
1121
1204
|
dem_image, (output_size, output_size), interpolation=cv2.INTER_NEAREST
|
|
@@ -595,19 +595,45 @@ class MeshComponent(Component):
|
|
|
595
595
|
if has_uv:
|
|
596
596
|
xml_vertices.set("uv0", "true")
|
|
597
597
|
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
598
|
+
# Pre-format ALL strings using vectorized operations
|
|
599
|
+
pos_strings = np.array([f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}" for v in vertices])
|
|
600
|
+
|
|
601
|
+
normal_strings = None
|
|
602
|
+
if has_normals:
|
|
603
|
+
normal_strings = np.array(
|
|
604
|
+
[f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}" for n in mesh.vertex_normals]
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
uv_strings = None
|
|
608
|
+
if has_uv:
|
|
609
|
+
uv_strings = np.array([f"{uv[0]:.6f} {uv[1]:.6f}" for uv in mesh.visual.uv])
|
|
610
|
+
|
|
611
|
+
# Batch process vertices for memory efficiency
|
|
612
|
+
batch_size = 2000
|
|
613
|
+
vertex_elements = []
|
|
614
|
+
|
|
615
|
+
for batch_start in tqdm(
|
|
616
|
+
range(0, len(vertices), batch_size), desc="Writing vertices", unit="batch"
|
|
617
|
+
):
|
|
618
|
+
batch_end = min(batch_start + batch_size, len(vertices))
|
|
619
|
+
batch_elements = []
|
|
620
|
+
|
|
621
|
+
for idx in range(batch_start, batch_end):
|
|
622
|
+
v_el = ET.Element("v")
|
|
623
|
+
v_el.set("p", pos_strings[idx])
|
|
624
|
+
|
|
625
|
+
if has_normals:
|
|
626
|
+
v_el.set("n", normal_strings[idx]) # type: ignore
|
|
627
|
+
|
|
628
|
+
if has_uv:
|
|
629
|
+
v_el.set("t0", uv_strings[idx]) # type: ignore
|
|
630
|
+
|
|
631
|
+
batch_elements.append(v_el)
|
|
632
|
+
|
|
633
|
+
vertex_elements.extend(batch_elements)
|
|
634
|
+
|
|
635
|
+
# Add all vertex elements at once
|
|
636
|
+
xml_vertices.extend(vertex_elements)
|
|
611
637
|
|
|
612
638
|
# Triangles block
|
|
613
639
|
xml_tris = ET.SubElement(shape, "Triangles")
|