Skip to content

Commit

Permalink
Updated grass generation.
Browse files Browse the repository at this point in the history
  • Loading branch information
iwatkot committed Dec 22, 2024
1 parent a3ac9af commit 7a1512d
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 52 deletions.
6 changes: 4 additions & 2 deletions data/fs25-texture-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
"count": 2,
"tags": { "natural": "grassland" },
"color": [34, 255, 34],
"priority": 0
"priority": 0,
"usage": "grass"
},
{
"name": "grassClovers",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions maps4fs/generator/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions maps4fs/generator/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -37,6 +39,7 @@ class Component:
def __init__(
self,
game: Game,
map: Map,
coordinates: tuple[float, float],
map_size: int,
map_rotated_size: int,
Expand All @@ -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
Expand Down
15 changes: 0 additions & 15 deletions maps4fs/generator/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
49 changes: 32 additions & 17 deletions maps4fs/generator/grle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)

Expand Down Expand Up @@ -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.
Expand All @@ -339,14 +357,11 @@ 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.
# Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
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)
17 changes: 16 additions & 1 deletion maps4fs/generator/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -82,6 +83,7 @@ def generate(self) -> Generator[str, None, None]:
self.logger,
**self.kwargs,
)
self.components.append(component)

yield component.__class__.__name__

Expand All @@ -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.
Expand Down
51 changes: 34 additions & 17 deletions maps4fs/generator/texture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 7a1512d

Please sign in to comment.