File size: 7,151 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 171 172 173 174 175 176 177 178 | #!/usr/bin/env python
"""Generate paired before/after images for every augmentation effect, for slides.
For each of the 25 configurable effects, renders two samples that are
identical in every respect (same template, signature, placement, box) except
that one has every augmentation disabled ("before") and the other has just
that one effect forced on at a fixed strength ("after"). Because placement is
frozen and both runs share the same (seed, sample_index), the underlying
document/signature/box are pixel-identical up to the effect itself.
Signature-level effects are saved as a tight crop around the signature box
(where the effect is visible at slide scale); document-level effects are
saved as the full page. Also writes a JSON manifest with the resolved
strength/params/formula description used per effect, for slide captions.
Usage:
python scripts/generate_before_after_assets.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from PIL import Image
from src.config.loader import load_config
from src.generator import GeneratorContext
from src.visualization.visualizer import _crop_region
SIGNATURE_EFFECTS = [
"rotation", "stroke_dilation",
"ink_fading", "ink_bleed", "motion_blur", "gaussian_blur",
]
DOCUMENT_EFFECTS = [
"perspective", "contrast", "gamma", "coffee_stain", "stain",
"shadow", "crumple", "discoloration", "page_tilt", "dust", "scanner_streaks",
"scanner_noise", "salt_pepper", "jpeg_compression",
]
ALL_EFFECTS = SIGNATURE_EFFECTS + DOCUMENT_EFFECTS
OUT_ROOT = Path(__file__).resolve().parents[1] / "degradation"
TEMPLATE_ID = "bank_doc_05"
FORCED_STRENGTH = 1.0 # max strength: matches the "range at strength=1.0" figures already on the formula slides
SAMPLE_INDEX = 2 # picked for a bolder/higher-ink-fill signature than index 1, more visible at slide scale
CROP_PAD = 45
CROP_UPSCALE = 2
# Purely tonal/textural document effects are nearly invisible at full-page slide
# scale, so also cut a fixed, upscaled body-text zoom for these (kept identical
# between before/after so the comparison is apples-to-apples). Structural effects
# (crumple, page_tilt, perspective, shadow, scanner_streaks) already read clearly
# on the full page and don't need one.
ZOOM_EFFECTS = {
"contrast", "gamma", "coffee_stain", "stain",
"discoloration", "dust", "scanner_noise", "salt_pepper", "jpeg_compression",
}
ZOOM_BOX = (40, 110, 460, 340) # (x1, y1, x2, y2) in full-page pixel space
DISPLAY_NAME = {
"rotation": "Rotation jitter",
"stroke_dilation": "Stroke dilation",
"ink_fading": "Ink fading",
"ink_bleed": "Ink bleed",
"motion_blur": "Motion blur",
"gaussian_blur": "Gaussian blur",
"perspective": "Perspective warp",
"contrast": "Contrast shift",
"gamma": "Gamma shift",
"coffee_stain": "Coffee stain",
"stain": "Water / paper stain",
"shadow": "Shadow",
"crumple": "Crumple / wrinkle",
"discoloration": "Discoloration",
"page_tilt": "Page tilt",
"dust": "Dust specks",
"scanner_streaks": "Scanner streaks",
"scanner_noise": "Scanner noise",
"salt_pepper": "Salt & pepper noise",
"jpeg_compression": "JPEG compression",
}
def base_overrides() -> list[str]:
return [
f"document.template={TEMPLATE_ID}",
"placement.rotation_deg.min=0", "placement.rotation_deg.max=0",
"placement.scale_factor.min=0.85", "placement.scale_factor.max=0.85",
"placement.x_offset=0", "placement.y_offset=0",
"placement.misplacement_probability=0.0",
"generation.run_validation=false",
]
def all_disabled_overrides() -> list[str]:
overrides = base_overrides()
for name in ALL_EFFECTS:
overrides.append(f"augmentation.{name}.enabled=false")
overrides.append(f"augmentation.{name}.probability=0.0")
return overrides
def effect_overrides(target_effect: str) -> list[str]:
overrides = base_overrides()
for name in ALL_EFFECTS:
is_target = name == target_effect
overrides.append(f"augmentation.{name}.enabled={'true' if is_target else 'false'}")
overrides.append(f"augmentation.{name}.probability={'1.0' if is_target else '0.0'}")
overrides.append(f"augmentation.{target_effect}.strength={FORCED_STRENGTH}")
return overrides
def main() -> None:
OUT_ROOT.mkdir(parents=True, exist_ok=True)
manifest: dict[str, dict] = {}
baseline_config = load_config(overrides=all_disabled_overrides())
baseline_ctx = GeneratorContext(baseline_config)
baseline_meta, baseline_img, _ = baseline_ctx.run_sample(SAMPLE_INDEX)
box = baseline_meta["geometry"]["target_box"]
for effect in ALL_EFFECTS:
out_dir = OUT_ROOT / effect
out_dir.mkdir(parents=True, exist_ok=True)
config = load_config(overrides=effect_overrides(effect))
ctx = GeneratorContext(config)
meta, after_img, _ = ctx.run_sample(SAMPLE_INDEX)
before_img, after_img_final = baseline_img, after_img
if effect in SIGNATURE_EFFECTS:
before_out = _crop_region(before_img, box, pad=CROP_PAD)
after_out = _crop_region(after_img_final, box, pad=CROP_PAD)
before_pil = Image.fromarray(before_out)
after_pil = Image.fromarray(after_out)
before_pil = before_pil.resize(
(before_pil.width * CROP_UPSCALE, before_pil.height * CROP_UPSCALE), Image.LANCZOS,
)
after_pil = after_pil.resize(
(after_pil.width * CROP_UPSCALE, after_pil.height * CROP_UPSCALE), Image.LANCZOS,
)
else:
before_pil, after_pil = Image.fromarray(before_img), Image.fromarray(after_img_final)
before_pil.save(out_dir / "before.png")
after_pil.save(out_dir / "after.png")
if effect in ZOOM_EFFECTS:
x1, y1, x2, y2 = ZOOM_BOX
zb = Image.fromarray(before_img[y1:y2, x1:x2])
za = Image.fromarray(after_img_final[y1:y2, x1:x2])
zb = zb.resize((zb.width * CROP_UPSCALE, zb.height * CROP_UPSCALE), Image.LANCZOS)
za = za.resize((za.width * CROP_UPSCALE, za.height * CROP_UPSCALE), Image.LANCZOS)
zb.save(out_dir / "before_zoom.png")
za.save(out_dir / "after_zoom.png")
rec = meta["augmentation"]["effects"].get(effect, {})
manifest[effect] = {
"display_name": DISPLAY_NAME[effect],
"category": "signature" if effect in SIGNATURE_EFFECTS else "document",
"strength": rec.get("strength"),
"params": rec.get("params"),
"difficulty_score": meta["difficulty"]["score"],
"difficulty_tier": meta["difficulty"]["tier"],
}
print(f"[{effect}] before/after -> {out_dir} (strength={rec.get('strength')}, params={rec.get('params')})")
manifest_path = OUT_ROOT / "before_after_manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
print(f"\nWrote manifest -> {manifest_path}")
if __name__ == "__main__":
main()
|