maps4fs 2.9.0__tar.gz → 2.9.2__tar.gz
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 maps4fs might be problematic. Click here for more details.
- {maps4fs-2.9.0 → maps4fs-2.9.2}/PKG-INFO +1 -1
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/background.py +90 -34
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component_mesh.py +39 -13
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/texture.py +38 -32
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/monitor.py +10 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/logger.py +2 -1
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/PKG-INFO +1 -1
- {maps4fs-2.9.0 → maps4fs-2.9.2}/pyproject.toml +1 -1
- {maps4fs-2.9.0 → maps4fs-2.9.2}/LICENSE.md +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/README.md +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/__init__.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/__init__.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/__init__.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/__init__.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component_image.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component_xml.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/config.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/dem.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/grle.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/i3d.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/layer.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/satellite.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/config.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/game.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/map.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/qgis.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/settings.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/statistics.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/utils.py +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/SOURCES.txt +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/dependency_links.txt +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/requires.txt +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/top_level.txt +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/setup.cfg +0 -0
- {maps4fs-2.9.0 → maps4fs-2.9.2}/tests/test_generator.py +0 -0
|
@@ -78,6 +78,9 @@ class Background(MeshComponent, ImageComponent):
|
|
|
78
78
|
self.background_directory, "not_substracted.png"
|
|
79
79
|
)
|
|
80
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
|
+
)
|
|
81
84
|
|
|
82
85
|
self.flatten_water_to: int | None = None
|
|
83
86
|
|
|
@@ -561,6 +564,10 @@ class Background(MeshComponent, ImageComponent):
|
|
|
561
564
|
|
|
562
565
|
if self.map.dem_settings.add_foundations:
|
|
563
566
|
dem_data = self.create_foundations(dem_data)
|
|
567
|
+
cv2.imwrite(self.not_resized_with_foundations_path, dem_data)
|
|
568
|
+
self.logger.debug(
|
|
569
|
+
"Not resized DEM with foundations saved: %s", self.not_resized_with_foundations_path
|
|
570
|
+
)
|
|
564
571
|
|
|
565
572
|
output_size = self.scaled_size + 1
|
|
566
573
|
|
|
@@ -1047,11 +1054,21 @@ class Background(MeshComponent, ImageComponent):
|
|
|
1047
1054
|
@monitor_performance
|
|
1048
1055
|
def flatten_roads(self) -> None:
|
|
1049
1056
|
"""Flattens the roads in the DEM data by averaging the height values along the road polylines."""
|
|
1050
|
-
|
|
1057
|
+
supported_files = [self.not_resized_with_foundations_path, self.not_resized_path]
|
|
1058
|
+
|
|
1059
|
+
base_image_path = None
|
|
1060
|
+
for supported_file in supported_files:
|
|
1061
|
+
if not supported_file or not os.path.isfile(supported_file):
|
|
1062
|
+
continue
|
|
1063
|
+
|
|
1064
|
+
base_image_path = supported_file
|
|
1065
|
+
break
|
|
1066
|
+
|
|
1067
|
+
if not base_image_path:
|
|
1051
1068
|
self.logger.warning("No DEM data found for flattening roads.")
|
|
1052
1069
|
return
|
|
1053
1070
|
|
|
1054
|
-
dem_image = cv2.imread(
|
|
1071
|
+
dem_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED)
|
|
1055
1072
|
if dem_image is None:
|
|
1056
1073
|
self.logger.warning("Failed to read DEM data.")
|
|
1057
1074
|
return
|
|
@@ -1088,41 +1105,80 @@ class Background(MeshComponent, ImageComponent):
|
|
|
1088
1105
|
total_length = polyline.length
|
|
1089
1106
|
self.logger.debug("Total length of the road polyline: %s", total_length)
|
|
1090
1107
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1108
|
+
# Step 1: Create complete road mask once
|
|
1109
|
+
road_mask = np.zeros(dem_image.shape, dtype=np.uint8)
|
|
1110
|
+
# OpenCV thickness is total width, not radius like Shapely buffer
|
|
1111
|
+
# So we need width * 4 to match the old buffer(width * 2) behavior
|
|
1112
|
+
line_thickness = int(width * 4)
|
|
1113
|
+
|
|
1114
|
+
# Get densely sampled points for smooth road
|
|
1115
|
+
dense_sample_distance = min(SEGMENT_LENGTH, total_length / 100) # At least 100 samples
|
|
1116
|
+
num_dense_points = max(100, int(total_length / dense_sample_distance))
|
|
1117
|
+
dense_distances = np.linspace(0, total_length, num_dense_points)
|
|
1118
|
+
dense_points = [polyline.interpolate(d) for d in dense_distances]
|
|
1119
|
+
dense_coords = np.array([(int(p.x), int(p.y)) for p in dense_points], dtype=np.int32)
|
|
1120
|
+
|
|
1121
|
+
# Draw entire road at once
|
|
1122
|
+
if len(dense_coords) > 1:
|
|
1123
|
+
cv2.polylines(road_mask, [dense_coords], False, 255, thickness=line_thickness)
|
|
1124
|
+
|
|
1125
|
+
# Step 2: Get all road pixels that need processing
|
|
1126
|
+
road_pixels = np.where(road_mask == 255)
|
|
1127
|
+
if len(road_pixels[0]) == 0:
|
|
1128
|
+
continue
|
|
1097
1129
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1130
|
+
road_y, road_x = road_pixels
|
|
1131
|
+
self.logger.debug("Processing %s road pixels", len(road_y))
|
|
1132
|
+
|
|
1133
|
+
# Step 3: Efficient distance-based smooth gradation
|
|
1134
|
+
# Use much larger segments (10-20x SEGMENT_LENGTH) but interpolate between them
|
|
1135
|
+
large_segment_length = SEGMENT_LENGTH * 15 # 30 units instead of 2
|
|
1136
|
+
num_large_segments = max(1, int(np.ceil(total_length / large_segment_length)))
|
|
1137
|
+
large_distances = np.linspace(0, total_length, num_large_segments + 1)
|
|
1138
|
+
|
|
1139
|
+
# Calculate elevation values at large segment points
|
|
1140
|
+
segment_elevations = []
|
|
1141
|
+
for dist in large_distances:
|
|
1142
|
+
sample_point = polyline.interpolate(dist)
|
|
1143
|
+
sample_x, sample_y = int(sample_point.x), int(sample_point.y)
|
|
1144
|
+
|
|
1145
|
+
# Sample elevation in small area around this point
|
|
1146
|
+
sample_radius = max(5, line_thickness // 4)
|
|
1147
|
+
y_min = max(0, sample_y - sample_radius)
|
|
1148
|
+
y_max = min(dem_image.shape[0], sample_y + sample_radius)
|
|
1149
|
+
x_min = max(0, sample_x - sample_radius)
|
|
1150
|
+
x_max = min(dem_image.shape[1], sample_x + sample_radius)
|
|
1151
|
+
|
|
1152
|
+
if y_max > y_min and x_max > x_min:
|
|
1153
|
+
sample_elevation = np.mean(dem_image[y_min:y_max, x_min:x_max]) # type: ignore
|
|
1154
|
+
else:
|
|
1155
|
+
sample_elevation = (
|
|
1156
|
+
dem_image[sample_y, sample_x] # type: ignore
|
|
1157
|
+
if 0 <= sample_y < dem_image.shape[0] and 0 <= sample_x < dem_image.shape[1]
|
|
1158
|
+
else 0
|
|
1159
|
+
)
|
|
1102
1160
|
|
|
1103
|
-
|
|
1161
|
+
segment_elevations.append(sample_elevation)
|
|
1104
1162
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
dem_image[mask == 255] = mean_value
|
|
1125
|
-
full_mask[mask == 255] = 255
|
|
1163
|
+
# Step 4: Interpolate elevations smoothly along entire road
|
|
1164
|
+
road_distances_from_start = []
|
|
1165
|
+
for i in range(len(road_y)): # pylint: disable=consider-using-enumerate
|
|
1166
|
+
px, py = road_x[i], road_y[i]
|
|
1167
|
+
# Find closest point on polyline (approximation)
|
|
1168
|
+
closest_point = polyline.interpolate(polyline.project(shapely.Point(px, py)))
|
|
1169
|
+
distance_along_road = polyline.project(closest_point)
|
|
1170
|
+
road_distances_from_start.append(distance_along_road)
|
|
1171
|
+
|
|
1172
|
+
road_distances_from_start = np.array(road_distances_from_start) # type: ignore
|
|
1173
|
+
|
|
1174
|
+
# Interpolate elevation values based on distance along road
|
|
1175
|
+
interpolated_elevations = np.interp(
|
|
1176
|
+
road_distances_from_start, large_distances, segment_elevations
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
# Step 5: Apply interpolated elevations to road pixels
|
|
1180
|
+
dem_image[road_y, road_x] = interpolated_elevations
|
|
1181
|
+
full_mask[road_y, road_x] = 255
|
|
1126
1182
|
|
|
1127
1183
|
main_dem_path = self.game.dem_file_path(self.map_directory)
|
|
1128
1184
|
dem_image = self.blur_by_mask(dem_image, full_mask, blur_radius=5)
|
|
@@ -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")
|
|
@@ -562,46 +562,52 @@ class Texture(ImageComponent):
|
|
|
562
562
|
files of the corresponding layer and saves the changes to the files.
|
|
563
563
|
"""
|
|
564
564
|
for layer in tqdm(self.layers, desc="Dissolving textures", unit="layer"):
|
|
565
|
-
|
|
566
|
-
self.logger.debug("Layer %s has no tags, there's nothing to dissolve.", layer.name)
|
|
567
|
-
continue
|
|
568
|
-
layer_path = layer.path(self._weights_dir)
|
|
569
|
-
layer_paths = layer.paths(self._weights_dir)
|
|
570
|
-
|
|
571
|
-
if len(layer_paths) < 2:
|
|
572
|
-
self.logger.debug("Layer %s has only one texture, skipping.", layer.name)
|
|
573
|
-
continue
|
|
574
|
-
|
|
575
|
-
self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
|
|
565
|
+
self.dissolve_layer(layer)
|
|
576
566
|
|
|
577
|
-
|
|
578
|
-
|
|
567
|
+
def dissolve_layer(self, layer: Layer) -> None:
|
|
568
|
+
"""Dissolves texture of the layer into sublayers."""
|
|
569
|
+
if not layer.tags:
|
|
570
|
+
self.logger.debug("Layer %s has no tags, there's nothing to dissolve.", layer.name)
|
|
571
|
+
return
|
|
572
|
+
layer_path = layer.path(self._weights_dir)
|
|
573
|
+
layer_paths = layer.paths(self._weights_dir)
|
|
579
574
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
)
|
|
584
|
-
continue
|
|
575
|
+
if len(layer_paths) < 2:
|
|
576
|
+
self.logger.debug("Layer %s has only one texture, skipping.", layer.name)
|
|
577
|
+
return
|
|
585
578
|
|
|
586
|
-
|
|
587
|
-
|
|
579
|
+
self.logger.debug("Dissolving layer from %s to %s.", layer_path, layer_paths)
|
|
580
|
+
# Check if the image contains any non-zero values, otherwise continue.
|
|
581
|
+
layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
|
582
|
+
if layer_image is None:
|
|
583
|
+
self.logger.debug("Layer %s image not found, skipping.", layer.name)
|
|
584
|
+
return
|
|
588
585
|
|
|
589
|
-
|
|
590
|
-
|
|
586
|
+
# Get mask of non-zero pixels. If there are no non-zero pixels, skip the layer.
|
|
587
|
+
mask = layer_image > 0
|
|
588
|
+
if not np.any(mask):
|
|
589
|
+
self.logger.debug(
|
|
590
|
+
"Layer %s does not contain any non-zero values, skipping.", layer.name
|
|
591
|
+
)
|
|
592
|
+
return
|
|
593
|
+
# Save the original image to use it for preview later, without combining the sublayers.
|
|
594
|
+
cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
|
|
591
595
|
|
|
592
|
-
|
|
593
|
-
|
|
596
|
+
# Create random assignment array for all pixels
|
|
597
|
+
random_assignment = np.random.randint(0, layer.count, size=layer_image.shape)
|
|
594
598
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
599
|
+
# Create sublayers using vectorized operations.
|
|
600
|
+
sublayers = []
|
|
601
|
+
for i in range(layer.count):
|
|
602
|
+
# Create sublayer: 255 where (mask is True AND random_assignment == i)
|
|
603
|
+
sublayer = np.where((mask) & (random_assignment == i), 255, 0).astype(np.uint8)
|
|
604
|
+
sublayers.append(sublayer)
|
|
598
605
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
self.logger.debug("Sublayer %s saved.", sublayer_path)
|
|
606
|
+
# Save sublayers
|
|
607
|
+
for sublayer, sublayer_path in zip(sublayers, layer_paths):
|
|
608
|
+
cv2.imwrite(sublayer_path, sublayer)
|
|
603
609
|
|
|
604
|
-
|
|
610
|
+
self.logger.debug("Dissolved layer %s.", layer.name)
|
|
605
611
|
|
|
606
612
|
def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
|
|
607
613
|
"""Draws base layer and saves it into the png file.
|
|
@@ -9,6 +9,9 @@ from time import perf_counter
|
|
|
9
9
|
from typing import Callable, Generator
|
|
10
10
|
|
|
11
11
|
from maps4fs.generator.utils import Singleton
|
|
12
|
+
from maps4fs.logger import Logger
|
|
13
|
+
|
|
14
|
+
logger = Logger(name="MAPS4FS_MONITOR")
|
|
12
15
|
|
|
13
16
|
_local = threading.local()
|
|
14
17
|
|
|
@@ -100,6 +103,13 @@ def monitor_performance(func: Callable) -> Callable:
|
|
|
100
103
|
try:
|
|
101
104
|
if session_name and time_taken > 0.001 and class_name:
|
|
102
105
|
PerformanceMonitor().add_record(session_name, class_name, function_name, time_taken)
|
|
106
|
+
logger.debug(
|
|
107
|
+
"[PERFORMANCE] %s | %s | %s | %s",
|
|
108
|
+
session_name,
|
|
109
|
+
class_name,
|
|
110
|
+
function_name,
|
|
111
|
+
time_taken,
|
|
112
|
+
)
|
|
103
113
|
except Exception:
|
|
104
114
|
pass
|
|
105
115
|
|
|
@@ -14,8 +14,9 @@ class Logger(logging.Logger):
|
|
|
14
14
|
self,
|
|
15
15
|
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO",
|
|
16
16
|
to_stdout: bool = True,
|
|
17
|
+
**kwargs,
|
|
17
18
|
):
|
|
18
|
-
super().__init__(LOGGER_NAME)
|
|
19
|
+
super().__init__(kwargs.pop("name", LOGGER_NAME))
|
|
19
20
|
self.setLevel(level)
|
|
20
21
|
self.stdout_handler = logging.StreamHandler(sys.stdout)
|
|
21
22
|
formatter = "%(name)s | %(levelname)s | %(asctime)s | %(message)s"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "maps4fs"
|
|
7
|
-
version = "2.9.
|
|
7
|
+
version = "2.9.2"
|
|
8
8
|
description = "Generate map templates for Farming Simulator from real places."
|
|
9
9
|
authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
|
|
10
10
|
license = {text = "GNU Affero General Public License v3.0"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|