playreel/backend/utils/image.py

55 lines
2.0 KiB
Python

import base64
import io
import numpy as np
from PIL import Image, ImageDraw
Image.MAX_IMAGE_PIXELS = None
LOGO_BAR_SEARCH_FROM = 0.88 # 하단 12%에서만 밝은 띠를 찾는다
LOGO_BAR_BRIGHTNESS = 200
LOGO_BAR_MIN_ROWS = 20
def content_box(frame: Image.Image) -> tuple[int, int, int, int]:
"""레터박스(검정·흰 여백)를 걷어낸 실제 포스터 영역."""
brightness = np.asarray(frame.convert("RGB")).astype(np.float32).mean(2)
live = (brightness > 14) & (brightness < 244)
cols = np.nonzero(live.any(0))[0]
rows = np.nonzero(live.any(1))[0]
return int(cols.min()), int(rows.min()), int(cols.max()) + 1, int(rows.max()) + 1
def bar_box(poster: Image.Image) -> tuple[int, int, int, int] | None:
"""하단 주최·주관·후원 로고 띠(밝은 가로 밴드)의 bbox."""
pixels = np.asarray(poster.convert("RGB")).astype(np.float32)
height, width = pixels.shape[:2]
search_from = int(height * LOGO_BAR_SEARCH_FROM)
rows = np.nonzero(pixels[search_from:].mean(axis=(1, 2)) > LOGO_BAR_BRIGHTNESS)[0]
if len(rows) < LOGO_BAR_MIN_ROWS:
return None
return 0, search_from + rows.min() - 4, width, min(height, search_from + rows.max() + 5)
def logo_bar(poster: Image.Image) -> Image.Image | None:
box = bar_box(poster)
return poster.crop(box) if box else None
def round_mask(size: tuple[int, int], radius_ratio: float = 0.08) -> Image.Image:
"""사각 패치를 둥근 카드 위에 얹을 때 모서리가 튀는 것을 막는 알파 마스크."""
width, height = size
mask = Image.new("L", size, 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, width - 1, height - 1),
radius=max(1, round(min(width, height) * radius_ratio)), fill=255)
return mask
def to_data_uri(image: Image.Image, max_size: tuple[int, int] = (1100, 1600)) -> str:
image = image.copy()
image.thumbnail(max_size, Image.LANCZOS)
buffer = io.BytesIO()
image.save(buffer, "JPEG", quality=92)
return "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()