Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest und als Kontakt bereitstellt. - Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken, entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt das ganze Foto als eine Karte. - Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht. - Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname über Nominatim, Browserstandort nur als Rückfallebene. - Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske. - Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster Termine nach einem Neustart. - vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert. - Anmeldung über ein Passwort, Sitzung als signiertes Cookie. - Deployment per Tag-Push nach den Konventionen in DEPLOY.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 lines
2.2 KiB
Python
60 lines
2.2 KiB
Python
"""Erzeugt Testfotos: mehrere Visitenkarten auf einem dunklen Untergrund."""
|
|
import io
|
|
import random
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
CARD_RATIO = 85.6 / 54.0
|
|
|
|
|
|
def _card(width: int = 420, seed: int = 0) -> Image.Image:
|
|
"""Eine weisse Karte mit ein paar dunklen Textzeilen."""
|
|
height = round(width / CARD_RATIO)
|
|
card = Image.new("RGB", (width, height), "white")
|
|
draw = ImageDraw.Draw(card)
|
|
rng = random.Random(seed)
|
|
draw.line([(0.08 * width, 0.30 * height), (0.62 * width, 0.30 * height)], fill="black", width=6)
|
|
draw.line([(0.08 * width, 0.44 * height), (0.48 * width, 0.44 * height)], fill=(70, 70, 70), width=3)
|
|
for index in range(3):
|
|
y = (0.62 + index * 0.10) * height
|
|
draw.line(
|
|
[(0.08 * width, y), ((0.30 + rng.random() * 0.35) * width, y)],
|
|
fill=(90, 90, 90),
|
|
width=2,
|
|
)
|
|
return card
|
|
|
|
|
|
def photo_with_cards(count: int = 6, size=(2400, 1800), angle_range: int = 8) -> np.ndarray:
|
|
"""Foto im BGR-Format mit `count` Karten in einem Raster."""
|
|
background = Image.new("RGB", size, (38, 40, 44))
|
|
rng = random.Random(count)
|
|
columns = 2 if count <= 6 else 3
|
|
rows = (count + columns - 1) // columns
|
|
cell_width = size[0] // columns
|
|
cell_height = size[1] // rows
|
|
card_width = int(min(cell_width * 0.72, cell_height * 0.72 * CARD_RATIO))
|
|
|
|
for index in range(count):
|
|
column, row = index % columns, index // columns
|
|
card = _card(card_width, seed=index)
|
|
angle = rng.uniform(-angle_range, angle_range)
|
|
rotated = card.rotate(angle, expand=True, resample=Image.BICUBIC)
|
|
mask = Image.new("L", card.size, 255).rotate(
|
|
angle, expand=True, resample=Image.BICUBIC
|
|
)
|
|
x = column * cell_width + (cell_width - rotated.width) // 2
|
|
y = row * cell_height + (cell_height - rotated.height) // 2
|
|
background.paste(rotated, (x, y), mask)
|
|
|
|
return np.asarray(background)[:, :, ::-1].copy()
|
|
|
|
|
|
def photo_bytes(count: int = 6) -> bytes:
|
|
array = photo_with_cards(count)
|
|
buffer = io.BytesIO()
|
|
Image.fromarray(array[:, :, ::-1]).save(buffer, format="JPEG", quality=92)
|
|
return buffer.getvalue()
|