Fold engine comparison into README examples

This commit is contained in:
Meta Vens 2026-04-24 01:32:43 +01:00
commit af4756a196
5 changed files with 102 additions and 41 deletions

View file

@ -10,22 +10,11 @@ Instead of pretending this is a resize problem, Repixelizer treats it as lattice
Repixelizer was built to rescue fake pixel art, but it can also be used to generate pixel art directly from non-pixel source art when the shapes are clean and the local structure is doing something useful.
Rows show the source art, a plain Lanczos downscale to the same target size, and the repixelized result. The bottom row also carries a sword-guard picture-in-picture, because that tiny region is one of the more honest tests of whether the tool is preserving local cell adjacency instead of just getting the vibes approximately correct.
Rows show the source art, a plain Lanczos downscale, the default `phase-field` solver, and the `tile-graph` alternate solver. The bottom row also carries the tracked sword-tip closeup, because that tiny patch is one of the more honest tests of whether the machine is preserving structure instead of just faking confidence.
![Repixelizer example comparison](docs/readme-assets/badge-example-sheet.png)
That is the two-headed pitch in one image: force non-pixel art onto a coherent grid, or take AI pixel art that only respects the grid locally and make it commit.
## Two Engines
There are two lean mean repixelating machines in here now.
- `phase-field` is the default path. It lays down one displacement vector per output cell and nudges that field until the cells settle into quieter source paint without collapsing into each other. It is the best-looking solver in the repo right now, especially on internal linework and overall structure.
- `tile-graph` is the source-owned alternate path. It extracts candidate tiles from source regions and solves a local adjacency problem instead of optimizing one displacement field. It is slower, moodier, and useful precisely because it fails differently.
Here is the pinned badge comparison on the same lattice, so the machines have to fight fair instead of hiding behind different size guesses:
![Phase-field vs tile-graph comparison](docs/readme-assets/engine-comparison-sheet.png)
That is the pitch in one image: two different machines, same mess, same ruler, nowhere to hide.
## Current Status
@ -160,10 +149,10 @@ Run the focused test suite with:
The README images are generated from repo-tracked fixtures, not from random artifacts left lying around:
```powershell
.venv\Scripts\python scripts\generate_readme_previews.py --vector-input tests\fixtures\real\ai-badge-vector.png --ai-input tests\fixtures\real\ai-badge-cleaned.png --out-sheet docs\readme-assets\badge-example-sheet.png --out-guard-crop docs\readme-assets\guard-right-crop-comparison.png --out-engine-sheet docs\readme-assets\engine-comparison-sheet.png --scratch-dir artifacts\readme-build --device auto
.venv\Scripts\python scripts\generate_readme_previews.py --vector-input tests\fixtures\real\ai-badge-vector.png --ai-input tests\fixtures\real\ai-badge-cleaned.png --out-sheet docs\readme-assets\badge-example-sheet.png --out-guard-crop docs\readme-assets\guard-right-crop-comparison.png --scratch-dir artifacts\readme-build --device auto
```
That regenerates the main README sheet, the standalone sword-guard closeup strip, the pinned phase-field-vs-tile-graph comparison sheet, and scratch outputs under `artifacts/readme-build/` so you can inspect the actual low-res results used to build the docs sample.
That regenerates the main README sheet, the standalone sword-guard closeup strip, and scratch outputs under `artifacts/readme-build/` so you can inspect the actual low-res results used to build the docs sample.
## Diagnostic Closeups

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

