maps4fs 2.0.6__tar.gz → 2.0.8__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.
- {maps4fs-2.0.6 → maps4fs-2.0.8}/PKG-INFO +1 -1
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/background.py +148 -18
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component.py +26 -1
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component_mesh.py +15 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/grle.py +13 -13
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/i3d.py +1 -22
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/texture.py +21 -13
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/map.py +1 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/settings.py +5 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/PKG-INFO +1 -1
- {maps4fs-2.0.6 → maps4fs-2.0.8}/pyproject.toml +1 -1
- {maps4fs-2.0.6 → maps4fs-2.0.8}/LICENSE.md +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/README.md +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/__init__.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/__init__.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/__init__.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/__init__.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component_image.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/base/component_xml.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/config.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/dem.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/layer.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/component/satellite.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/game.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/qgis.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/generator/statistics.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs/logger.py +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/SOURCES.txt +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/dependency_links.txt +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/requires.txt +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/maps4fs.egg-info/top_level.txt +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/setup.cfg +0 -0
- {maps4fs-2.0.6 → maps4fs-2.0.8}/tests/test_generator.py +0 -0
@@ -10,6 +10,8 @@ from typing import Any
|
|
10
10
|
|
11
11
|
import cv2
|
12
12
|
import numpy as np
|
13
|
+
import shapely
|
14
|
+
import trimesh
|
13
15
|
from tqdm import tqdm
|
14
16
|
from trimesh import Trimesh
|
15
17
|
|
@@ -144,7 +146,7 @@ class Background(MeshComponent, ImageComponent):
|
|
144
146
|
self.logger.debug("Could not create mask for building with error: %s", e)
|
145
147
|
continue
|
146
148
|
|
147
|
-
mean_value = cv2.mean(dem_image, mask=mask)[0]
|
149
|
+
mean_value = cv2.mean(dem_image, mask=mask)[0] # type: ignore
|
148
150
|
mean_value = np.round(mean_value).astype(dem_image.dtype)
|
149
151
|
self.logger.debug("Mean value of the building area: %s", mean_value)
|
150
152
|
|
@@ -231,13 +233,13 @@ class Background(MeshComponent, ImageComponent):
|
|
231
233
|
if self.map.output_size is not None:
|
232
234
|
scaled_background_size = int(self.background_size * self.map.size_scale)
|
233
235
|
dem_data = cv2.resize(
|
234
|
-
dem_data,
|
236
|
+
dem_data, # type: ignore
|
235
237
|
(scaled_background_size, scaled_background_size),
|
236
238
|
interpolation=cv2.INTER_NEAREST,
|
237
239
|
)
|
238
240
|
|
239
241
|
self.plane_from_np(
|
240
|
-
dem_data,
|
242
|
+
dem_data, # type: ignore
|
241
243
|
save_path,
|
242
244
|
create_preview=True,
|
243
245
|
remove_center=self.map.background_settings.remove_center,
|
@@ -255,7 +257,7 @@ class Background(MeshComponent, ImageComponent):
|
|
255
257
|
"""
|
256
258
|
dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
|
257
259
|
half_size = self.map_size // 2
|
258
|
-
dem_data = self.cut_out_np(dem_data, half_size, return_cutout=True)
|
260
|
+
dem_data = self.cut_out_np(dem_data, half_size, return_cutout=True) # type: ignore
|
259
261
|
|
260
262
|
if save_path:
|
261
263
|
cv2.imwrite(save_path, dem_data)
|
@@ -362,7 +364,7 @@ class Background(MeshComponent, ImageComponent):
|
|
362
364
|
background_dem_preview_image = cv2.imread(self.output_path, cv2.IMREAD_UNCHANGED)
|
363
365
|
|
364
366
|
background_dem_preview_image = cv2.resize(
|
365
|
-
background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
|
367
|
+
background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4 # type: ignore
|
366
368
|
)
|
367
369
|
background_dem_preview_image = cv2.normalize(
|
368
370
|
background_dem_preview_image,
|
@@ -411,7 +413,7 @@ class Background(MeshComponent, ImageComponent):
|
|
411
413
|
self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
|
412
414
|
|
413
415
|
dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
414
|
-
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
|
416
|
+
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB) # type: ignore
|
415
417
|
cv2.imwrite(grayscale_dem_path, dem_data_rgb)
|
416
418
|
return grayscale_dem_path
|
417
419
|
|
@@ -435,7 +437,7 @@ class Background(MeshComponent, ImageComponent):
|
|
435
437
|
dem_data_normalized = np.empty_like(dem_data)
|
436
438
|
|
437
439
|
# Normalize the DEM data to the range [0, 255]
|
438
|
-
cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
|
440
|
+
cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX) # type: ignore
|
439
441
|
dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
|
440
442
|
|
441
443
|
cv2.imwrite(colored_dem_path, dem_data_colored)
|
@@ -470,6 +472,7 @@ class Background(MeshComponent, ImageComponent):
|
|
470
472
|
logger=self.logger,
|
471
473
|
texture_custom_schema=background_layers, # type: ignore
|
472
474
|
skip_scaling=True, # type: ignore
|
475
|
+
info_layer_path=os.path.join(self.info_layers_directory, "background.json"), # type: ignore
|
473
476
|
)
|
474
477
|
|
475
478
|
self.background_texture.preprocess()
|
@@ -513,8 +516,8 @@ class Background(MeshComponent, ImageComponent):
|
|
513
516
|
)
|
514
517
|
|
515
518
|
dem_image = self.subtract_by_mask(
|
516
|
-
dem_image,
|
517
|
-
water_resources_image,
|
519
|
+
dem_image, # type: ignore
|
520
|
+
water_resources_image, # type: ignore
|
518
521
|
int(self.map.dem_settings.water_depth * z_scaling_factor),
|
519
522
|
)
|
520
523
|
|
@@ -534,8 +537,135 @@ class Background(MeshComponent, ImageComponent):
|
|
534
537
|
|
535
538
|
return blur_power
|
536
539
|
|
540
|
+
def generate_linebased_water(self) -> None:
|
541
|
+
"""Generates water resources based on line-based polylines from the background info layer.
|
542
|
+
It creates polygons from the polylines, fits them into the map bounds, and generates a mesh.
|
543
|
+
"""
|
544
|
+
self.logger.debug("Starting line-based water generation...")
|
545
|
+
water_polygons = self.get_infolayer_data(Parameters.BACKGROUND, Parameters.WATER)
|
546
|
+
self.logger.debug(
|
547
|
+
"Found %s water polygons in background info layer.", len(water_polygons) # type: ignore
|
548
|
+
)
|
549
|
+
if not water_polygons:
|
550
|
+
self.logger.warning("No water polygons found in background info layer.")
|
551
|
+
return
|
552
|
+
|
553
|
+
polygons: list[shapely.Polygon] = []
|
554
|
+
for polygon_points in water_polygons:
|
555
|
+
if not polygon_points or len(polygon_points) < 2:
|
556
|
+
self.logger.warning("Skipping polygon with insufficient points...")
|
557
|
+
continue
|
558
|
+
|
559
|
+
polygon = shapely.Polygon(polygon_points)
|
560
|
+
|
561
|
+
if polygon.is_empty or not polygon.is_valid:
|
562
|
+
self.logger.warning("Skipping empty or invalid polygon...")
|
563
|
+
continue
|
564
|
+
|
565
|
+
# Make Polygon a little bit bigger to hide under the terrain when creating water planes.
|
566
|
+
polygon = polygon.buffer(Parameters.WATER_ADD_WIDTH, resolution=4)
|
567
|
+
|
568
|
+
polygons.append(polygon)
|
569
|
+
|
570
|
+
fitted_polygons = []
|
571
|
+
for polygon in polygons:
|
572
|
+
try:
|
573
|
+
fitted_polygon_points = self.fit_object_into_bounds(
|
574
|
+
polygon_points=polygon.exterior.coords,
|
575
|
+
angle=self.rotation,
|
576
|
+
canvas_size=self.background_size,
|
577
|
+
)
|
578
|
+
fitted_polygon = shapely.Polygon(fitted_polygon_points)
|
579
|
+
fitted_polygons.append(fitted_polygon)
|
580
|
+
except Exception as e:
|
581
|
+
self.logger.warning(
|
582
|
+
"Could not fit polygon into bounds with error: %s, polygon: %s", e, polygon
|
583
|
+
)
|
584
|
+
continue
|
585
|
+
|
586
|
+
if not fitted_polygons:
|
587
|
+
self.logger.warning("No valid water polygons created from polylines.")
|
588
|
+
return
|
589
|
+
|
590
|
+
# Create a mesh from the 3D polygons
|
591
|
+
mesh = self.mesh_from_3d_polygons(fitted_polygons)
|
592
|
+
if mesh is None:
|
593
|
+
self.logger.warning("No mesh could be created from the water polygons.")
|
594
|
+
return
|
595
|
+
self.logger.debug("Created mesh from %s water polygons.", len(fitted_polygons))
|
596
|
+
|
597
|
+
mesh = self.rotate_mesh(mesh)
|
598
|
+
mesh = self.invert_faces(mesh)
|
599
|
+
|
600
|
+
line_based_save_path = os.path.join(self.water_directory, "line_based_water.obj")
|
601
|
+
mesh.export(line_based_save_path)
|
602
|
+
self.logger.debug("Line-based water mesh saved to %s", line_based_save_path)
|
603
|
+
|
604
|
+
def mesh_from_3d_polygons(self, polygons: list[shapely.Polygon]) -> Trimesh | None:
|
605
|
+
"""Create a simple mesh from a list of 3D shapely Polygons.
|
606
|
+
Each polygon must be flat (all Z the same or nearly the same for each polygon).
|
607
|
+
Returns a single Trimesh mesh.
|
608
|
+
|
609
|
+
Arguments:
|
610
|
+
polygons (list[shapely.Polygon]): List of 3D shapely Polygons to create the mesh from.
|
611
|
+
|
612
|
+
Returns:
|
613
|
+
Trimesh: A single Trimesh object containing the mesh created from the polygons.
|
614
|
+
"""
|
615
|
+
|
616
|
+
all_vertices = []
|
617
|
+
all_faces = []
|
618
|
+
vertex_offset = 0
|
619
|
+
|
620
|
+
not_resized_dem = cv2.imread(self.not_resized_path, cv2.IMREAD_UNCHANGED)
|
621
|
+
|
622
|
+
for polygon in polygons:
|
623
|
+
# Get exterior 3D coordinates
|
624
|
+
exterior_coords = np.array(polygon.exterior.coords)
|
625
|
+
# Project to 2D for triangulation
|
626
|
+
exterior_2d = exterior_coords[:, :2]
|
627
|
+
poly_2d = shapely.geometry.Polygon(
|
628
|
+
exterior_2d, [np.array(ring.coords)[:, :2] for ring in polygon.interiors]
|
629
|
+
)
|
630
|
+
|
631
|
+
# Triangulate in 2D
|
632
|
+
vertices_2d, faces = trimesh.creation.triangulate_polygon(poly_2d)
|
633
|
+
# tris.vertices is 2D, tris.faces are indices
|
634
|
+
|
635
|
+
# Map 2D triangulated vertices back to 3D by matching to original 3D coords
|
636
|
+
vertices_3d = []
|
637
|
+
for v in vertices_2d:
|
638
|
+
# Find closest original 2D point to get Z
|
639
|
+
dists = np.linalg.norm(exterior_2d - v[:2], axis=1)
|
640
|
+
idx = np.argmin(dists)
|
641
|
+
# z = exterior_coords[idx, 2]
|
642
|
+
z = self.get_z_coordinate_from_dem(
|
643
|
+
not_resized_dem, exterior_coords[idx, 0], exterior_coords[idx, 1] # type: ignore
|
644
|
+
)
|
645
|
+
vertices_3d.append([v[0], v[1], z])
|
646
|
+
vertices_3d = np.array(vertices_3d) # type: ignore
|
647
|
+
|
648
|
+
faces = faces + vertex_offset
|
649
|
+
all_vertices.append(vertices_3d)
|
650
|
+
all_faces.append(faces)
|
651
|
+
vertex_offset += len(vertices_3d)
|
652
|
+
|
653
|
+
if not all_vertices:
|
654
|
+
return None
|
655
|
+
|
656
|
+
vertices = np.vstack(all_vertices)
|
657
|
+
faces = np.vstack(all_faces)
|
658
|
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
|
659
|
+
return mesh
|
660
|
+
|
537
661
|
def generate_water_resources_obj(self) -> None:
|
538
662
|
"""Generates 3D obj files based on water resources data."""
|
663
|
+
self.logger.debug("Starting water resources generation...")
|
664
|
+
try:
|
665
|
+
self.generate_linebased_water()
|
666
|
+
except Exception as e:
|
667
|
+
self.logger.error("Error during line-based water generation: %s", e)
|
668
|
+
|
539
669
|
if not os.path.isfile(self.water_resources_path):
|
540
670
|
self.logger.warning("Water resources texture not found.")
|
541
671
|
return
|
@@ -544,12 +674,12 @@ class Background(MeshComponent, ImageComponent):
|
|
544
674
|
plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
|
545
675
|
|
546
676
|
# Check if the image contains non-zero values.
|
547
|
-
if not np.any(plane_water):
|
677
|
+
if not np.any(plane_water): # type: ignore
|
548
678
|
self.logger.debug("Water resources image is empty, skipping water generation.")
|
549
679
|
return
|
550
680
|
|
551
681
|
dilated_plane_water = cv2.dilate(
|
552
|
-
plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
|
682
|
+
plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5 # type: ignore
|
553
683
|
).astype(np.uint8)
|
554
684
|
plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
|
555
685
|
self.plane_from_np(dilated_plane_water, plane_save_path, include_zeros=False)
|
@@ -560,12 +690,12 @@ class Background(MeshComponent, ImageComponent):
|
|
560
690
|
if self.map.output_size is not None:
|
561
691
|
scaled_background_size = int(self.background_size * self.map.size_scale)
|
562
692
|
plane_water = cv2.resize(
|
563
|
-
plane_water,
|
693
|
+
plane_water, # type: ignore
|
564
694
|
(scaled_background_size, scaled_background_size),
|
565
695
|
interpolation=cv2.INTER_NEAREST,
|
566
696
|
)
|
567
697
|
background_dem = cv2.resize(
|
568
|
-
background_dem,
|
698
|
+
background_dem, # type: ignore
|
569
699
|
(scaled_background_size, scaled_background_size),
|
570
700
|
interpolation=cv2.INTER_NEAREST,
|
571
701
|
)
|
@@ -574,20 +704,20 @@ class Background(MeshComponent, ImageComponent):
|
|
574
704
|
# Apply Gaussian blur to the background dem.
|
575
705
|
blur_power = self._get_blur_power()
|
576
706
|
background_dem = cv2.GaussianBlur(
|
577
|
-
background_dem, (blur_power, blur_power), sigmaX=blur_power, sigmaY=blur_power
|
707
|
+
background_dem, (blur_power, blur_power), sigmaX=blur_power, sigmaY=blur_power # type: ignore
|
578
708
|
)
|
579
709
|
|
580
710
|
# Remove all the values from the background dem where the plane_water is 0.
|
581
|
-
background_dem[plane_water == 0] = 0
|
711
|
+
background_dem[plane_water == 0] = 0 # type: ignore
|
582
712
|
|
583
713
|
# Dilate the background dem to make the water more smooth.
|
584
|
-
elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10)
|
714
|
+
elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10) # type: ignore
|
585
715
|
|
586
716
|
# Use the background dem as a mask to prevent the original values from being overwritten.
|
587
|
-
mask = background_dem > 0
|
717
|
+
mask = background_dem > 0 # type: ignore
|
588
718
|
|
589
719
|
# Combine the dilated background dem with non-dilated background dem.
|
590
|
-
elevated_water = np.where(mask, background_dem, elevated_water)
|
720
|
+
elevated_water = np.where(mask, background_dem, elevated_water) # type: ignore
|
591
721
|
elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
|
592
722
|
|
593
723
|
self.assets.water_mesh = elevated_save_path
|
@@ -8,6 +8,7 @@ from copy import deepcopy
|
|
8
8
|
from typing import TYPE_CHECKING, Any
|
9
9
|
|
10
10
|
import cv2
|
11
|
+
import numpy as np
|
11
12
|
import osmnx as ox
|
12
13
|
from pyproj import Transformer
|
13
14
|
from shapely.affinity import rotate, translate
|
@@ -363,6 +364,7 @@ class Component:
|
|
363
364
|
margin: int = 0,
|
364
365
|
angle: int = 0,
|
365
366
|
border: int = 0,
|
367
|
+
canvas_size: int | None = None,
|
366
368
|
) -> list[tuple[int, int]]:
|
367
369
|
"""Fits a polygon into the bounds of the map.
|
368
370
|
|
@@ -379,8 +381,10 @@ class Component:
|
|
379
381
|
if polygon_points is None and linestring_points is None:
|
380
382
|
raise ValueError("Either polygon or linestring points must be provided.")
|
381
383
|
|
384
|
+
limit = canvas_size or self.scaled_size
|
385
|
+
|
382
386
|
min_x = min_y = 0 + border
|
383
|
-
max_x = max_y =
|
387
|
+
max_x = max_y = limit - border
|
384
388
|
|
385
389
|
object_type = Polygon if polygon_points else LineString
|
386
390
|
|
@@ -586,3 +590,24 @@ class Component:
|
|
586
590
|
int: The output size of the map or the map size.
|
587
591
|
"""
|
588
592
|
return self.map_size if self.map.output_size is None else self.map.output_size
|
593
|
+
|
594
|
+
def get_z_coordinate_from_dem(self, not_resized_dem: np.ndarray, x: int, y: int) -> float:
|
595
|
+
"""Gets the Z coordinate from the DEM image for the given coordinates.
|
596
|
+
|
597
|
+
Arguments:
|
598
|
+
not_resized_dem (np.ndarray): The not resized DEM image.
|
599
|
+
x (int): The x coordinate.
|
600
|
+
y (int): The y coordinate.
|
601
|
+
|
602
|
+
Returns:
|
603
|
+
float: The Z coordinate.
|
604
|
+
"""
|
605
|
+
dem_x_size, dem_y_size = not_resized_dem.shape
|
606
|
+
|
607
|
+
x = int(max(0, min(x, dem_x_size - 1)))
|
608
|
+
y = int(max(0, min(y, dem_y_size - 1)))
|
609
|
+
|
610
|
+
z = not_resized_dem[y, x]
|
611
|
+
z *= self.get_z_scaling_factor(ignore_height_scale_multiplier=True)
|
612
|
+
|
613
|
+
return z
|
@@ -252,3 +252,18 @@ class MeshComponent(Component):
|
|
252
252
|
distance = int(round(x_size) / 2)
|
253
253
|
mesh_copy.apply_translation([-distance, distance, 0])
|
254
254
|
return mesh_copy
|
255
|
+
|
256
|
+
@staticmethod
|
257
|
+
def invert_faces(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
|
258
|
+
"""
|
259
|
+
Inverts the faces (normals) of the mesh by reversing the order of indices in each face.
|
260
|
+
|
261
|
+
Arguments:
|
262
|
+
mesh (trimesh.Trimesh): The mesh whose faces are to be inverted.
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
trimesh.Trimesh: A new mesh with inverted faces.
|
266
|
+
"""
|
267
|
+
mesh_copy = mesh.copy()
|
268
|
+
mesh_copy.faces = mesh_copy.faces[:, ::-1]
|
269
|
+
return mesh_copy
|
@@ -118,14 +118,14 @@ class GRLE(ImageComponent, XMLComponent):
|
|
118
118
|
# Resize the preview image to the maximum size allowed for previews.
|
119
119
|
image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
|
120
120
|
if (
|
121
|
-
image.shape[0] > Parameters.PREVIEW_MAXIMUM_SIZE
|
122
|
-
or image.shape[1] > Parameters.PREVIEW_MAXIMUM_SIZE
|
121
|
+
image.shape[0] > Parameters.PREVIEW_MAXIMUM_SIZE # type: ignore
|
122
|
+
or image.shape[1] > Parameters.PREVIEW_MAXIMUM_SIZE # type: ignore
|
123
123
|
):
|
124
124
|
image = cv2.resize(
|
125
|
-
image, (Parameters.PREVIEW_MAXIMUM_SIZE, Parameters.PREVIEW_MAXIMUM_SIZE)
|
125
|
+
image, (Parameters.PREVIEW_MAXIMUM_SIZE, Parameters.PREVIEW_MAXIMUM_SIZE) # type: ignore
|
126
126
|
)
|
127
127
|
image_normalized = np.empty_like(image)
|
128
|
-
cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
|
128
|
+
cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX) # type: ignore
|
129
129
|
image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
|
130
130
|
cv2.imwrite(save_path, image_colored)
|
131
131
|
preview_paths.append(save_path)
|
@@ -163,7 +163,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
163
163
|
return None
|
164
164
|
fields_np = cv2.imread(fields_layer_path)
|
165
165
|
# Resize fields_np to the same size as farmlands_np.
|
166
|
-
fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0]))
|
166
|
+
fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0])) # type: ignore
|
167
167
|
|
168
168
|
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
169
169
|
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
@@ -235,7 +235,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
235
235
|
break
|
236
236
|
|
237
237
|
try:
|
238
|
-
cv2.fillPoly(image, [farmland_np], (float(farmland_id),))
|
238
|
+
cv2.fillPoly(image, [farmland_np], (float(farmland_id),)) # type: ignore
|
239
239
|
except Exception as e:
|
240
240
|
self.logger.debug(
|
241
241
|
"Farmland %s could not be added to the InfoLayer PNG file with error: %s",
|
@@ -257,9 +257,9 @@ class GRLE(ImageComponent, XMLComponent):
|
|
257
257
|
|
258
258
|
# Replace all the zero values on the info layer image with 255.
|
259
259
|
if self.map.grle_settings.fill_empty_farmlands:
|
260
|
-
image[image == 0] = 255
|
260
|
+
image[image == 0] = 255 # type: ignore
|
261
261
|
|
262
|
-
cv2.imwrite(info_layer_farmlands_path, image)
|
262
|
+
cv2.imwrite(info_layer_farmlands_path, image) # type: ignore
|
263
263
|
|
264
264
|
self.assets.farmlands = info_layer_farmlands_path
|
265
265
|
|
@@ -303,8 +303,8 @@ class GRLE(ImageComponent, XMLComponent):
|
|
303
303
|
# Density map of the fruits is 2X size of the base image, so we need to resize it.
|
304
304
|
# We'll resize the base image to make it bigger, so we can compare the values.
|
305
305
|
grass_image = cv2.resize(
|
306
|
-
grass_image,
|
307
|
-
(grass_image.shape[1] * 2, grass_image.shape[0] * 2),
|
306
|
+
grass_image, # type: ignore
|
307
|
+
(grass_image.shape[1] * 2, grass_image.shape[0] * 2), # type: ignore
|
308
308
|
interpolation=cv2.INTER_NEAREST,
|
309
309
|
)
|
310
310
|
if forest_image is not None:
|
@@ -352,15 +352,15 @@ class GRLE(ImageComponent, XMLComponent):
|
|
352
352
|
# Three channeled 8-bit image, where non-zero values are the
|
353
353
|
# different types of plants (only in the R channel).
|
354
354
|
density_map_fruits = cv2.imread(density_map_fruit_path, cv2.IMREAD_UNCHANGED)
|
355
|
-
self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
|
355
|
+
self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape) # type: ignore
|
356
356
|
|
357
357
|
# Put the updated base image as the B channel in the density map.
|
358
|
-
density_map_fruits[:, :, 0] = grass_image_copy
|
358
|
+
density_map_fruits[:, :, 0] = grass_image_copy # type: ignore
|
359
359
|
self.logger.debug("Updated base image added as the B channel in the density map.")
|
360
360
|
|
361
361
|
# Save the updated density map.
|
362
362
|
# Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
|
363
|
-
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
|
363
|
+
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB) # type: ignore
|
364
364
|
cv2.imwrite(density_map_fruit_path, density_map_fruits)
|
365
365
|
|
366
366
|
self.assets.plants = density_map_fruit_path
|
@@ -543,7 +543,7 @@ class I3d(XMLComponent):
|
|
543
543
|
|
544
544
|
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
|
545
545
|
for x, y in self.non_empty_pixels(
|
546
|
-
forest_image, step=self.map.i3d_settings.forest_density
|
546
|
+
forest_image, step=self.map.i3d_settings.forest_density # type: ignore
|
547
547
|
):
|
548
548
|
shifted_x, shifted_y = self.randomize_coordinates(
|
549
549
|
(x, y),
|
@@ -638,24 +638,3 @@ class I3d(XMLComponent):
|
|
638
638
|
not_resized_dem = cv2.imread(background_component.not_resized_path, cv2.IMREAD_UNCHANGED)
|
639
639
|
|
640
640
|
return not_resized_dem
|
641
|
-
|
642
|
-
def get_z_coordinate_from_dem(self, not_resized_dem: np.ndarray, x: int, y: int) -> float:
|
643
|
-
"""Gets the Z coordinate from the DEM image for the given coordinates.
|
644
|
-
|
645
|
-
Arguments:
|
646
|
-
not_resized_dem (np.ndarray): The not resized DEM image.
|
647
|
-
x (int): The x coordinate.
|
648
|
-
y (int): The y coordinate.
|
649
|
-
|
650
|
-
Returns:
|
651
|
-
float: The Z coordinate.
|
652
|
-
"""
|
653
|
-
dem_x_size, dem_y_size = not_resized_dem.shape
|
654
|
-
|
655
|
-
x = int(max(0, min(x, dem_x_size - 1)))
|
656
|
-
y = int(max(0, min(y, dem_y_size - 1)))
|
657
|
-
|
658
|
-
z = not_resized_dem[y, x]
|
659
|
-
z *= self.get_z_scaling_factor(ignore_height_scale_multiplier=True)
|
660
|
-
|
661
|
-
return z
|
@@ -44,7 +44,10 @@ class Texture(ImageComponent):
|
|
44
44
|
os.makedirs(self.procedural_dir, exist_ok=True)
|
45
45
|
|
46
46
|
self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
|
47
|
-
|
47
|
+
if not self.kwargs.get("info_layer_path"):
|
48
|
+
self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
|
49
|
+
else:
|
50
|
+
self.info_layer_path = self.kwargs["info_layer_path"] # type: ignore
|
48
51
|
|
49
52
|
def read_layers(self, layers_schema: list[dict[str, Any]]) -> None:
|
50
53
|
"""Reads layers from the schema.
|
@@ -174,9 +177,9 @@ class Texture(ImageComponent):
|
|
174
177
|
if not border:
|
175
178
|
continue
|
176
179
|
|
177
|
-
self.transfer_border(layer_image, base_layer_image, border)
|
180
|
+
self.transfer_border(layer_image, base_layer_image, border) # type: ignore
|
178
181
|
|
179
|
-
cv2.imwrite(layer.path(self._weights_dir), layer_image)
|
182
|
+
cv2.imwrite(layer.path(self._weights_dir), layer_image) # type: ignore
|
180
183
|
self.logger.debug("Borders added to layer %s.", layer.name)
|
181
184
|
|
182
185
|
if base_layer_image is not None:
|
@@ -261,7 +264,7 @@ class Texture(ImageComponent):
|
|
261
264
|
self.logger.debug("Scaling layer %s.", layer_path)
|
262
265
|
img = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
263
266
|
img = cv2.resize(
|
264
|
-
img,
|
267
|
+
img, # type: ignore
|
265
268
|
(self.map.output_size, self.map.output_size),
|
266
269
|
interpolation=cv2.INTER_NEAREST,
|
267
270
|
)
|
@@ -384,12 +387,12 @@ class Texture(ImageComponent):
|
|
384
387
|
self.logger.debug("First layer, creating new cumulative image.")
|
385
388
|
cumulative_image = layer_image
|
386
389
|
|
387
|
-
mask = cv2.bitwise_not(cumulative_image)
|
388
|
-
self._draw_layer(layer, info_layer_data, layer_image)
|
390
|
+
mask = cv2.bitwise_not(cumulative_image) # type: ignore
|
391
|
+
self._draw_layer(layer, info_layer_data, layer_image) # type: ignore
|
389
392
|
self._add_roads(layer, info_layer_data)
|
390
393
|
|
391
|
-
output_image = cv2.bitwise_and(layer_image, mask)
|
392
|
-
cumulative_image = cv2.bitwise_or(cumulative_image, output_image)
|
394
|
+
output_image = cv2.bitwise_and(layer_image, mask) # type: ignore
|
395
|
+
cumulative_image = cv2.bitwise_or(cumulative_image, output_image) # type: ignore
|
393
396
|
|
394
397
|
cv2.imwrite(layer_path, output_image)
|
395
398
|
self.logger.debug("Texture %s saved.", layer_path)
|
@@ -452,7 +455,11 @@ class Texture(ImageComponent):
|
|
452
455
|
layer (Layer): Layer with textures and tags.
|
453
456
|
info_layer_data (dict[list[list[int]]]): Dictionary to store info layer data.
|
454
457
|
"""
|
455
|
-
|
458
|
+
linestring_infolayers = ["roads"]
|
459
|
+
if self.kwargs.get("info_layer_path", None):
|
460
|
+
linestring_infolayers.append("water")
|
461
|
+
|
462
|
+
if layer.info_layer in linestring_infolayers:
|
456
463
|
for linestring in self.objects_generator(
|
457
464
|
layer.tags, layer.width, layer.info_layer, yield_linestrings=True
|
458
465
|
):
|
@@ -464,6 +471,7 @@ class Texture(ImageComponent):
|
|
464
471
|
linestring_entry = {
|
465
472
|
"points": linestring,
|
466
473
|
"tags": str(layer.tags),
|
474
|
+
"width": layer.width,
|
467
475
|
}
|
468
476
|
info_layer_data[f"{layer.info_layer}_polylines"].append(linestring_entry) # type: ignore
|
469
477
|
|
@@ -490,17 +498,17 @@ class Texture(ImageComponent):
|
|
490
498
|
# Check if the image contains any non-zero values, otherwise continue.
|
491
499
|
layer_image = cv2.imread(layer_path, cv2.IMREAD_UNCHANGED)
|
492
500
|
|
493
|
-
if not np.any(layer_image):
|
501
|
+
if not np.any(layer_image): # type: ignore
|
494
502
|
self.logger.debug(
|
495
503
|
"Layer %s does not contain any non-zero values, skipping.", layer.name
|
496
504
|
)
|
497
505
|
continue
|
498
506
|
|
499
507
|
# Save the original image to use it for preview later, without combining the sublayers.
|
500
|
-
cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy())
|
508
|
+
cv2.imwrite(layer.path_preview(self._weights_dir), layer_image.copy()) # type: ignore
|
501
509
|
|
502
510
|
# Get the coordinates of non-zero values.
|
503
|
-
non_zero_coords = np.column_stack(np.where(layer_image > 0))
|
511
|
+
non_zero_coords = np.column_stack(np.where(layer_image > 0)) # type: ignore
|
504
512
|
|
505
513
|
# Prepare sublayers.
|
506
514
|
sublayers = [np.zeros_like(layer_image) for _ in range(layer.count)]
|
@@ -817,7 +825,7 @@ class Texture(ImageComponent):
|
|
817
825
|
|
818
826
|
images = [
|
819
827
|
cv2.resize(
|
820
|
-
cv2.imread(layer.get_preview_or_path(self._weights_dir), cv2.IMREAD_UNCHANGED),
|
828
|
+
cv2.imread(layer.get_preview_or_path(self._weights_dir), cv2.IMREAD_UNCHANGED), # type: ignore
|
821
829
|
preview_size,
|
822
830
|
)
|
823
831
|
for layer in active_layers
|
@@ -445,6 +445,7 @@ def fix_osm_file(input_file_path: str, output_file_path: str | None = None) -> t
|
|
445
445
|
and the number of fixed errors.
|
446
446
|
"""
|
447
447
|
broken_entries = ["relation", ".//*[@action='delete']"]
|
448
|
+
output_file_path = output_file_path or input_file_path
|
448
449
|
|
449
450
|
tree = ET.parse(input_file_path)
|
450
451
|
root = tree.getroot()
|
@@ -15,8 +15,11 @@ class Parameters:
|
|
15
15
|
FIELDS = "fields"
|
16
16
|
BUILDINGS = "buildings"
|
17
17
|
TEXTURES = "textures"
|
18
|
+
BACKGROUND = "background"
|
18
19
|
FOREST = "forest"
|
19
20
|
ROADS_POLYLINES = "roads_polylines"
|
21
|
+
WATER_POLYLINES = "water_polylines"
|
22
|
+
WATER = "water"
|
20
23
|
FARMYARDS = "farmyards"
|
21
24
|
|
22
25
|
PREVIEW_MAXIMUM_SIZE = 2048
|
@@ -35,6 +38,8 @@ class Parameters:
|
|
35
38
|
PLANTS_ISLAND_VERTEX_COUNT = 30
|
36
39
|
PLANTS_ISLAND_ROUNDING_RADIUS = 15
|
37
40
|
|
41
|
+
WATER_ADD_WIDTH = 2
|
42
|
+
|
38
43
|
|
39
44
|
class SharedSettings(BaseModel):
|
40
45
|
"""Represents the shared settings for all components."""
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "maps4fs"
|
7
|
-
version = "2.0.
|
7
|
+
version = "2.0.8"
|
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 = "Apache License 2.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
|