maps4fs 1.0.9__tar.gz → 1.1.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.
- {maps4fs-1.0.9 → maps4fs-1.1.1}/PKG-INFO +6 -8
- {maps4fs-1.0.9 → maps4fs-1.1.1}/README.md +5 -7
- maps4fs-1.1.1/maps4fs/generator/background.py +414 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/component.py +108 -30
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/config.py +13 -9
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/dem.py +70 -72
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/game.py +1 -2
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/grle.py +31 -10
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/i3d.py +24 -8
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/map.py +19 -11
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/texture.py +47 -15
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs.egg-info/PKG-INFO +6 -8
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs.egg-info/SOURCES.txt +0 -2
- {maps4fs-1.0.9 → maps4fs-1.1.1}/pyproject.toml +1 -1
- {maps4fs-1.0.9 → maps4fs-1.1.1}/tests/test_generator.py +6 -6
- maps4fs-1.0.9/maps4fs/generator/background.py +0 -354
- maps4fs-1.0.9/maps4fs/generator/path_steps.py +0 -97
- maps4fs-1.0.9/maps4fs/generator/tile.py +0 -51
- {maps4fs-1.0.9 → maps4fs-1.1.1}/LICENSE.md +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/__init__.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/__init__.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/generator/qgis.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/logger.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/toolbox/__init__.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/toolbox/background.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs/toolbox/dem.py +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs.egg-info/dependency_links.txt +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs.egg-info/requires.txt +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/maps4fs.egg-info/top_level.txt +0 -0
- {maps4fs-1.0.9 → maps4fs-1.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.1.1
|
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
|
@@ -63,15 +63,17 @@ Requires-Dist: pympler
|
|
63
63
|
</div>
|
64
64
|
|
65
65
|
🗺️ Supports 2x2, 4x4, 8x8, 16x16 and any custom size maps<br>
|
66
|
+
🔄 Support map rotation 🆕<br>
|
67
|
+
🌾 Automatically generates fields 🆕<br>
|
68
|
+
🌽 Automatically generates farmlands 🆕<br>
|
66
69
|
🌍 Based on real-world data from OpenStreetMap<br>
|
67
|
-
🏞️ Generates height using SRTM dataset<br>
|
70
|
+
🏞️ Generates height map using SRTM dataset<br>
|
68
71
|
📦 Provides a ready-to-use map template for the Giants Editor<br>
|
69
72
|
🚜 Supports Farming Simulator 22 and 25<br>
|
70
73
|
🔷 Generates *.obj files for background terrain based on the real-world height map<br>
|
71
74
|
📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
|
75
|
+
📕 Detailed [documentation](/docs) and tutorials <br>
|
72
76
|
🧰 Modder Toolbox to help you with various tasks <br>
|
73
|
-
🌾 Automatically generates fields 🆕<br>
|
74
|
-
🌽 Automatically generates farmlands 🆕<br>
|
75
77
|
|
76
78
|
<p align="center">
|
77
79
|
<img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
|
@@ -467,10 +469,6 @@ You can also apply some advanced settings to the map generation process. Note th
|
|
467
469
|
|
468
470
|
- Plateau height: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0.
|
469
471
|
|
470
|
-
### Background Terrain Advanced settings
|
471
|
-
|
472
|
-
- Background Terrain Generate only full tiles: if checked (by default) the small tiles (N, NE, E, and so on) will not be generated, only the full tile will be created. It's useful when you don't want to work with separate tiles, but with one big file. Since the new method of cutting the map from the background terrain added to the documentation, and now it's possible to perfectly align the map with the background terrain, this option will remain just as a legacy one.
|
473
|
-
|
474
472
|
### Texture Advanced settings
|
475
473
|
|
476
474
|
- Fields padding - this value (in meters) will be applied to each field, making it smaller. It's useful when the fields are too close to each other and you want to make them smaller. By default, it's set to 0.
|
@@ -38,15 +38,17 @@
|
|
38
38
|
</div>
|
39
39
|
|
40
40
|
🗺️ Supports 2x2, 4x4, 8x8, 16x16 and any custom size maps<br>
|
41
|
+
🔄 Support map rotation 🆕<br>
|
42
|
+
🌾 Automatically generates fields 🆕<br>
|
43
|
+
🌽 Automatically generates farmlands 🆕<br>
|
41
44
|
🌍 Based on real-world data from OpenStreetMap<br>
|
42
|
-
🏞️ Generates height using SRTM dataset<br>
|
45
|
+
🏞️ Generates height map using SRTM dataset<br>
|
43
46
|
📦 Provides a ready-to-use map template for the Giants Editor<br>
|
44
47
|
🚜 Supports Farming Simulator 22 and 25<br>
|
45
48
|
🔷 Generates *.obj files for background terrain based on the real-world height map<br>
|
46
49
|
📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
|
50
|
+
📕 Detailed [documentation](/docs) and tutorials <br>
|
47
51
|
🧰 Modder Toolbox to help you with various tasks <br>
|
48
|
-
🌾 Automatically generates fields 🆕<br>
|
49
|
-
🌽 Automatically generates farmlands 🆕<br>
|
50
52
|
|
51
53
|
<p align="center">
|
52
54
|
<img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
|
@@ -442,10 +444,6 @@ You can also apply some advanced settings to the map generation process. Note th
|
|
442
444
|
|
443
445
|
- Plateau height: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0.
|
444
446
|
|
445
|
-
### Background Terrain Advanced settings
|
446
|
-
|
447
|
-
- Background Terrain Generate only full tiles: if checked (by default) the small tiles (N, NE, E, and so on) will not be generated, only the full tile will be created. It's useful when you don't want to work with separate tiles, but with one big file. Since the new method of cutting the map from the background terrain added to the documentation, and now it's possible to perfectly align the map with the background terrain, this option will remain just as a legacy one.
|
448
|
-
|
449
447
|
### Texture Advanced settings
|
450
448
|
|
451
449
|
- Fields padding - this value (in meters) will be applied to each field, making it smaller. It's useful when the fields are too close to each other and you want to make them smaller. By default, it's set to 0.
|
@@ -0,0 +1,414 @@
|
|
1
|
+
"""This module contains the Background component, which generates 3D obj files based on DEM data
|
2
|
+
around the map."""
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
import os
|
7
|
+
import shutil
|
8
|
+
|
9
|
+
import cv2
|
10
|
+
import numpy as np
|
11
|
+
import trimesh # type: ignore
|
12
|
+
|
13
|
+
from maps4fs.generator.component import Component
|
14
|
+
from maps4fs.generator.dem import (
|
15
|
+
DEFAULT_BLUR_RADIUS,
|
16
|
+
DEFAULT_MULTIPLIER,
|
17
|
+
DEFAULT_PLATEAU,
|
18
|
+
DEM,
|
19
|
+
)
|
20
|
+
|
21
|
+
DEFAULT_DISTANCE = 2048
|
22
|
+
RESIZE_FACTOR = 1 / 8
|
23
|
+
FULL_NAME = "FULL"
|
24
|
+
FULL_PREVIEW_NAME = "PREVIEW"
|
25
|
+
ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
|
26
|
+
|
27
|
+
|
28
|
+
class Background(Component):
|
29
|
+
"""Component for creating 3D obj files based on DEM data around the map.
|
30
|
+
|
31
|
+
Arguments:
|
32
|
+
game (Game): The game instance for which the map is generated.
|
33
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
34
|
+
map_size (int): The size of the map in pixels (it's a square).
|
35
|
+
rotated_map_size (int): The size of the map in pixels after rotation.
|
36
|
+
rotation (int): The rotation angle of the map.
|
37
|
+
map_directory (str): The directory where the map files are stored.
|
38
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
39
|
+
info, warning. If not provided, default logging will be used.
|
40
|
+
"""
|
41
|
+
|
42
|
+
# pylint: disable=R0801
|
43
|
+
def preprocess(self) -> None:
|
44
|
+
"""Registers the DEMs for the background terrain."""
|
45
|
+
self.light_version = self.kwargs.get("light_version", False)
|
46
|
+
self.stl_preview_path: str | None = None
|
47
|
+
|
48
|
+
if self.rotation:
|
49
|
+
self.logger.debug("Rotation is enabled: %s.", self.rotation)
|
50
|
+
output_size_multiplier = 1.5
|
51
|
+
else:
|
52
|
+
output_size_multiplier = 1
|
53
|
+
|
54
|
+
background_size = self.map_size + DEFAULT_DISTANCE * 2
|
55
|
+
rotated_size = int(background_size * output_size_multiplier)
|
56
|
+
|
57
|
+
self.background_directory = os.path.join(self.map_directory, "background")
|
58
|
+
os.makedirs(self.background_directory, exist_ok=True)
|
59
|
+
|
60
|
+
autoprocesses = [self.kwargs.get("auto_process", False), False]
|
61
|
+
dems = []
|
62
|
+
|
63
|
+
for name, autoprocess in zip(ELEMENTS, autoprocesses):
|
64
|
+
dem = DEM(
|
65
|
+
self.game,
|
66
|
+
self.coordinates,
|
67
|
+
background_size,
|
68
|
+
rotated_size,
|
69
|
+
self.rotation,
|
70
|
+
self.map_directory,
|
71
|
+
self.logger,
|
72
|
+
auto_process=autoprocess,
|
73
|
+
blur_radius=self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS),
|
74
|
+
multiplier=self.kwargs.get("multiplier", DEFAULT_MULTIPLIER),
|
75
|
+
plateau=self.kwargs.get("plateau", DEFAULT_PLATEAU),
|
76
|
+
)
|
77
|
+
dem.preprocess()
|
78
|
+
dem.is_preview = self.is_preview(name) # type: ignore
|
79
|
+
dem.set_output_resolution((rotated_size, rotated_size))
|
80
|
+
dem.set_dem_path(os.path.join(self.background_directory, f"{name}.png"))
|
81
|
+
dems.append(dem)
|
82
|
+
|
83
|
+
self.dems = dems
|
84
|
+
|
85
|
+
def is_preview(self, name: str) -> bool:
|
86
|
+
"""Checks if the DEM is a preview.
|
87
|
+
|
88
|
+
Arguments:
|
89
|
+
name (str): The name of the DEM.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
bool: True if the DEM is a preview, False otherwise.
|
93
|
+
"""
|
94
|
+
return name == FULL_PREVIEW_NAME
|
95
|
+
|
96
|
+
def process(self) -> None:
|
97
|
+
"""Launches the component processing. Iterates over all tiles and processes them
|
98
|
+
as a result the DEM files will be saved, then based on them the obj files will be
|
99
|
+
generated."""
|
100
|
+
for dem in self.dems:
|
101
|
+
dem.process()
|
102
|
+
if not dem.is_preview: # type: ignore
|
103
|
+
cutted_dem_path = self.cutout(dem.dem_path)
|
104
|
+
if self.game.additional_dem_name is not None:
|
105
|
+
self.make_copy(cutted_dem_path, self.game.additional_dem_name)
|
106
|
+
|
107
|
+
if not self.light_version:
|
108
|
+
self.generate_obj_files()
|
109
|
+
else:
|
110
|
+
self.logger.info("Light version is enabled, obj files will not be generated.")
|
111
|
+
|
112
|
+
def make_copy(self, dem_path: str, dem_name: str) -> None:
|
113
|
+
"""Copies DEM data to additional DEM file.
|
114
|
+
|
115
|
+
Arguments:
|
116
|
+
dem_path (str): Path to the DEM file.
|
117
|
+
dem_name (str): Name of the additional DEM file.
|
118
|
+
"""
|
119
|
+
dem_directory = os.path.dirname(dem_path)
|
120
|
+
|
121
|
+
additional_dem_path = os.path.join(dem_directory, dem_name)
|
122
|
+
|
123
|
+
shutil.copyfile(dem_path, additional_dem_path)
|
124
|
+
self.logger.info("Additional DEM data was copied to %s.", additional_dem_path)
|
125
|
+
|
126
|
+
def info_sequence(self) -> dict[str, str | float | int]:
|
127
|
+
"""Returns a dictionary with information about the background terrain.
|
128
|
+
Adds the EPSG:3857 string to the data for convenient usage in QGIS.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
dict[str, str, float | int] -- A dictionary with information about the background
|
132
|
+
terrain.
|
133
|
+
"""
|
134
|
+
self.qgis_sequence()
|
135
|
+
|
136
|
+
dem = self.dems[0]
|
137
|
+
|
138
|
+
north, south, east, west = dem.bbox
|
139
|
+
epsg3857_string = dem.get_epsg3857_string()
|
140
|
+
epsg3857_string_with_margin = dem.get_epsg3857_string(add_margin=True)
|
141
|
+
|
142
|
+
data = {
|
143
|
+
"center_latitude": dem.coordinates[0],
|
144
|
+
"center_longitude": dem.coordinates[1],
|
145
|
+
"epsg3857_string": epsg3857_string,
|
146
|
+
"epsg3857_string_with_margin": epsg3857_string_with_margin,
|
147
|
+
"height": dem.map_size,
|
148
|
+
"width": dem.map_size,
|
149
|
+
"north": north,
|
150
|
+
"south": south,
|
151
|
+
"east": east,
|
152
|
+
"west": west,
|
153
|
+
}
|
154
|
+
return data # type: ignore
|
155
|
+
|
156
|
+
def qgis_sequence(self) -> None:
|
157
|
+
"""Generates QGIS scripts for creating bounding box layers and rasterizing them."""
|
158
|
+
qgis_layer = (f"Background_{FULL_NAME}", *self.dems[0].get_espg3857_bbox())
|
159
|
+
qgis_layer_with_margin = (
|
160
|
+
f"Background_{FULL_NAME}_margin",
|
161
|
+
*self.dems[0].get_espg3857_bbox(add_margin=True),
|
162
|
+
)
|
163
|
+
self.create_qgis_scripts([qgis_layer, qgis_layer_with_margin])
|
164
|
+
|
165
|
+
def generate_obj_files(self) -> None:
|
166
|
+
"""Iterates over all dems and generates 3D obj files based on DEM data.
|
167
|
+
If at least one DEM file is missing, the generation will be stopped at all.
|
168
|
+
"""
|
169
|
+
for dem in self.dems:
|
170
|
+
if not os.path.isfile(dem.dem_path):
|
171
|
+
self.logger.warning(
|
172
|
+
"DEM file not found, generation will be stopped: %s", dem.dem_path
|
173
|
+
)
|
174
|
+
return
|
175
|
+
|
176
|
+
self.logger.debug("DEM file for found: %s", dem.dem_path)
|
177
|
+
|
178
|
+
filename = os.path.splitext(os.path.basename(dem.dem_path))[0]
|
179
|
+
save_path = os.path.join(self.background_directory, f"{filename}.obj")
|
180
|
+
self.logger.debug("Generating obj file in path: %s", save_path)
|
181
|
+
|
182
|
+
dem_data = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
183
|
+
self.plane_from_np(dem_data, save_path, is_preview=dem.is_preview) # type: ignore
|
184
|
+
|
185
|
+
# pylint: disable=too-many-locals
|
186
|
+
def cutout(self, dem_path: str) -> str:
|
187
|
+
"""Cuts out the center of the DEM (the actual map) and saves it as a separate file.
|
188
|
+
|
189
|
+
Arguments:
|
190
|
+
dem_path (str): The path to the DEM file.
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
str -- The path to the cutout DEM file.
|
194
|
+
"""
|
195
|
+
dem_data = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
196
|
+
|
197
|
+
center = (dem_data.shape[0] // 2, dem_data.shape[1] // 2)
|
198
|
+
half_size = self.map_size // 2
|
199
|
+
x1 = center[0] - half_size
|
200
|
+
x2 = center[0] + half_size
|
201
|
+
y1 = center[1] - half_size
|
202
|
+
y2 = center[1] + half_size
|
203
|
+
dem_data = dem_data[x1:x2, y1:y2]
|
204
|
+
|
205
|
+
output_size = self.map_size + 1
|
206
|
+
|
207
|
+
main_dem_path = self.game.dem_file_path(self.map_directory)
|
208
|
+
|
209
|
+
try:
|
210
|
+
os.remove(main_dem_path)
|
211
|
+
except FileNotFoundError:
|
212
|
+
pass
|
213
|
+
|
214
|
+
# pylint: disable=no-member
|
215
|
+
resized_dem_data = cv2.resize(
|
216
|
+
dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
|
217
|
+
)
|
218
|
+
|
219
|
+
cv2.imwrite(main_dem_path, resized_dem_data) # pylint: disable=no-member
|
220
|
+
self.logger.info("DEM cutout saved: %s", main_dem_path)
|
221
|
+
|
222
|
+
return main_dem_path
|
223
|
+
|
224
|
+
# pylint: disable=too-many-locals
|
225
|
+
def plane_from_np(self, dem_data: np.ndarray, save_path: str, is_preview: bool = False) -> None:
|
226
|
+
"""Generates a 3D obj file based on DEM data.
|
227
|
+
|
228
|
+
Arguments:
|
229
|
+
dem_data (np.ndarray) -- The DEM data as a numpy array.
|
230
|
+
save_path (str) -- The path where the obj file will be saved.
|
231
|
+
is_preview (bool, optional) -- If True, the preview mesh will be generated.
|
232
|
+
"""
|
233
|
+
dem_data = cv2.resize( # pylint: disable=no-member
|
234
|
+
dem_data, (0, 0), fx=RESIZE_FACTOR, fy=RESIZE_FACTOR
|
235
|
+
)
|
236
|
+
self.logger.debug(
|
237
|
+
"DEM data resized to shape: %s with factor: %s", dem_data.shape, RESIZE_FACTOR
|
238
|
+
)
|
239
|
+
|
240
|
+
# Invert the height values.
|
241
|
+
dem_data = dem_data.max() - dem_data
|
242
|
+
|
243
|
+
rows, cols = dem_data.shape
|
244
|
+
x = np.linspace(0, cols - 1, cols)
|
245
|
+
y = np.linspace(0, rows - 1, rows)
|
246
|
+
x, y = np.meshgrid(x, y)
|
247
|
+
z = dem_data
|
248
|
+
|
249
|
+
self.logger.debug(
|
250
|
+
"Starting to generate a mesh for with shape: %s x %s. This may take a while...",
|
251
|
+
cols,
|
252
|
+
rows,
|
253
|
+
)
|
254
|
+
|
255
|
+
vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
|
256
|
+
faces = []
|
257
|
+
|
258
|
+
for i in range(rows - 1):
|
259
|
+
for j in range(cols - 1):
|
260
|
+
top_left = i * cols + j
|
261
|
+
top_right = top_left + 1
|
262
|
+
bottom_left = top_left + cols
|
263
|
+
bottom_right = bottom_left + 1
|
264
|
+
|
265
|
+
faces.append([top_left, bottom_left, bottom_right])
|
266
|
+
faces.append([top_left, bottom_right, top_right])
|
267
|
+
|
268
|
+
faces = np.array(faces) # type: ignore
|
269
|
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
270
|
+
|
271
|
+
# Apply rotation: 180 degrees around Y-axis and Z-axis
|
272
|
+
rotation_matrix_y = trimesh.transformations.rotation_matrix(np.pi, [0, 1, 0])
|
273
|
+
rotation_matrix_z = trimesh.transformations.rotation_matrix(np.pi, [0, 0, 1])
|
274
|
+
mesh.apply_transform(rotation_matrix_y)
|
275
|
+
mesh.apply_transform(rotation_matrix_z)
|
276
|
+
|
277
|
+
if is_preview:
|
278
|
+
# Simplify the preview mesh to reduce the size of the file.
|
279
|
+
mesh = mesh.simplify_quadric_decimation(face_count=len(mesh.faces) // 2**7)
|
280
|
+
|
281
|
+
# Apply scale to make the preview mesh smaller in the UI.
|
282
|
+
mesh.apply_scale([0.5, 0.5, 0.5])
|
283
|
+
self.mesh_to_stl(mesh)
|
284
|
+
else:
|
285
|
+
multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
|
286
|
+
if multiplier != 1:
|
287
|
+
z_scaling_factor = 1 / multiplier
|
288
|
+
else:
|
289
|
+
z_scaling_factor = 1 / 2**5
|
290
|
+
self.logger.debug("Z scaling factor: %s", z_scaling_factor)
|
291
|
+
mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
|
292
|
+
|
293
|
+
mesh.export(save_path)
|
294
|
+
self.logger.debug("Obj file saved: %s", save_path)
|
295
|
+
|
296
|
+
def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
|
297
|
+
"""Converts the mesh to an STL file and saves it in the previews directory.
|
298
|
+
Uses powerful simplification to reduce the size of the file since it will be used
|
299
|
+
only for the preview.
|
300
|
+
|
301
|
+
Arguments:
|
302
|
+
mesh (trimesh.Trimesh) -- The mesh to convert to an STL file.
|
303
|
+
"""
|
304
|
+
preview_path = os.path.join(self.previews_directory, "background_dem.stl")
|
305
|
+
mesh.export(preview_path)
|
306
|
+
|
307
|
+
self.logger.info("STL file saved: %s", preview_path)
|
308
|
+
|
309
|
+
self.stl_preview_path = preview_path # pylint: disable=attribute-defined-outside-init
|
310
|
+
|
311
|
+
# pylint: disable=no-member
|
312
|
+
def previews(self) -> list[str]:
|
313
|
+
"""Returns the path to the image previews paths and the path to the STL preview file.
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
list[str] -- A list of paths to the previews.
|
317
|
+
"""
|
318
|
+
preview_paths = self.dem_previews(self.game.dem_file_path(self.map_directory))
|
319
|
+
for dem in self.dems:
|
320
|
+
if dem.is_preview: # type: ignore
|
321
|
+
background_dem_preview_path = os.path.join(
|
322
|
+
self.previews_directory, "background_dem.png"
|
323
|
+
)
|
324
|
+
background_dem_preview_image = cv2.imread(dem.dem_path, cv2.IMREAD_UNCHANGED)
|
325
|
+
|
326
|
+
background_dem_preview_image = cv2.resize(
|
327
|
+
background_dem_preview_image, (0, 0), fx=1 / 4, fy=1 / 4
|
328
|
+
)
|
329
|
+
background_dem_preview_image = cv2.normalize( # type: ignore
|
330
|
+
background_dem_preview_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
|
331
|
+
)
|
332
|
+
background_dem_preview_image = cv2.cvtColor(
|
333
|
+
background_dem_preview_image, cv2.COLOR_GRAY2BGR
|
334
|
+
)
|
335
|
+
|
336
|
+
cv2.imwrite(background_dem_preview_path, background_dem_preview_image)
|
337
|
+
preview_paths.append(background_dem_preview_path)
|
338
|
+
|
339
|
+
if self.stl_preview_path:
|
340
|
+
preview_paths.append(self.stl_preview_path)
|
341
|
+
|
342
|
+
return preview_paths
|
343
|
+
|
344
|
+
def dem_previews(self, image_path: str) -> list[str]:
|
345
|
+
"""Get list of preview images.
|
346
|
+
|
347
|
+
Arguments:
|
348
|
+
image_path (str): Path to the DEM file.
|
349
|
+
|
350
|
+
Returns:
|
351
|
+
list[str]: List of preview images.
|
352
|
+
"""
|
353
|
+
self.logger.debug("Starting DEM previews generation.")
|
354
|
+
return [self.grayscale_preview(image_path), self.colored_preview(image_path)]
|
355
|
+
|
356
|
+
def grayscale_preview(self, image_path: str) -> str:
|
357
|
+
"""Converts DEM image to grayscale RGB image and saves it to the map directory.
|
358
|
+
Returns path to the preview image.
|
359
|
+
|
360
|
+
Arguments:
|
361
|
+
image_path (str): Path to the DEM file.
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
str: Path to the preview image.
|
365
|
+
"""
|
366
|
+
grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
|
367
|
+
|
368
|
+
self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
|
369
|
+
|
370
|
+
dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
371
|
+
dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
|
372
|
+
cv2.imwrite(grayscale_dem_path, dem_data_rgb)
|
373
|
+
return grayscale_dem_path
|
374
|
+
|
375
|
+
def colored_preview(self, image_path: str) -> str:
|
376
|
+
"""Converts DEM image to colored RGB image and saves it to the map directory.
|
377
|
+
Returns path to the preview image.
|
378
|
+
|
379
|
+
Arguments:
|
380
|
+
image_path (str): Path to the DEM file.
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
list[str]: List with a single path to the DEM file
|
384
|
+
"""
|
385
|
+
colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
|
386
|
+
|
387
|
+
self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
|
388
|
+
|
389
|
+
dem_data = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
390
|
+
|
391
|
+
self.logger.debug(
|
392
|
+
"DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
393
|
+
dem_data.shape,
|
394
|
+
dem_data.dtype,
|
395
|
+
dem_data.min(),
|
396
|
+
dem_data.max(),
|
397
|
+
)
|
398
|
+
|
399
|
+
# Create an empty array with the same shape and type as dem_data.
|
400
|
+
dem_data_normalized = np.empty_like(dem_data)
|
401
|
+
|
402
|
+
# Normalize the DEM data to the range [0, 255]
|
403
|
+
cv2.normalize(dem_data, dem_data_normalized, 0, 255, cv2.NORM_MINMAX)
|
404
|
+
self.logger.debug(
|
405
|
+
"DEM data after normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
|
406
|
+
dem_data_normalized.shape,
|
407
|
+
dem_data_normalized.dtype,
|
408
|
+
dem_data_normalized.min(),
|
409
|
+
dem_data_normalized.max(),
|
410
|
+
)
|
411
|
+
dem_data_colored = cv2.applyColorMap(dem_data_normalized, cv2.COLORMAP_JET)
|
412
|
+
|
413
|
+
cv2.imwrite(colored_dem_path, dem_data_colored)
|
414
|
+
return colored_dem_path
|