@ -287,6 +287,7 @@ def _save_summary(path: Path, payload: dict[str, object]) -> None:
def _build_engine_comparison_sheet(
*,
source_rgba: np.ndarray,
lanczos_rgba: np.ndarray,
phase_field_rgba: np.ndarray,
tile_graph_rgba: np.ndarray,
focus_source_bbox: tuple[int, int, int, int],
@ -315,6 +316,14 @@ def _build_engine_comparison_sheet(
panel_height=top_panel_height,
subtitle="Cleaned AI fake pixel badge",
),
_render_panel(
"Lanczos",
rgba=lanczos_rgba,
pixelated=True,
panel_width=top_panel_width,
panel_height=top_panel_height,
subtitle=f"{target_width}x{target_height} naive baseline",
),
_render_panel(
"Phase-field",
rgba=phase_field_rgba,
@ -341,11 +350,17 @@ def _build_engine_comparison_sheet(
_draw_source_bbox(
top_panels[1],
focus_cell_bbox,
source_width=lanczos_rgba.shape[1],
source_height=lanczos_rgba.shape[0],
)
_draw_source_bbox(
top_panels[2],
focus_cell_bbox,
source_width=phase_field_rgba.shape[1],
source_height=phase_field_rgba.shape[0],
)
_draw_source_bbox(
top_panels[2],
top_panels[3],
focus_cell_bbox,
source_width=tile_graph_rgba.shape[1],
source_height=tile_graph_rgba.shape[0],
@ -360,6 +375,14 @@ def _build_engine_comparison_sheet(
panel_height=crop_panel_height,
subtitle="Tracked sword-tip crop",
),
_render_panel(
"Lanczos focus",
rgba=_crop_rgba(lanczos_rgba, focus_cell_bbox),
pixelated=True,
panel_width=crop_panel_width,
panel_height=crop_panel_height,
subtitle="Naive resize",
),
_render_panel(
"Phase-field focus",
rgba=_crop_rgba(phase_field_rgba, focus_cell_bbox),
@ -378,7 +401,7 @@ def _build_engine_comparison_sheet(
),
]
width = margin * 2 + top_panel_width * 3 + col_gap * 2
width = margin * 2 + top_panel_width * 4 + col_gap * 3
height = (
margin * 2
+ title_height
@ -427,13 +450,31 @@ def main() -> None:
out_guard_crop.parent.mkdir(parents=True, exist_ok=True)
out_engine_sheet.parent.mkdir(parents=True, exist_ok=True)
vector_result = run_pipeline(
vector_phase_field_result = run_pipeline(
vector_input,
scratch_dir / "badge-vector-repixelized.png",
target_size=args.vector_target_size,
diagnostics_dir=scratch_dir / "vector-diag",
scratch_dir / "badge-vector-phase-field.png",
target_width=args.vector_target_size,
target_height=args.vector_target_size,
phase_x=0.0,
phase_y=0.0,
diagnostics_dir=scratch_dir / "vector-phase-field-diag",
steps=args.steps,
device=args.device,
reconstruction_mode="phase-field",
enable_phase_rerank=False,
)
vector_tile_graph_result = run_pipeline(
vector_input,
scratch_dir / "badge-vector-tile-graph.png",
target_width=args.vector_target_size,
target_height=args.vector_target_size,
phase_x=0.0,
phase_y=0.0,
diagnostics_dir=scratch_dir / "vector-tile-graph-diag",
steps=args.steps,
device=args.device,
reconstruction_mode="tile-graph",
enable_phase_rerank=False,
)
ai_result = run_pipeline(
ai_input,
@ -503,15 +544,23 @@ def main() -> None:
pixelated=True,
panel_width=panel_width,
panel_height=panel_height,
subtitle=f"{args.vector_target_size}x{args.vector_target_size}",
subtitle=f"{args.vector_target_size}x{args.vector_target_size} naive baseline",
),
_render_panel(
"Repixelized",
rgba=vector_result.output_rgba,
"Phase-field",
rgba=vector_phase_field_result.output_rgba,
pixelated=True,
panel_width=panel_width,
panel_height=panel_height,
subtitle=f"{vector_result.inference.target_width}x{vector_result.inference.target_height}",
subtitle=f"{args.vector_target_size}x{args.vector_target_size} pinned lattice",
),
_render_panel(
"Tile-graph",
rgba=vector_tile_graph_result.output_rgba,
pixelated=True,
panel_width=panel_width,
panel_height=panel_height,
subtitle=f"{args.vector_target_size}x{args.vector_target_size} pinned lattice",
),
]
ai_panels = [
@ -529,15 +578,23 @@ def main() -> None:
pixelated=True,
panel_width=panel_width,
panel_height=panel_height,
subtitle=f"{ai_result.inference.target_width}x{ai_result.inference.target_height}",
subtitle=f"{args.engine_target_width}x{args.engine_target_height} naive baseline",
),
_render_panel(
"Repixelized",
rgba=ai_result.output_rgba,
"Phase-field",
rgba=phase_field_engine_result.output_rgba,
pixelated=True,
panel_width=panel_width,
panel_height=panel_height,
subtitle=f"{ai_result.inference.target_width}x{ai_result.inference.target_height} auto",
subtitle=f"{args.engine_target_width}x{args.engine_target_height} pinned lattice",
),
_render_panel(
"Tile-graph",
rgba=tile_graph_engine_result.output_rgba,
pixelated=True,
panel_width=panel_width,
panel_height=panel_height,
subtitle=f"{args.engine_target_width}x{args.engine_target_height} pinned lattice",
),
]
@ -557,8 +614,13 @@ def main() -> None:
)
vector_guard_inset_bbox = _shrink_bbox_bottom_right(vector_guard_bbox, scale_x=0.5, scale_y=0.5)
vector_lanczos_preview = nearest_resize(vector_lanczos, width=vector_source.shape[1], height=vector_source.shape[0])
vector_repixelized_preview = nearest_resize(
vector_result.output_rgba,
vector_phase_field_preview = nearest_resize(
vector_phase_field_result.output_rgba,
width=vector_source.shape[1],
height=vector_source.shape[0],
)
vector_tile_graph_preview = nearest_resize(
vector_tile_graph_result.output_rgba,
width=vector_source.shape[1],
height=vector_source.shape[0],
)
@ -572,13 +634,17 @@ def main() -> None:
vector_crops = [
_crop_rgba(vector_source, vector_guard_inset_bbox),
_crop_rgba(vector_lanczos_preview, vector_guard_inset_bbox),
_crop_rgba(vector_repixelized_preview, vector_guard_inset_bbox),
_crop_rgba(vector_phase_field_preview, vector_guard_inset_bbox),
_crop_rgba(vector_tile_graph_preview, vector_guard_inset_bbox),
]
for panel, crop in zip(vector_panels, vector_crops, strict=True):
_add_pip_inset(panel, _build_pip_inset(crop, width=139, height=103), margin_x=4, margin_y=4)
ai_lanczos_preview = nearest_resize(ai_lanczos, width=ai_source.shape[1], height=ai_source.shape[0])
ai_repixelized_preview = nearest_resize(ai_result.output_rgba, width=ai_source.shape[1], height=ai_source.shape[0])
ai_lanczos_preview = nearest_resize(
ai_lanczos,
width=phase_field_engine_result.output_rgba.shape[1],
height=phase_field_engine_result.output_rgba.shape[0],
)
ai_guard_inset_bbox = _shrink_bbox_bottom_right(guard_bbox, scale_x=0.5, scale_y=0.5)
for panel in ai_panels:
_draw_source_bbox(
@ -590,25 +656,30 @@ def main() -> None:
guard_strip = _build_guard_strip(
_crop_rgba(ai_source, guard_bbox),
_crop_rgba(ai_lanczos_preview, guard_bbox),
_crop_rgba(ai_repixelized_preview, guard_bbox),
_crop_rgba(ai_lanczos, tuple(args.engine_cell_bbox)),
_crop_rgba(phase_field_engine_result.output_rgba, tuple(args.engine_cell_bbox)),
title=None,
)
guard_strip.save(out_guard_crop)
ai_crops = [
_crop_rgba(ai_source, ai_guard_inset_bbox),
_crop_rgba(ai_lanczos_preview, ai_guard_inset_bbox),
_crop_rgba(ai_repixelized_preview, ai_guard_inset_bbox),
_crop_rgba(ai_lanczos, tuple(_shrink_bbox_bottom_right(tuple(args.engine_cell_bbox), scale_x=0.5, scale_y=0.5))),
_crop_rgba(phase_field_engine_result.output_rgba, tuple(_shrink_bbox_bottom_right(tuple(args.engine_cell_bbox), scale_x=0.5, scale_y=0.5))),
_crop_rgba(tile_graph_engine_result.output_rgba, tuple(_shrink_bbox_bottom_right(tuple(args.engine_cell_bbox), scale_x=0.5, scale_y=0.5))),
]
for panel, crop in zip(ai_panels, ai_crops, strict=True):
_add_pip_inset(panel, _build_pip_inset(crop, width=139, height=103), margin_x=4, margin_y=4)
width = margin * 2 + panel_width * 3 + col_gap * 2
width = margin * 2 + panel_width * 4 + col_gap * 3
height = margin * 2 + row_title_height * 2 + panel_height * 2 + row_gap
canvas = Image.new("RGBA", (width, height), (11, 11, 13, 255))
draw = ImageDraw.Draw(canvas)
draw.text((margin, margin - 2), "Non-pixel source -> 128x128", fill=(255, 255, 255, 255))
draw.text((margin, margin + row_title_height + panel_height + row_gap - 2), "AI fake pixel art -> auto lattice", fill=(255, 255, 255, 255))
draw.text((margin, margin - 2), "Non-pixel source -> 128x128 pinned lattice", fill=(255, 255, 255, 255))
draw.text(
(margin, margin + row_title_height + panel_height + row_gap - 2),
f"AI fake pixel art -> {args.engine_target_width}x{args.engine_target_height} pinned lattice @ ({args.engine_phase_x:.1f}, {args.engine_phase_y:.1f})",
fill=(255, 255, 255, 255),
)
row1_y = margin + row_title_height
row2_y = row1_y + panel_height + row_gap + row_title_height
@ -633,6 +704,7 @@ def main() -> None:
)
engine_sheet = _build_engine_comparison_sheet(
source_rgba=ai_source,
lanczos_rgba=ai_lanczos,
phase_field_rgba=phase_field_engine_result.output_rgba,
tile_graph_rgba=tile_graph_engine_result.output_rgba,
focus_source_bbox=engine_focus_source_bbox,