maps4fs 1.8.13__tar.gz → 1.8.15__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.
Files changed (48) hide show
  1. {maps4fs-1.8.13 → maps4fs-1.8.15}/PKG-INFO +7 -1
  2. {maps4fs-1.8.13 → maps4fs-1.8.15}/README.md +6 -0
  3. {maps4fs-1.8.13/maps4fs/generator → maps4fs-1.8.15/maps4fs/generator/component}/background.py +50 -214
  4. maps4fs-1.8.15/maps4fs/generator/component/base/component_image.py +90 -0
  5. maps4fs-1.8.15/maps4fs/generator/component/base/component_mesh.py +125 -0
  6. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/component/base/component_xml.py +13 -0
  7. {maps4fs-1.8.13/maps4fs/generator → maps4fs-1.8.15/maps4fs/generator/component}/grle.py +160 -178
  8. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/dtm.py +0 -4
  9. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/game.py +54 -2
  10. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/map.py +1 -1
  11. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/satellite.py +2 -2
  12. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/settings.py +10 -0
  13. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/texture.py +6 -1
  14. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs.egg-info/PKG-INFO +7 -1
  15. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs.egg-info/SOURCES.txt +4 -2
  16. {maps4fs-1.8.13 → maps4fs-1.8.15}/pyproject.toml +1 -1
  17. {maps4fs-1.8.13 → maps4fs-1.8.15}/LICENSE.md +0 -0
  18. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/__init__.py +0 -0
  19. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/__init__.py +0 -0
  20. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/component/__init__.py +0 -0
  21. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/component/base/__init__.py +0 -0
  22. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/component/base/component.py +0 -0
  23. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/component/config.py +0 -0
  24. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/component/i3d.py +0 -0
  25. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dem.py +0 -0
  26. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/__init__.py +0 -0
  27. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/base/wcs.py +0 -0
  28. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/base/wms.py +0 -0
  29. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/bavaria.py +0 -0
  30. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/canada.py +0 -0
  31. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/england.py +0 -0
  32. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/hessen.py +0 -0
  33. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/niedersachsen.py +0 -0
  34. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/nrw.py +0 -0
  35. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/srtm.py +0 -0
  36. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/usgs.py +0 -0
  37. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/dtm/utils.py +0 -0
  38. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/generator/qgis.py +0 -0
  39. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/logger.py +0 -0
  40. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/toolbox/__init__.py +0 -0
  41. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/toolbox/background.py +0 -0
  42. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/toolbox/custom_osm.py +0 -0
  43. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs/toolbox/dem.py +0 -0
  44. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs.egg-info/dependency_links.txt +0 -0
  45. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs.egg-info/requires.txt +0 -0
  46. {maps4fs-1.8.13 → maps4fs-1.8.15}/maps4fs.egg-info/top_level.txt +0 -0
  47. {maps4fs-1.8.13 → maps4fs-1.8.15}/setup.cfg +0 -0
  48. {maps4fs-1.8.13 → maps4fs-1.8.15}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.8.13
3
+ Version: 1.8.15
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -595,6 +595,10 @@ You can also apply some advanced settings to the map generation process.<br>
595
595
 
596
596
  - Add Farmyards - if enabled, the tool will create farmlands from the regions that are marked as farmyards in the OSM data. Those farmlands will not have fields and also will not be drawn on textures. By default, it's turned off.
597
597
 
598
+ - Base price - the base price of the farmland. It's used to calculate the price of the farmland in the game. In default in-game maps this value equals to 60000.
599
+
600
+ - Price scale - is a value in percent which will be applied to all farmnlands. The price per Ha will be calculated as `base_price * price_scale / 100`. By default, it's set to 100%.
601
+
598
602
  - Base grass - you can select which plant will be used as a base grass on the map.
599
603
 
600
604
  - Plants island minimum size - when random plants are enabled, the generator will add islands of differents plants to the map and choose the random size of those island between the minimum and maximum values. This one is the minimum size of the island in meters.
@@ -621,6 +625,8 @@ You can also apply some advanced settings to the map generation process.<br>
621
625
 
