Document the two repixelization engines
This commit is contained in:
parent
68a45359f9
commit
f95440351c
5 changed files with 241 additions and 8 deletions
25
README.md
25
README.md
|
|
@ -16,6 +16,17 @@ Rows show the source art, a plain Lanczos downscale to the same target size, and
|
|||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## Current Status
|
||||
|
||||
This repo is past the "pile of hopeful heuristics" stage and into "real machine, still experimental."
|
||||
|
|
@ -24,7 +35,7 @@ What exists now:
|
|||
|
||||
- lattice inference with CUDA support
|
||||
- a lean displacement-field optimizer in `src/repixelizer/phase_field.py`
|
||||
- a source-owned alternate solver in `--reconstruction-mode tile-graph`
|
||||
- a source-owned alternate solver in `src/repixelizer/tile_graph.py`
|
||||
- automatic diagnostics, comparisons, and benchmark runs
|
||||
- a tuning harness for offline parameter sweeps
|
||||
- metrics that finally care about visible structure instead of only pleasing the lattice accountant
|
||||
|
|
@ -38,14 +49,14 @@ What changed recently:
|
|||
|
||||
Current read on the engines:
|
||||
|
||||
- `phase-field` is the main line now. It is the best-looking badge result the repo has produced so far, especially on internal structure, even though it still widens the sword-tip stroke a bit too much in the tracked focus crop.
|
||||
- `tile-graph` is still useful and still very real, but it remains slower and more temperamental on messy badge-scale inputs. Its best full-emblem probe under `artifacts/full-emblem-tile-graph-atomic-v3-cuda/` is still a strong result.
|
||||
- `phase-field` is the release path. It currently produces the best-looking badge result in the repo, especially on internal linework, even though it still widens the tracked sword-tip stroke a bit too much.
|
||||
- `tile-graph` is still valuable as the alternate machine. It keeps hard source ownership and often preserves cell identity in a different, sometimes more stubborn way, but it is slower and more temperamental on badge-scale inputs.
|
||||
|
||||
Current weak spots:
|
||||
|
||||
- `phase-field` still needs better along-stroke versus across-stroke behavior near tapered contours
|
||||
- tile-graph cold-build time is still dominated by connected-component labeling / region extraction on large fixtures
|
||||
- lattice selection is still low-confidence on some ugly generated inputs, so pinned-size iteration remains an important workflow
|
||||
- lattice selection is still low-confidence on some ugly generated inputs, so pinned-size iteration remains an important workflow for both engines
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
|
@ -146,13 +157,13 @@ Run the focused test suite with:
|
|||
|
||||
## Regenerating README Assets
|
||||
|
||||
The README comparison sheet is generated from repo-tracked fixtures, not from random artifacts left lying around:
|
||||
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 --scratch-dir artifacts\readme-build --device cpu
|
||||
.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
|
||||
```
|
||||
|
||||
That regenerates the 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.
|
||||
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.
|
||||
|
||||
## Diagnostic Closeups
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 393 KiB |
BIN
docs/readme-assets/engine-comparison-sheet.png
Normal file
BIN
docs/readme-assets/engine-comparison-sheet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -20,7 +20,7 @@ class RenderedPanel:
|
|||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Regenerate the README comparison sheet and guard closeup from repo fixtures.")
|
||||
parser = argparse.ArgumentParser(description="Regenerate the README comparison sheets and closeups from repo fixtures.")
|
||||
parser.add_argument(
|
||||
"--vector-input",
|
||||
default="tests/fixtures/real/ai-badge-vector.png",
|
||||
|
|
@ -41,6 +41,11 @@ def _parse_args() -> argparse.Namespace:
|
|||
default="docs/readme-assets/guard-right-crop-comparison.png",
|
||||
help="Output path for the standalone sword-guard closeup strip.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out-engine-sheet",
|
||||
default="docs/readme-assets/engine-comparison-sheet.png",
|
||||
help="Output path for the pinned phase-field vs tile-graph engine comparison sheet.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scratch-dir",
|
||||
default="artifacts/readme-build",
|
||||
|
|
@ -60,6 +65,38 @@ def _parse_args() -> argparse.Namespace:
|
|||
metavar=("X0", "Y0", "X1", "Y1"),
|
||||
help="Sword-guard crop bounds in output-grid cell coordinates for the AI row.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--engine-target-width",
|
||||
type=int,
|
||||
default=126,
|
||||
help="Pinned target width for the engine-vs-engine badge comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--engine-target-height",
|
||||
type=int,
|
||||
default=126,
|
||||
help="Pinned target height for the engine-vs-engine badge comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--engine-phase-x",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help="Pinned lattice phase X for the engine-vs-engine badge comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--engine-phase-y",
|
||||
type=float,
|
||||
default=-0.2,
|
||||
help="Pinned lattice phase Y for the engine-vs-engine badge comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--engine-cell-bbox",
|
||||
nargs=4,
|
||||
type=int,
|
||||
default=(67, 90, 97, 122),
|
||||
metavar=("X0", "Y0", "X1", "Y1"),
|
||||
help="Tracked sword-tip crop bounds in output-grid cell coordinates for the pinned engine comparison.",
|
||||
)
|
||||
parser.add_argument("--steps", type=int, default=48, help="Optimizer step budget for both runs.")
|
||||
parser.add_argument("--device", default="cpu", choices=("auto", "cpu", "cuda"), help="Torch device for generation.")
|
||||
return parser.parse_args()
|
||||
|
|
@ -247,6 +284,135 @@ def _save_summary(path: Path, payload: dict[str, object]) -> None:
|
|||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _build_engine_comparison_sheet(
|
||||
*,
|
||||
source_rgba: np.ndarray,
|
||||
phase_field_rgba: np.ndarray,
|
||||
tile_graph_rgba: np.ndarray,
|
||||
focus_source_bbox: tuple[int, int, int, int],
|
||||
focus_cell_bbox: tuple[int, int, int, int],
|
||||
target_width: int,
|
||||
target_height: int,
|
||||
phase_x: float,
|
||||
phase_y: float,
|
||||
) -> Image.Image:
|
||||
top_panel_width = 320
|
||||
top_panel_height = 338
|
||||
crop_panel_width = 240
|
||||
crop_panel_height = 210
|
||||
col_gap = 18
|
||||
row_gap = 18
|
||||
margin = 24
|
||||
title_height = 24
|
||||
row_title_height = 20
|
||||
|
||||
top_panels = [
|
||||
_render_panel(
|
||||
"Source",
|
||||
rgba=source_rgba,
|
||||
pixelated=False,
|
||||
panel_width=top_panel_width,
|
||||
panel_height=top_panel_height,
|
||||
subtitle="Cleaned AI fake pixel badge",
|
||||
),
|
||||
_render_panel(
|
||||
"Phase-field",
|
||||
rgba=phase_field_rgba,
|
||||
pixelated=True,
|
||||
panel_width=top_panel_width,
|
||||
panel_height=top_panel_height,
|
||||
subtitle=f"{target_width}x{target_height} pinned lattice",
|
||||
),
|
||||
_render_panel(
|
||||
"Tile-graph",
|
||||
rgba=tile_graph_rgba,
|
||||
pixelated=True,
|
||||
panel_width=top_panel_width,
|
||||
panel_height=top_panel_height,
|
||||
subtitle=f"{target_width}x{target_height} pinned lattice",
|
||||
),
|
||||
]
|
||||
_draw_source_bbox(
|
||||
top_panels[0],
|
||||
focus_source_bbox,
|
||||
source_width=source_rgba.shape[1],
|
||||
source_height=source_rgba.shape[0],
|
||||
)
|
||||
_draw_source_bbox(
|
||||
top_panels[1],
|
||||
focus_cell_bbox,
|
||||
source_width=phase_field_rgba.shape[1],
|
||||
source_height=phase_field_rgba.shape[0],
|
||||
)
|
||||
_draw_source_bbox(
|
||||
top_panels[2],
|
||||
focus_cell_bbox,
|
||||
source_width=tile_graph_rgba.shape[1],
|
||||
source_height=tile_graph_rgba.shape[0],
|
||||
)
|
||||
|
||||
crop_panels = [
|
||||
_render_panel(
|
||||
"Source focus",
|
||||
rgba=_crop_rgba(source_rgba, focus_source_bbox),
|
||||
pixelated=True,
|
||||
panel_width=crop_panel_width,
|
||||
panel_height=crop_panel_height,
|
||||
subtitle="Tracked sword-tip crop",
|
||||
),
|
||||
_render_panel(
|
||||
"Phase-field focus",
|
||||
rgba=_crop_rgba(phase_field_rgba, focus_cell_bbox),
|
||||
pixelated=True,
|
||||
panel_width=crop_panel_width,
|
||||
panel_height=crop_panel_height,
|
||||
subtitle="Default optimizer",
|
||||
),
|
||||
_render_panel(
|
||||
"Tile-graph focus",
|
||||
rgba=_crop_rgba(tile_graph_rgba, focus_cell_bbox),
|
||||
pixelated=True,
|
||||
panel_width=crop_panel_width,
|
||||
panel_height=crop_panel_height,
|
||||
subtitle="Source-owned alternate solver",
|
||||
),
|
||||
]
|
||||
|
||||
width = margin * 2 + top_panel_width * 3 + col_gap * 2
|
||||
height = (
|
||||
margin * 2
|
||||
+ title_height
|
||||
+ row_title_height * 2
|
||||
+ top_panel_height
|
||||
+ crop_panel_height
|
||||
+ row_gap
|
||||
)
|
||||
canvas = Image.new("RGBA", (width, height), (11, 11, 13, 255))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
draw.text(
|
||||
(margin, margin - 2),
|
||||
f"Pinned badge lattice: {target_width}x{target_height} @ ({phase_x:.1f}, {phase_y:.1f})",
|
||||
fill=(255, 255, 255, 255),
|
||||
)
|
||||
draw.text((margin, margin + title_height), "Whole image", fill=(255, 255, 255, 255))
|
||||
top_y = margin + title_height + row_title_height
|
||||
crop_row_y = top_y + top_panel_height + row_gap
|
||||
draw.text((margin, crop_row_y), "Tracked sword-tip crop", fill=(255, 255, 255, 255))
|
||||
crop_y = crop_row_y + row_title_height
|
||||
|
||||
x = margin
|
||||
for panel in top_panels:
|
||||
canvas.alpha_composite(panel.image, (x, top_y))
|
||||
x += top_panel_width + col_gap
|
||||
|
||||
x = margin
|
||||
for panel in crop_panels:
|
||||
canvas.alpha_composite(panel.image, (x, crop_y))
|
||||
x += crop_panel_width + col_gap
|
||||
|
||||
return canvas
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
scratch_dir = Path(args.scratch_dir)
|
||||
|
|
@ -256,8 +422,10 @@ def main() -> None:
|
|||
ai_input = Path(args.ai_input)
|
||||
out_sheet = Path(args.out_sheet)
|
||||
out_guard_crop = Path(args.out_guard_crop)
|
||||
out_engine_sheet = Path(args.out_engine_sheet)
|
||||
out_sheet.parent.mkdir(parents=True, exist_ok=True)
|
||||
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_input,
|
||||
|
|
@ -274,6 +442,32 @@ def main() -> None:
|
|||
steps=args.steps,
|
||||
device=args.device,
|
||||
)
|
||||
phase_field_engine_result = run_pipeline(
|
||||
ai_input,
|
||||
scratch_dir / "badge-ai-phase-field-pinned.png",
|
||||
target_width=args.engine_target_width,
|
||||
target_height=args.engine_target_height,
|
||||
phase_x=args.engine_phase_x,
|
||||
phase_y=args.engine_phase_y,
|
||||
diagnostics_dir=scratch_dir / "engine-phase-field-diag",
|
||||
steps=args.steps,
|
||||
device=args.device,
|
||||
reconstruction_mode="phase-field",
|
||||
enable_phase_rerank=False,
|
||||
)
|
||||
tile_graph_engine_result = run_pipeline(
|
||||
ai_input,
|
||||
scratch_dir / "badge-ai-tile-graph-pinned.png",
|
||||
target_width=args.engine_target_width,
|
||||
target_height=args.engine_target_height,
|
||||
phase_x=args.engine_phase_x,
|
||||
phase_y=args.engine_phase_y,
|
||||
diagnostics_dir=scratch_dir / "engine-tile-graph-diag",
|
||||
steps=args.steps,
|
||||
device=args.device,
|
||||
reconstruction_mode="tile-graph",
|
||||
enable_phase_rerank=False,
|
||||
)
|
||||
|
||||
vector_source = load_rgba(vector_input)
|
||||
ai_source = load_rgba(ai_input)
|
||||
|
|
@ -430,6 +624,26 @@ def main() -> None:
|
|||
out_sheet.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(out_sheet)
|
||||
|
||||
engine_focus_source_bbox = _cell_bbox_to_source_bbox(
|
||||
tuple(args.engine_cell_bbox),
|
||||
source_width=ai_source.shape[1],
|
||||
source_height=ai_source.shape[0],
|
||||
target_width=args.engine_target_width,
|
||||
target_height=args.engine_target_height,
|
||||
)
|
||||
engine_sheet = _build_engine_comparison_sheet(
|
||||
source_rgba=ai_source,
|
||||
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,
|
||||
focus_cell_bbox=tuple(args.engine_cell_bbox),
|
||||
target_width=args.engine_target_width,
|
||||
target_height=args.engine_target_height,
|
||||
phase_x=args.engine_phase_x,
|
||||
phase_y=args.engine_phase_y,
|
||||
)
|
||||
engine_sheet.save(out_engine_sheet)
|
||||
|
||||
_save_summary(
|
||||
scratch_dir / "readme-preview-summary.json",
|
||||
{
|
||||
|
|
@ -443,14 +657,22 @@ def main() -> None:
|
|||
"vector_guard_inset_source_bbox": list(vector_guard_inset_bbox),
|
||||
"guard_source_bbox": list(guard_bbox),
|
||||
"ai_guard_inset_source_bbox": list(ai_guard_inset_bbox),
|
||||
"engine_target_width": args.engine_target_width,
|
||||
"engine_target_height": args.engine_target_height,
|
||||
"engine_phase_x": args.engine_phase_x,
|
||||
"engine_phase_y": args.engine_phase_y,
|
||||
"engine_cell_bbox": list(args.engine_cell_bbox),
|
||||
"engine_focus_source_bbox": list(engine_focus_source_bbox),
|
||||
"out_sheet": str(out_sheet),
|
||||
"out_guard_crop": str(out_guard_crop),
|
||||
"out_engine_sheet": str(out_engine_sheet),
|
||||
"scratch_dir": str(scratch_dir),
|
||||
},
|
||||
)
|
||||
|
||||
print(str(out_sheet))
|
||||
print(str(out_guard_crop))
|
||||
print(str(out_engine_sheet))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in a new issue