maps4fs 2.9.0__tar.gz → 2.9.1__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.

Files changed (36) hide show
  1. {maps4fs-2.9.0 → maps4fs-2.9.1}/PKG-INFO +1 -1
  2. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/background.py +90 -34
  3. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/base/component_mesh.py +48 -11
  4. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/texture.py +38 -32
  5. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/monitor.py +10 -0
  6. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/logger.py +2 -1
  7. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs.egg-info/PKG-INFO +1 -1
  8. {maps4fs-2.9.0 → maps4fs-2.9.1}/pyproject.toml +1 -1
  9. {maps4fs-2.9.0 → maps4fs-2.9.1}/LICENSE.md +0 -0
  10. {maps4fs-2.9.0 → maps4fs-2.9.1}/README.md +0 -0
  11. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/__init__.py +0 -0
  12. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/__init__.py +0 -0
  13. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/__init__.py +0 -0
  14. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/base/__init__.py +0 -0
  15. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/base/component.py +0 -0
  16. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/base/component_image.py +0 -0
  17. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/base/component_xml.py +0 -0
  18. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/config.py +0 -0
  19. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/dem.py +0 -0
  20. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/grle.py +0 -0
  21. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/i3d.py +0 -0
  22. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/layer.py +0 -0
  23. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/component/satellite.py +0 -0
  24. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/config.py +0 -0
  25. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/game.py +0 -0
  26. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/map.py +0 -0
  27. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/qgis.py +0 -0
  28. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/settings.py +0 -0
  29. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/statistics.py +0 -0
  30. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs/generator/utils.py +0 -0
  31. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs.egg-info/SOURCES.txt +0 -0
  32. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs.egg-info/dependency_links.txt +0 -0
  33. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs.egg-info/requires.txt +0 -0
  34. {maps4fs-2.9.0 → maps4fs-2.9.1}/maps4fs.egg-info/top_level.txt +0 -0
  35. {maps4fs-2.9.0 → maps4fs-2.9.1}/setup.cfg +0 -0
  36. {maps4fs-2.9.0 → maps4fs-2.9.1}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.9.0
3
+ Version: 2.9.1
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.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
- if not self.not_resized_path or not os.path.isfile(self.not_resized_path):
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(self.not_resized_path, cv2.IMREAD_UNCHANGED)
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
- current_distance = 0
1092
- segments: list[shapely.LineString] = []
1093
- while current_distance < total_length:
1094
- start_point = polyline.interpolate(current_distance)
1095
- end_distance = min(current_distance + SEGMENT_LENGTH, total_length)
1096
- end_point = polyline.interpolate(end_distance)
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
- # Create a small segment LineString
1099
- segment = shapely.LineString([start_point, end_point])
1100
- segments.append(segment)
1101
- current_distance += SEGMENT_LENGTH
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
- self.logger.debug("Number of segments created: %s", len(segments))
1161
+ segment_elevations.append(sample_elevation)
1104
1162
 
1105
- road_polygons: list[shapely.Polygon] = []
1106
- for segment in segments:
1107
- polygon = segment.buffer(
1108
- width * 2, resolution=4, cap_style="flat", join_style="mitre"
1109
- )
1110
- road_polygons.append(polygon)
1111
-
1112
- for polygon in road_polygons:
1113
- polygon_points = polygon.exterior.coords
1114
- road_np = self.polygon_points_to_np(polygon_points)
1115
- mask = np.zeros(dem_image.shape, dtype=np.uint8)
1116
-
1117
- try:
1118
- cv2.fillPoly(mask, [road_np], 255) # type: ignore
1119
- except Exception as e:
1120
- self.logger.debug("Could not create mask for road with error: %s", e)
1121
- continue
1122
-
1123
- mean_value = cv2.mean(dem_image, mask=mask)[0] # type: ignore
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)
@@ -1,5 +1,6 @@
1
1
  """Base class for all components that primarily used to work with meshes."""
2
2
 
3
+ import logging
3
4
  import os
4
5
  import shutil
5
6
  import xml.etree.ElementTree as ET
@@ -15,6 +16,8 @@ from tqdm import tqdm
15
16
  from maps4fs.generator.component.base.component import Component
16
17
  from maps4fs.generator.settings import Parameters
