maps4fs 1.8.13__py3-none-any.whl → 1.8.15__py3-none-any.whl
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/generator/{background.py → component/background.py} +50 -214
- maps4fs/generator/component/base/component_image.py +90 -0
- maps4fs/generator/component/base/component_mesh.py +125 -0
- maps4fs/generator/component/base/component_xml.py +13 -0
- maps4fs/generator/{grle.py → component/grle.py} +160 -178
- maps4fs/generator/dtm/dtm.py +0 -4
- maps4fs/generator/game.py +54 -2
- maps4fs/generator/map.py +1 -1
- maps4fs/generator/satellite.py +2 -2
- maps4fs/generator/settings.py +10 -0
- maps4fs/generator/texture.py +6 -1
- {maps4fs-1.8.13.dist-info → maps4fs-1.8.15.dist-info}/METADATA +7 -1
- {maps4fs-1.8.13.dist-info → maps4fs-1.8.15.dist-info}/RECORD +16 -14
- {maps4fs-1.8.13.dist-info → maps4fs-1.8.15.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.8.13.dist-info → maps4fs-1.8.15.dist-info}/WHEEL +0 -0
- {maps4fs-1.8.13.dist-info → maps4fs-1.8.15.dist-info}/top_level.txt +0 -0
@@ -3,17 +3,15 @@
|
|
3
3
|
import json
|
4
4
|
import os
|
5
5
|
from random import choice, randint
|
6
|
-
from xml.etree import ElementTree as ET
|
7
6
|
|
8
7
|
import cv2
|
9
8
|
import numpy as np
|
10
|
-
from shapely.geometry import Polygon
|
9
|
+
from shapely.geometry import Polygon
|
11
10
|
from tqdm import tqdm
|
12
11
|
|
13
|
-
from maps4fs.generator.component.base.
|
14
|
-
from maps4fs.generator.
|
15
|
-
|
16
|
-
ISLAND_DISTORTION = 0.3
|
12
|
+
from maps4fs.generator.component.base.component_image import ImageComponent
|
13
|
+
from maps4fs.generator.component.base.component_xml import XMLComponent
|
14
|
+
from maps4fs.generator.settings import Parameters
|
17
15
|
|
18
16
|
|
19
17
|
def plant_to_pixel_value(plant_name: str) -> int | None:
|
@@ -33,8 +31,7 @@ def plant_to_pixel_value(plant_name: str) -> int | None:
|
|
33
31
|
return plants.get(plant_name)
|
34
32
|
|
35
33
|
|
36
|
-
|
37
|
-
class GRLE(Component):
|
34
|
+
class GRLE(ImageComponent, XMLComponent):
|
38
35
|
"""Component for to generate InfoLayer PNG files based on GRLE schema.
|
39
36
|
|
40
37
|
Arguments:
|
@@ -48,34 +45,41 @@ class GRLE(Component):
|
|
48
45
|
info, warning. If not provided, default logging will be used.
|
49
46
|
"""
|
50
47
|
|
51
|
-
_grle_schema: dict[str, float | int | str] | None = None
|
52
|
-
|
53
48
|
def preprocess(self) -> None:
|
54
49
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
55
50
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
56
51
|
self.preview_paths: dict[str, str] = {}
|
52
|
+
try:
|
53
|
+
self.xml_path = self.game.get_farmlands_xml_path(self.map_directory)
|
54
|
+
except NotImplementedError:
|
55
|
+
self.logger.warning("Farmlands XML file processing is not implemented for this game.")
|
56
|
+
self.xml_path = None
|
57
57
|
|
58
|
+
def _read_grle_schema(self) -> dict[str, float | int | str] | None:
|
58
59
|
try:
|
59
60
|
grle_schema_path = self.game.grle_schema
|
60
61
|
except ValueError:
|
61
62
|
self.logger.warning("GRLE schema processing is not implemented for this game.")
|
62
|
-
return
|
63
|
+
return None
|
63
64
|
|
64
65
|
try:
|
65
66
|
with open(grle_schema_path, "r", encoding="utf-8") as file:
|
66
|
-
|
67
|
+
grle_schema = json.load(file)
|
67
68
|
self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
|
68
69
|
except (json.JSONDecodeError, FileNotFoundError) as error:
|
69
70
|
self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
|
70
|
-
|
71
|
+
grle_schema = None
|
72
|
+
|
73
|
+
return grle_schema
|
71
74
|
|
72
75
|
def process(self) -> None:
|
73
76
|
"""Generates InfoLayer PNG files based on the GRLE schema."""
|
74
|
-
|
77
|
+
grle_schema = self._read_grle_schema()
|
78
|
+
if not grle_schema:
|
75
79
|
self.logger.debug("GRLE schema is not obtained, skipping the processing.")
|
76
80
|
return
|
77
81
|
|
78
|
-
for info_layer in tqdm(
|
82
|
+
for info_layer in tqdm(grle_schema, desc="Preparing GRLE files", unit="layer"):
|
79
83
|
if isinstance(info_layer, dict):
|
80
84
|
file_path = os.path.join(
|
81
85
|
self.game.weights_dir_path(self.map_directory), info_layer["name"]
|
@@ -98,8 +102,7 @@ class GRLE(Component):
|
|
98
102
|
self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
|
99
103
|
|
100
104
|
self._add_farmlands()
|
101
|
-
if self.game.
|
102
|
-
self.logger.debug("Game is %s, plants will be added.", self.game.code)
|
105
|
+
if self.game.plants_processing:
|
103
106
|
self._add_plants()
|
104
107
|
else:
|
105
108
|
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
|
@@ -116,8 +119,13 @@ class GRLE(Component):
|
|
116
119
|
save_path = os.path.join(self.previews_directory, f"{preview_name}.png")
|
117
120
|
# Resize the preview image to the maximum size allowed for previews.
|
118
121
|
image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
|
119
|
-
if
|
120
|
-
image
|
122
|
+
if (
|
123
|
+
image.shape[0] > Parameters.PREVIEW_MAXIMUM_SIZE
|
124
|
+
or image.shape[1] > Parameters.PREVIEW_MAXIMUM_SIZE
|
125
|
+
):
|
126
|
+
image = cv2.resize(
|
127
|
+
image, (Parameters.PREVIEW_MAXIMUM_SIZE, Parameters.PREVIEW_MAXIMUM_SIZE)
|
128
|
+
)
|
121
129
|
image_normalized = np.empty_like(image)
|
122
130
|
cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
|
123
131
|
image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
|
@@ -144,13 +152,12 @@ class GRLE(Component):
|
|
144
152
|
Returns:
|
145
153
|
np.ndarray | None: The farmlands preview image with fields overlayed on top of it.
|
146
154
|
"""
|
147
|
-
|
148
|
-
if not
|
149
|
-
self.logger.warning("
|
155
|
+
fields_layer = self.map.get_texture_layer(by_usage="field")
|
156
|
+
if not fields_layer:
|
157
|
+
self.logger.warning("Fields layer not found in the texture component.")
|
150
158
|
return None
|
151
159
|
|
152
|
-
|
153
|
-
fields_layer_path = fields_layer.get_preview_or_path( # type: ignore
|
160
|
+
fields_layer_path = fields_layer.get_preview_or_path(
|
154
161
|
self.game.weights_dir_path(self.map_directory)
|
155
162
|
)
|
156
163
|
if not fields_layer_path or not os.path.isfile(fields_layer_path):
|
@@ -163,24 +170,15 @@ class GRLE(Component):
|
|
163
170
|
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
164
171
|
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
165
172
|
|
166
|
-
# pylint: disable=R0801, R0914, R0915
|
167
173
|
def _add_farmlands(self) -> None:
|
168
174
|
"""Adds farmlands to the InfoLayer PNG file."""
|
169
|
-
|
170
|
-
textures_info_layer_path = self.get_infolayer_path("textures")
|
171
|
-
if not textures_info_layer_path:
|
172
|
-
return
|
173
|
-
|
174
|
-
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
175
|
-
textures_info_layer = json.load(textures_info_layer_file)
|
176
|
-
|
177
175
|
farmlands = []
|
178
|
-
farmyards
|
176
|
+
farmyards = self.get_infolayer_data(Parameters.TEXTURES, Parameters.FARMYARDS)
|
179
177
|
if farmyards and self.map.grle_settings.add_farmyards:
|
180
178
|
farmlands.extend(farmyards)
|
181
179
|
self.logger.debug("Found %s farmyards in textures info layer.", len(farmyards))
|
182
180
|
|
183
|
-
fields
|
181
|
+
fields = self.get_infolayer_data(Parameters.TEXTURES, Parameters.FIELDS)
|
184
182
|
if not fields:
|
185
183
|
self.logger.warning("Fields data not found in textures info layer.")
|
186
184
|
return
|
@@ -188,9 +186,7 @@ class GRLE(Component):
|
|
188
186
|
|
189
187
|
self.logger.debug("Found %s fields in textures info layer.", len(fields))
|
190
188
|
|
191
|
-
info_layer_farmlands_path =
|
192
|
-
self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
|
193
|
-
)
|
189
|
+
info_layer_farmlands_path = self.game.get_farmlands_path(self.map_directory)
|
194
190
|
|
195
191
|
self.logger.debug(
|
196
192
|
"Adding farmlands to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
@@ -201,22 +197,21 @@ class GRLE(Component):
|
|
201
197
|
return
|
202
198
|
|
203
199
|
image = cv2.imread(info_layer_farmlands_path, cv2.IMREAD_UNCHANGED)
|
204
|
-
farmlands_xml_path = os.path.join(self.map_directory, "map/config/farmlands.xml")
|
205
|
-
if not os.path.isfile(farmlands_xml_path):
|
206
|
-
self.logger.warning("Farmlands XML file %s not found.", farmlands_xml_path)
|
207
|
-
return
|
208
200
|
|
209
|
-
tree =
|
210
|
-
|
201
|
+
tree = self.get_tree()
|
202
|
+
root = tree.getroot()
|
203
|
+
farmlands_node = root.find("farmlands")
|
204
|
+
if farmlands_node is None:
|
205
|
+
raise ValueError("Farmlands XML element not found in the farmlands XML file.")
|
206
|
+
|
207
|
+
self.update_element(farmlands_node, {"pricePerHa": str(self.map.grle_settings.base_price)})
|
211
208
|
|
212
|
-
# Not using enumerate because in case of the error, we do not increment
|
213
|
-
# the farmland_id. So as a result we do not have a gap in the farmland IDs.
|
214
209
|
farmland_id = 1
|
215
210
|
|
216
|
-
for
|
211
|
+
for farmland in tqdm(farmlands, desc="Adding farmlands", unit="farmland"):
|
217
212
|
try:
|
218
|
-
|
219
|
-
polygon_points=
|
213
|
+
fitted_farmland = self.fit_object_into_bounds(
|
214
|
+
polygon_points=farmland,
|
220
215
|
margin=self.map.grle_settings.farmland_margin,
|
221
216
|
angle=self.rotation,
|
222
217
|
)
|
@@ -228,23 +223,11 @@ class GRLE(Component):
|
|
228
223
|
)
|
229
224
|
continue
|
230
225
|
|
231
|
-
self.
|
232
|
-
|
233
|
-
field_np = np.array(fitted_field, np.int32)
|
234
|
-
field_np = field_np.reshape((-1, 1, 2))
|
235
|
-
|
236
|
-
self.logger.debug(
|
237
|
-
"Created a numpy array and reshaped it. Number of points: %s", len(field_np)
|
238
|
-
)
|
239
|
-
|
240
|
-
# Infolayer image is 1/2 of the size of the map image, that's why we need to divide
|
241
|
-
# the coordinates by 2.
|
242
|
-
field_np = field_np // 2
|
243
|
-
self.logger.debug("Divided the coordinates by 2.")
|
226
|
+
farmland_np = self.polygon_points_to_np(fitted_farmland, divide=2)
|
244
227
|
|
245
228
|
try:
|
246
|
-
cv2.fillPoly(image, [
|
247
|
-
except Exception as e:
|
229
|
+
cv2.fillPoly(image, [farmland_np], (float(farmland_id),))
|
230
|
+
except Exception as e:
|
248
231
|
self.logger.debug(
|
249
232
|
"Farmland %s could not be added to the InfoLayer PNG file with error: %s",
|
250
233
|
farmland_id,
|
@@ -252,37 +235,24 @@ class GRLE(Component):
|
|
252
235
|
)
|
253
236
|
continue
|
254
237
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
238
|
+
data = {
|
239
|
+
"id": str(farmland_id),
|
240
|
+
"priceScale": "1",
|
241
|
+
"npcName": "FORESTER",
|
242
|
+
}
|
243
|
+
self.create_subelement(farmlands_node, "farmland", data)
|
260
244
|
|
261
245
|
farmland_id += 1
|
262
246
|
|
263
|
-
|
264
|
-
|
265
|
-
self.logger.debug("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
|
247
|
+
self.save_tree(tree)
|
266
248
|
|
267
249
|
cv2.imwrite(info_layer_farmlands_path, image)
|
268
|
-
self.logger.debug(
|
269
|
-
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
270
|
-
)
|
271
250
|
|
272
|
-
self.preview_paths["farmlands"] = info_layer_farmlands_path
|
251
|
+
self.preview_paths["farmlands"] = info_layer_farmlands_path
|
273
252
|
|
274
|
-
# pylint: disable=R0915
|
275
253
|
def _add_plants(self) -> None:
|
276
254
|
"""Adds plants to the InfoLayer PNG file."""
|
277
|
-
|
278
|
-
# 2. Get the path to the base layer (grass).
|
279
|
-
# 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
|
280
|
-
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
281
|
-
if not texture_component:
|
282
|
-
self.logger.warning("Texture component not found in the map.")
|
283
|
-
return
|
284
|
-
|
285
|
-
grass_layer = texture_component.get_layer_by_usage("grass")
|
255
|
+
grass_layer = self.map.get_texture_layer(by_usage="grass")
|
286
256
|
if not grass_layer:
|
287
257
|
self.logger.warning("Grass layer not found in the texture component.")
|
288
258
|
return
|
@@ -291,7 +261,7 @@ class GRLE(Component):
|
|
291
261
|
grass_image_path = grass_layer.get_preview_or_path(weights_directory)
|
292
262
|
self.logger.debug("Grass image path: %s.", grass_image_path)
|
293
263
|
|
294
|
-
forest_layer =
|
264
|
+
forest_layer = self.map.get_texture_layer(by_usage="forest")
|
295
265
|
forest_image = None
|
296
266
|
if forest_layer:
|
297
267
|
forest_image_path = forest_layer.get_preview_or_path(weights_directory)
|
@@ -304,9 +274,7 @@ class GRLE(Component):
|
|
304
274
|
self.logger.warning("Base image not found in %s.", grass_image_path)
|
305
275
|
return
|
306
276
|
|
307
|
-
density_map_fruit_path =
|
308
|
-
self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
|
309
|
-
)
|
277
|
+
density_map_fruit_path = self.game.get_density_map_fruits_path(self.map_directory)
|
310
278
|
|
311
279
|
self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path)
|
312
280
|
|
@@ -334,88 +302,14 @@ class GRLE(Component):
|
|
334
302
|
# Add non zero values from the forest image to the grass image.
|
335
303
|
grass_image[forest_image != 0] = 255
|
336
304
|
|
337
|
-
|
338
|
-
|
305
|
+
base_grass = self.map.grle_settings.base_grass
|
306
|
+
if isinstance(base_grass, tuple):
|
307
|
+
base_grass = base_grass[0]
|
339
308
|
|
340
|
-
base_layer_pixel_value = plant_to_pixel_value(
|
341
|
-
self.map.grle_settings.base_grass # type:ignore
|
342
|
-
)
|
309
|
+
base_layer_pixel_value = plant_to_pixel_value(str(base_grass))
|
343
310
|
if not base_layer_pixel_value:
|
344
311
|
base_layer_pixel_value = 131
|
345
312
|
|
346
|
-
def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
|
347
|
-
"""Create an island of plants in the image.
|
348
|
-
|
349
|
-
Arguments:
|
350
|
-
image (np.ndarray): The image where the island of plants will be created.
|
351
|
-
count (int): The number of islands of plants to create.
|
352
|
-
|
353
|
-
Returns:
|
354
|
-
np.ndarray: The image with the islands of plants.
|
355
|
-
"""
|
356
|
-
for _ in tqdm(range(count), desc="Adding islands of plants", unit="island"):
|
357
|
-
# Randomly choose the value for the island.
|
358
|
-
plant_value = choice(possible_R_values)
|
359
|
-
# Randomly choose the size of the island.
|
360
|
-
island_size = randint(
|
361
|
-
self.map.grle_settings.plants_island_minimum_size, # type:ignore
|
362
|
-
self.map.grle_settings.plants_island_maximum_size, # type:ignore
|
363
|
-
)
|
364
|
-
# Randomly choose the position of the island.
|
365
|
-
x = randint(0, image.shape[1] - island_size)
|
366
|
-
y = randint(0, image.shape[0] - island_size)
|
367
|
-
|
368
|
-
try:
|
369
|
-
polygon_points = get_rounded_polygon(
|
370
|
-
num_vertices=self.map.grle_settings.plants_island_vertex_count,
|
371
|
-
center=(x + island_size // 2, y + island_size // 2),
|
372
|
-
radius=island_size // 2,
|
373
|
-
rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
|
374
|
-
)
|
375
|
-
if not polygon_points:
|
376
|
-
continue
|
377
|
-
|
378
|
-
nodes = np.array(polygon_points, np.int32) # type: ignore
|
379
|
-
cv2.fillPoly(image, [nodes], plant_value) # type: ignore
|
380
|
-
except Exception: # pylint: disable=W0703
|
381
|
-
continue
|
382
|
-
|
383
|
-
return image
|
384
|
-
|
385
|
-
def get_rounded_polygon(
|
386
|
-
num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
|
387
|
-
) -> list[tuple[int, int]] | None:
|
388
|
-
"""Get a randomly rounded polygon.
|
389
|
-
|
390
|
-
Arguments:
|
391
|
-
num_vertices (int): The number of vertices of the polygon.
|
392
|
-
center (tuple[int, int]): The center of the polygon.
|
393
|
-
radius (int): The radius of the polygon.
|
394
|
-
rounding_radius (int): The rounding radius of the polygon.
|
395
|
-
|
396
|
-
Returns:
|
397
|
-
list[tuple[int, int]] | None: The rounded polygon.
|
398
|
-
"""
|
399
|
-
angle_offset = np.pi / num_vertices
|
400
|
-
angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
|
401
|
-
random_angles = angles + np.random.uniform(
|
402
|
-
-ISLAND_DISTORTION, ISLAND_DISTORTION, num_vertices
|
403
|
-
) # Add randomness to angles
|
404
|
-
random_radii = radius + np.random.uniform(
|
405
|
-
-radius * ISLAND_DISTORTION, radius * ISLAND_DISTORTION, num_vertices
|
406
|
-
) # Add randomness to radii
|
407
|
-
|
408
|
-
points = [
|
409
|
-
(center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
|
410
|
-
for a, r in zip(random_angles, random_radii)
|
411
|
-
]
|
412
|
-
polygon = Polygon(points)
|
413
|
-
buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
|
414
|
-
rounded_polygon = list(buffered_polygon.exterior.coords)
|
415
|
-
if not rounded_polygon:
|
416
|
-
return None
|
417
|
-
return rounded_polygon
|
418
|
-
|
419
313
|
grass_image_copy = grass_image.copy()
|
420
314
|
if forest_image is not None:
|
421
315
|
# Add the forest layer to the base image, to merge the masks.
|
@@ -427,7 +321,7 @@ class GRLE(Component):
|
|
427
321
|
island_count = int(self.map_size * self.map.grle_settings.plants_island_percent // 100)
|
428
322
|
self.logger.debug("Adding %s islands of plants to the base image.", island_count)
|
429
323
|
if self.map.grle_settings.random_plants:
|
430
|
-
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
|
324
|
+
grass_image_copy = self.create_island_of_plants(grass_image_copy, island_count)
|
431
325
|
self.logger.debug("Added %s islands of plants to the base image.", island_count)
|
432
326
|
|
433
327
|
# Sligtly reduce the size of the grass_image, that we'll use as mask.
|
@@ -438,15 +332,7 @@ class GRLE(Component):
|
|
438
332
|
grass_image_copy[grass_image == 0] = 0
|
439
333
|
self.logger.debug("Removed the values where the base image has zeros.")
|
440
334
|
|
441
|
-
|
442
|
-
grass_image_copy[0, :] = 0 # Top side
|
443
|
-
grass_image_copy[-1, :] = 0 # Bottom side
|
444
|
-
grass_image_copy[:, 0] = 0 # Left side
|
445
|
-
grass_image_copy[:, -1] = 0 # Right side
|
446
|
-
|
447
|
-
# After painting it with base grass, we'll create multiple islands of different plants.
|
448
|
-
# On the final step, we'll remove all the values which in pixels
|
449
|
-
# where zerons in the original base image (so we don't paint grass where it should not be).
|
335
|
+
grass_image_copy = self.remove_edge_pixel_values(grass_image_copy)
|
450
336
|
|
451
337
|
# Three channeled 8-bit image, where non-zero values are the
|
452
338
|
# different types of plants (only in the R channel).
|
@@ -462,3 +348,99 @@ class GRLE(Component):
|
|
462
348
|
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
|
463
349
|
cv2.imwrite(density_map_fruit_path, density_map_fruits)
|
464
350
|
self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
|
351
|
+
|
352
|
+
def create_island_of_plants(self, image: np.ndarray, count: int) -> np.ndarray:
|
353
|
+
"""Create an island of plants in the image.
|
354
|
+
|
355
|
+
Arguments:
|
356
|
+
image (np.ndarray): The image where the island of plants will be created.
|
357
|
+
count (int): The number of islands of plants to create.
|
358
|
+
|
359
|
+
Returns:
|
360
|
+
np.ndarray: The image with the islands of plants.
|
361
|
+
"""
|
362
|
+
# B and G channels remain the same (zeros), while we change the R channel.
|
363
|
+
possible_r_values = [65, 97, 129, 161, 193, 225]
|
364
|
+
|
365
|
+
for _ in tqdm(range(count), desc="Adding islands of plants", unit="island"):
|
366
|
+
# Randomly choose the value for the island.
|
367
|
+
plant_value = choice(possible_r_values)
|
368
|
+
# Randomly choose the size of the island.
|
369
|
+
island_size = randint(
|
370
|
+
self.map.grle_settings.plants_island_minimum_size,
|
371
|
+
self.map.grle_settings.plants_island_maximum_size,
|
372
|
+
)
|
373
|
+
# Randomly choose the position of the island.
|
374
|
+
x = randint(0, image.shape[1] - island_size)
|
375
|
+
y = randint(0, image.shape[0] - island_size)
|
376
|
+
|
377
|
+
try:
|
378
|
+
polygon_points = self.get_rounded_polygon(
|
379
|
+
num_vertices=self.map.grle_settings.plants_island_vertex_count,
|
380
|
+
center=(x + island_size // 2, y + island_size // 2),
|
381
|
+
radius=island_size // 2,
|
382
|
+
rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
|
383
|
+
)
|
384
|
+
if not polygon_points:
|
385
|
+
continue
|
386
|
+
|
387
|
+
nodes = np.array(polygon_points, np.int32)
|
388
|
+
cv2.fillPoly(image, [nodes], (float(plant_value),))
|
389
|
+
except Exception:
|
390
|
+
continue
|
391
|
+
|
392
|
+
return image
|
393
|
+
|
394
|
+
@staticmethod
|
395
|
+
def get_rounded_polygon(
|
396
|
+
num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
|
397
|
+
) -> list[tuple[int, int]] | None:
|
398
|
+
"""Get a randomly rounded polygon.
|
399
|
+
|
400
|
+
Arguments:
|
401
|
+
num_vertices (int): The number of vertices of the polygon.
|
402
|
+
center (tuple[int, int]): The center of the polygon.
|
403
|
+
radius (int): The radius of the polygon.
|
404
|
+
rounding_radius (int): The rounding radius of the polygon.
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
list[tuple[int, int]] | None: The rounded polygon.
|
408
|
+
"""
|
409
|
+
island_distortion = 0.3
|
410
|
+
|
411
|
+
angle_offset = np.pi / num_vertices
|
412
|
+
angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
|
413
|
+
random_angles = angles + np.random.uniform(
|
414
|
+
-island_distortion, island_distortion, num_vertices
|
415
|
+
) # Add randomness to angles
|
416
|
+
random_radii = radius + np.random.uniform(
|
417
|
+
-radius * island_distortion, radius * island_distortion, num_vertices
|
418
|
+
) # Add randomness to radii
|
419
|
+
|
420
|
+
points = [
|
421
|
+
(center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
|
422
|
+
for a, r in zip(random_angles, random_radii)
|
423
|
+
]
|
424
|
+
polygon = Polygon(points)
|
425
|
+
buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
|
426
|
+
rounded_polygon = list(buffered_polygon.exterior.coords)
|
427
|
+
if not rounded_polygon:
|
428
|
+
return None
|
429
|
+
return rounded_polygon
|
430
|
+
|
431
|
+
@staticmethod
|
432
|
+
def remove_edge_pixel_values(image_np: np.ndarray) -> np.ndarray:
|
433
|
+
"""Remove the edge pixel values from the image.
|
434
|
+
|
435
|
+
Arguments:
|
436
|
+
image_np (np.ndarray): The image to remove the edge pixel values from.
|
437
|
+
|
438
|
+
Returns:
|
439
|
+
np.ndarray: The image with the edge pixel values removed.
|
440
|
+
"""
|
441
|
+
# Set zeros on all sides of the image
|
442
|
+
image_np[0, :] = 0 # Top side
|
443
|
+
image_np[-1, :] = 0 # Bottom side
|
444
|
+
image_np[:, 0] = 0 # Left side
|
445
|
+
image_np[:, -1] = 0 # Right side
|
446
|
+
return image_np
|
maps4fs/generator/dtm/dtm.py
CHANGED
@@ -297,8 +297,6 @@ class DTMProvider(ABC):
|
|
297
297
|
with rasterio.open(tile) as src:
|
298
298
|
crs = src.crs
|
299
299
|
if crs != "EPSG:4326":
|
300
|
-
print("crs:", crs)
|
301
|
-
print("reprojecting to EPSG:4326")
|
302
300
|
self.logger.debug(f"Reprojecting GeoTIFF from {crs} to EPSG:4326...")
|
303
301
|
tile = self.reproject_geotiff(tile)
|
304
302
|
|
@@ -499,12 +497,10 @@ class DTMProvider(ABC):
|
|
499
497
|
# Open all input GeoTIFF files as datasets
|
500
498
|
self.logger.debug("Merging tiff files...")
|
501
499
|
datasets = [rasterio.open(file) for file in input_files]
|
502
|
-
print("datasets:", datasets)
|
503
500
|
|
504
501
|
# Merge datasets
|
505
502
|
crs = datasets[0].crs
|
506
503
|
mosaic, out_transform = merge(datasets, nodata=0)
|
507
|
-
print("mosaic:", mosaic)
|
508
504
|
|
509
505
|
# Get metadata from the first file and update it for the output
|
510
506
|
out_meta = datasets[0].meta.copy()
|
maps4fs/generator/game.py
CHANGED
@@ -6,10 +6,10 @@ from __future__ import annotations
|
|
6
6
|
|
7
7
|
import os
|
8
8
|
|
9
|
-
from maps4fs.generator.background import Background
|
9
|
+
from maps4fs.generator.component.background import Background
|
10
10
|
from maps4fs.generator.component.config import Config
|
11
|
+
from maps4fs.generator.component.grle import GRLE
|
11
12
|
from maps4fs.generator.component.i3d import I3d
|
12
|
-
from maps4fs.generator.grle import GRLE
|
13
13
|
from maps4fs.generator.satellite import Satellite
|
14
14
|
from maps4fs.generator.texture import Texture
|
15
15
|
|
@@ -39,6 +39,7 @@ class Game:
|
|
39
39
|
_grle_schema: str | None = None
|
40
40
|
_tree_schema: str | None = None
|
41
41
|
_i3d_processing: bool = True
|
42
|
+
_plants_processing: bool = True
|
42
43
|
|
43
44
|
# Order matters! Some components depend on others.
|
44
45
|
components = [Texture, Background, GRLE, I3d, Config, Satellite]
|
@@ -147,6 +148,38 @@ class Game:
|
|
147
148
|
str: The path to the weights directory."""
|
148
149
|
raise NotImplementedError
|
149
150
|
|
151
|
+
def get_density_map_fruits_path(self, map_directory: str) -> str:
|
152
|
+
"""Returns the path to the density map fruits file.
|
153
|
+
|
154
|
+
Arguments:
|
155
|
+
map_directory (str): The path to the map directory.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
str: The path to the density map fruits file."""
|
159
|
+
weights_dir = self.weights_dir_path(map_directory)
|
160
|
+
return os.path.join(weights_dir, "densityMap_fruits.png")
|
161
|
+
|
162
|
+
def get_farmlands_path(self, map_directory: str) -> str:
|
163
|
+
"""Returns the path to the farmlands file.
|
164
|
+
|
165
|
+
Arguments:
|
166
|
+
map_directory (str): The path to the map directory.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
str: The path to the farmlands file."""
|
170
|
+
weights_dir = self.weights_dir_path(map_directory)
|
171
|
+
return os.path.join(weights_dir, "infoLayer_farmlands.png")
|
172
|
+
|
173
|
+
def get_farmlands_xml_path(self, map_directory: str) -> str:
|
174
|
+
"""Returns the path to the farmlands xml file.
|
175
|
+
|
176
|
+
Arguments:
|
177
|
+
map_directory (str): The path to the map directory.
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
str: The path to the farmlands xml file."""
|
181
|
+
raise NotImplementedError
|
182
|
+
|
150
183
|
def i3d_file_path(self, map_directory: str) -> str:
|
151
184
|
"""Returns the path to the i3d file.
|
152
185
|
|
@@ -165,6 +198,14 @@ class Game:
|
|
165
198
|
bool: True if the i3d file should be processed, False otherwise."""
|
166
199
|
return self._i3d_processing
|
167
200
|
|
201
|
+
@property
|
202
|
+
def plants_processing(self) -> bool:
|
203
|
+
"""Returns whether the plants should be processed.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
bool: True if the plants should be processed, False otherwise."""
|
207
|
+
return self._plants_processing
|
208
|
+
|
168
209
|
@property
|
169
210
|
def additional_dem_name(self) -> str | None:
|
170
211
|
"""Returns the name of the additional DEM file.
|
@@ -193,6 +234,7 @@ class FS22(Game):
|
|
193
234
|
_map_template_path = os.path.join(working_directory, "data", "fs22-map-template.zip")
|
194
235
|
_texture_schema = os.path.join(working_directory, "data", "fs22-texture-schema.json")
|
195
236
|
_i3d_processing = False
|
237
|
+
_plants_processing = False
|
196
238
|
|
197
239
|
def dem_file_path(self, map_directory: str) -> str:
|
198
240
|
"""Returns the path to the DEM file.
|
@@ -276,3 +318,13 @@ class FS25(Game):
|
|
276
318
|
Returns:
|
277
319
|
str: The path to the i3d file."""
|
278
320
|
return os.path.join(map_directory, "map", "map.i3d")
|
321
|
+
|
322
|
+
def get_farmlands_xml_path(self, map_directory: str) -> str:
|
323
|
+
"""Returns the path to the farmlands xml file.
|
324
|
+
|
325
|
+
Arguments:
|
326
|
+
map_directory (str): The path to the map directory.
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
str: The path to the farmlands xml file."""
|
330
|
+
return os.path.join(map_directory, "map", "config", "farmlands.xml")
|
maps4fs/generator/map.py
CHANGED
@@ -7,7 +7,7 @@ import os
|
|
7
7
|
import shutil
|
8
8
|
from typing import Any, Generator
|
9
9
|
|
10
|
-
from maps4fs.generator.background import Background
|
10
|
+
from maps4fs.generator.component.background import Background
|
11
11
|
from maps4fs.generator.component.base.component import Component
|
12
12
|
from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
|
13
13
|
from maps4fs.generator.game import Game
|
maps4fs/generator/satellite.py
CHANGED
@@ -6,8 +6,8 @@ import os
|
|
6
6
|
import cv2
|
7
7
|
from pygmdl import save_image # type: ignore
|
8
8
|
|
9
|
-
from maps4fs.generator.background import DEFAULT_DISTANCE
|
10
9
|
from maps4fs.generator.component.base.component import Component
|
10
|
+
from maps4fs.generator.settings import Parameters
|
11
11
|
from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE
|
12
12
|
|
13
13
|
|
@@ -41,7 +41,7 @@ class Satellite(Component):
|
|
41
41
|
overview_size = (self.map_size + margin) * 2
|
42
42
|
overwiew_path = os.path.join(self.satellite_directory, "satellite_overview.png")
|
43
43
|
|
44
|
-
background_size = self.map_size + (
|
44
|
+
background_size = self.map_size + (Parameters.BACKGROUND_DISTANCE + margin) * 2
|
45
45
|
background_path = os.path.join(self.satellite_directory, "satellite_background.png")
|
46
46
|
|
47
47
|
sizes = [overview_size, background_size]
|