Coverage for src / puzzletree / reconstruct / io.py: 92.11%
56 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 20:35 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 20:35 +0000
1from __future__ import annotations
3from dataclasses import dataclass
4from pathlib import Path
5from typing import List
7import numpy as np
8from PIL import Image
11@dataclass
12class TileWriteResult:
13 output_dir: Path
14 paths: List[Path]
15 rows: int
16 cols: int
17 tile_width: int
18 tile_height: int
21def to_float_array(img: Image.Image) -> np.ndarray:
22 arr = np.asarray(img.convert("RGB"), dtype=np.float32) / 255.0
23 return arr
26def load_tiles_from_dir(input_dir: Path) -> List[np.ndarray]:
27 files = sorted([p for p in input_dir.iterdir() if p.suffix.lower() in {".bmp", ".png", ".jpg", ".jpeg"}])
28 if not files: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 raise FileNotFoundError(f"No image tiles found in {input_dir}")
31 tiles = [to_float_array(Image.open(path)) for path in files]
32 h, w = tiles[0].shape[:2]
33 for i, t in enumerate(tiles):
34 if t.shape[:2] != (h, w): 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 raise ValueError(f"Tile {files[i]} has shape {t.shape[:2]}, expected {(h, w)}")
36 return tiles
39def split_image_into_tiles(img: Image.Image, rows: int, cols: int) -> List[Image.Image]:
40 if rows <= 0 or cols <= 0: 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true
41 raise ValueError("rows and cols must both be positive integers")
43 source = img.convert("RGB")
44 if source.height % rows != 0 or source.width % cols != 0:
45 raise ValueError(
46 f"Image size {(source.width, source.height)} is not evenly divisible by rows={rows}, cols={cols}.",
47 )
49 tile_h = source.height // rows
50 tile_w = source.width // cols
51 tiles: List[Image.Image] = []
52 for row in range(rows):
53 for col in range(cols):
54 box = (col * tile_w, row * tile_h, (col + 1) * tile_w, (row + 1) * tile_h)
55 tiles.append(source.crop(box).copy())
56 return tiles
59def save_tiles_from_image(
60 input_image: Path,
61 output_dir: Path,
62 rows: int,
63 cols: int,
64 prefix: str = "tile",
65 overwrite: bool = False,
66) -> TileWriteResult:
67 existing_paths = sorted(output_dir.glob(f"{prefix}_*.png")) if output_dir.exists() else []
68 if existing_paths and not overwrite:
69 raise FileExistsError(
70 f"Output directory {output_dir} already contains {prefix}_*.png files. Use --overwrite to replace them.",
71 )
73 output_dir.mkdir(parents=True, exist_ok=True)
74 for path in existing_paths:
75 path.unlink()
77 with Image.open(input_image) as img:
78 tiles = split_image_into_tiles(img, rows=rows, cols=cols)
80 written_paths: List[Path] = []
81 for index, tile in enumerate(tiles):
82 path = output_dir / f"{prefix}_{index:03d}.png"
83 tile.save(path)
84 written_paths.append(path)
86 return TileWriteResult(
87 output_dir=output_dir,
88 paths=written_paths,
89 rows=rows,
90 cols=cols,
91 tile_width=tiles[0].width,
92 tile_height=tiles[0].height,
93 )