622
626
  - Skip drains - if enabled, the tool will not generate the drains and ditches on the map. By default, it's set to False. Use this if you don't need the drains on the map.
623
627
 
628
+ - Use cache - if enabled, the tool will use the cached OSM data for generating the map. It's useful when you're generating the same map multiple times and don't want to download the OSM data each time. But if you've made some changes to the OSM data, you should disable this option to get the updated data. By default, it's set to True. This option has no effect when you're using the custom OSM file.
629
+
624
630
  ### Splines Advanced settings
625
631
 
626
632
  - Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
@@ -569,6 +569,10 @@ You can also apply some advanced settings to the map generation process.<br>
569
569
 
570
570
  - Add Farmyards - if enabled, the tool will create farmlands from the regions that are marked as farmyards in the OSM data. Those farmlands will not have fields and also will not be drawn on textures. By default, it's turned off.
571
571
 
572
+ - Base price - the base price of the farmland. It's used to calculate the price of the farmland in the game. In default in-game maps this value equals to 60000.
573
+
574
+ - Price scale - is a value in percent which will be applied to all farmnlands. The price per Ha will be calculated as `base_price * price_scale / 100`. By default, it's set to 100%.
575
+
572
576
  - Base grass - you can select which plant will be used as a base grass on the map.
573
577
 
574
578
  - Plants island minimum size - when random plants are enabled, the generator will add islands of differents plants to the map and choose the random size of those island between the minimum and maximum values. This one is the minimum size of the island in meters.
@@ -595,6 +599,8 @@ You can also apply some advanced settings to the map generation process.<br>
595
599
 
596
600
  - Skip drains - if enabled, the tool will not generate the drains and ditches on the map. By default, it's set to False. Use this if you don't need the drains on the map.
597
601
 
602
+ - Use cache - if enabled, the tool will use the cached OSM data for generating the map. It's useful when you're generating the same map multiple times and don't want to download the OSM data each time. But if you've made some changes to the OSM data, you should disable this option to get the updated data. By default, it's set to True. This option has no effect when you're using the custom OSM file.
603
+
598
604
  ### Splines Advanced settings
599
605
 
600
606
  - Splines density - number of points, which will be added (interpolate) between each pair of existing points. The higher the value, the denser the spline will be. It can smooth the splines, but high values can in opposite make the splines look unnatural.
@@ -10,21 +10,15 @@ from copy import deepcopy
10
10
 
11
11
  import cv2
12
12
  import numpy as np
13
- import trimesh # type: ignore
14
- from tqdm import tqdm
15
13
 
16
- from maps4fs.generator.component.base.component import Component
14
+ from maps4fs.generator.component.base.component_image import ImageComponent
15
+ from maps4fs.generator.component.base.component_mesh import MeshComponent
17
16
  from maps4fs.generator.dem import DEM
17
+ from maps4fs.generator.settings import Parameters
18
18
  from maps4fs.generator.texture import Texture
19
19
 
20
- DEFAULT_DISTANCE = 2048
21
- FULL_NAME = "FULL"
22
- FULL_PREVIEW_NAME = "PREVIEW"
23
- ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
24
20
 
25
-
26
- # pylint: disable=R0902
27
- class Background(Component):
21
+ class Background(MeshComponent, ImageComponent):
28
22
  """Component for creating 3D obj files based on DEM data around the map.
29
23
 
30
24
  Arguments:
@@ -38,11 +32,9 @@ class Background(Component):
38
32
  info, warning. If not provided, default logging will be used.
39
33
  """
40
34
 
41
- # pylint: disable=R0801
42
35
  def preprocess(self) -> None:
43
36
  """Registers the DEMs for the background terrain."""
44
- self.stl_preview_path: str | None = None
45
- self.water_resources_path: str | None = None
37
+ self.stl_preview_path = os.path.join(self.previews_directory, "background_dem.stl")
46
38
 
47
39
  if self.rotation:
48
40
  self.logger.debug("Rotation is enabled: %s.", self.rotation)
