Skip to content

Commit

Permalink
Auto preset feature
Browse files Browse the repository at this point in the history
* Auto preset.

* Minor code updates.
  • Loading branch information
iwatkot authored Nov 23, 2024
1 parent 0677b4b commit 2d2c68a
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 52 deletions.
1 change: 1 addition & 0 deletions maps4fs/generator/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Component:
"""Base class for all map generation components.
Args:
game (Game): The game instance for which the map is generated.
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
map_height (int): The height of the map in pixels.
map_width (int): The width of the map in pixels.
Expand Down
62 changes: 51 additions & 11 deletions maps4fs/generator/dem.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
DEFAULT_MULTIPLIER = 1
DEFAULT_BLUR_RADIUS = 21
DEFAULT_BLUR_RADIUS = 35


# pylint: disable=R0903
Expand Down Expand Up @@ -47,6 +47,8 @@ def preprocess(self) -> None:
"DEM value multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
)

self.auto_process = self.kwargs.get("auto_process", False)

# pylint: disable=no-member
def process(self) -> None:
"""Reads SRTM file, crops it to map size, normalizes and blurs it,
Expand Down Expand Up @@ -105,15 +107,12 @@ def process(self) -> None:
resampled_data.min(),
)

resampled_data = resampled_data * self.multiplier
self.logger.debug(
"DEM data multiplied by %s. Shape: %s, dtype: %s. Min: %s, max: %s.",
self.multiplier,
resampled_data.shape,
resampled_data.dtype,
resampled_data.min(),
resampled_data.max(),
)
if self.auto_process:
self.logger.debug("Auto processing is enabled, will normalize DEM data.")
resampled_data = self._normalize_dem(resampled_data)
else:
self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
resampled_data = resampled_data * self.multiplier

self.logger.debug(
"DEM data was resampled. Shape: %s, dtype: %s. Min: %s, max: %s.",
Expand All @@ -123,7 +122,9 @@ def process(self) -> None:
resampled_data.max(),
)

