Add AI-style benchmark corruption and tuning weights

This commit is contained in:
Meta Vens 2026-04-22 11:43:22 +01:00
commit 6f566d2731
5 changed files with 258 additions and 4 deletions

View file

@ -132,6 +132,8 @@ def run_roundtrip_benchmark(
"warp_strength": settings["warp_strength"],
"warp_detail": settings["warp_detail"],
"warp_sample_mode": settings["warp_sample_mode"],
"artifact_density": settings["artifact_density"],
"artifact_strength": settings["artifact_strength"],
"primary_metric": "foreground_premultiplied_mae",
"reference_foreground_coverage": foreground_coverage(original, original),
"target_size_locked": not infer_size,
@ -217,6 +219,8 @@ def _variant_settings(seed: int, profile: str) -> dict[str, Any]:
"warp_strength": float(rng.uniform(0.14, 0.46)),
"warp_detail": int(rng.integers(5, 9)),
"warp_sample_mode": "nearest",
"artifact_density": 0.0,
"artifact_strength": 0.0,
}
if profile == "soft":
return {
@ -227,6 +231,20 @@ def _variant_settings(seed: int, profile: str) -> dict[str, Any]:
"warp_strength": float(rng.uniform(0.18, 0.55)),
"warp_detail": int(rng.integers(4, 8)),
"warp_sample_mode": "bilinear",
"artifact_density": 0.0,
"artifact_strength": 0.0,
}
if profile == "ai":
return {
"upscale": int(rng.integers(8, 15)),
"phase_x": float(rng.uniform(-0.48, 0.48)),
"phase_y": float(rng.uniform(-0.48, 0.48)),
"blur_radius": float(rng.uniform(0.18, 0.95)),
"warp_strength": float(rng.uniform(0.24, 0.68)),
"warp_detail": int(rng.integers(5, 10)),
"warp_sample_mode": "bilinear" if rng.random() < 0.7 else "nearest",
"artifact_density": float(rng.uniform(0.08, 0.24)),
"artifact_strength": float(rng.uniform(0.35, 0.95)),
}
raise ValueError(f"Unsupported benchmark profile: {profile}")

View file

@ -46,7 +46,7 @@ def build_parser() -> argparse.ArgumentParser:
benchmark_parser.add_argument(
"--profile",
action="append",
choices=("soft", "crisp"),
choices=("soft", "crisp", "ai"),
default=None,
help="Corruption profile to include; may be repeated. Defaults to both soft and crisp.",
)
@ -78,7 +78,7 @@ def build_parser() -> argparse.ArgumentParser:
tune_parser.add_argument(
"--profile",
action="append",
choices=("soft", "crisp"),
choices=("soft", "crisp", "ai"),
default=None,
help="Corruption profile to include; may be repeated. Defaults to soft only for tuning.",
)

View file