@@ -50,7 +42,7 @@ class Background(Component):
50
42
  else:
51
43
  output_size_multiplier = 1
52
44
 
53
- self.background_size = self.map_size + DEFAULT_DISTANCE * 2
45
+ self.background_size = self.map_size + Parameters.BACKGROUND_DISTANCE * 2
54
46
  self.rotated_size = int(self.background_size * output_size_multiplier)
55
47
 
56
48
  self.background_directory = os.path.join(self.map_directory, "background")
@@ -58,13 +50,17 @@ class Background(Component):
58
50
  os.makedirs(self.background_directory, exist_ok=True)
59
51
  os.makedirs(self.water_directory, exist_ok=True)
60
52
 
61
- self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
53
+ self.water_resources_path = os.path.join(self.water_directory, "water_resources.png")
54
+
55
+ self.output_path = os.path.join(self.background_directory, f"{Parameters.FULL}.png")
62
56
  if self.map.custom_background_path:
63
- self.check_custom_background(self.map.custom_background_path)
57
+ self.validate_np_for_mesh(self.map.custom_background_path, self.map_size)
64
58
  shutil.copyfile(self.map.custom_background_path, self.output_path)
65
59
 
66
- self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
67
- self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
60
+ self.not_substracted_path: str = os.path.join(
61
+ self.background_directory, "not_substracted.png"
62
+ )
63
+ self.not_resized_path: str = os.path.join(self.background_directory, "not_resized.png")
68
64
 
