Add dense landscape preview fixture
This commit is contained in:
parent
32f6055ca4
commit
6e81a27b62
8 changed files with 235 additions and 64 deletions
17
README.md
17
README.md
|
|
@ -39,12 +39,13 @@ If you want to support the official hosted deployment and help us scale the poor
|
|||
|
||||
## Example
|
||||
|
||||
This badge sheet is the whole pitch in one image: same ugly fake-pixel source,
|
||||
same target grid, same cheap Lanczos baseline, and one `phase-field` result
|
||||
that finally commits to the lattice.
|
||||
This sheet carries two cases: the ugly fake-pixel badge and a dense
|
||||
higher-resolution landscape. The badge row shows the core salvage story. The
|
||||
landscape row shows a different failure mode: auto lattice inference still
|
||||
undershoots that source badly, so the `phase-field` panel is the tracked fixed
|
||||
`512x512` result next to the same cheap Lanczos baseline.
|
||||
|
||||
Each panel also carries the same sword-guard inset, so the nasty little taper
|
||||
problem has nowhere to hide.
|
||||
Each row carries its own inset, so the tiny details have nowhere to hide.
|
||||
|
||||

|
||||
|
||||
|
|
@ -192,10 +193,12 @@ Run the focused test suite with:
|
|||
## Regenerating README Assets
|
||||
|
||||
The README images are generated from repo-tracked fixtures, not from random
|
||||
artifacts left lying around:
|
||||
artifacts left lying around. The dense landscape row reuses the tracked fixed
|
||||
`512x512` output fixture because auto lattice inference still undershoots that
|
||||
case:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python scripts\generate_readme_previews.py --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
|
||||
.venv\Scripts\python scripts\generate_readme_previews.py --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
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 754 KiB |
|
|
@ -19,6 +19,21 @@ class RenderedPanel:
|
|||
content_box: tuple[int, int, int, int]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreviewRow:
|
||||
row_id: str
|
||||
title: str
|
||||
source_rgba: np.ndarray
|
||||
source_subtitle: str
|
||||
lanczos_rgba: np.ndarray
|
||||
lanczos_subtitle: str
|
||||
phase_field_rgba: np.ndarray
|
||||
phase_field_subtitle: str
|
||||
target_width: int
|
||||
target_height: int
|
||||
focus_cell_bbox: tuple[int, int, int, int]
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Regenerate the README comparison sheet and closeups from repo fixtures.")
|
||||
parser.add_argument(
|
||||
|
|
@ -41,6 +56,16 @@ def _parse_args() -> argparse.Namespace:
|
|||
default="artifacts/readme-build",
|
||||
help="Directory for intermediate low-res outputs and diagnostics.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dense-input",
|
||||
default="tests/fixtures/real/dense-landscape.png",
|
||||
help="Dense high-resolution pixel-art source used for the second README row.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dense-phase-field-output",
|
||||
default="tests/fixtures/real/dense-landscape-fixed-512.png",
|
||||
help="Tracked fixed-lattice phase-field output used for the dense landscape row.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--guard-cell-bbox",
|
||||
nargs=4,
|
||||
|
|
@ -49,6 +74,14 @@ 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(
|
||||
"--dense-focus-cell-bbox",
|
||||
nargs=4,
|
||||
type=int,
|
||||
default=(160, 200, 280, 320),
|
||||
metavar=("X0", "Y0", "X1", "Y1"),
|
||||
help="Focus crop bounds in output-grid cell coordinates for the dense landscape row.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-width",
|
||||
type=int,
|
||||
|
|
@ -73,6 +106,30 @@ def _parse_args() -> argparse.Namespace:
|
|||
default=-0.2,
|
||||
help="Pinned lattice phase Y for the canonical badge comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dense-target-width",
|
||||
type=int,
|
||||
default=512,
|
||||
help="Pinned target width for the dense landscape comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dense-target-height",
|
||||
type=int,
|
||||
default=512,
|
||||
help="Pinned target height for the dense landscape comparison.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dense-auto-inferred-width",
|
||||
type=int,
|
||||
default=251,
|
||||
help="Current automatic lattice width guess for the dense landscape case.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dense-auto-inferred-height",
|
||||
type=int,
|
||||
default=251,
|
||||
help="Current automatic lattice height guess for the dense landscape case.",
|
||||
)
|
||||
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()
|
||||
|
|
@ -260,12 +317,72 @@ def _save_summary(path: Path, payload: dict[str, object]) -> None:
|
|||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _build_row_panels(
|
||||
row: PreviewRow,
|
||||
*,
|
||||
panel_width: int,
|
||||
panel_height: int,
|
||||
inset_width: int = 139,
|
||||
inset_height: int = 103,
|
||||
) -> tuple[list[RenderedPanel], tuple[int, int, int, int]]:
|
||||
panels = [
|
||||
_render_panel(
|
||||
"Source",
|
||||
rgba=row.source_rgba,
|
||||
pixelated=False,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=row.source_subtitle,
|
||||
),
|
||||
_render_panel(
|
||||
"Lanczos",
|
||||
rgba=row.lanczos_rgba,
|
||||
pixelated=True,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=row.lanczos_subtitle,
|
||||
),
|
||||
_render_panel(
|
||||
"Phase-field",
|
||||
rgba=row.phase_field_rgba,
|
||||
pixelated=True,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=row.phase_field_subtitle,
|
||||
),
|
||||
]
|
||||
focus_source_bbox = _cell_bbox_to_source_bbox(
|
||||
row.focus_cell_bbox,
|
||||
source_width=row.source_rgba.shape[1],
|
||||
source_height=row.source_rgba.shape[0],
|
||||
target_width=row.target_width,
|
||||
target_height=row.target_height,
|
||||
)
|
||||
for panel in panels:
|
||||
_draw_source_bbox(
|
||||
panel,
|
||||
focus_source_bbox,
|
||||
source_width=row.source_rgba.shape[1],
|
||||
source_height=row.source_rgba.shape[0],
|
||||
)
|
||||
inset_crops = [
|
||||
_crop_rgba(row.source_rgba, focus_source_bbox),
|
||||
_crop_rgba(row.lanczos_rgba, row.focus_cell_bbox),
|
||||
_crop_rgba(row.phase_field_rgba, row.focus_cell_bbox),
|
||||
]
|
||||
for panel, crop in zip(panels, inset_crops, strict=True):
|
||||
_add_pip_inset(panel, _build_pip_inset(crop, width=inset_width, height=inset_height), margin_x=4, margin_y=4)
|
||||
return panels, focus_source_bbox
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
scratch_dir = Path(args.scratch_dir)
|
||||
scratch_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
source_input = Path(args.input)
|
||||
dense_input = Path(args.dense_input)
|
||||
dense_phase_field_output = Path(args.dense_phase_field_output)
|
||||
out_sheet = Path(args.out_sheet)
|
||||
out_guard_crop = Path(args.out_guard_crop)
|
||||
out_sheet.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -286,41 +403,69 @@ def main() -> None:
|
|||
|
||||
source_rgba = load_rgba(source_input)
|
||||
lanczos = lanczos_resize_baseline(source_rgba, width=args.target_width, height=args.target_height)
|
||||
dense_source_rgba = load_rgba(dense_input)
|
||||
dense_phase_field_rgba = load_rgba(dense_phase_field_output)
|
||||
dense_lanczos = lanczos_resize_baseline(
|
||||
dense_source_rgba,
|
||||
width=args.dense_target_width,
|
||||
height=args.dense_target_height,
|
||||
)
|
||||
|
||||
save_rgba(scratch_dir / "badge-ai-lanczos.png", lanczos)
|
||||
save_rgba(scratch_dir / "dense-landscape-lanczos-512.png", dense_lanczos)
|
||||
|
||||
panel_width = 320
|
||||
panel_height = 360
|
||||
col_gap = 18
|
||||
margin = 24
|
||||
row_title_height = 22
|
||||
row_gap = 20
|
||||
|
||||
panels = [
|
||||
_render_panel(
|
||||
"Source",
|
||||
rgba=source_rgba,
|
||||
pixelated=False,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle="Cleaned AI fake pixel art",
|
||||
badge_focus_cell_bbox = _shrink_bbox_bottom_right(
|
||||
tuple(args.guard_cell_bbox),
|
||||
scale_x=0.5,
|
||||
scale_y=0.5,
|
||||
)
|
||||
rows = [
|
||||
PreviewRow(
|
||||
row_id="badge",
|
||||
title=f"AI fake pixel art -> {args.target_width}x{args.target_height} pinned lattice @ ({args.phase_x:.1f}, {args.phase_y:.1f})",
|
||||
source_rgba=source_rgba,
|
||||
source_subtitle="Cleaned AI fake pixel art",
|
||||
lanczos_rgba=lanczos,
|
||||
lanczos_subtitle=f"{args.target_width}x{args.target_height} baseline",
|
||||
phase_field_rgba=phase_field_result.output_rgba,
|
||||
phase_field_subtitle=f"{args.target_width}x{args.target_height} pinned lattice",
|
||||
target_width=args.target_width,
|
||||
target_height=args.target_height,
|
||||
focus_cell_bbox=badge_focus_cell_bbox,
|
||||
),
|
||||
_render_panel(
|
||||
"Lanczos",
|
||||
rgba=lanczos,
|
||||
pixelated=True,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=f"{args.target_width}x{args.target_height} baseline",
|
||||
),
|
||||
_render_panel(
|
||||
"Phase-field",
|
||||
rgba=phase_field_result.output_rgba,
|
||||
pixelated=True,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=f"{args.target_width}x{args.target_height} pinned lattice",
|
||||
PreviewRow(
|
||||
row_id="dense-landscape",
|
||||
title=(
|
||||
"Dense 1254px pixel art -> auto inference "
|
||||
f"{args.dense_auto_inferred_width}x{args.dense_auto_inferred_height}; "
|
||||
f"fixed {args.dense_target_width}x{args.dense_target_height} shown"
|
||||
),
|
||||
source_rgba=dense_source_rgba,
|
||||
source_subtitle=f"{dense_source_rgba.shape[1]}px source",
|
||||
lanczos_rgba=dense_lanczos,
|
||||
lanczos_subtitle=f"{args.dense_target_width}x{args.dense_target_height} baseline",
|
||||
phase_field_rgba=dense_phase_field_rgba,
|
||||
phase_field_subtitle=f"fixed {args.dense_target_width}x{args.dense_target_height} lattice",
|
||||
target_width=args.dense_target_width,
|
||||
target_height=args.dense_target_height,
|
||||
focus_cell_bbox=tuple(args.dense_focus_cell_bbox),
|
||||
),
|
||||
]
|
||||
built_rows: list[tuple[PreviewRow, list[RenderedPanel], tuple[int, int, int, int]]] = []
|
||||
for row in rows:
|
||||
panels, focus_source_bbox = _build_row_panels(
|
||||
row,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
)
|
||||
built_rows.append((row, panels, focus_source_bbox))
|
||||
|
||||
guard_bbox = _cell_bbox_to_source_bbox(
|
||||
tuple(args.guard_cell_bbox),
|
||||
|
|
@ -329,16 +474,6 @@ def main() -> None:
|
|||
target_width=args.target_width,
|
||||
target_height=args.target_height,
|
||||
)
|
||||
guard_cell_inset_bbox = _shrink_bbox_bottom_right(tuple(args.guard_cell_bbox), scale_x=0.5, scale_y=0.5)
|
||||
guard_inset_bbox = _shrink_bbox_bottom_right(guard_bbox, scale_x=0.5, scale_y=0.5)
|
||||
for panel in panels:
|
||||
_draw_source_bbox(
|
||||
panel,
|
||||
guard_inset_bbox,
|
||||
source_width=source_rgba.shape[1],
|
||||
source_height=source_rgba.shape[0],
|
||||
)
|
||||
|
||||
guard_strip = _build_guard_strip(
|
||||
_crop_rgba(source_rgba, guard_bbox),
|
||||
_crop_rgba(lanczos, tuple(args.guard_cell_bbox)),
|
||||
|
|
@ -346,29 +481,21 @@ def main() -> None:
|
|||
title=None,
|
||||
)
|
||||
guard_strip.save(out_guard_crop)
|
||||
inset_crops = [
|
||||
_crop_rgba(source_rgba, guard_inset_bbox),
|
||||
_crop_rgba(lanczos, guard_cell_inset_bbox),
|
||||
_crop_rgba(phase_field_result.output_rgba, guard_cell_inset_bbox),
|
||||
]
|
||||
for panel, crop in zip(panels, inset_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
|
||||
height = margin * 2 + row_title_height + panel_height
|
||||
row_block_height = row_title_height + panel_height
|
||||
height = margin * 2 + row_block_height * len(built_rows) + row_gap * max(0, len(built_rows) - 1)
|
||||
canvas = Image.new("RGBA", (width, height), (11, 11, 13, 255))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
draw.text(
|
||||
(margin, margin - 2),
|
||||
f"AI fake pixel art -> {args.target_width}x{args.target_height} pinned lattice @ ({args.phase_x:.1f}, {args.phase_y:.1f})",
|
||||
fill=(255, 255, 255, 255),
|
||||
)
|
||||
|
||||
row1_y = margin + row_title_height
|
||||
x = margin
|
||||
for panel in panels:
|
||||
canvas.alpha_composite(panel.image, (x, row1_y))
|
||||
x += panel_width + col_gap
|
||||
row_y = margin
|
||||
for row, panels, _focus_source_bbox in built_rows:
|
||||
draw.text((margin, row_y - 2), row.title, fill=(255, 255, 255, 255))
|
||||
panel_y = row_y + row_title_height
|
||||
x = margin
|
||||
for panel in panels:
|
||||
canvas.alpha_composite(panel.image, (x, panel_y))
|
||||
x += panel_width + col_gap
|
||||
row_y += row_block_height + row_gap
|
||||
|
||||
out_sheet.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(out_sheet)
|
||||
|
|
@ -376,14 +503,22 @@ def main() -> None:
|
|||
_save_summary(
|
||||
scratch_dir / "readme-preview-summary.json",
|
||||
{
|
||||
"input": str(source_input),
|
||||
"rows": [
|
||||
{
|
||||
"row_id": row.row_id,
|
||||
"title": row.title,
|
||||
"target_width": row.target_width,
|
||||
"target_height": row.target_height,
|
||||
"focus_cell_bbox": list(row.focus_cell_bbox),
|
||||
"focus_source_bbox": list(focus_source_bbox),
|
||||
}
|
||||
for row, _panels, focus_source_bbox in built_rows
|
||||
],
|
||||
"badge_input": str(source_input),
|
||||
"dense_input": str(dense_input),
|
||||
"dense_phase_field_output": str(dense_phase_field_output),
|
||||
"guard_cell_bbox": list(args.guard_cell_bbox),
|
||||
"guard_source_bbox": list(guard_bbox),
|
||||
"guard_inset_source_bbox": list(guard_inset_bbox),
|
||||
"target_width": args.target_width,
|
||||
"target_height": args.target_height,
|
||||
"phase_x": args.phase_x,
|
||||
"phase_y": args.phase_y,
|
||||
"out_sheet": str(out_sheet),
|
||||
"out_guard_crop": str(out_guard_crop),
|
||||
"scratch_dir": str(scratch_dir),
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@
|
|||
{"ts": "2026-04-26T03:57:11+00:00", "type": "docs", "status": "ok", "note": "Audit pass clarified note boundaries: the algorithm map remains the live control-flow truth, the implementation plan remains the forward hypothesis ledger, and hosted-auth-architecture is explicitly future design rather than landed auth machinery."}
|
||||
{"ts": "2026-04-26T04:05:06+00:00", "type": "design", "status": "accepted", "note": "Hosted auth was split into a reusable GameCult hosted-access architecture plus a thin Repixelizer-specific access profile. Recommended extraction path is an embedded shared gamecult_access layer first, with dedicated service only after a second app proves the need."}
|
||||
{"ts": "2026-04-26T04:52:53+00:00", "type": "design", "status": "accepted", "note": "Hosted convenience is the monetization path for Repixelizer; keep the code open, let self-hosting remain the escape hatch, and do not prioritize native desktop packaging unless it clearly beats hosted access on revenue, support burden, or strategy."}
|
||||
{"ts": "2026-04-26T12:56:28+00:00", "type": "research", "status": "ok", "note": "Added dense landscape fixture tests/fixtures/real/dense-landscape.png plus tracked fixed-512 output. Current automatic lattice inference on the 1254px source undershoots to 251x251, so README previews now include that case with the fixed 512x512 result beside Lanczos."}
|
||||
|
|
|
|||
BIN
tests/fixtures/real/dense-landscape-fixed-512.png
vendored
Normal file
BIN
tests/fixtures/real/dense-landscape-fixed-512.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 605 KiB |
10
tests/fixtures/real/dense-landscape-focus.json
vendored
Normal file
10
tests/fixtures/real/dense-landscape-focus.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"description": "Focused preview crop for the dense fixed-512 landscape row. Use this when comparing high-density detail retention against Lanczos and when checking the known size-inference miss.",
|
||||
"input": "tests/fixtures/real/dense-landscape.png",
|
||||
"fixed_output": "tests/fixtures/real/dense-landscape-fixed-512.png",
|
||||
"auto_inferred_width": 251,
|
||||
"auto_inferred_height": 251,
|
||||
"target_width": 512,
|
||||
"target_height": 512,
|
||||
"cell_bbox": [160, 200, 280, 320]
|
||||
}
|
||||
22
tests/fixtures/real/dense-landscape.json
vendored
Normal file
22
tests/fixtures/real/dense-landscape.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "dense-landscape",
|
||||
"kind": "real-high-density-pixel-landscape",
|
||||
"description": "Dense high-resolution landscape fixture used for README previews and lattice-size inference regressions.",
|
||||
"source_image": "D:\\Downloads\\ig_00e36939ddfb2c0b0169eb80e9f29481919f784bbedd0dbd45.png",
|
||||
"paired_phase_field_output": "tests/fixtures/real/dense-landscape-fixed-512.png",
|
||||
"expected_target_width": 512,
|
||||
"expected_target_height": 512,
|
||||
"auto_inference_snapshot": {
|
||||
"date": "2026-04-26",
|
||||
"target_width": 251,
|
||||
"target_height": 251,
|
||||
"phase_x": 0.0,
|
||||
"phase_y": -0.2
|
||||
},
|
||||
"preview_focus_cell_bbox": [160, 200, 280, 320],
|
||||
"notes": [
|
||||
"Use this when testing dense higher-resolution pixel art that already has a coherent lattice but still fools the current size inference search.",
|
||||
"The tracked phase-field output was produced from a fixed 512x512 lattice and is kept around so README preview generation does not depend on rerunning that exact pass.",
|
||||
"The current automatic lattice guess on the source fixture undershoots badly at 251x251, which makes this a useful regression case for future inference work."
|
||||
]
|
||||
}
|
||||
BIN
tests/fixtures/real/dense-landscape.png
vendored
Normal file
BIN
tests/fixtures/real/dense-landscape.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Loading…
Reference in a new issue