Files
Webseite-f-r-Amin-B.Ahmadi-…/scripts/grain.py
T
Claude 734c2775b5 Filmkorn auf allen Fotos
- Hero: feine, dezente Körnung (Variante A)
- Team-Porträts, Galerie und Menü-Fotos: Film-Look ISO 800 mit leicht
  farbigem Korn, etwas angehobenem Schwarz und leicht entsättigt (Variante B)
- Unbearbeitete Originale in design/original/, scripts/grain.py wendet den
  Effekt reproduzierbar an; Vorschaubild und README aktualisiert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBHsCnWNXAB2Le3gAEsivF
2026-09-29 15:09:01 +00:00

71 lines
2.5 KiB
Python

"""Filmkorn auf die Website-Fotos rechnen.
Liest die unbearbeiteten Bilder aus design/original/ und schreibt die gekörnten Versionen
nach src/assets/. Hero: Variante A (fein), alle anderen Fotos: Variante B (Film, ISO 800).
pip install numpy pillow
python3 scripts/grain.py
"""
from pathlib import Path
import numpy as np
from PIL import Image, ImageFilter
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / 'design' / 'original'
DST = ROOT / 'src' / 'assets'
VARIANTS = {
# fein & dezent
'A': dict(amount=0.016, size=1.2),
# Film, ISO 800: sichtbares, leicht farbiges Korn, etwas angehobenes Schwarz, leicht entsättigt
'B': dict(amount=0.028, size=1.5, color=0.15, lift=0.03, contrast=1.05, sat=0.92),
}
HERO = {'photos/hero.webp', 'photos/hero-1000.webp', 'photos/hero-mobile.webp'}
LUMA = np.array([0.2126, 0.7152, 0.0722], np.float32)
def noise(rng, h, w, size, color):
"""Gauß-Rauschen, auf Korngröße weichgezeichnet und wieder auf Standardabweichung 1 normiert."""
def one():
n = rng.normal(0, 1, (h, w)).astype(np.float32)
if size > 1:
im = Image.fromarray((n * 40 + 128).clip(0, 255).astype(np.uint8))
n = (np.asarray(im.filter(ImageFilter.GaussianBlur(size * 0.5)), np.float32) - 128) / 40
n /= n.std() + 1e-6
return n
mono = one()
if not color:
return np.stack([mono] * 3, -1)
return np.stack([mono + color * one() for _ in range(3)], -1)
def grain(img, rng, amount, size, color=0.0, lift=0.0, contrast=1.0, sat=1.0):
a = np.asarray(img.convert('RGB'), np.float32) / 255
h, w, _ = a.shape
a = (a - 0.5) * contrast + 0.5
lum = a @ LUMA
a = lum[..., None] + (a - lum[..., None]) * sat
a = lift + a * (1 - lift)
# Korn vor allem in den Mitteltönen, wie bei Film
lum = np.clip(a @ LUMA, 0, 1)
weight = 4 * lum * (1 - lum) + 0.35
a = a + noise(rng, h, w, size, color) * amount * weight[..., None]
return Image.fromarray((np.clip(a, 0, 1) * 255).astype(np.uint8))
def main():
for i, src in enumerate(sorted(SRC.rglob('*.webp'))):
rel = src.relative_to(SRC).as_posix()
variant = 'A' if rel in HERO else 'B'
out = grain(Image.open(src), np.random.default_rng(i + 1), **VARIANTS[variant])
dst = DST / rel
out.save(dst, 'WEBP', quality=82, method=6)
print(f'{variant} {rel:28} {src.stat().st_size // 1024:4} KB -> {dst.stat().st_size // 1024:4} KB')
if __name__ == '__main__':
main()