diff --git a/data/fs25-texture-schema.json b/data/fs25-texture-schema.json index 998f6ea..54dd9c5 100644 --- a/data/fs25-texture-schema.json +++ b/data/fs25-texture-schema.json @@ -69,7 +69,8 @@ "count": 2, "tags": { "natural": "grassland" }, "color": [34, 255, 34], - "priority": 0 + "priority": 0, + "usage": "grass" }, { "name": "grassClovers", @@ -84,7 +85,8 @@ "count": 2, "tags": { "natural": ["wood", "tree_row", "forest"] }, "width": 2, - "color": [0, 252, 124] + "color": [0, 252, 124], + "usage": "forest" }, { "name": "grassDirtPatchyDry", diff --git a/maps4fs/generator/background.py b/maps4fs/generator/background.py index 57e5a5d..46c7b36 100644 --- a/maps4fs/generator/background.py +++ b/maps4fs/generator/background.py @@ -63,6 +63,7 @@ def preprocess(self) -> None: for name, autoprocess in zip(ELEMENTS, autoprocesses): dem = DEM( self.game, + self.map, self.coordinates, background_size, rotated_size, diff --git a/maps4fs/generator/component.py b/maps4fs/generator/component.py index dce0b02..82a223d 100644 --- a/maps4fs/generator/component.py +++ b/maps4fs/generator/component.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from maps4fs.generator.game import Game + from maps4fs.generator.map import Map # pylint: disable=R0801, R0903, R0902, R0904 @@ -25,6 +26,7 @@ class Component: Arguments: game (Game): The game instance for which the map is generated. + map (Map): The map instance for which the component is generated. coordinates (tuple[float, float]): The latitude and longitude of the center of the map. map_size (int): The size of the map in pixels. map_rotated_size (int): The size of the map in pixels after rotation. @@ -37,6 +39,7 @@ class Component: def __init__( self, game: Game, + map: Map, coordinates: tuple[float, float], map_size: int, map_rotated_size: int, @@ -46,6 +49,7 @@ def __init__( **kwargs, # pylint: disable=W0613, R0913, R0917 ): self.game = game + self.map = map self.coordinates = coordinates self.map_size = map_size self.map_rotated_size = map_rotated_size diff --git a/maps4fs/generator/game.py b/maps4fs/generator/game.py index 186154e..e576a83 100644 --- a/maps4fs/generator/game.py +++ b/maps4fs/generator/game.py @@ -36,7 +36,6 @@ class Game: _map_template_path: str | None = None _texture_schema: str | None = None _grle_schema: str | None = None - _base_image: str | None = None # Order matters! Some components depend on others. components = [Texture, I3d, GRLE, Background, Config] @@ -131,19 +130,6 @@ def weights_dir_path(self, map_directory: str) -> str: str: The path to the weights directory.""" raise NotImplementedError - def base_image_path(self, map_directory: str) -> str | None: - """Returns the path to the base density map image. - - Arguments: - map_directory (str): The path to the map directory. - - Returns: - str: The path to the base density map image or None if not set. - """ - if self._base_image: - return os.path.join(self.weights_dir_path(map_directory), self._base_image) - return None - def i3d_file_path(self, map_directory: str) -> str: """Returns the path to the i3d file. @@ -201,7 +187,6 @@ class FS25(Game): _map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip") _texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json") _grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json") - _base_image = "base.png" def dem_file_path(self, map_directory: str) -> str: """Returns the path to the DEM file. diff --git a/maps4fs/generator/grle.py b/maps4fs/generator/grle.py index eed1ebe..c2c11fb 100644 --- a/maps4fs/generator/grle.py +++ b/maps4fs/generator/grle.py @@ -10,6 +10,7 @@ from shapely.geometry import Polygon from maps4fs.generator.component import Component +from maps4fs.generator.texture import Texture ISLAND_SIZE_MIN = 10 ISLAND_SIZE_MAX = 200 @@ -203,27 +204,44 @@ def _add_plants(self) -> None: # 1. Get the path to the densityMap_fruits.png. # 2. Get the path to the base layer (grass). # 3. Detect non-zero areas in the base layer (it's where the plants will be placed). - base_image_path = self.game.base_image_path(self.map_directory) - if not base_image_path or not os.path.isfile(base_image_path): - self.logger.warning("Base image not found in %s.", base_image_path) + texture_component: Texture | None = self.map.get_component("Texture") + if not texture_component: + self.logger.warning("Texture component not found in the map.") + return + + grass_layer = texture_component.get_layer_by_usage("grass") + if not grass_layer: + self.logger.warning("Grass layer not found in the texture component.") + return + + weights_directory = self.game.weights_dir_path(self.map_directory) + grass_image_path = grass_layer.get_preview_or_path(weights_directory) + self.logger.debug("Grass image path: %s.", grass_image_path) + + if not grass_image_path or not os.path.isfile(grass_image_path): + self.logger.warning("Base image not found in %s.", grass_image_path) return density_map_fruit_path = os.path.join( self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png" ) + self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path) + if not os.path.isfile(density_map_fruit_path): self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path) return # Single channeled 8-bit image, where non-zero values (255) are where the grass is. - base_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member + grass_image = cv2.imread( + grass_image_path, cv2.IMREAD_UNCHANGED + ) # pylint: disable=no-member # Density map of the fruits is 2X size of the base image, so we need to resize it. # We'll resize the base image to make it bigger, so we can compare the values. - base_image = cv2.resize( # pylint: disable=no-member - base_image, - (base_image.shape[1] * 2, base_image.shape[0] * 2), + grass_image = cv2.resize( # pylint: disable=no-member + grass_image, + (grass_image.shape[1] * 2, grass_image.shape[0] * 2), interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member ) @@ -310,22 +328,22 @@ def get_rounded_polygon( return None return rounded_polygon - updated_base_image = base_image.copy() + grass_image_copy = grass_image.copy() # Set all the non-zero values to 33. - updated_base_image[base_image != 0] = 33 + grass_image_copy[grass_image != 0] = 33 # Add islands of plants to the base image. island_count = self.map_size self.logger.info("Adding %s islands of plants to the base image.", island_count) - updated_base_image = create_island_of_plants(updated_base_image, island_count) + grass_image_copy = create_island_of_plants(grass_image_copy, island_count) self.logger.debug("Islands of plants added to the base image.") - # Sligtly reduce the size of the base_image, that we'll use as mask. + # Sligtly reduce the size of the grass_image, that we'll use as mask. kernel = np.ones((3, 3), np.uint8) - base_image = cv2.erode(base_image, kernel, iterations=1) + grass_image = cv2.erode(grass_image, kernel, iterations=1) # Remove the values where the base image has zeros. - updated_base_image[base_image == 0] = 0 + grass_image_copy[grass_image == 0] = 0 self.logger.debug("Removed the values where the base image has zeros.") # Value of 33 represents the base grass plant. @@ -339,7 +357,7 @@ def get_rounded_polygon( self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape) # Put the updated base image as the B channel in the density map. - density_map_fruits[:, :, 0] = updated_base_image + density_map_fruits[:, :, 0] = grass_image_copy self.logger.debug("Updated base image added as the B channel in the density map.") # Save the updated density map. @@ -347,6 +365,3 @@ def get_rounded_polygon( density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB) cv2.imwrite(density_map_fruit_path, density_map_fruits) self.logger.info("Updated density map for fruits saved in %s.", density_map_fruit_path) - - debug_save_path = "debug.png" - cv2.imwrite(debug_save_path, density_map_fruits) diff --git a/maps4fs/generator/map.py b/maps4fs/generator/map.py index 5b7083a..2c5ab20 100644 --- a/maps4fs/generator/map.py +++ b/maps4fs/generator/map.py @@ -74,6 +74,7 @@ def generate(self) -> Generator[str, None, None]: for game_component in self.game.components: component = game_component( self.game, + self, self.coordinates, self.size, self.rotated_size, @@ -82,6 +83,7 @@ def generate(self) -> Generator[str, None, None]: self.logger, **self.kwargs, ) + self.components.append(component) yield component.__class__.__name__ @@ -104,7 +106,20 @@ def generate(self) -> Generator[str, None, None]: e, ) raise e - self.components.append(component) + + def get_component(self, component_name: str) -> Component | None: + """Get component by name. + + Arguments: + component_name (str): Name of the component. + + Returns: + Component | None: Component instance or None if not found. + """ + for component in self.components: + if component.__class__.__name__ == component_name: + return component + return None def previews(self) -> list[str]: """Get list of preview images. diff --git a/maps4fs/generator/texture.py b/maps4fs/generator/texture.py index 9ae211c..bbcd30b 100644 --- a/maps4fs/generator/texture.py +++ b/maps4fs/generator/texture.py @@ -64,6 +64,7 @@ def __init__( # pylint: disable=R0917 exclude_weight: bool = False, priority: int | None = None, info_layer: str | None = None, + usage: str | None = None, ): self.name = name self.count = count @@ -73,6 +74,7 @@ def __init__( # pylint: disable=R0917 self.exclude_weight = exclude_weight self.priority = priority self.info_layer = info_layer + self.usage = usage def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore """Returns dictionary with layer data. @@ -88,6 +90,7 @@ def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore "exclude_weight": self.exclude_weight, "priority": self.priority, "info_layer": self.info_layer, + "usage": self.usage, } data = {k: v for k, v in data.items() if v is not None} @@ -118,29 +121,29 @@ def path(self, weights_directory: str) -> str: weight_postfix = "_weight" if not self.exclude_weight else "" return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png") - def path_preview(self, previews_directory: str) -> str: + def path_preview(self, weights_directory: str) -> str: """Returns path to the preview of the first texture of the layer. Arguments: - previews_directory (str): Path to the directory with previews. + weights_directory (str): Path to the directory with weights. Returns: str: Path to the preview. """ - return self.path(previews_directory).replace(".png", "_preview.png") + return self.path(weights_directory).replace(".png", "_preview.png") - def get_preview_or_path(self, previews_directory: str) -> str: + def get_preview_or_path(self, weights_directory: str) -> str: """Returns path to the preview of the first texture of the layer if it exists, otherwise returns path to the texture. Arguments: - previews_directory (str): Path to the directory with previews. + weights_directory (str): Path to the directory with weights. Returns: str: Path to the preview or texture. """ - preview_path = self.path_preview(previews_directory) - return preview_path if os.path.isfile(preview_path) else self.path(previews_directory) + preview_path = self.path_preview(weights_directory) + return preview_path if os.path.isfile(preview_path) else self.path(weights_directory) def paths(self, weights_directory: str) -> list[str]: """Returns a list of paths to the textures of the layer. @@ -213,6 +216,20 @@ def get_base_layer(self) -> Layer | None: return layer return None + def get_layer_by_usage(self, usage: str) -> Layer | None: + """Returns layer by usage. + + Arguments: + usage (str): Usage of the layer. + + Returns: + Layer | None: Layer. + """ + for layer in self.layers: + if layer.usage == usage: + return layer + return None + def process(self): self._prepare_weights() self._read_parameters() @@ -243,16 +260,16 @@ def rotate_textures(self) -> None: "Skipping rotation of layer %s because it has no tags.", layer.name ) - base_path = self.game.base_image_path(self.map_directory) - if base_path: - base_layer = self.get_base_layer() - if base_layer: - base_layer_path = base_layer.get_preview_or_path(self._weights_dir) - self.logger.debug( - "Copying base layer to use it later for density map to %s.", base_path - ) - # Make a copy of a base layer to the fruits density map. - shutil.copy(base_layer_path, base_path) + # base_path = self.game.base_image_path(self.map_directory) + # if base_path: + # base_layer = self.get_base_layer() + # if base_layer: + # base_layer_path = base_layer.get_preview_or_path(self._weights_dir) + # self.logger.debug( + # "Copying base layer to use it later for density map to %s.", base_path + # ) + # # Make a copy of a base layer to the fruits density map. + # shutil.copy(base_layer_path, base_path) # pylint: disable=W0201 def _read_parameters(self) -> None: