File size: 7,037 Bytes
1a12965
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python
"""Generate WORST-CASE samples: many degradations stacked at high strength on one template.

Like scripts/generate_multiple_degradation_gallery.py, but every augmentation
family's probability and strength range are pushed high (instead of using
configs/generate.yaml's mild "realistic batch" defaults), and placement/
rendering are also pushed toward their hardest settings (heavy misplacement,
small/faint signature). Most effects fire on most samples, so each sample
ends up with a large, clearly-visible stack of simultaneous degradations.

The caption band at the top of each image lists not just which effects
fired but their resolved *extent*: strength, plus any effect-specific
parameter (blob count, shadow type, JPEG quality, wrinkle count/amplitude,
etc.) alongside the resolved placement/rendering severity for that sample.

Usage:
    python scripts/generate_multiple_degradation_extreme_gallery.py
"""

from __future__ import annotations

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from PIL import Image, ImageDraw, ImageFont

from src.config.loader import load_config
from src.generator import GeneratorContext
from src.visualization.visualizer import save_visualization
from scripts.generate_degradation_gallery import ALL_EFFECTS

ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "multiple_degradation_extreme"
N_SAMPLES = 10
TEMPLATE_ID = "bank_doc_05"

EFFECT_PROBABILITY = 0.85   # near-certain per effect, but still leaves sample-to-sample variety
STRENGTH_MIN, STRENGTH_MAX = 0.7, 1.0


def _load_font(size: int):
    for candidate in (r"C:\Windows\Fonts\consola.ttf", r"C:\Windows\Fonts\arial.ttf"):
        if Path(candidate).exists():
            return ImageFont.truetype(candidate, size)
    return ImageFont.load_default()


def build_overrides() -> list[str]:
    overrides = [
        f"document.template={TEMPLATE_ID}",
        "generation.run_validation=false",
        # Worst-case placement: large rotation, small scale, frequent/strong misplacement.
        "placement.rotation_deg.min=-15", "placement.rotation_deg.max=15",
        "placement.scale_factor.min=0.3", "placement.scale_factor.max=0.6",
        "placement.margin=0.03",
        "placement.misplacement_probability=0.5",
        "placement.misplacement_strength.min=0.5", "placement.misplacement_strength.max=1.0",
        "placement.allow_partial_outside=true",
        # Worst-case rendering: faint, blurred, heavily feathered ink.
        "rendering.ink_opacity.min=0.3", "rendering.ink_opacity.max=0.55",
        "rendering.blur_sigma.min=0.6", "rendering.blur_sigma.max=1.4",
        "rendering.feather_px.min=1.5", "rendering.feather_px.max=3.0",
        "rendering.scanner_noise=0.05",
    ]
    for name in ALL_EFFECTS:
        overrides.append(f"augmentation.{name}.enabled=true")
        overrides.append(f"augmentation.{name}.probability={EFFECT_PROBABILITY}")
        if name == "jpeg_compression":
            overrides += [
                "augmentation.jpeg_compression.strength=0.9",
                "augmentation.jpeg_compression.min_quality=10",
                "augmentation.jpeg_compression.max_quality=30",
            ]
        else:
            overrides.append(f"augmentation.{name}.strength.min={STRENGTH_MIN}")
            overrides.append(f"augmentation.{name}.strength.max={STRENGTH_MAX}")
    return overrides


def _fmt_effect(name: str, rec: dict) -> str:
    params = rec.get("params") or {}
    if name == "jpeg_compression" and "quality" in params:
        return f"{name}=q{params['quality']}"
    tag = f"{name}={rec['strength']:.2f}"
    if "resolved_type" in params:
        tag += f"({params['resolved_type']})"
    elif "resolved_tone" in params:
        tag += f"({params['resolved_tone']})"
    elif name == "crumple" and "wrinkle_lines" in params:
        tag += f"({params['wrinkle_lines']}L,{params['displacement_amplitude_px']:.1f}px)"
    elif name == "coffee_stain" and "blob_count" in params:
        tag += f"({params['blob_count']}blobs)"
    elif name == "page_tilt" and "tilt_deg" in params:
        tag += f"({params['tilt_deg']:.1f}deg)"
    elif name == "dust" and "speck_count" in params:
        tag += f"({params['speck_count']}px)"
    elif name == "scanner_streaks" and "streak_count" in params:
        tag += f"({params['streak_count']}streaks)"
    return tag


def _annotate(path: Path, metadata: dict) -> list[str]:
    effects = metadata["augmentation"]["effects"]
    applied_names = [n for n, r in effects.items() if r["applied"]]
    applied_tags = [_fmt_effect(n, effects[n]) for n in applied_names]

    placement, rendering, difficulty = metadata["placement"], metadata["rendering"], metadata["difficulty"]
    header = (
        f"{metadata['document_id']}  |  difficulty={difficulty['score']:.3f} ({difficulty['tier']})  |  "
        f"placement: rot={placement['rotation_deg']:.1f}deg scale={placement['scale_factor']:.2f} "
        f"misplaced={placement['misplaced']}  |  "
        f"render: ink_opacity={rendering['ink_opacity']:.2f} blur_sigma={rendering['blur_sigma']:.2f}"
    )
    body = f"Applied ({len(applied_tags)}/{len(effects)}): " + (", ".join(applied_tags) if applied_tags else "none")

    img = Image.open(path).convert("RGB")
    font = _load_font(15)
    draw_probe = ImageDraw.Draw(img)
    max_w = img.width - 28

    # Pixel-accurate wrap of the (potentially long) applied-effects line.
    words = body.replace(", ", " ,SPLIT_ME ").split(" ,SPLIT_ME ")
    lines, current = [], ""
    for tok in words:
        candidate = (current + ", " + tok) if current else tok
        if draw_probe.textlength(candidate, font=font) <= max_w:
            current = candidate
        else:
            if current:
                lines.append(current)
            current = tok
    if current:
        lines.append(current)

    all_lines = [header] + lines
    line_h = 22
    band_h = 14 + line_h * len(all_lines)

    canvas = Image.new("RGB", (img.width, img.height + band_h), "white")
    canvas.paste(img, (0, band_h))
    draw = ImageDraw.Draw(canvas)
    draw.rectangle([0, 0, canvas.width, band_h], fill=(60, 18, 18))
    y = 8
    for i, line in enumerate(all_lines):
        draw.text((14, y), line, fill="white" if i == 0 else (255, 214, 194), font=font)
        y += line_h
    canvas.save(path)
    return applied_names


def main() -> None:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    config = load_config(overrides=build_overrides())
    ctx = GeneratorContext(config)

    for i in range(1, N_SAMPLES + 1):
        metadata, final_image, _ = ctx.run_sample(i)
        out_path = OUT_DIR / f"sample_{i}.png"
        save_visualization(out_path, final_image, metadata)
        applied = _annotate(out_path, metadata)
        print(f"[sample_{i}] {len(applied)}/{len(metadata['augmentation']['effects'])} effects -> {', '.join(applied)}")

    print(f"\nWrote {N_SAMPLES} extreme samples -> {OUT_DIR}")


if __name__ == "__main__":
    main()