17
18
 
19
+ logger = logging.getLogger("maps4fs")
20
+
18
21
 
19
22
  class MeshComponent(Component):
20
23
  """Base class for all components that primarily used to work with meshes."""
@@ -595,19 +598,53 @@ class MeshComponent(Component):
595
598
  if has_uv:
596
599
  xml_vertices.set("uv0", "true")
597
600
 
598
- # Write vertex data
599
- for idx in tqdm(range(len(vertices)), desc="Writing vertices", unit="vertex"):
600
- v = vertices[idx]
601
- v_el = ET.SubElement(xml_vertices, "v")
602
- v_el.set("p", f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")
601
+ # Pre-format ALL strings using vectorized operations
602
+ pos_strings = np.array([f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}" for v in vertices])
603
+
604
+ normal_strings = None
605
+ if has_normals:
606
+ normal_strings = np.array(
607
+ [f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}" for n in mesh.vertex_normals]
608
+ )
609
+
610
+ if normal_strings is None:
611
+ logger.warning("Normals are missing despite has_normals being True. Can't write i3d.")
612
+ return
613
+
614
+ uv_strings = None
615
+ if has_uv:
616
+ uv_strings = np.array([f"{uv[0]:.6f} {uv[1]:.6f}" for uv in mesh.visual.uv])
617
+
618
+ if uv_strings is None:
619
+ logger.warning("UV coordinates are missing despite has_uv being True. Can't write i3d.")
620
+ return
621
+
622
+ # Batch process vertices for memory efficiency
623
+ batch_size = 2000
624
+ vertex_elements = []
625
+
626
+ for batch_start in tqdm(
627
+ range(0, len(vertices), batch_size), desc="Writing vertices", unit="batch"
628
+ ):
629
+ batch_end = min(batch_start + batch_size, len(vertices))
630
+ batch_elements = []
631
+
632
+ for idx in range(batch_start, batch_end):
633
+ v_el = ET.Element("v")
634
+ v_el.set("p", pos_strings[idx])
635
+
636
+ if has_normals:
637
+ v_el.set("n", normal_strings[idx])
638
+
639
+ if has_uv:
640
+ v_el.set("t0", uv_strings[idx])
641
+
642
+ batch_elements.append(v_el)
603
643
 
604
- if has_normals:
605
- n = mesh.vertex_normals[idx]
606
- v_el.set("n", f"{n[0]:.6f} {n[1]:.6f} {n[2]:.6f}")
644
+ vertex_elements.extend(batch_elements)
607
645
 
608
- if has_uv:
609
- uv = mesh.visual.uv[idx]
610
- v_el.set("t0", f"{uv[0]:.6f} {uv[1]:.6f}")
646
+ # Add all vertex elements at once
647
+ xml_vertices.extend(vertex_elements)
611
648
 
612
649
  # Triangles block
613
650
  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
- if not layer.tags:
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
- # Check if the image contains any non-zero values, otherwise continue.
578
- layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
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
- if not np.any(layer_image): # type: ignore
581
- self.logger.debug(
582
- "Layer %s does not contain any non-zero values, skipping.", layer.name
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
- # Save the original image to use it for preview later, without combining the sublayers.
587
- cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
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
- # Get the coordinates of non-zero values.
590
- non_zero_coords = np.column_stack(np.where(layer_image > 0)) # type: ignore
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
- # Prepare sublayers.
593
- sublayers = [np.zeros_like(layer_image) for _ in range(layer.count)]
596
+ # Create random assignment array for all pixels
597
+ random_assignment = np.random.randint(0, layer.count, size=layer_image.shape)
594
598
 
595
- # Randomly assign non-zero values to sublayers.
596
- for coord in non_zero_coords:
597
- sublayers[np.random.randint(0, layer.count)][coord[0], coord[1]] = 255 # type: ignore
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
- # Save the sublayers.
600
- for sublayer, sublayer_path in zip(sublayers, layer_paths):
601
- cv2.imwrite(sublayer_path, sublayer)
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
- self.logger.debug("Dissolved layer %s.", layer.name)
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.info(
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.9.0
3
+ Version: 2.9.1
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: GNU Affero General Public License v3.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "2.9.0"
7
+ version = "2.9.1"
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