Add reproducible README preview generator
This commit is contained in:
parent
6f566d2731
commit
e8ef6be027
9 changed files with 415 additions and 5 deletions
12
README.md
12
README.md
|
|
@ -10,7 +10,7 @@ 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.
|
||||
|
||||
Top row: glossy non-pixel badge art repixelized at `128x128`. Bottom row: an AI fake-pixel interpretation of the same badge cleaned up with automatic lattice inference, which landed on `122x122` for this image.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
|
@ -118,6 +118,16 @@ Run the focused test suite with:
|
|||
.venv\Scripts\python -m pytest -q
|
||||
```
|
||||
|
||||
## Regenerating README Assets
|
||||
|
||||
The README comparison sheet is 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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Diagnostic Closeups
|
||||
|
||||
For docs or regression notes, you can generate reproducible source/output closeups from output-grid cell coordinates with:
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 693 KiB After Width: | Height: | Size: 382 KiB |
BIN
docs/readme-assets/guard-right-crop-comparison.png
Normal file
BIN
docs/readme-assets/guard-right-crop-comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
386
scripts/generate_readme_previews.py
Normal file
386
scripts/generate_readme_previews.py
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from repixelizer.baselines import lanczos_resize_baseline
|
||||
from repixelizer.io import load_rgba, nearest_resize, save_rgba
|
||||
from repixelizer.pipeline import run_pipeline
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RenderedPanel:
|
||||
image: Image.Image
|
||||
content_box: tuple[int, int, int, int]
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Regenerate the README comparison sheet and guard closeup from repo fixtures.")
|
||||
parser.add_argument(
|
||||
"--vector-input",
|
||||
default="tests/fixtures/real/ai-badge-vector.png",
|
||||
help="High-resolution non-pixel badge source.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ai-input",
|
||||
default="tests/fixtures/real/ai-badge-cleaned.png",
|
||||
help="Cleaned AI fake-pixel badge source.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out-sheet",
|
||||
default="docs/readme-assets/badge-example-sheet.png",
|
||||
help="Output path for the full README comparison sheet.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out-guard-crop",
|
||||
default="docs/readme-assets/guard-right-crop-comparison.png",
|
||||
help="Output path for the standalone sword-guard closeup strip.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scratch-dir",
|
||||
default="artifacts/readme-build",
|
||||
help="Directory for intermediate low-res outputs and diagnostics.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vector-target-size",
|
||||
type=int,
|
||||
default=128,
|
||||
help="Target size for the non-pixel source repixelization run.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--guard-cell-bbox",
|
||||
nargs=4,
|
||||
type=int,
|
||||
default=(73, 20, 107, 44),
|
||||
metavar=("X0", "Y0", "X1", "Y1"),
|
||||
help="Sword-guard crop bounds in output-grid cell coordinates for the AI row.",
|
||||
)
|
||||
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()
|
||||
|
||||
|
||||
def _to_image(rgba: np.ndarray) -> Image.Image:
|
||||
rgba8 = np.clip(np.rint(rgba * 255.0), 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(rgba8, mode="RGBA")
|
||||
|
||||
|
||||
def _checkerboard(width: int, height: int, tile: int = 12) -> Image.Image:
|
||||
image = Image.new("RGBA", (width, height), (236, 236, 236, 255))
|
||||
draw = ImageDraw.Draw(image)
|
||||
accent = (218, 218, 218, 255)
|
||||
for y in range(0, height, tile):
|
||||
for x in range(0, width, tile):
|
||||
if ((x // tile) + (y // tile)) % 2:
|
||||
draw.rectangle((x, y, min(width, x + tile), min(height, y + tile)), fill=accent)
|
||||
return image
|
||||
|
||||
|
||||
def _fit_rgba(
|
||||
rgba: np.ndarray,
|
||||
*,
|
||||
target_width: int,
|
||||
target_height: int,
|
||||
pixelated: bool,
|
||||
) -> tuple[Image.Image, tuple[int, int, int, int]]:
|
||||
image = _to_image(rgba)
|
||||
scale = min(target_width / max(1, image.width), target_height / max(1, image.height))
|
||||
resized_width = max(1, int(round(image.width * scale)))
|
||||
resized_height = max(1, int(round(image.height * scale)))
|
||||
resample = Image.Resampling.NEAREST if pixelated else Image.Resampling.LANCZOS
|
||||
resized = image.resize((resized_width, resized_height), resample=resample)
|
||||
offset_x = (target_width - resized_width) // 2
|
||||
offset_y = (target_height - resized_height) // 2
|
||||
return resized, (offset_x, offset_y, resized_width, resized_height)
|
||||
|
||||
|
||||
def _render_panel(
|
||||
label: str,
|
||||
*,
|
||||
rgba: np.ndarray,
|
||||
pixelated: bool,
|
||||
panel_width: int,
|
||||
panel_height: int,
|
||||
subtitle: str | None = None,
|
||||
) -> RenderedPanel:
|
||||
header_height = 38 if subtitle else 28
|
||||
gutter = 10
|
||||
canvas = Image.new("RGBA", (panel_width, panel_height), (18, 18, 20, 255))
|
||||
checker = _checkerboard(panel_width - gutter * 2, panel_height - header_height - gutter * 2)
|
||||
canvas.alpha_composite(checker, (gutter, header_height + gutter))
|
||||
|
||||
fitted, (inner_x, inner_y, inner_w, inner_h) = _fit_rgba(
|
||||
rgba,
|
||||
target_width=checker.width,
|
||||
target_height=checker.height,
|
||||
pixelated=pixelated,
|
||||
)
|
||||
content_x = gutter + inner_x
|
||||
content_y = header_height + gutter + inner_y
|
||||
canvas.alpha_composite(fitted, (content_x, content_y))
|
||||
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
draw.text((gutter, 6), label, fill=(255, 255, 255, 255))
|
||||
if subtitle:
|
||||
draw.text((gutter, 22), subtitle, fill=(200, 200, 200, 255))
|
||||
draw.rounded_rectangle((0, 0, panel_width - 1, panel_height - 1), radius=10, outline=(72, 72, 80, 255), width=1)
|
||||
return RenderedPanel(canvas, (content_x, content_y, inner_w, inner_h))
|
||||
|
||||
|
||||
def _draw_source_bbox(render: RenderedPanel, source_bbox: tuple[int, int, int, int], *, source_width: int, source_height: int) -> None:
|
||||
x0, y0, x1, y1 = source_bbox
|
||||
inner_x, inner_y, inner_w, inner_h = render.content_box
|
||||
left = inner_x + int(round(inner_w * (x0 / max(1, source_width))))
|
||||
right = inner_x + int(round(inner_w * (x1 / max(1, source_width))))
|
||||
top = inner_y + int(round(inner_h * (y0 / max(1, source_height))))
|
||||
bottom = inner_y + int(round(inner_h * (y1 / max(1, source_height))))
|
||||
draw = ImageDraw.Draw(render.image)
|
||||
for delta, color in ((0, (255, 245, 120, 255)), (1, (20, 20, 20, 255))):
|
||||
draw.rectangle((left - delta, top - delta, right + delta, bottom + delta), outline=color, width=1)
|
||||
|
||||
|
||||
def _build_guard_strip(
|
||||
source_crop: np.ndarray,
|
||||
lanczos_crop: np.ndarray,
|
||||
repixelized_crop: np.ndarray,
|
||||
*,
|
||||
include_lanczos: bool = True,
|
||||
panel_width: int = 220,
|
||||
panel_height: int = 170,
|
||||
title: str = "Sword guard close-up",
|
||||
) -> Image.Image:
|
||||
panels = [
|
||||
_render_panel("Source", rgba=source_crop, pixelated=True, panel_width=panel_width, panel_height=panel_height),
|
||||
_render_panel("Repixelized", rgba=repixelized_crop, pixelated=True, panel_width=panel_width, panel_height=panel_height),
|
||||
]
|
||||
if include_lanczos:
|
||||
panels.insert(
|
||||
1,
|
||||
_render_panel("Lanczos", rgba=lanczos_crop, pixelated=True, panel_width=panel_width, panel_height=panel_height),
|
||||
)
|
||||
title_height = 24
|
||||
gap = 10
|
||||
width = sum(panel.image.width for panel in panels) + gap * (len(panels) + 1)
|
||||
height = title_height + max(panel.image.height for panel in panels) + gap * 2
|
||||
canvas = Image.new("RGBA", (width, height), (14, 14, 16, 242))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
draw.rounded_rectangle((0, 0, width - 1, height - 1), radius=12, fill=(14, 14, 16, 242), outline=(92, 92, 104, 255), width=1)
|
||||
draw.text((gap, 4), title, fill=(255, 255, 255, 255))
|
||||
x = gap
|
||||
for panel in panels:
|
||||
canvas.alpha_composite(panel.image, (x, title_height + gap))
|
||||
x += panel.image.width + gap
|
||||
return canvas
|
||||
|
||||
|
||||
def _cell_bbox_to_source_bbox(
|
||||
cell_bbox: tuple[int, int, int, int],
|
||||
*,
|
||||
source_width: int,
|
||||
source_height: int,
|
||||
target_width: int,
|
||||
target_height: int,
|
||||
) -> tuple[int, int, int, int]:
|
||||
x0, y0, x1, y1 = cell_bbox
|
||||
return (
|
||||
int(np.floor(x0 * source_width / max(1, target_width))),
|
||||
int(np.floor(y0 * source_height / max(1, target_height))),
|
||||
int(np.ceil(x1 * source_width / max(1, target_width))),
|
||||
int(np.ceil(y1 * source_height / max(1, target_height))),
|
||||
)
|
||||
|
||||
|
||||
def _crop_rgba(rgba: np.ndarray, bbox: tuple[int, int, int, int]) -> np.ndarray:
|
||||
x0, y0, x1, y1 = bbox
|
||||
return rgba[y0:y1, x0:x1]
|
||||
|
||||
|
||||
def _save_summary(path: Path, payload: dict[str, object]) -> None:
|
||||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
scratch_dir = Path(args.scratch_dir)
|
||||
scratch_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
vector_input = Path(args.vector_input)
|
||||
ai_input = Path(args.ai_input)
|
||||
out_sheet = Path(args.out_sheet)
|
||||
out_guard_crop = Path(args.out_guard_crop)
|
||||
out_sheet.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_guard_crop.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
vector_result = run_pipeline(
|
||||
vector_input,
|
||||
scratch_dir / "badge-vector-repixelized.png",
|
||||
target_size=args.vector_target_size,
|
||||
diagnostics_dir=scratch_dir / "vector-diag",
|
||||
steps=args.steps,
|
||||
device=args.device,
|
||||
)
|
||||
ai_result = run_pipeline(
|
||||
ai_input,
|
||||
scratch_dir / "badge-ai-repixelized.png",
|
||||
diagnostics_dir=scratch_dir / "ai-diag",
|
||||
steps=args.steps,
|
||||
device=args.device,
|
||||
)
|
||||
|
||||
vector_source = load_rgba(vector_input)
|
||||
ai_source = load_rgba(ai_input)
|
||||
vector_lanczos = lanczos_resize_baseline(vector_source, width=args.vector_target_size, height=args.vector_target_size)
|
||||
ai_lanczos = lanczos_resize_baseline(
|
||||
ai_source,
|
||||
width=ai_result.inference.target_width,
|
||||
height=ai_result.inference.target_height,
|
||||
)
|
||||
|
||||
save_rgba(scratch_dir / "badge-vector-lanczos.png", vector_lanczos)
|
||||
save_rgba(scratch_dir / "badge-ai-lanczos.png", ai_lanczos)
|
||||
|
||||
panel_width = 320
|
||||
panel_height = 360
|
||||
row_gap = 18
|
||||
col_gap = 18
|
||||
margin = 24
|
||||
row_title_height = 22
|
||||
|
||||
vector_panels = [
|
||||
_render_panel(
|
||||
"Source",
|
||||
rgba=vector_source,
|
||||
pixelated=False,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle="Glossy badge art",
|
||||
),
|
||||
_render_panel(
|
||||
"Lanczos",
|
||||
rgba=vector_lanczos,
|
||||
pixelated=True,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=f"{args.vector_target_size}x{args.vector_target_size}",
|
||||
),
|
||||
_render_panel(
|
||||
"Repixelized",
|
||||
rgba=vector_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}",
|
||||
),
|
||||
]
|
||||
ai_panels = [
|
||||
_render_panel(
|
||||
"Source",
|
||||
rgba=ai_source,
|
||||
pixelated=False,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle="Cleaned AI fake pixel art",
|
||||
),
|
||||
_render_panel(
|
||||
"Lanczos",
|
||||
rgba=ai_lanczos,
|
||||
pixelated=True,
|
||||
panel_width=panel_width,
|
||||
panel_height=panel_height,
|
||||
subtitle=f"{ai_result.inference.target_width}x{ai_result.inference.target_height}",
|
||||
),
|
||||
_render_panel(
|
||||
"Repixelized",
|
||||
rgba=ai_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",
|
||||
),
|
||||
]
|
||||
|
||||
guard_bbox = _cell_bbox_to_source_bbox(
|
||||
tuple(args.guard_cell_bbox),
|
||||
source_width=ai_source.shape[1],
|
||||
source_height=ai_source.shape[0],
|
||||
target_width=ai_result.inference.target_width,
|
||||
target_height=ai_result.inference.target_height,
|
||||
)
|
||||
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])
|
||||
for panel in ai_panels:
|
||||
_draw_source_bbox(
|
||||
panel,
|
||||
guard_bbox,
|
||||
source_width=ai_source.shape[1],
|
||||
source_height=ai_source.shape[0],
|
||||
)
|
||||
|
||||
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),
|
||||
)
|
||||
guard_strip.save(out_guard_crop)
|
||||
guard_inset = _build_guard_strip(
|
||||
_crop_rgba(ai_source, guard_bbox),
|
||||
_crop_rgba(ai_lanczos_preview, guard_bbox),
|
||||
_crop_rgba(ai_repixelized_preview, guard_bbox),
|
||||
include_lanczos=False,
|
||||
panel_width=190,
|
||||
panel_height=142,
|
||||
title="Sword guard",
|
||||
)
|
||||
|
||||
width = margin * 2 + panel_width * 3 + col_gap * 2
|
||||
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))
|
||||
|
||||
row1_y = margin + row_title_height
|
||||
row2_y = row1_y + panel_height + row_gap + row_title_height
|
||||
x = margin
|
||||
for panel in vector_panels:
|
||||
canvas.alpha_composite(panel.image, (x, row1_y))
|
||||
x += panel_width + col_gap
|
||||
x = margin
|
||||
for panel in ai_panels:
|
||||
canvas.alpha_composite(panel.image, (x, row2_y))
|
||||
x += panel_width + col_gap
|
||||
|
||||
inset_x = width - guard_inset.width - margin
|
||||
inset_y = row2_y + panel_height - guard_inset.height - 18
|
||||
canvas.alpha_composite(guard_inset, (inset_x, inset_y))
|
||||
|
||||
out_sheet.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(out_sheet)
|
||||
|
||||
_save_summary(
|
||||
scratch_dir / "readme-preview-summary.json",
|
||||
{
|
||||
"vector_input": str(vector_input),
|
||||
"ai_input": str(ai_input),
|
||||
"vector_target_size": args.vector_target_size,
|
||||
"ai_inferred_width": ai_result.inference.target_width,
|
||||
"ai_inferred_height": ai_result.inference.target_height,
|
||||
"guard_cell_bbox": list(args.guard_cell_bbox),
|
||||
"guard_source_bbox": list(guard_bbox),
|
||||
"out_sheet": str(out_sheet),
|
||||
"out_guard_crop": str(out_guard_crop),
|
||||
"scratch_dir": str(scratch_dir),
|
||||
},
|
||||
)
|
||||
|
||||
print(str(out_sheet))
|
||||
print(str(out_guard_crop))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import numpy as np
|
||||
|
||||
from .io import box_resize
|
||||
from .io import box_resize, lanczos_resize
|
||||
from .palette import derive_palette
|
||||
|
||||
|
||||
|
|
@ -10,6 +10,10 @@ def naive_resize_baseline(rgba: np.ndarray, width: int, height: int) -> np.ndarr
|
|||
return box_resize(rgba, width=width, height=height)
|
||||
|
||||
|
||||
def lanczos_resize_baseline(rgba: np.ndarray, width: int, height: int) -> np.ndarray:
|
||||
return lanczos_resize(rgba, width=width, height=height)
|
||||
|
||||
|
||||
def _nearest_palette_color(pixel: np.ndarray, palette: np.ndarray) -> np.ndarray:
|
||||
distances = np.sum((palette - pixel[None, :]) ** 2, axis=1)
|
||||
return palette[int(np.argmin(distances))]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from typing import Any
|
|||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from .baselines import error_diffusion_baseline, naive_resize_baseline
|
||||
from .baselines import error_diffusion_baseline, lanczos_resize_baseline, naive_resize_baseline
|
||||
from .diagnostics import write_compare_csv, write_json
|
||||
from .io import nearest_resize, save_rgba
|
||||
from .metrics import (
|
||||
|
|
@ -30,6 +30,7 @@ def _to_uint8(rgba: np.ndarray) -> np.ndarray:
|
|||
def _contact_sheet(
|
||||
source: np.ndarray,
|
||||
optimized: np.ndarray,
|
||||
lanczos: np.ndarray,
|
||||
naive: np.ndarray,
|
||||
diffusion: np.ndarray,
|
||||
path: str | Path,
|
||||
|
|
@ -38,6 +39,7 @@ def _contact_sheet(
|
|||
panels = [
|
||||
("Source", source),
|
||||
("Optimized", nearest_resize(optimized, width=width, height=height)),
|
||||
("Lanczos", nearest_resize(lanczos, width=width, height=height)),
|
||||
("Naive", nearest_resize(naive, width=width, height=height)),
|
||||
("Diffusion", nearest_resize(diffusion, width=width, height=height)),
|
||||
]
|
||||
|
|
@ -79,6 +81,7 @@ def run_compare(
|
|||
)
|
||||
source = result.source_rgba
|
||||
palette = load_palette(palette_path) if palette_path else None
|
||||
lanczos = lanczos_resize_baseline(source, width=result.inference.target_width, height=result.inference.target_height)
|
||||
naive = naive_resize_baseline(source, width=result.inference.target_width, height=result.inference.target_height)
|
||||
diffusion = error_diffusion_baseline(
|
||||
source,
|
||||
|
|
@ -86,10 +89,11 @@ def run_compare(
|
|||
height=result.inference.target_height,
|
||||
palette=palette,
|
||||
)
|
||||
_contact_sheet(source, result.output_rgba, naive, diffusion, diagnostics_path / "compare-sheet.png")
|
||||
_contact_sheet(source, result.output_rgba, lanczos, naive, diffusion, diagnostics_path / "compare-sheet.png")
|
||||
rows = []
|
||||
for name, image in (
|
||||
("optimized", result.output_rgba),
|
||||
("lanczos", lanczos),
|
||||
("naive", naive),
|
||||
("diffusion", diffusion),
|
||||
):
|
||||
|
|
|
|||
|
|
@ -45,3 +45,9 @@ def bilinear_resize(rgba: np.ndarray, width: int, height: int) -> np.ndarray:
|
|||
image = Image.fromarray(np.clip(np.rint(rgba * 255.0), 0, 255).astype(np.uint8), mode="RGBA")
|
||||
resized = image.resize((width, height), resample=Image.Resampling.BILINEAR)
|
||||
return np.asarray(resized, dtype=np.float32) / 255.0
|
||||
|
||||
|
||||
def lanczos_resize(rgba: np.ndarray, width: int, height: int) -> np.ndarray:
|
||||
image = Image.fromarray(np.clip(np.rint(rgba * 255.0), 0, 255).astype(np.uint8), mode="RGBA")
|
||||
resized = image.resize((width, height), resample=Image.Resampling.LANCZOS)
|
||||
return np.asarray(resized, dtype=np.float32) / 255.0
|
||||
|
|
|
|||
BIN
tests/fixtures/real/ai-badge-vector.png
vendored
Normal file
BIN
tests/fixtures/real/ai-badge-vector.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 880 KiB |
|
|
@ -19,4 +19,4 @@ def test_compare_writes_comparison_artifacts(tmp_path: Path) -> None:
|
|||
assert output_path.exists()
|
||||
assert (diagnostics_dir / "compare-sheet.png").exists()
|
||||
assert (diagnostics_dir / "compare.csv").exists()
|
||||
assert len(payload["rows"]) == 3
|
||||
assert len(payload["rows"]) == 4
|
||||
|
|
|
|||
Loading…
Reference in a new issue