@ -37,6 +37,8 @@ def fake_pixelize(
warp_strength: float = 0.0,
warp_detail: int = 4,
warp_sample_mode: str = "bilinear",
artifact_density: float = 0.0,
artifact_strength: float = 0.0,
seed: int = 0,
) -> np.ndarray:
source = Image.fromarray(np.clip(np.rint(rgba * 255.0), 0, 255).astype(np.uint8), mode="RGBA")
@ -53,7 +55,17 @@ def fake_pixelize(
warp_x, warp_y = _warp_fields(width_px, height_px, upscale=upscale, detail=warp_detail, rng=rng)
sample_x -= warp_x * strength_px
sample_y -= warp_y * strength_px
else:
rng = np.random.default_rng(seed)
warped = _sample_rgba(target_rgba, sample_x, sample_y, mode=warp_sample_mode)
if artifact_density > 0.0 and artifact_strength > 0.0:
warped = _inject_fake_cell_artifacts(
warped,
upscale=upscale,
density=artifact_density,
strength=artifact_strength,
rng=rng,
)
if blur_radius <= 0.0:
return warped
blurred = Image.fromarray(np.clip(np.rint(warped * 255.0), 0, 255).astype(np.uint8), mode="RGBA").filter(
@ -62,6 +74,118 @@ def fake_pixelize(
return np.asarray(blurred, dtype=np.float32) / 255.0
def _inject_fake_cell_artifacts(
rgba: np.ndarray,
*,
upscale: int,
density: float,
strength: float,
rng: np.random.Generator,
) -> np.ndarray:
premult = premultiply(rgba)
out = premult.copy()
height_px, width_px = premult.shape[:2]
cell_w = max(1, int(upscale))
cell_h = max(1, int(upscale))
cells_x = max(1, width_px // cell_w)
cells_y = max(1, height_px // cell_h)
density = float(np.clip(density, 0.0, 1.0))
strength = float(np.clip(strength, 0.0, 1.0))
op_count = max(1, int(round(cells_x * cells_y * density * 1.8)))
max_sliver = max(1, int(round(cell_w * (0.08 + 0.18 * strength))))
max_patch = max(1, int(round(cell_w * (0.10 + 0.22 * strength))))
blend_alpha = 0.55 + 0.35 * strength
def cell_bounds(cx: int, cy: int) -> tuple[int, int, int, int]:
x0 = cx * cell_w
y0 = cy * cell_h
return x0, y0, min(width_px, x0 + cell_w), min(height_px, y0 + cell_h)
def neighbors(cx: int, cy: int) -> list[tuple[int, int]]:
items: list[tuple[int, int]] = []
for oy in (-1, 0, 1):
for ox in (-1, 0, 1):
if ox == 0 and oy == 0:
continue
nx = cx + ox
ny = cy + oy
if 0 <= nx < cells_x and 0 <= ny < cells_y:
items.append((nx, ny))
return items
for _ in range(op_count):
cx = int(rng.integers(0, cells_x))
cy = int(rng.integers(0, cells_y))
x0, y0, x1, y1 = cell_bounds(cx, cy)
if x1 - x0 < 2 or y1 - y0 < 2:
continue
choices = neighbors(cx, cy)
if not choices:
continue
nx, ny = choices[int(rng.integers(0, len(choices)))]
nx0, ny0, nx1, ny1 = cell_bounds(nx, ny)
op = str(rng.choice(["sliver", "ghost", "facet"]))
if op == "sliver":
strip = int(rng.integers(1, max_sliver + 1))
dx = nx - cx
dy = ny - cy
if abs(dx) >= abs(dy):
strip = min(strip, x1 - x0, nx1 - nx0)
if strip < 1:
continue
if dx < 0:
src = premult[ny0:ny1, nx1 - strip : nx1]
out[y0:y1, x0 : x0 + strip] = src[: y1 - y0, :strip]
else:
src = premult[ny0:ny1, nx0 : nx0 + strip]
out[y0:y1, x1 - strip : x1] = src[: y1 - y0, :strip]
else:
strip = min(strip, y1 - y0, ny1 - ny0)
if strip < 1:
continue
if dy < 0:
src = premult[ny1 - strip : ny1, nx0:nx1]
out[y0 : y0 + strip, x0:x1] = src[:strip, : x1 - x0]
else:
src = premult[ny0 : ny0 + strip, nx0:nx1]
out[y1 - strip : y1, x0:x1] = src[:strip, : x1 - x0]
continue
if op == "ghost":
patch_w = min(x1 - x0 - 1, nx1 - nx0, max_patch)
patch_h = min(y1 - y0 - 1, ny1 - ny0, max_patch)
if patch_w < 1 or patch_h < 1:
continue
patch_w = int(rng.integers(1, patch_w + 1))
patch_h = int(rng.integers(1, patch_h + 1))
dst_x = int(rng.integers(x0, x1 - patch_w + 1))
dst_y = int(rng.integers(y0, y1 - patch_h + 1))
src_x = int(rng.integers(nx0, nx1 - patch_w + 1))
src_y = int(rng.integers(ny0, ny1 - patch_h + 1))
patch = premult[src_y : src_y + patch_h, src_x : src_x + patch_w]
existing = out[dst_y : dst_y + patch_h, dst_x : dst_x + patch_w]
out[dst_y : dst_y + patch_h, dst_x : dst_x + patch_w] = existing * (1.0 - blend_alpha) + patch * blend_alpha
continue
yy, xx = np.mgrid[0 : y1 - y0, 0 : x1 - x0]
band = max(1.0, float(max_sliver))
slope = float(rng.uniform(-0.55, 0.55))
bias = float(rng.uniform(-0.2 * cell_w, 1.2 * cell_w))
if rng.random() < 0.5:
seam = xx.astype(np.float32) - (yy.astype(np.float32) * slope + bias)
else:
seam = xx.astype(np.float32) + (yy.astype(np.float32) * slope - bias)
mask = np.abs(seam) <= band
patch = premult[ny0:ny1, nx0:nx1][: y1 - y0, : x1 - x0]
existing = out[y0:y1, x0:x1]
mix = np.where(mask[..., None], patch, existing)
blend = 0.35 * strength
out[y0:y1, x0:x1] = existing * (1.0 - blend) + mix * blend
return unpremultiply(np.clip(out, 0.0, 1.0))
def _warp_fields(width: int, height: int, *, upscale: int, detail: int, rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]:
smooth_tile = max(float(upscale * (detail + 2)), 2.0)
block_tile = max(float(upscale * max(1.5, 7.5 - detail)), 2.0)

View file

@ -23,7 +23,7 @@ def _row_score(row: dict[str, Any]) -> float:
float(row["naive_adjacency_error_to_original"]),
)
motif_ratio = _safe_ratio(float(row["optimized_motif_error_to_original"]), float(row["naive_motif_error_to_original"]))
return error_ratio * 0.50 + adjacency_ratio * 0.25 + motif_ratio * 0.25
return error_ratio * 0.20 + adjacency_ratio * 0.45 + motif_ratio * 0.35
def _score_summary(summary: dict[str, Any]) -> dict[str, float]:
@ -140,10 +140,98 @@ def _mutate_params(base: SolverHyperParams, rng: np.random.Generator, scale: flo
low=0.02,
high=0.18,
),
refine_motif_weight=_mutate_linear(
rng,
base.refine_motif_weight,
scale=scale,
sigma=0.05,
low=0.02,
high=0.40,
),
refine_line_weight=_mutate_linear(
rng,
base.refine_line_weight,
scale=scale,
sigma=0.04,
low=0.02,
high=0.30,
),
relax_anchor_scale=_mutate_linear(
rng,
base.relax_anchor_scale,
scale=scale,
sigma=0.16,
low=0.15,
high=1.0,
),
relax_orthogonal_weight=_mutate_linear(
rng,
base.relax_orthogonal_weight,
scale=scale,
sigma=0.08,
low=0.10,
high=0.70,
),
relax_diagonal_weight=_mutate_linear(
rng,
base.relax_diagonal_weight,
scale=scale,
sigma=0.05,
low=0.02,
high=0.35,
),
relax_source_adjacency_weight=_mutate_linear(
rng,
base.relax_source_adjacency_weight,
scale=scale,
sigma=0.10,
low=0.05,
high=0.80,
),
relax_source_motif_weight=_mutate_linear(
rng,
base.relax_source_motif_weight,
scale=scale,
sigma=0.10,
low=0.05,
high=0.80,
),
relax_source_line_weight=_mutate_linear(
rng,
base.relax_source_line_weight,
scale=scale,
sigma=0.08,
low=0.02,
high=0.45,
),
structure_boundary_weight=float(structure_group[0]),
structure_anchor_adjacency_weight=float(structure_group[1]),
structure_anchor_motif_weight=float(structure_group[2]),
structure_representative_weight=float(structure_group[3]),
structure_source_adjacency_weight=_mutate_linear(
rng,
base.structure_source_adjacency_weight,
scale=scale,
sigma=0.12,
low=0.05,
high=0.90,
),
structure_source_motif_weight=_mutate_linear(
rng,
base.structure_source_motif_weight,
scale=scale,
sigma=0.12,
low=0.05,
high=0.90,
),
structure_source_line_weight=_mutate_linear(
rng,
base.structure_source_line_weight,
scale=scale,
sigma=0.08,
low=0.02,
high=0.50,
),
phase_rerank_margin=_mutate_linear(
rng,
base.phase_rerank_margin,
@ -261,7 +349,7 @@ def tune_solver_hyperparams(
"infer_size": infer_size,
"include_cases": include_cases or [],
"limit_cases_effective": effective_limit,
"objective": "0.5 * error_ratio_to_naive + 0.25 * adjacency_ratio_to_naive + 0.25 * motif_ratio_to_naive; lower is better",
"objective": "0.20 * error_ratio_to_naive + 0.45 * adjacency_ratio_to_naive + 0.35 * motif_ratio_to_naive; lower is better",
"trial_results": trials_payload,
"best_score": final_score["score"],
"best_mean_row_score": final_score["mean_row_score"],

View file

@ -69,6 +69,30 @@ def test_roundtrip_benchmark_can_filter_cases(tmp_path: Path) -> None:
assert payload["rows"][0]["profile"] == "crisp"
def test_roundtrip_benchmark_supports_ai_profile(tmp_path: Path) -> None:
corpus_dir = tmp_path / "corpus"
originals_dir = corpus_dir / "originals"
originals_dir.mkdir(parents=True)
save_rgba(originals_dir / "crest.png", make_emblem(16, 16))
out_dir = tmp_path / "benchmark-out"
payload = run_roundtrip_benchmark(
corpus_dir,
out_dir,
variants=1,
profiles=["ai"],
steps=2,
seed=5,
)
assert payload["profiles"] == ["ai"]
assert payload["row_count"] == 1
row = payload["rows"][0]
assert row["profile"] == "ai"
assert row["artifact_density"] > 0.0
assert row["artifact_strength"] > 0.0
def test_roundtrip_benchmark_clears_existing_output_dir_by_default(tmp_path: Path) -> None:
corpus_dir = tmp_path / "corpus"
originals_dir = corpus_dir / "originals"