69
65
  self.dem = DEM(
70
66
  self.game,
@@ -80,39 +76,6 @@ class Background(Component):
80
76
  self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
81
77
  self.dem.set_dem_path(self.output_path)
82
78
 
83
- def check_custom_background(self, image_path: str) -> None:
84
- """Checks if the custom background image meets the requirements.
85
-
86
- Arguments:
87
- image_path (str): The path to the custom background image.
88
-
89
- Raises:
90
- ValueError: If the custom background image does not meet the requirements.
91
- """
92
- image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
93
- if image.shape[0] != image.shape[1]:
94
- raise ValueError("The custom background image must be a square.")
95
-
96
- if image.shape[0] != self.map_size + DEFAULT_DISTANCE * 2:
97
- raise ValueError("The custom background image must have the size of the map + 4096.")
98
-
99
- if len(image.shape) != 2:
100
- raise ValueError("The custom background image must be a grayscale image.")
101
-
102
- if image.dtype != np.uint16:
103
- raise ValueError("The custom background image must be a 16-bit grayscale image.")
104
-
105
- def is_preview(self, name: str) -> bool:
106
- """Checks if the DEM is a preview.
107
-
108
- Arguments:
109
- name (str): The name of the DEM.
110
-
111
- Returns:
112
- bool: True if the DEM is a preview, False otherwise.
113
- """
114
- return name == FULL_PREVIEW_NAME
115
-
116
79
  def process(self) -> None:
117
80
  """Launches the component processing. Iterates over all tiles and processes them
118
81
  as a result the DEM files will be saved, then based on them the obj files will be
@@ -123,12 +86,12 @@ class Background(Component):
123
86
  self.dem.process()
124
87
 
125
88
  shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
126
- self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
89
+ self.save_map_dem(self.dem.dem_path, save_path=self.not_resized_path)
127
90
 
128
91
  if self.map.dem_settings.water_depth:
129
92
  self.subtraction()
130
93
 
131
- cutted_dem_path = self.cutout(self.dem.dem_path)
94
+ cutted_dem_path = self.save_map_dem(self.dem.dem_path)
132
95
  if self.game.additional_dem_name is not None:
133
96
  self.make_copy(cutted_dem_path, self.game.additional_dem_name)
134
97
 
@@ -184,9 +147,9 @@ class Background(Component):
184
147
 
185
148
  def qgis_sequence(self) -> None:
186
149
  """Generates QGIS scripts for creating bounding box layers and rasterizing them."""
187
- qgis_layer = (f"Background_{FULL_NAME}", *self.dem.get_espg3857_bbox())
150
+ qgis_layer = (f"Background_{Parameters.FULL}", *self.dem.get_espg3857_bbox())
188
151
  qgis_layer_with_margin = (
189
- f"Background_{FULL_NAME}_margin",
152
+ f"Background_{Parameters.FULL}_margin",
190
153
  *self.dem.get_espg3857_bbox(add_margin=True),
191
154
  )
192
155
  self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
@@ -214,10 +177,9 @@ class Background(Component):
214
177
  create_preview=True,
215
178
  remove_center=self.map.background_settings.remove_center,
216
179
  include_zeros=False,
217
- ) # type: ignore
180
+ )
218
181
 
219
- # pylint: disable=too-many-locals
220
- def cutout(self, dem_path: str, save_path: str | None = None) -> str:
182
+ def save_map_dem(self, dem_path: str, save_path: str | None = None) -> str:
221
183
  """Cuts out the center of the DEM (the actual map) and saves it as a separate file.
222
184
 
223
185
  Arguments:
@@ -228,14 +190,8 @@ class Background(Component):
228
190
  str -- The path to the cutout DEM file.
229
191
  """
230
192
  dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
231
-
232
- center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
233
193
  half_size = self.map_size // 2
234
- x1 = center[0] - half_size
235
- x2 = center[0] + half_size
236
- y1 = center[1] - half_size
237
- y2 = center[1] + half_size
238
- dem_data = dem_data[x1:x2, y1:y2]
194
+ dem_data = self.cut_out_np(dem_data, half_size, return_cutout=True)
239
195
 
240
196
  if save_path:
241
197
  cv2.imwrite(save_path, dem_data)
@@ -260,26 +216,6 @@ class Background(Component):
260
216
 
261
217
  return main_dem_path
262
218
 
263
- def remove_center(self, dem_data: np.ndarray, resize_factor: float) -> np.ndarray:
264
- """Removes the center part of the DEM data.
265
-
266
- Arguments:
267
- dem_data (np.ndarray) -- The DEM data as a numpy array.
268
- resize_factor (float) -- The resize factor of the DEM data.
269
-
270
- Returns:
271
- np.ndarray -- The DEM data with the center part removed.
272
- """
273
- center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
274
- half_size = int(self.map_size // 2 * resize_factor)
275
- x1 = center[0] - half_size
276
- x2 = center[0] + half_size
277
- y1 = center[1] - half_size
278
- y2 = center[1] + half_size
279
- dem_data[x1:x2, y1:y2] = 0
280
- return dem_data
281
-
282
- # pylint: disable=R0913, R0917, R0915
283
219
  def plane_from_np(
284
220
  self,
285
221
  dem_data: np.ndarray,
@@ -302,110 +238,29 @@ class Background(Component):
302
238
  resize_factor = 1 / self.map.background_settings.resize_factor
303
239
  dem_data = cv2.resize(dem_data, (0, 0), fx=resize_factor, fy=resize_factor)
304
240
  if remove_center:
305
- dem_data = self.remove_center(dem_data, resize_factor)
241
+ half_size = int(self.map_size // 2 * resize_factor)
242
+ dem_data = self.cut_out_np(dem_data, half_size, set_zeros=True)
306
243
  self.logger.debug("Center removed from DEM data.")
307
244
  self.logger.debug(
308
245
  "DEM data resized to shape: %s with factor: %s", dem_data.shape, resize_factor
309
246
  )
310
247
 
311
- # Invert the height values.
312
- dem_data = dem_data.max() - dem_data
313
-
314
- rows, cols = dem_data.shape
315
- x = np.linspace(0, cols - 1, cols)
316
- y = np.linspace(0, rows - 1, rows)
317
- x, y = np.meshgrid(x, y)
318
- z = dem_data
319
-
320
- ground = z.max()
321
- self.logger.debug("Ground level: %s", ground)
322
-
323
- self.logger.debug(
324
- "Starting to generate a mesh for with shape: %s x %s. This may take a while.",
325
- cols,
326
- rows,
248
+ mesh = self.mesh_from_np(
249
+ dem_data,
250
+ include_zeros=include_zeros,
251
+ z_scaling_factor=self.get_z_scaling_factor(),
252
+ resize_factor=resize_factor,
253
+ apply_decimation=self.map.background_settings.apply_decimation,
254
+ decimation_percent=self.map.background_settings.decimation_percent,
255
+ decimation_agression=self.map.background_settings.decimation_agression,
327
256
  )
328
257
 
329
- vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
330
- faces = []
331
-
332
- skipped = 0
333
-
334
- for i in tqdm(range(rows - 1), desc="Generating mesh", unit="row"):
335
- for j in range(cols - 1):
336
- top_left = i * cols + j
337
- top_right = top_left + 1
338
- bottom_left = top_left + cols
339
- bottom_right = bottom_left + 1
340
-
341
- if (
342
- ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]
343
- and not include_zeros
344
- ):
345
- skipped += 1
346
- continue
347
-
348
- faces.append([top_left, bottom_left, bottom_right])
349
- faces.append([top_left, bottom_right, top_right])
350
-
351
- self.logger.debug("Skipped faces: %s", skipped)
352
-
353
- faces = np.array(faces) # type: ignore
354
- mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
355
-
356
- # Apply rotation: 180 degrees around Y-axis and Z-axis
357
- rotation_matrix_y = trimesh.transformations.rotation_matrix(np.pi, [0, 1, 0])
358
- rotation_matrix_z = trimesh.transformations.rotation_matrix(np.pi, [0, 0, 1])
359
- mesh.apply_transform(rotation_matrix_y)
360
- mesh.apply_transform(rotation_matrix_z)
361
-
362
- # if not include_zeros:
363
- z_scaling_factor = self.get_z_scaling_factor()
364
- self.logger.debug("Z scaling factor: %s", z_scaling_factor)
365
- mesh.apply_scale([1 / resize_factor, 1 / resize_factor, z_scaling_factor])
366
-
367
- old_faces = len(mesh.faces)
368
- self.logger.debug("Mesh generated with %s faces.", old_faces)
369
-
370
- if self.map.background_settings.apply_decimation:
371
- percent = self.map.background_settings.decimation_percent / 100
372
- mesh = mesh.simplify_quadric_decimation(
373
- percent=percent, aggression=self.map.background_settings.decimation_agression
374
- )
375
-
376
- new_faces = len(mesh.faces)
377
- decimation_percent = (old_faces - new_faces) / old_faces * 100
378
-
379
- self.logger.debug(
380
- "Mesh simplified to %s faces. Decimation percent: %s", new_faces, decimation_percent
381
- )
382
-
383
258
  mesh.export(save_path)
384
259
  self.logger.debug("Obj file saved: %s", save_path)
385
260
 
386
261
  if create_preview:
387
- # Simplify the preview mesh to reduce the size of the file.
388
- # mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
389
-
390
- # Apply scale to make the preview mesh smaller in the UI.
391
262
  mesh.apply_scale([0.5, 0.5, 0.5])
392
- self.mesh_to_stl(mesh)
393
-
394
- def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
395
- """Converts the mesh to an STL file and saves it in the previews directory.
396
- Uses powerful simplification to reduce the size of the file since it will be used
397
- only for the preview.
398
-
399
- Arguments:
400
- mesh (trimesh.Trimesh) -- The mesh to convert to an STL file.
401
- """
402
- mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**6)
403
- preview_path = os.path.join(self.previews_directory, "background_dem.stl")
404
- mesh.export(preview_path)
405
-
406
- self.logger.debug("STL file saved: %s", preview_path)
407
-
408
- self.stl_preview_path = preview_path # pylint: disable=attribute-defined-outside-init
263
+ self.mesh_to_stl(mesh, save_path=self.stl_preview_path)
409
264
 
410
265
  def previews(self) -> list[str]:
411
266
  """Returns the path to the image previews paths and the path to the STL preview file.
@@ -421,8 +276,13 @@ class Background(Component):
421
276
  background_dem_preview_image = cv2.resize(
422
277
  background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
423
278
  )
424
- background_dem_preview_image = cv2.normalize( # type: ignore
425
- background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
279
+ background_dem_preview_image = cv2.normalize(
280
+ background_dem_preview_image,
281
+ dst=np.empty_like(background_dem_preview_image),
282
+ alpha=0,
283
+ beta=255,
284
+ norm_type=cv2.NORM_MINMAX,
285
+ dtype=cv2.CV_8U,
426
286
  )
427
287
  background_dem_preview_image = cv2.cvtColor(
428
288
  background_dem_preview_image, cv2.COLOR_GRAY2BGR
@@ -431,7 +291,7 @@ class Background(Component):
431
291
  cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
432
292
  preview_paths.append(background_dem_preview_path)
433
293
 
434
- if self.stl_preview_path:
294
+ if os.path.isfile(self.stl_preview_path):
435
295
  preview_paths.append(self.stl_preview_path)
436
296
 
437
297
  return preview_paths
@@ -483,26 +343,11 @@ class Background(Component):
483
343
 
484
344
  dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
485
345
 
486
- self.logger.debug(
487
- "DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
488
- dem_data.shape,
489
- dem_data.dtype,
490
- dem_data.min(),
491
- dem_data.max(),
492
- )
493
-
494
346
  # Create an empty array with the same shape and type as dem_data.
495
347
  dem_data_normalized = np.empty_like(dem_data)
496
348
 
497
349
  # Normalize the DEM data to the range [0, 255]
498
350
  cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
499
- self.logger.debug(
500
- "DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
501
- dem_data_normalized.shape,
502
- dem_data_normalized.dtype,
503
- dem_data_normalized.min(),
504
- dem_data_normalized.max(),
505
- )
506
351
  dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
507
352
 
508
353
  cv2.imwrite(colored_dem_path, dem_data_colored)
@@ -528,7 +373,7 @@ class Background(Component):
528
373
  if not background_layers:
529
374
  return
530
375
 
531
- self.background_texture = Texture( # pylint: disable=W0201
376
+ self.background_texture = Texture(
532
377
  self.game,
533
378
  self.map,
534
379
  self.coordinates,
@@ -555,13 +400,10 @@ class Background(Component):
555
400
  # Merge all images into one.
556
401
  background_image = np.zeros((self.background_size, self.background_size), dtype=np.uint8)
557
402
  for path in background_paths:
558
- layer = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
559
- background_image = cv2.add(background_image, layer) # type: ignore
403
+ background_layer = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
404
+ background_image = cv2.add(background_image, background_layer) # type: ignore
560
405
 
561
- background_save_path = os.path.join(self.water_directory, "water_resources.png")
562
- cv2.imwrite(background_save_path, background_image)
563
- self.logger.debug("Background texture saved: %s", background_save_path)
564
- self.water_resources_path = background_save_path # pylint: disable=W0201
406
+ cv2.imwrite(self.water_resources_path, background_image)
565
407
 
566
408
  def subtraction(self) -> None:
567
409
  """Subtracts the water depth from the DEM data where the water resources are located."""
@@ -569,20 +411,14 @@ class Background(Component):
569
411
  self.logger.warning("Water resources texture not found.")
570
412
  return
571
413
 
572
- # Single channeled 8 bit image, where the water have values of 255, and the rest 0.
573
414
  water_resources_image = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
574
- mask = water_resources_image == 255
575
-
576
- # Make mask a little bit smaller (1 pixel).
577
- mask = cv2.erode(mask.astype(np.uint8), np.ones((3, 3), np.uint8), iterations=1).astype(
578
- bool
579
- )
580
-
581
415
  dem_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
582
416
 
583
- # Create a mask where water_resources_image is 255 (or not 0)
584
- # Subtract water_depth from dem_image where mask is True
585
- dem_image[mask] = dem_image[mask] - self.map.dem_settings.water_depth
417
+ dem_image = self.subtract_by_mask(
418
+ dem_image,
419
+ water_resources_image,
420
+ self.map.dem_settings.water_depth,
421
+ )
586
422
 
587
423
  # Save the modified dem_image back to the output path
588
424
  cv2.imwrite(self.output_path, dem_image)
@@ -590,7 +426,7 @@ class Background(Component):
590
426
 
591
427
  def generate_water_resources_obj(self) -> None:
592
428
  """Generates 3D obj files based on water resources data."""
593
- if not self.water_resources_path:
429
+ if not os.path.isfile(self.water_resources_path):
594
430
  self.logger.warning("Water resources texture not found.")
595
431
  return
596
432
 
@@ -0,0 +1,90 @@
1
+ """Base class for all components that primarily used to work with images."""
2
+
3
+ import cv2
4
+ import numpy as np
5
+
6
+ from maps4fs.generator.component.base.component import Component
7
+
8
+
9
+ class ImageComponent(Component):
10
+ """Base class for all components that primarily used to work with images."""
11
+
12
+ @staticmethod
13
+ def polygon_points_to_np(
14
+ polygon_points: list[tuple[int, int]], divide: int | None = None
15
+ ) -> np.ndarray:
16
+ """Converts the polygon points to a NumPy array.
17
+
18
+ Arguments:
19
+ polygon_points (list[tuple[int, int]]): The polygon points.
20
+ divide (int, optional): The number to divide the points by. Defaults to None.
21
+
22
+ Returns:
23
+ np.array: The NumPy array of the polygon points.
24
+ """
25
+ array = np.array(polygon_points, dtype=np.int32).reshape((-1, 1, 2))
26
+ if divide:
27
+ return array // divide
28
+ return array
29
+
30
+ @staticmethod
31
+ def cut_out_np(
32
+ image: np.ndarray, half_size: int, set_zeros: bool = False, return_cutout: bool = False
33
+ ) -> np.ndarray:
34
+ """Cuts out a square from the center of the image.
35
+
36
+ Arguments:
37
+ image (np.ndarray): The image.
38
+ half_size (int): The half size of the square.
39
+ set_zeros (bool, optional): Whether to set the cutout to zeros. Defaults to False.
40
+ return_cutout (bool, optional): Whether to return the cutout. Defaults to False.
41
+
42
+ Returns:
43
+ np.ndarray: The image with the cutout or the cutout itself.
44
+ """
45
+ center = (image.shape[0] // 2, image.shape[1] // 2)
46
+ x1 = center[0] - half_size
47
+ x2 = center[0] + half_size
48
+ y1 = center[1] - half_size
49
+ y2 = center[1] + half_size
50
+
51
+ if return_cutout:
52
+ return image[x1:x2, y1:y2]
53
+
54
+ if set_zeros:
55
+ image[x1:x2, y1:y2] = 0
56
+
57
+ return image
58
+
59
+ @staticmethod
60
+ def subtract_by_mask(
61
+ image: np.ndarray,
62
+ image_mask: np.ndarray,
63
+ subtract_by: int,
64
+ mask_by: int = 255,
65
+ erode_kernel: int | None = 3,
66
+ erode_iter: int | None = 1,
67
+ ) -> np.ndarray:
68
+ """Subtracts a value from the image where the mask is equal to the mask by value.
69
+
70
+ Arguments:
71
+ image (np.ndarray): The image.
72
+ image_mask (np.ndarray): The mask.
73
+ subtract_by (int): The value to subtract by.
74
+ mask_by (int, optional): The value to mask by. Defaults to 255.
75
+ erode_kernel (int, optional): The kernel size for the erosion. Defaults to 3.
76
+ erode_iter (int, optional): The number of iterations for the erosion. Defaults to 1.
77
+
78
+ Returns:
79
+ np.ndarray: The image with the subtracted value.
80
+ """
81
+ mask = image_mask == mask_by
82
+ if erode_kernel and erode_iter:
83
+ mask = cv2.erode(
84
+ mask.astype(np.uint8),
85
+ np.ones((erode_kernel, erode_kernel), np.uint8),
86
+ iterations=erode_iter,
87
+ ).astype(bool)
88
+
89
+ image[mask] = image[mask] - subtract_by
90
+ return image