Skip to content

Instantly share code, notes, and snippets.

@vincentsarago
Last active July 2, 2024 21:53
Show Gist options
  • Save vincentsarago/0746121e5a0207c10982570e0649a843 to your computer and use it in GitHub Desktop.
Save vincentsarago/0746121e5a0207c10982570e0649a843 to your computer and use it in GitHub Desktop.
"""
You'll need to download https://github.com/RemotePixel/poster-py/tree/master/poster/assets first
"""
import os
from math import ceil
from typing import Callable, Literal, Optional
from dataclasses import dataclass
import numpy
from pydantic import conint
from fastapi import Path, FastAPI, Depends
from titiler.core import factory
from titiler.core.algorithm import BaseAlgorithm
from titiler.core.algorithm import algorithms as default_algorithms
from titiler.core.resources.enums import ImageType
from titiler.core.utils import render_image
from starlette.responses import Response
from typing_extensions import Annotated
from rio_tiler.models import ImageData
import rasterio
from PIL import Image, ImageDraw, ImageFont
class Legofy(BaseAlgorithm):
"""Apply Legofy filter."""
title: str = "legofy"
description: str = "Apply legofy filter"
block_size: Literal[4, 8, 16, 32, 64] = 8
def __call__(self, img: ImageData) -> ImageData:
def dims(total, chop):
for a in range(int(ceil(total / chop))):
offset = a * chop
diff = total - offset
if diff <= chop:
size = diff
else:
size = chop
yield offset, size
def overlay_effect(color, overlay):
"""Actual overlay effect function
"""
if color < 33:
return overlay - 100
elif color > 233:
return overlay + 100
else:
return overlay - 133 + color
def apply_color_overlay(image, color):
"""Small function to apply an effect over an entire image
"""
overlay_red, overlay_green, overlay_blue = color
channels = image.split()
r = channels[0].point(lambda v: overlay_effect(v, overlay_red))
g = channels[1].point(lambda v: overlay_effect(v, overlay_green))
b = channels[2].point(lambda v: overlay_effect(v, overlay_blue))
channels[0].paste(r)
channels[1].paste(g)
channels[2].paste(b)
return Image.merge(image.mode, channels)
brick_path = os.path.join(
os.path.dirname(__file__),
'assets',
f'{self.block_size}x{self.block_size}.png',
)
brick_image = Image.open(brick_path)
src_image = Image.fromarray(img.data.transpose(2, 1, 0))
base_width, base_height = src_image.size
lego_image = Image.new("RGB", (base_width, base_height), "white")
for xoff, xsize in dims(img.width, self.block_size):
for yoff, ysize in dims(img.height, self.block_size):
block = src_image.crop((xoff, yoff, xoff + xsize, yoff + ysize))
img_width, img_height = block.size
color = block.resize((1, 1), Image.LANCZOS).getpixel((0, 0))
if img_width != self.block_size or img_height != self.block_size:
bk = brick_image.crop((0, 0, img_width, img_height))
block = apply_color_overlay(bk, color)
bk = None
else:
block = apply_color_overlay(brick_image, color)
lego_image.paste(block, (xoff, yoff))
output_image = numpy.ma.asarray(lego_image).transpose(2, 1, 0)
output_image.mask = img.array.mask
return ImageData(
output_image,
)
class Asciify(BaseAlgorithm):
"""Apply ASCII Art filter."""
title: str = "asciify"
description: str = "Apply ASCII Art filter"
font_size: int = 10
flatness: float = 1.0
def __call__(self, img: ImageData) -> ImageData:
def get_char_from_brightness(brightness):
ascii_chars = "|%#*+=-:. "
length = len(ascii_chars)
unit = 256 / length
return ascii_chars[int(brightness / unit)]
def image_to_ascii(image, font_size, flatness):
image = image.convert("L")
width, height = image.size
aspect_ratio = height / width
new_height = int(aspect_ratio * font_size * flatness)
image = image.resize((font_size, new_height))
image_array = numpy.array(image)
ascii_image = "\n".join(
"".join(get_char_from_brightness(pixel) for pixel in row) for row in image_array
)
return ascii_image
def ascii_to_image(ascii_art):
font = ImageFont.load_default()
font_width, font_height = font.getbbox("A")[2:]
lines = ascii_art.split("\n")
image_width = max(len(line) for line in lines) * font_width
image_height = len(lines) * font_height
image = Image.new("RGB", (image_width, image_height), "white")
draw = ImageDraw.Draw(image)
y = 0
for line in lines:
draw.text((0, y), line, fill="black", font=font)
y += font_height
image = image.resize([256, 256])
return image
src_image = Image.fromarray(img.data.transpose(2, 1, 0))
font_size = int((1 / self.font_size) * 256)
ascii_art = image_to_ascii(src_image, font_size, self.flatness)
output_image = numpy.ma.asarray(ascii_to_image(ascii_art)).transpose(2, 1, 0)
output_image.mask = img.array.mask
return ImageData(output_image)
algorithms = default_algorithms.register(
{
"legofy": Legofy,
"ascii": Asciify,
}
)
@dataclass
class TilerFactory(factory.TilerFactory):
# Post Processing Dependencies (algorithm)
process_dependency: Callable[
..., Optional[BaseAlgorithm]
] = algorithms.dependency
def tile(self): # noqa: C901
"""Register /tiles endpoint."""
@self.router.get(r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **factory.img_endpoint_params)
@self.router.get(
r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **factory.img_endpoint_params
)
@self.router.get(
r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", **factory.img_endpoint_params
)
@self.router.get(
r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}",
**factory.img_endpoint_params,
)
def tile(
z: Annotated[
int,
Path(
description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.",
),
],
x: Annotated[
int,
Path(
description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.",
),
],
y: Annotated[
int,
Path(
description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.",
),
],
tileMatrixSetId: Annotated[
Literal[tuple(self.supported_tms.list())],
f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')",
] = self.default_tms,
scale: Annotated[
conint(gt=0, le=4), "Tile size scale. 1=256x256, 2=512x512..."
] = 1,
format: Annotated[
ImageType,
"Default will be automatically defined if the output image needs a mask (png) or not (jpeg).",
] = None,
src_path=Depends(self.path_dependency),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
tile_params=Depends(self.tile_dependency),
post_process=Depends(self.process_dependency),
rescale=Depends(self.rescale_dependency),
color_formula=Depends(self.color_formula_dependency),
colormap=Depends(self.colormap_dependency),
render_params=Depends(self.render_dependency),
reader_params=Depends(self.reader_dependency),
env=Depends(self.environment_dependency),
):
"""Create map tile from a dataset."""
tms = self.supported_tms.get(tileMatrixSetId)
with rasterio.Env(**env):
with self.reader(src_path, tms=tms, **reader_params) as src_dst:
image = src_dst.tile(
x,
y,
z,
tilesize=scale * 256,
**tile_params,
**layer_params,
**dataset_params,
)
dst_colormap = getattr(src_dst, "colormap", None)
if rescale:
image.rescale(rescale)
if color_formula:
image.apply_color_formula(color_formula)
if colormap or dst_colormap:
image = image.apply_colormap(colormap or dst_colormap)
if post_process:
image = post_process(image)
content, media_type = render_image(
image,
output_format=format,
**render_params,
)
return Response(content, media_type=media_type)
app = FastAPI()
app.include_router(TilerFactory().router)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment