113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""⑩ hybrid 설정을 포스터에서 뽑는다.
|
|
|
|
원본은 이 값을 사람이 손으로 재서 configs/nol_<slug>.json에 적었고, 신규 공연은 거기서
|
|
멈췄다. 좌표는 detect가 이미 찾은 영역을 쓰고, 인물 사각만 따로 묻는다.
|
|
|
|
타이밍(punches·sweep·glows)은 이미지에서 잴 수 있는 값이 아니라 기본 템플릿이다.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
from answers.person_box_answer import PersonBoxAnswer
|
|
from models.detect import Box, Regions
|
|
from models.hybrid import LayerConfig, MaskSpec, Rect
|
|
from services.detect import detect
|
|
from settings import settings
|
|
from utils.common_llm import StructuredLLM
|
|
from utils.image import to_data_uri, vlm_grid_overlay
|
|
from utils.prompt import load_prompt
|
|
|
|
# 고정층은 글리프보다 넓게 잡는다. 글자만 덮으면 베벨·그림자가 밀려 겹쳐 보인다.
|
|
FIXED_RECT_PAD = 0.012
|
|
DARK_POSTER_LUMINANCE = 60 # 이보다 어두우면 사각 고정층이 검은 상자가 된다
|
|
|
|
SWEEP_AT = 2.4
|
|
FIRST_PUNCH_AT = 3.1
|
|
PUNCH_GAP = 0.7 # 겹치면 이웃 복원이 안 된다
|
|
DATE_PUNCH_AT = 5.5
|
|
PUNCH_PEAK_MAX = 1.5
|
|
PUNCH_PEAK_MIN = 1.05
|
|
|
|
PUNCH_KINDS = ("title", "datetime")
|
|
PROTECT_KINDS = ("place", "logo")
|
|
|
|
PERSON_BOX_PROMPT = load_prompt("person_box")
|
|
|
|
person_llm = StructuredLLM("gpt-4o", settings.chatgpt_api_key)
|
|
|
|
|
|
def padded_rect(box: Box, pad: float = FIXED_RECT_PAD) -> Rect:
|
|
return (max(0.0, box.x0 - pad), max(0.0, box.y0 - pad),
|
|
min(1.0, box.x1 + pad), min(1.0, box.y1 + pad))
|
|
|
|
|
|
def punch_peak(rect: Rect) -> float:
|
|
"""넓은 블록일수록 덜 키운다 — 키운 크롭이 프레임을 넘으면 잘린다."""
|
|
width = max(rect[2] - rect[0], 1e-3)
|
|
return round(min(PUNCH_PEAK_MAX, max(PUNCH_PEAK_MIN, 1 / width)), 2)
|
|
|
|
|
|
def is_dark_poster(poster: Image.Image) -> bool:
|
|
return float(np.asarray(poster.convert("L")).mean()) < DARK_POSTER_LUMINANCE
|
|
|
|
|
|
async def ask_person_box(poster: Image.Image) -> Rect | None:
|
|
answer = await person_llm.ask_with_images(
|
|
PersonBoxAnswer, PERSON_BOX_PROMPT,
|
|
[("격자를 씌운 포스터:", to_data_uri(vlm_grid_overlay(poster)))])
|
|
if answer.person is None:
|
|
return None
|
|
box = answer.person
|
|
return (box.x0 / 100, box.y0 / 100, box.x1 / 100, box.y1 / 100)
|
|
|
|
|
|
def emphasis(regions: Regions, dark: bool) -> tuple[list, list]:
|
|
"""밝은 포스터는 펀치로, 검정 포스터는 glow로 강조한다.
|
|
|
|
검정 포스터에서 사각을 키워 덮으면 그 사각이 검은 상자로 보인다.
|
|
"""
|
|
punches, glows = [], []
|
|
at = FIRST_PUNCH_AT
|
|
for kind in PUNCH_KINDS:
|
|
box = regions.regions.get(kind)
|
|
if box is None:
|
|
continue
|
|
rect = padded_rect(box)
|
|
when = DATE_PUNCH_AT if kind == "datetime" else at
|
|
if dark:
|
|
glows.append((rect, when))
|
|
else:
|
|
punches.append((rect, when, punch_peak(rect)))
|
|
at += PUNCH_GAP
|
|
return punches, glows
|
|
|
|
|
|
def build_from_regions(regions: Regions, person: Rect | None, dark: bool) -> LayerConfig:
|
|
fixed_rects = [padded_rect(box) for box in regions.regions.values()]
|
|
title = regions.regions.get("title")
|
|
punches, glows = emphasis(regions, dark)
|
|
|
|
return LayerConfig(
|
|
mask=MaskSpec(mode="glyph" if dark else "rect"),
|
|
fixed_rects=fixed_rects,
|
|
sil_box=person or (0.0, 0.0, 0.0, 0.0),
|
|
sil_poly=[], # 다각형은 손으로 그려야 해서 비운다
|
|
glyphs={"title": padded_rect(title)} if title else {},
|
|
sweep_at={"title": SWEEP_AT} if title else {},
|
|
punches=punches,
|
|
glows=glows,
|
|
protect=[padded_rect(regions.regions[kind]) for kind in PROTECT_KINDS
|
|
if kind in regions.regions],
|
|
glyph_dark_ink=not dark,
|
|
)
|
|
|
|
|
|
async def build_layer_config(poster: Path | Image.Image,
|
|
regions: Regions | None = None) -> LayerConfig:
|
|
source = (Image.open(poster) if isinstance(poster, Path) else poster).convert("RGB")
|
|
if regions is None:
|
|
regions = (await detect(source)).data
|
|
return build_from_regions(regions, await ask_person_box(source), is_dark_poster(source))
|