Last active
July 2, 2024 21:53
-
-
Save vincentsarago/0746121e5a0207c10982570e0649a843 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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