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()
|