Add AI-style benchmark corruption and tuning weights
This commit is contained in:
parent
f5ffab61a0
commit
6f566d2731
5 changed files with 258 additions and 4 deletions
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue