Coverage for src / puzzletree / reconstruct / pipeline.py: 100.00%

65 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-12 20:35 +0000

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from pathlib import Path 

5from typing import Callable, List, cast 

6 

7import numpy as np 

8from PIL import Image 

9 

10from puzzletree.reconstruct.core import AdjList, Coord, build_weight_matrices, msgt, reconstruct_layout 

11from puzzletree.reconstruct.io import load_tiles_from_dir 

12from puzzletree.reconstruct.render import render_reconstruction, save_tree_build_animation 

13 

14ProgressCallback = Callable[[str], None] 

15 

16 

17@dataclass 

18class ReconstructionRun: 

19 adjs: AdjList 

20 placements: dict[int, Coord] 

21 output: Image.Image 

22 

23 

24@dataclass 

25class ReconstructionRunWithHistory(ReconstructionRun): 

26 history: List[AdjList] 

27 

28 

29@dataclass 

30class ReconstructOptions: 

31 input_dir: Path 

32 output: Path = Path("reconstructed.png") 

33 r: float = 12.0 

34 minset: float = 0.1 

35 animation: Path | None = None 

36 animation_seed: int = 0 

37 animation_size: int = 1024 

38 animation_max_angle: float = 35.0 

39 animation_duration_ms: int = 1000 

40 animation_frames_dir: Path | None = None 

41 

42 

43def _notify_progress(progress_callback: ProgressCallback | None, stage: str) -> None: 

44 if progress_callback is not None: 

45 progress_callback(stage) 

46 

47 

48def run_reconstruction( 

49 tiles: List[np.ndarray], 

50 r: float, 

51 minset: float, 

52 progress_callback: ProgressCallback | None = None, 

53) -> ReconstructionRun: 

54 h, w = tiles[0].shape[:2] 

55 _notify_progress(progress_callback, "Computing edge weights") 

56 lr, ud = build_weight_matrices(tiles, r=r) 

57 _notify_progress(progress_callback, "Assembling reconstruction tree") 

58 adjs = cast("AdjList", msgt(lr, ud, minset=minset, lr_side_size=w, ud_side_size=h)) 

59 _notify_progress(progress_callback, "Rendering reconstructed image") 

60 placements = reconstruct_layout(adjs) 

61 output = render_reconstruction(tiles, placements) 

62 return ReconstructionRun(adjs=adjs, placements=placements, output=output) 

63 

64 

65def run_reconstruction_with_history( 

66 tiles: List[np.ndarray], 

67 r: float, 

68 minset: float, 

69 progress_callback: ProgressCallback | None = None, 

70) -> ReconstructionRunWithHistory: 

71 h, w = tiles[0].shape[:2] 

72 _notify_progress(progress_callback, "Computing edge weights") 

73 lr, ud = build_weight_matrices(tiles, r=r) 

74 _notify_progress(progress_callback, "Assembling reconstruction tree") 

75 adjs, history = cast( 

76 "tuple[AdjList, List[AdjList]]", 

77 msgt(lr, ud, minset=minset, lr_side_size=w, ud_side_size=h, record_history=True), 

78 ) 

79 _notify_progress(progress_callback, "Rendering reconstructed image") 

80 placements = reconstruct_layout(adjs) 

81 output = render_reconstruction(tiles, placements) 

82 return ReconstructionRunWithHistory(adjs=adjs, placements=placements, output=output, history=history) 

83 

84 

85def run_from_options( 

86 options: ReconstructOptions, 

87 progress_callback: ProgressCallback | None = None, 

88) -> ReconstructionRun | ReconstructionRunWithHistory: 

89 _notify_progress(progress_callback, "Loading tiles") 

90 tiles = load_tiles_from_dir(options.input_dir) 

91 

92 if options.animation is None: 

93 result = run_reconstruction(tiles, r=options.r, minset=options.minset, progress_callback=progress_callback) 

94 else: 

95 result = run_reconstruction_with_history( 

96 tiles, 

97 r=options.r, 

98 minset=options.minset, 

99 progress_callback=progress_callback, 

100 ) 

101 

102 _notify_progress(progress_callback, "Saving reconstructed image") 

103 result.output.save(options.output) 

104 

105 if options.animation is not None and isinstance(result, ReconstructionRunWithHistory): 

106 _notify_progress(progress_callback, "Rendering animation") 

107 save_tree_build_animation( 

108 tiles=tiles, 

109 history=result.history, 

110 output_path=options.animation, 

111 seed=options.animation_seed, 

112 frame_size=options.animation_size, 

113 max_angle=options.animation_max_angle, 

114 duration_ms=options.animation_duration_ms, 

115 frames_dir=options.animation_frames_dir, 

116 ) 

117 

118 return result