Skip to content

Commit

Permalink
Render polygons for terrain instead of images
Browse files Browse the repository at this point in the history
  • Loading branch information
wilsonzlin committed May 3, 2024
1 parent 5dcf75d commit cb52a5a
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 41 deletions.
85 changes: 44 additions & 41 deletions common/terrain.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from io import BytesIO
from PIL import Image
from scipy.ndimage import gaussian_filter
from typing import Dict
from typing import List
from typing import Tuple
import cv2
import numpy as np
import numpy.typing as npt
import pandas as pd
Expand All @@ -16,26 +18,23 @@ def render_terrain(
upscale: int = 1,
# Gaussian blur parameter. Higher = more blur. Must be a positive integer. Set to zero to disable altogether.
sigma: int = 1,
# This is what Google Maps does with terrain; it's not a smooth gradient. Set to zero to disable.
# This is what Google Maps does with terrain; it's not a smooth gradient. Must be at least one.
contours: int = 4,
color_land=(144, 224, 190),
color_water=(108, 210, 231),
use_log_scale=True,
):
x_min, x_max = (xs.min(), xs.max())
y_min, y_max = (ys.min(), ys.max())
grid_width = int((x_max - x_min) * dpi)
grid_height = int((y_max - y_min) * dpi)

USE_LOG_SCALE = True

gv = pd.DataFrame(
{
"x": ((xs - x_min) * dpi).clip(0, grid_width - 1).astype("int32"),
"y": ((ys - y_min) * dpi).clip(0, grid_height - 1).astype("int32"),
}
)
gv = gv.groupby(["x", "y"]).value_counts().reset_index(name="density")
if USE_LOG_SCALE:
if use_log_scale:
gv["density"] = np.log(gv["density"] + 1)

grid = np.zeros((grid_height, grid_width), dtype=np.float32)
Expand All @@ -45,39 +44,43 @@ def render_terrain(
if sigma:
grid = gaussian_filter(grid, sigma=sigma * upscale)

if contours:
g_min, g_max = grid.min(), grid.max()
buckets = contours
bucket_size = (g_max - g_min) / buckets
# Values fall into [0, buckets - 1].
# Yes, this means that some points will fall onto a grid cell with value 0 i.e. some will be on water. This looks nicer than trying to force land onto every point (i.e. bucket minimum value of 1).
grid = (grid - g_min) // bucket_size
# Some values may lie exactly on the max and will end up with a bucket of `bucket`.
grid = np.clip(grid, 0, buckets)
# Divide by (buckets - 1) to get [0, 1] as otherwise nothing is full alpha.
grid = grid / (buckets - 1)
g_min, g_max = grid.min(), grid.max()
buckets = contours
bucket_size = (g_max - g_min) / buckets
# Values fall into [0, buckets - 1].
# Yes, this means that some points will fall onto a grid cell with value 0 i.e. some will be on water. This looks nicer than trying to force land onto every point (i.e. bucket minimum value of 1), because it creates too many sparse random-looking dull blotches.
grid = (grid - g_min) // bucket_size
# Some values may lie exactly on the max and will end up with a bucket of `buckets`.
grid = np.clip(grid, 0, buckets - 1)

img = np.full(
(grid_height * upscale, grid_width * upscale, 4),
(*color_land, 0),
dtype=np.uint8,
)
img[:, :, 3] = (grid * 255).astype(np.uint8)
webp_out = BytesIO()
Image.fromarray(img, "RGBA").save(webp_out, format="webp", lossless=True)
land = webp_out.getvalue()
# Map from level to list of paths, where a path is a list of (x, y) points.
shapes: Dict[int, List[List[Tuple[float, float]]]] = {}
for bucket in range(buckets):
shapes[bucket] = []
num_shapes, labelled_image = cv2.connectedComponents(
(grid == bucket).astype(np.uint8)
)
# Ignore label 0 as it's the background.
for shape_no in range(1, num_shapes):
shape_mask = labelled_image == shape_no
# Use RETR_EXTERNAL as we only want the outer edges, and don't care about inner holes since they'll be represented by other larger-bucket shapes.
shape_contours, _ = cv2.findContours(
shape_mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
for shape_border_points in shape_contours:
# The resulting shape is (N, 1, 2), where N is the number of points. Remove unnecessary second dimension.
shape_border_points = shape_border_points.squeeze(1)
if shape_border_points.shape[0] < 4:
# Not a polygon.
continue
# We want bucket 0 only when it cuts out an inner hole in a larger bucket.
if bucket == 0 and (0, 0) in shape_border_points:
continue

img = np.full(
(grid_height * upscale, grid_width * upscale, 4),
(*color_water, 0),
dtype=np.uint8,
)
img[:, :, 3] = (grid == 0).astype(np.uint8) * 255
webp_out = BytesIO()
Image.fromarray(img, "RGBA").save(webp_out, format="webp", lossless=True)
water = webp_out.getvalue()
# Convert back to original scale.
shape_border_points = shape_border_points / upscale
shape_border_points[:, 0] = shape_border_points[:, 0] / dpi + x_min
shape_border_points[:, 1] = shape_border_points[:, 1] / dpi + y_min
shapes[bucket].append(shape_border_points)

return {
"land": land,
"water": water,
}
return shapes
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ implicit
joblib
msgpipe
numpy
opencv-contrib-python-headless
pandas
Pillow
pyarrow
Expand Down

0 comments on commit cb52a5a

Please sign in to comment.