resampled_data = cv2.GaussianBlur(resampled_data, (self.blur_radius, self.blur_radius), 0)
resampled_data = cv2.GaussianBlur(
resampled_data, (self.blur_radius, self.blur_radius), sigmaX=40, sigmaY=40
)
self.logger.debug(
"Gaussion blur applied to DEM data with kernel size %s.",
self.blur_radius,
Expand Down Expand Up @@ -289,3 +290,42 @@ def previews(self) -> list[str]:
"""
self.logger.debug("Starting DEM previews generation.")
return [self.grayscale_preview(), self.colored_preview()]

def _get_scaling_factor(self, maximum_deviation: int) -> float:
ESTIMATED_MAXIMUM_DEVIATION = 1000 # pylint: disable=C0103
scaling_factor = maximum_deviation / ESTIMATED_MAXIMUM_DEVIATION
return scaling_factor if scaling_factor < 1 else 1

def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
"""Normalize DEM data to 16-bit unsigned integer using max height from settings.
Args:
data (np.ndarray): DEM data from SRTM file after cropping.
Returns:
np.ndarray: Normalized DEM data.
"""
self.logger.debug("Starting DEM data normalization.")
# Calculate the difference between the maximum and minimum values in the DEM data.

max_height = data.max() # 1800
min_height = data.min() # 1700
max_dev = max_height - min_height # 100
self.logger.debug(
"Maximum deviation: %s with maximum at %s and minimum at %s.",
max_dev,
max_height,
min_height,
)

scaling_factor = self._get_scaling_factor(max_dev)
adjusted_max_height = int(65535 * scaling_factor)
self.logger.debug(
f"Maximum deviation: {max_dev}. Scaling factor: {scaling_factor}. "
f"Adjusted max height: {adjusted_max_height}."
)
normalized_data = (
(data - data.min()) / (data.max() - data.min()) * adjusted_max_height
).astype("uint16")
self.logger.debug(
f"DEM data was normalized to {normalized_data.min()} - {normalized_data.max()}."
)
return normalized_data
24 changes: 23 additions & 1 deletion maps4fs/generator/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from maps4fs.generator.config import Config
from maps4fs.generator.dem import DEM
from maps4fs.generator.i3d import I3d
from maps4fs.generator.texture import Texture

working_directory = os.getcwd()
Expand All @@ -34,7 +35,7 @@ class Game:
_map_template_path: str | None = None
_texture_schema: str | None = None

components = [Config, Texture, DEM]
components = [Config, Texture, DEM, I3d]

def __init__(self, map_template_path: str | None = None):
if map_template_path:
Expand Down Expand Up @@ -113,6 +114,16 @@ def weights_dir_path(self, map_directory: str) -> str:
str: The path to the weights directory."""
raise NotImplementedError

def i3d_file_path(self, map_directory: str) -> str:
"""Returns the path to the i3d file.
Arguments:
map_directory (str): The path to the map directory.
Returns:
str: The path to the i3d file."""
raise NotImplementedError

@property
def additional_dem_name(self) -> str | None:
"""Returns the name of the additional DEM file.
Expand All @@ -122,6 +133,7 @@ def additional_dem_name(self) -> str | None:
return self._additional_dem_name


# pylint: disable=W0223
class FS22(Game):
"""Class used to define the game version FS22."""

Expand Down Expand Up @@ -189,3 +201,13 @@ def weights_dir_path(self, map_directory: str) -> str:
Returns:
str: The path to the weights directory."""
return os.path.join(map_directory, "mapUS", "data")

def i3d_file_path(self, map_directory: str) -> str:
"""Returns the path to the i3d file.
Arguments:
map_directory (str): The path to the map directory.
Returns:
str: The path to the i3d file."""
return os.path.join(map_directory, "mapUS", "mapUS.i3d")
77 changes: 77 additions & 0 deletions maps4fs/generator/i3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""This module contains the Config class for map settings and configuration."""

from __future__ import annotations

import os
from xml.etree import ElementTree as ET

from maps4fs.generator.component import Component

DEFAULT_HEIGHT_SCALE = 2000
DEFAULT_MAX_LOD_DISTANCE = 10000


# pylint: disable=R0903
class I3d(Component):
"""Component for map i3d file settings and configuration.
Args:
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
map_height (int): The height of the map in pixels.
map_width (int): The width of the map in pixels.
map_directory (str): The directory where the map files are stored.
logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
info, warning. If not provided, default logging will be used.
"""

_map_i3d_path: str | None = None

def preprocess(self) -> None:
try:
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
except NotImplementedError:
self.logger.info("I3D file processing is not implemented for this game.")
self._map_i3d_path = None

def process(self) -> None:
self._update_i3d_file()

def _update_i3d_file(self) -> None:
if not self._map_i3d_path:
self.logger.info("I3D is not obtained, skipping the update.")
return
if not os.path.isfile(self._map_i3d_path):
self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
return

tree = ET.parse(self._map_i3d_path)

self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)

root = tree.getroot()
for map_elem in root.iter("Scene"):
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
self.logger.debug(
"heightScale attribute set to %s in TerrainTransformGroup element.",
DEFAULT_HEIGHT_SCALE,
)
terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE))
self.logger.debug(
"maxLODDistance attribute set to %s in TerrainTransformGroup element.",
DEFAULT_MAX_LOD_DISTANCE,
)
self.logger.debug("TerrainTransformGroup element updated in I3D file.")

tree.write(self._map_i3d_path)
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)

def previews(self) -> list[str]:
"""Returns a list of paths to the preview images (empty list).
The component does not generate any preview images so it returns an empty list.
Returns:
list[str]: An empty list.
"""
return []
1 change: 1 addition & 0 deletions maps4fs/generator/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__( # pylint: disable=R0917
self.logger.debug("Game was set to %s", game.code)

self.kwargs = kwargs
self.logger.debug("Additional arguments: %s", kwargs)

os.makedirs(self.map_directory, exist_ok=True)
self.logger.debug("Map directory created: %s", self.map_directory)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "maps4fs"
version = "0.7.9"
version = "0.8.0"
description = "Generate map templates for Farming Simulator from real places."
authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
license = {text = "MIT License"}
Expand Down
110 changes: 71 additions & 39 deletions webui/webui.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def __init__(self):
st.title("Maps4FS")
st.write("Generate map templates for Farming Simulator from real places.")

st.info(
"ℹ️ When opening map first time in the Giants Editor, select **terrain** object, "
"open **Terrain** tab in the **Attributes** window, scroll down to the end "
"and press the **Reload material** button. Otherwise you may (and will) face some "
"glitches."
)

st.markdown("---")

if "generated" not in st.session_state:
Expand Down Expand Up @@ -69,50 +76,74 @@ def add_widgets(self) -> None:

self.map_size_input = f"{custom_map_size_input}x{custom_map_size_input}"

# Add checkbox for advanced settings.
st.write("Advanced settings (do not change if you are not sure):")
self.advanced_settings = st.checkbox("Show advanced settings", key="advanced_settings")
self.multiplier_input = DEFAULT_MULTIPLIER
self.blur_radius_input = DEFAULT_BLUR_RADIUS
st.info(
"ℹ️ Remember to adjust the ***heightScale*** parameter in the Giants Editor to a value "
"that suits your map. Learn more about it in repo's "
"[README](https://github.com/iwatkot/maps4fs)."
)

if self.advanced_settings:
st.warning("⚠️ Changing these settings can lead to unexpected results.")
self.auto_process = st.checkbox("Use auto preset", value=True, key="auto_process")
if self.auto_process:
st.info(
"ℹ️ [DEM] is for settings related to the Digital Elevation Model (elevation map). "
"This file is used to generate the terrain of the map (hills, valleys, etc.)."
)
# Show multiplier and blur radius inputs.
st.write("[DEM] Enter multiplier for the elevation map:")
st.write(
"This multiplier can be used to make the terrain more pronounced. "
"By default the DEM file will be exact copy of the real terrain. "
"If you want to make it more steep, you can increase this value."
)
self.multiplier_input = st.number_input(
"Multiplier",
value=DEFAULT_MULTIPLIER,
min_value=0,
max_value=10000,
key="multiplier",
label_visibility="collapsed",
"Auto preset will automatically apply different algorithms to make terrain more "
"realistic. It's recommended for most cases. If you want to have more control over the "
"terrain generation, you can disable this option and change the advanced settings."
)

st.write("[DEM] Enter blur radius for the elevation map:")
st.write(
"This value is used to blur the elevation map. Without blurring the terrain "
"may look too sharp and unrealistic. By default the blur radius is set to 21 "
"which corresponds to a 21x21 pixel kernel. You can increase this value to make "
"the terrain more smooth. Or make it smaller to make the terrain more sharp."
)
self.blur_radius_input = st.number_input(
"Blur Radius",
value=DEFAULT_BLUR_RADIUS,
min_value=1,
max_value=300,
key="blur_radius",
label_visibility="collapsed",
step=2,
self.multiplier_input = DEFAULT_MULTIPLIER
self.blur_radius_input = DEFAULT_BLUR_RADIUS

if not self.auto_process:
st.info(
"Auto preset is disabled. In this case you probably receive a full black DEM "
"image file. But it is NOT EMPTY. Dem image value range is from 0 to 65535, "
"while on Earth the highest point is 8848 meters. So, unless you are not "
"working with map for Everest, you probably can't see anything on the DEM image "
"by eye, because it is too dark. You can use the "
"multiplier option from Advanced settings to make the terrain more pronounced."
)
# Add checkbox for advanced settings.
st.write("Advanced settings (do not change if you are not sure):")
self.advanced_settings = st.checkbox("Show advanced settings", key="advanced_settings")

if self.advanced_settings:
st.warning("⚠️ Changing these settings can lead to unexpected results.")
st.info(
"ℹ️ [DEM] is for settings related to the Digital Elevation Model (elevation map). "
"This file is used to generate the terrain of the map (hills, valleys, etc.)."
)
# Show multiplier and blur radius inputs.
st.write("[DEM] Enter multiplier for the elevation map:")
st.write(
"This multiplier can be used to make the terrain more pronounced. "
"By default the DEM file will be exact copy of the real terrain. "
"If you want to make it more steep, you can increase this value."
)
self.multiplier_input = st.number_input(
"Multiplier",
value=DEFAULT_MULTIPLIER,
min_value=0,
max_value=10000,
key="multiplier",
label_visibility="collapsed",
)

st.write("[DEM] Enter blur radius for the elevation map:")
st.write(
"This value is used to blur the elevation map. Without blurring the terrain "
"may look too sharp and unrealistic. By default the blur radius is set to 21 "
"which corresponds to a 21x21 pixel kernel. You can increase this value to make "
"the terrain more smooth. Or make it smaller to make the terrain more sharp."
)
self.blur_radius_input = st.number_input(
"Blur Radius",
value=DEFAULT_BLUR_RADIUS,
min_value=1,
max_value=300,
key="blur_radius",
label_visibility="collapsed",
step=2,
)

# Add an empty container for status messages.
self.status_container = st.empty()
Expand Down Expand Up @@ -191,6 +222,7 @@ def generate_map(self) -> None:
logger=self.logger,
multiplier=self.multiplier_input,
blur_radius=self.blur_radius_input,
auto_process=self.auto_process,
)
mp.generate()

Expand Down

0 comments on commit 2d2c68a

Please sign in to comment.