maps4fs 1.8.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- maps4fs/__init__.py +22 -0
- maps4fs/generator/__init__.py +1 -0
- maps4fs/generator/background.py +625 -0
- maps4fs/generator/component.py +553 -0
- maps4fs/generator/config.py +109 -0
- maps4fs/generator/dem.py +297 -0
- maps4fs/generator/dtm/__init__.py +0 -0
- maps4fs/generator/dtm/base/wcs.py +71 -0
- maps4fs/generator/dtm/base/wms.py +70 -0
- maps4fs/generator/dtm/bavaria.py +113 -0
- maps4fs/generator/dtm/dtm.py +637 -0
- maps4fs/generator/dtm/england.py +31 -0
- maps4fs/generator/dtm/hessen.py +31 -0
- maps4fs/generator/dtm/niedersachsen.py +39 -0
- maps4fs/generator/dtm/nrw.py +30 -0
- maps4fs/generator/dtm/srtm.py +127 -0
- maps4fs/generator/dtm/usgs.py +87 -0
- maps4fs/generator/dtm/utils.py +61 -0
- maps4fs/generator/game.py +247 -0
- maps4fs/generator/grle.py +470 -0
- maps4fs/generator/i3d.py +624 -0
- maps4fs/generator/map.py +275 -0
- maps4fs/generator/qgis.py +196 -0
- maps4fs/generator/satellite.py +92 -0
- maps4fs/generator/settings.py +187 -0
- maps4fs/generator/texture.py +893 -0
- maps4fs/logger.py +46 -0
- maps4fs/toolbox/__init__.py +1 -0
- maps4fs/toolbox/background.py +63 -0
- maps4fs/toolbox/custom_osm.py +67 -0
- maps4fs/toolbox/dem.py +112 -0
- maps4fs-1.8.0.dist-info/LICENSE.md +190 -0
- maps4fs-1.8.0.dist-info/METADATA +693 -0
- maps4fs-1.8.0.dist-info/RECORD +36 -0
- maps4fs-1.8.0.dist-info/WHEEL +5 -0
- maps4fs-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,470 @@
|
|
1
|
+
"""This module contains the GRLE class for generating InfoLayer PNG files based on GRLE schema."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
from random import choice, randint
|
6
|
+
from xml.etree import ElementTree as ET
|
7
|
+
|
8
|
+
import cv2
|
9
|
+
import numpy as np
|
10
|
+
from shapely.geometry import Polygon # type: ignore
|
11
|
+
from tqdm import tqdm
|
12
|
+
|
13
|
+
from maps4fs.generator.component import Component
|
14
|
+
from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE, Texture
|
15
|
+
|
16
|
+
ISLAND_DISTORTION = 0.3
|
17
|
+
|
18
|
+
|
19
|
+
def plant_to_pixel_value(plant_name: str) -> int | None:
|
20
|
+
"""Returns the pixel value representation of the plant.
|
21
|
+
If not found, returns None.
|
22
|
+
|
23
|
+
Arguments:
|
24
|
+
plant_name (str): name of the plant
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
int | None: pixel value of the plant or None if not found.
|
28
|
+
"""
|
29
|
+
plants = {
|
30
|
+
"smallDenseMix": 33,
|
31
|
+
"meadow": 131,
|
32
|
+
}
|
33
|
+
return plants.get(plant_name)
|
34
|
+
|
35
|
+
|
36
|
+
# pylint: disable=W0223
|
37
|
+
class GRLE(Component):
|
38
|
+
"""Component for to generate InfoLayer PNG files based on GRLE schema.
|
39
|
+
|
40
|
+
Arguments:
|
41
|
+
game (Game): The game instance for which the map is generated.
|
42
|
+
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
43
|
+
map_size (int): The size of the map in pixels.
|
44
|
+
map_rotated_size (int): The size of the map in pixels after rotation.
|
45
|
+
rotation (int): The rotation angle of the map.
|
46
|
+
map_directory (str): The directory where the map files are stored.
|
47
|
+
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
|
48
|
+
info, warning. If not provided, default logging will be used.
|
49
|
+
"""
|
50
|
+
|
51
|
+
_grle_schema: dict[str, float | int | str] | None = None
|
52
|
+
|
53
|
+
def preprocess(self) -> None:
|
54
|
+
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
55
|
+
attribute. If the game does not support I3D files, the attribute is set to None."""
|
56
|
+
self.preview_paths: dict[str, str] = {}
|
57
|
+
|
58
|
+
try:
|
59
|
+
grle_schema_path = self.game.grle_schema
|
60
|
+
except ValueError:
|
61
|
+
self.logger.warning("GRLE schema processing is not implemented for this game.")
|
62
|
+
return
|
63
|
+
|
64
|
+
try:
|
65
|
+
with open(grle_schema_path, "r", encoding="utf-8") as file:
|
66
|
+
self._grle_schema = json.load(file)
|
67
|
+
self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
|
68
|
+
except (json.JSONDecodeError, FileNotFoundError) as error:
|
69
|
+
self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
|
70
|
+
self._grle_schema = None
|
71
|
+
|
72
|
+
def process(self) -> None:
|
73
|
+
"""Generates InfoLayer PNG files based on the GRLE schema."""
|
74
|
+
if not self._grle_schema:
|
75
|
+
self.logger.debug("GRLE schema is not obtained, skipping the processing.")
|
76
|
+
return
|
77
|
+
|
78
|
+
for info_layer in tqdm(self._grle_schema, desc="Preparing GRLE files", unit="layer"):
|
79
|
+
if isinstance(info_layer, dict):
|
80
|
+
file_path = os.path.join(
|
81
|
+
self.game.weights_dir_path(self.map_directory), info_layer["name"]
|
82
|
+
)
|
83
|
+
|
84
|
+
height = int(self.map_size * info_layer["height_multiplier"])
|
85
|
+
width = int(self.map_size * info_layer["width_multiplier"])
|
86
|
+
channels = info_layer["channels"]
|
87
|
+
data_type = info_layer["data_type"]
|
88
|
+
|
89
|
+
# Create the InfoLayer PNG file with zeros.
|
90
|
+
if channels == 1:
|
91
|
+
info_layer_data = np.zeros((height, width), dtype=data_type)
|
92
|
+
else:
|
93
|
+
info_layer_data = np.zeros((height, width, channels), dtype=data_type)
|
94
|
+
self.logger.debug("Shape of %s: %s.", info_layer["name"], info_layer_data.shape)
|
95
|
+
cv2.imwrite(file_path, info_layer_data) # pylint: disable=no-member
|
96
|
+
self.logger.debug("InfoLayer PNG file %s created.", file_path)
|
97
|
+
else:
|
98
|
+
self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
|
99
|
+
|
100
|
+
self._add_farmlands()
|
101
|
+
if self.game.code == "FS25":
|
102
|
+
self.logger.debug("Game is %s, plants will be added.", self.game.code)
|
103
|
+
self._add_plants()
|
104
|
+
else:
|
105
|
+
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
|
106
|
+
|
107
|
+
# pylint: disable=no-member
|
108
|
+
def previews(self) -> list[str]:
|
109
|
+
"""Returns a list of paths to the preview images (empty list).
|
110
|
+
The component does not generate any preview images so it returns an empty list.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
list[str]: An empty list.
|
114
|
+
"""
|
115
|
+
preview_paths = []
|
116
|
+
for preview_name, preview_path in self.preview_paths.items():
|
117
|
+
save_path = os.path.join(self.previews_directory, f"{preview_name}.png")
|
118
|
+
# Resize the preview image to the maximum size allowed for previews.
|
119
|
+
image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
|
120
|
+
if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
|
121
|
+
image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
|
122
|
+
image_normalized = np.empty_like(image)
|
123
|
+
cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
|
124
|
+
image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
|
125
|
+
cv2.imwrite(save_path, image_colored)
|
126
|
+
preview_paths.append(save_path)
|
127
|
+
|
128
|
+
with_fields_save_path = os.path.join(
|
129
|
+
self.previews_directory, f"{preview_name}_with_fields.png"
|
130
|
+
)
|
131
|
+
image_with_fields = self.overlay_fields(image_colored)
|
132
|
+
if image_with_fields is None:
|
133
|
+
continue
|
134
|
+
cv2.imwrite(with_fields_save_path, image_with_fields) # pylint: disable=no-member
|
135
|
+
preview_paths.append(with_fields_save_path)
|
136
|
+
|
137
|
+
return preview_paths
|
138
|
+
|
139
|
+
def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
|
140
|
+
"""Overlay fields on the farmlands preview image.
|
141
|
+
|
142
|
+
Arguments:
|
143
|
+
farmlands_np (np.ndarray): The farmlands preview image.
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
np.ndarray | None: The farmlands preview image with fields overlayed on top of it.
|
147
|
+
"""
|
148
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
149
|
+
if not texture_component:
|
150
|
+
self.logger.warning("Texture component not found in the map.")
|
151
|
+
return None
|
152
|
+
|
153
|
+
fields_layer = texture_component.get_layer_by_usage("field")
|
154
|
+
fields_layer_path = fields_layer.get_preview_or_path( # type: ignore
|
155
|
+
self.game.weights_dir_path(self.map_directory)
|
156
|
+
)
|
157
|
+
if not fields_layer_path or not os.path.isfile(fields_layer_path):
|
158
|
+
self.logger.warning("Fields layer not found in the texture component.")
|
159
|
+
return None
|
160
|
+
fields_np = cv2.imread(fields_layer_path)
|
161
|
+
# Resize fields_np to the same size as farmlands_np.
|
162
|
+
fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0]))
|
163
|
+
|
164
|
+
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
165
|
+
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
166
|
+
|
167
|
+
# pylint: disable=R0801, R0914, R0915
|
168
|
+
def _add_farmlands(self) -> None:
|
169
|
+
"""Adds farmlands to the InfoLayer PNG file."""
|
170
|
+
|
171
|
+
textures_info_layer_path = self.get_infolayer_path("textures")
|
172
|
+
if not textures_info_layer_path:
|
173
|
+
return
|
174
|
+
|
175
|
+
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
176
|
+
textures_info_layer = json.load(textures_info_layer_file)
|
177
|
+
|
178
|
+
farmlands = []
|
179
|
+
farmyards: list[list[tuple[int, int]]] | None = textures_info_layer.get("farmyards")
|
180
|
+
if farmyards and self.map.grle_settings.add_farmyards:
|
181
|
+
farmlands.extend(farmyards)
|
182
|
+
self.logger.debug("Found %s farmyards in textures info layer.", len(farmyards))
|
183
|
+
|
184
|
+
fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
|
185
|
+
if not fields:
|
186
|
+
self.logger.warning("Fields data not found in textures info layer.")
|
187
|
+
return
|
188
|
+
farmlands.extend(fields)
|
189
|
+
|
190
|
+
self.logger.debug("Found %s fields in textures info layer.", len(fields))
|
191
|
+
|
192
|
+
info_layer_farmlands_path = os.path.join(
|
193
|
+
self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
|
194
|
+
)
|
195
|
+
|
196
|
+
self.logger.debug(
|
197
|
+
"Adding farmlands to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
198
|
+
)
|
199
|
+
|
200
|
+
if not os.path.isfile(info_layer_farmlands_path):
|
201
|
+
self.logger.warning("InfoLayer PNG file %s not found.", info_layer_farmlands_path)
|
202
|
+
return
|
203
|
+
|
204
|
+
# pylint: disable=no-member
|
205
|
+
image = cv2.imread(info_layer_farmlands_path, cv2.IMREAD_UNCHANGED)
|
206
|
+
farmlands_xml_path = os.path.join(self.map_directory, "map/config/farmlands.xml")
|
207
|
+
if not os.path.isfile(farmlands_xml_path):
|
208
|
+
self.logger.warning("Farmlands XML file %s not found.", farmlands_xml_path)
|
209
|
+
return
|
210
|
+
|
211
|
+
tree = ET.parse(farmlands_xml_path)
|
212
|
+
farmlands_xml = tree.find("farmlands")
|
213
|
+
|
214
|
+
# Not using enumerate because in case of the error, we do not increment
|
215
|
+
# the farmland_id. So as a result we do not have a gap in the farmland IDs.
|
216
|
+
farmland_id = 1
|
217
|
+
|
218
|
+
for farmland_data in tqdm(farmlands, desc="Adding farmlands", unit="farmland"):
|
219
|
+
try:
|
220
|
+
fitted_field = self.fit_object_into_bounds(
|
221
|
+
polygon_points=farmland_data,
|
222
|
+
margin=self.map.grle_settings.farmland_margin,
|
223
|
+
angle=self.rotation,
|
224
|
+
)
|
225
|
+
except ValueError as e:
|
226
|
+
self.logger.debug(
|
227
|
+
"Farmland %s could not be fitted into the map bounds with error: %s",
|
228
|
+
farmland_id,
|
229
|
+
e,
|
230
|
+
)
|
231
|
+
continue
|
232
|
+
|
233
|
+
self.logger.debug("Fitted field %s contains %s points.", farmland_id, len(fitted_field))
|
234
|
+
|
235
|
+
field_np = np.array(fitted_field, np.int32)
|
236
|
+
field_np = field_np.reshape((-1, 1, 2))
|
237
|
+
|
238
|
+
self.logger.debug(
|
239
|
+
"Created a numpy array and reshaped it. Number of points: %s", len(field_np)
|
240
|
+
)
|
241
|
+
|
242
|
+
# Infolayer image is 1/2 of the size of the map image, that's why we need to divide
|
243
|
+
# the coordinates by 2.
|
244
|
+
field_np = field_np // 2
|
245
|
+
self.logger.debug("Divided the coordinates by 2.")
|
246
|
+
|
247
|
+
# pylint: disable=no-member
|
248
|
+
try:
|
249
|
+
cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
|
250
|
+
except Exception as e: # pylint: disable=W0718
|
251
|
+
self.logger.debug(
|
252
|
+
"Farmland %s could not be added to the InfoLayer PNG file with error: %s",
|
253
|
+
farmland_id,
|
254
|
+
e,
|
255
|
+
)
|
256
|
+
continue
|
257
|
+
|
258
|
+
# Add the field to the farmlands XML.
|
259
|
+
farmland = ET.SubElement(farmlands_xml, "farmland") # type: ignore
|
260
|
+
farmland.set("id", str(farmland_id))
|
261
|
+
farmland.set("priceScale", "1")
|
262
|
+
farmland.set("npcName", "FORESTER")
|
263
|
+
|
264
|
+
farmland_id += 1
|
265
|
+
|
266
|
+
tree.write(farmlands_xml_path)
|
267
|
+
|
268
|
+
self.logger.debug("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
|
269
|
+
|
270
|
+
cv2.imwrite(info_layer_farmlands_path, image) # pylint: disable=no-member
|
271
|
+
self.logger.debug(
|
272
|
+
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
273
|
+
)
|
274
|
+
|
275
|
+
self.preview_paths["farmlands"] = info_layer_farmlands_path # type: ignore
|
276
|
+
|
277
|
+
# pylint: disable=R0915
|
278
|
+
def _add_plants(self) -> None:
|
279
|
+
"""Adds plants to the InfoLayer PNG file."""
|
280
|
+
# 1. Get the path to the densityMap_fruits.png.
|
281
|
+
# 2. Get the path to the base layer (grass).
|
282
|
+
# 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
|
283
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
284
|
+
if not texture_component:
|
285
|
+
self.logger.warning("Texture component not found in the map.")
|
286
|
+
return
|
287
|
+
|
288
|
+
grass_layer = texture_component.get_layer_by_usage("grass")
|
289
|
+
if not grass_layer:
|
290
|
+
self.logger.warning("Grass layer not found in the texture component.")
|
291
|
+
return
|
292
|
+
|
293
|
+
weights_directory = self.game.weights_dir_path(self.map_directory)
|
294
|
+
grass_image_path = grass_layer.get_preview_or_path(weights_directory)
|
295
|
+
self.logger.debug("Grass image path: %s.", grass_image_path)
|
296
|
+
|
297
|
+
forest_layer = texture_component.get_layer_by_usage("forest")
|
298
|
+
forest_image = None
|
299
|
+
if forest_layer:
|
300
|
+
forest_image_path = forest_layer.get_preview_or_path(weights_directory)
|
301
|
+
self.logger.debug("Forest image path: %s.", forest_image_path)
|
302
|
+
if forest_image_path:
|
303
|
+
# pylint: disable=no-member
|
304
|
+
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
|
305
|
+
|
306
|
+
if not grass_image_path or not os.path.isfile(grass_image_path):
|
307
|
+
self.logger.warning("Base image not found in %s.", grass_image_path)
|
308
|
+
return
|
309
|
+
|
310
|
+
density_map_fruit_path = os.path.join(
|
311
|
+
self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
|
312
|
+
)
|
313
|
+
|
314
|
+
self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path)
|
315
|
+
|
316
|
+
if not os.path.isfile(density_map_fruit_path):
|
317
|
+
self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path)
|
318
|
+
return
|
319
|
+
|
320
|
+
# Single channeled 8-bit image, where non-zero values (255) are where the grass is.
|
321
|
+
grass_image = cv2.imread( # pylint: disable=no-member
|
322
|
+
grass_image_path, cv2.IMREAD_UNCHANGED # pylint: disable=no-member
|
323
|
+
)
|
324
|
+
|
325
|
+
# Density map of the fruits is 2X size of the base image, so we need to resize it.
|
326
|
+
# We'll resize the base image to make it bigger, so we can compare the values.
|
327
|
+
grass_image = cv2.resize( # pylint: disable=no-member
|
328
|
+
grass_image,
|
329
|
+
(grass_image.shape[1] * 2, grass_image.shape[0] * 2),
|
330
|
+
interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
|
331
|
+
)
|
332
|
+
if forest_image is not None:
|
333
|
+
forest_image = cv2.resize( # pylint: disable=no-member
|
334
|
+
forest_image,
|
335
|
+
(forest_image.shape[1] * 2, forest_image.shape[0] * 2),
|
336
|
+
interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
|
337
|
+
)
|
338
|
+
|
339
|
+
# Add non zero values from the forest image to the grass image.
|
340
|
+
grass_image[forest_image != 0] = 255
|
341
|
+
|
342
|
+
# B and G channels remain the same (zeros), while we change the R channel.
|
343
|
+
possible_R_values = [65, 97, 129, 161, 193, 225] # pylint: disable=C0103
|
344
|
+
|
345
|
+
base_layer_pixel_value = plant_to_pixel_value(
|
346
|
+
self.map.grle_settings.base_grass # type:ignore
|
347
|
+
)
|
348
|
+
if not base_layer_pixel_value:
|
349
|
+
base_layer_pixel_value = 131
|
350
|
+
|
351
|
+
# pylint: disable=no-member
|
352
|
+
def create_island_of_plants(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
|
+
for _ in tqdm(range(count), desc="Adding islands of plants", unit="island"):
|
363
|
+
# Randomly choose the value for the island.
|
364
|
+
plant_value = choice(possible_R_values)
|
365
|
+
# Randomly choose the size of the island.
|
366
|
+
island_size = randint(
|
367
|
+
self.map.grle_settings.plants_island_minimum_size, # type:ignore
|
368
|
+
self.map.grle_settings.plants_island_maximum_size, # type:ignore
|
369
|
+
)
|
370
|
+
# Randomly choose the position of the island.
|
371
|
+
x = randint(0, image.shape[1] - island_size)
|
372
|
+
y = randint(0, image.shape[0] - island_size)
|
373
|
+
|
374
|
+
try:
|
375
|
+
polygon_points = get_rounded_polygon(
|
376
|
+
num_vertices=self.map.grle_settings.plants_island_vertex_count,
|
377
|
+
center=(x + island_size // 2, y + island_size // 2),
|
378
|
+
radius=island_size // 2,
|
379
|
+
rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
|
380
|
+
)
|
381
|
+
if not polygon_points:
|
382
|
+
continue
|
383
|
+
|
384
|
+
nodes = np.array(polygon_points, np.int32) # type: ignore
|
385
|
+
cv2.fillPoly(image, [nodes], plant_value) # type: ignore
|
386
|
+
except Exception: # pylint: disable=W0703
|
387
|
+
continue
|
388
|
+
|
389
|
+
return image
|
390
|
+
|
391
|
+
def get_rounded_polygon(
|
392
|
+
num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
|
393
|
+
) -> list[tuple[int, int]] | None:
|
394
|
+
"""Get a randomly rounded polygon.
|
395
|
+
|
396
|
+
Arguments:
|
397
|
+
num_vertices (int): The number of vertices of the polygon.
|
398
|
+
center (tuple[int, int]): The center of the polygon.
|
399
|
+
radius (int): The radius of the polygon.
|
400
|
+
rounding_radius (int): The rounding radius of the polygon.
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
list[tuple[int, int]] | None: The rounded polygon.
|
404
|
+
"""
|
405
|
+
angle_offset = np.pi / num_vertices
|
406
|
+
angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
|
407
|
+
random_angles = angles + np.random.uniform(
|
408
|
+
-ISLAND_DISTORTION, ISLAND_DISTORTION, num_vertices
|
409
|
+
) # Add randomness to angles
|
410
|
+
random_radii = radius + np.random.uniform(
|
411
|
+
-radius * ISLAND_DISTORTION, radius * ISLAND_DISTORTION, num_vertices
|
412
|
+
) # Add randomness to radii
|
413
|
+
|
414
|
+
points = [
|
415
|
+
(center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
|
416
|
+
for a, r in zip(random_angles, random_radii)
|
417
|
+
]
|
418
|
+
polygon = Polygon(points)
|
419
|
+
buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
|
420
|
+
rounded_polygon = list(buffered_polygon.exterior.coords)
|
421
|
+
if not rounded_polygon:
|
422
|
+
return None
|
423
|
+
return rounded_polygon
|
424
|
+
|
425
|
+
grass_image_copy = grass_image.copy()
|
426
|
+
if forest_image is not None:
|
427
|
+
# Add the forest layer to the base image, to merge the masks.
|
428
|
+
grass_image_copy[forest_image != 0] = base_layer_pixel_value
|
429
|
+
|
430
|
+
grass_image_copy[grass_image != 0] = base_layer_pixel_value
|
431
|
+
|
432
|
+
# Add islands of plants to the base image.
|
433
|
+
island_count = int(self.map_size * self.map.grle_settings.plants_island_percent // 100)
|
434
|
+
self.logger.debug("Adding %s islands of plants to the base image.", island_count)
|
435
|
+
if self.map.grle_settings.random_plants:
|
436
|
+
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
|
437
|
+
self.logger.debug("Added %s islands of plants to the base image.", island_count)
|
438
|
+
|
439
|
+
# Sligtly reduce the size of the grass_image, that we'll use as mask.
|
440
|
+
kernel = np.ones((3, 3), np.uint8)
|
441
|
+
grass_image = cv2.erode(grass_image, kernel, iterations=1)
|
442
|
+
|
443
|
+
# Remove the values where the base image has zeros.
|
444
|
+
grass_image_copy[grass_image == 0] = 0
|
445
|
+
self.logger.debug("Removed the values where the base image has zeros.")
|
446
|
+
|
447
|
+
# Set zeros on all sides of the image
|
448
|
+
grass_image_copy[0, :] = 0 # Top side
|
449
|
+
grass_image_copy[-1, :] = 0 # Bottom side
|
450
|
+
grass_image_copy[:, 0] = 0 # Left side
|
451
|
+
grass_image_copy[:, -1] = 0 # Right side
|
452
|
+
|
453
|
+
# After painting it with base grass, we'll create multiple islands of different plants.
|
454
|
+
# On the final step, we'll remove all the values which in pixels
|
455
|
+
# where zerons in the original base image (so we don't paint grass where it should not be).
|
456
|
+
|
457
|
+
# Three channeled 8-bit image, where non-zero values are the
|
458
|
+
# different types of plants (only in the R channel).
|
459
|
+
density_map_fruits = cv2.imread(density_map_fruit_path, cv2.IMREAD_UNCHANGED)
|
460
|
+
self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
|
461
|
+
|
462
|
+
# Put the updated base image as the B channel in the density map.
|
463
|
+
density_map_fruits[:, :, 0] = grass_image_copy
|
464
|
+
self.logger.debug("Updated base image added as the B channel in the density map.")
|
465
|
+
|
466
|
+
# Save the updated density map.
|
467
|
+
# Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
|
468
|
+
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
|
469
|
+
cv2.imwrite(density_map_fruit_path, density_map_fruits)
|
470
|
+
self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
|