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.

Files changed (36) hide show
  1. {maps4fs-2.9.0 → maps4fs-2.9.2}/PKG-INFO +1 -1
  2. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/background.py +90 -34
  3. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component_mesh.py +39 -13
  4. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/texture.py +38 -32
  5. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/monitor.py +10 -0
  6. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/logger.py +2 -1
  7. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/PKG-INFO +1 -1
  8. {maps4fs-2.9.0 → maps4fs-2.9.2}/pyproject.toml +1 -1
  9. {maps4fs-2.9.0 → maps4fs-2.9.2}/LICENSE.md +0 -0
  10. {maps4fs-2.9.0 → maps4fs-2.9.2}/README.md +0 -0
  11. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/__init__.py +0 -0
  12. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/__init__.py +0 -0
  13. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/__init__.py +0 -0
  14. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/__init__.py +0 -0
  15. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component.py +0 -0
  16. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component_image.py +0 -0
  17. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/base/component_xml.py +0 -0
  18. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/config.py +0 -0
  19. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/dem.py +0 -0
  20. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/grle.py +0 -0
  21. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/i3d.py +0 -0
  22. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/layer.py +0 -0
  23. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/component/satellite.py +0 -0
  24. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/config.py +0 -0
  25. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/game.py +0 -0
  26. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/map.py +0 -0
  27. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/qgis.py +0 -0
  28. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/settings.py +0 -0
  29. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/statistics.py +0 -0
  30. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs/generator/utils.py +0 -0
  31. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/SOURCES.txt +0 -0
  32. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/dependency_links.txt +0 -0
  33. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/requires.txt +0 -0
  34. {maps4fs-2.9.0 → maps4fs-2.9.2}/maps4fs.egg-info/top_level.txt +0 -0
  35. {maps4fs-2.9.0 → maps4fs-2.9.2}/setup.cfg +0 -0
  36. {maps4fs-2.9.0 → maps4fs-2.9.2}/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.2
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)
@@ -595,19 +595,45 @@ class MeshComponent(Component):
595
595
  if has_uv:
596
596
  xml_vertices.set("uv0", "true")
597
597
 
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}")
603
-
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}")
607
-
608
- if has_uv:
609
- uv = mesh.visual.uv[idx]
610
- v_el.set("t0", f"{uv[0]:.6f} {uv[1]:.6f}")
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
- 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.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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maps4fs
3
- Version: 2.9.0
3
+ Version: 2.9.2
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.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