Coverage for src/dictk/cli.py: 99%
82 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-27 23:53 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-27 23:53 +0000
1"""Command-line interface for dictk.
3Each subcommand is a thin wrapper around the corresponding `dictk` API
4function: it calls the array-returning API function, then writes the result
5to disk. See the `dictk` package docstring for the CLI/API split.
6"""
8import argparse
9from pathlib import Path
10import sys
12import numpy as np
14from dictk.image import astronaut, checkerboard, write
15from dictk.rosta import rosta
17IMAGE_FORMATS = ("tiff", "png", "jpg", "svg")
20def _rosta_filename(
21 width: int,
22 height: int,
23 dot_size: float,
24 density: float,
25 smoothness: float,
26 image_format: str,
27) -> str:
28 return (
29 f"rosta_{width}w_by_{height}h"
30 f"_dot_{dot_size}_den_{density}_smo_{smoothness}.{image_format}"
31 )
34def _checkerboard_filename(
35 width: int, height: int, count_x: int, count_y: int, image_format: str
36) -> str:
37 return f"checkerboard_{width}w_by_{height}h_{count_x}x{count_y}.{image_format}"
40def _astronaut_filename(width: int, height: int, image_format: str) -> str:
41 return f"astronaut_{width}w_by_{height}h.{image_format}"
44def _output_write(*, arr: np.ndarray, output: Path | None, filename: str) -> int:
45 output_dir = output if output else Path.cwd()
46 try:
47 output_dir.mkdir(parents=True, exist_ok=True)
48 path = output_dir / filename
49 write(arr=arr, path=path)
50 except OSError as e:
51 print(f"error: {e}", file=sys.stderr)
52 return 1
53 print(f"Saved image: {path}")
54 return 0
57def _rosta_create(args: argparse.Namespace) -> int:
58 try:
59 arr = rosta(
60 width=args.width,
61 height=args.height,
62 dot_size=args.dot_size,
63 density=args.density,
64 smoothness=args.smoothness,
65 random_seed=args.random_seed,
66 )
67 except ValueError as e:
68 print(f"error: {e}", file=sys.stderr)
69 return 1
71 filename = _rosta_filename(
72 args.width,
73 args.height,
74 args.dot_size,
75 args.density,
76 args.smoothness,
77 args.format,
78 )
79 return _output_write(arr=arr, output=args.output, filename=filename)
82def _checkerboard_create(args: argparse.Namespace) -> int:
83 try:
84 arr = checkerboard(
85 width=args.width,
86 height=args.height,
87 count_x=args.count_x,
88 count_y=args.count_y,
89 )
90 except ValueError as e:
91 print(f"error: {e}", file=sys.stderr)
92 return 1
94 filename = _checkerboard_filename(
95 args.width, args.height, args.count_x, args.count_y, args.format
96 )
97 return _output_write(arr=arr, output=args.output, filename=filename)
100def _astronaut_create(args: argparse.Namespace) -> int:
101 try:
102 arr = astronaut(width=args.width, height=args.height)
103 except ValueError as e:
104 print(f"error: {e}", file=sys.stderr)
105 return 1
107 filename = _astronaut_filename(args.width, args.height, args.format)
108 return _output_write(arr=arr, output=args.output, filename=filename)
111def build_parser() -> argparse.ArgumentParser:
112 """Build the `dictk` argument parser, with one subparser per subcommand."""
113 parser = argparse.ArgumentParser(
114 prog="dictk", description="Digital Image Correlation Toolkit"
115 )
116 subparsers = parser.add_subparsers(dest="command", required=True)
118 rosta_parser = subparsers.add_parser(
119 "rosta", help="Create a Rosta speckle pattern and save it as a TIFF image."
120 )
121 rosta_parser.add_argument(
122 "width",
123 type=int,
124 nargs="?",
125 default=200,
126 help="Image width in pixels (int), default: %(default)s.",
127 )
128 rosta_parser.add_argument(
129 "height",
130 type=int,
131 nargs="?",
132 default=200,
133 help="Image height in pixels (int), default: %(default)s.",
134 )
135 rosta_parser.add_argument(
136 "--dot-size",
137 "-s",
138 type=float,
139 default=4.0,
140 help="Dot pattern size factor, 0.0 to 100.0 (float), default: %(default)s.",
141 )
142 rosta_parser.add_argument(
143 "--density",
144 "-d",
145 type=float,
146 default=0.32,
147 help="Dot pattern density, 0.0 to 1.0 (float), default: %(default)s.",
148 )
149 rosta_parser.add_argument(
150 "--smoothness",
151 "-m",
152 type=float,
153 default=2.0,
154 help="Smoothness factor, 0.0 to 100.0 (float), default: %(default)s.",
155 )
156 rosta_parser.add_argument(
157 "--random-seed",
158 "-r",
159 type=int,
160 default=42,
161 help="Seed for reproducible pattern generation (int), default: %(default)s.",
162 )
163 rosta_parser.add_argument(
164 "--output",
165 "-o",
166 type=Path,
167 default=None,
168 help="Output directory (path), default: current directory.",
169 )
170 rosta_parser.add_argument(
171 "--format",
172 "-f",
173 choices=IMAGE_FORMATS,
174 default="tiff",
175 help="Output image format (str), default: %(default)s.",
176 )
177 rosta_parser.set_defaults(func=_rosta_create)
179 checkerboard_parser = subparsers.add_parser(
180 "checkerboard",
181 help="Create a checkerboard test image and save it as a TIFF image.",
182 )
183 checkerboard_parser.add_argument(
184 "width",
185 type=int,
186 nargs="?",
187 default=200,
188 help="Image width in pixels (int), default: %(default)s.",
189 )
190 checkerboard_parser.add_argument(
191 "height",
192 type=int,
193 nargs="?",
194 default=200,
195 help="Image height in pixels (int), default: %(default)s.",
196 )
197 checkerboard_parser.add_argument(
198 "--count-x",
199 "-x",
200 type=int,
201 default=8,
202 help="Number of rectangles along the width (int), default: %(default)s.",
203 )
204 checkerboard_parser.add_argument(
205 "--count-y",
206 "-y",
207 type=int,
208 default=8,
209 help="Number of rectangles along the height (int), default: %(default)s.",
210 )
211 checkerboard_parser.add_argument(
212 "--output",
213 "-o",
214 type=Path,
215 default=None,
216 help="Output directory (path), default: current directory.",
217 )
218 checkerboard_parser.add_argument(
219 "--format",
220 "-f",
221 choices=IMAGE_FORMATS,
222 default="tiff",
223 help="Output image format (str), default: %(default)s.",
224 )
225 checkerboard_parser.set_defaults(func=_checkerboard_create)
227 astronaut_parser = subparsers.add_parser(
228 "astronaut",
229 help="Save a bundled real-world grayscale reference image as a TIFF image.",
230 )
231 astronaut_parser.add_argument(
232 "width",
233 type=int,
234 nargs="?",
235 default=512,
236 help="Image width in pixels (int), default: %(default)s.",
237 )
238 astronaut_parser.add_argument(
239 "height",
240 type=int,
241 nargs="?",
242 default=512,
243 help="Image height in pixels (int), default: %(default)s.",
244 )
245 astronaut_parser.add_argument(
246 "--output",
247 "-o",
248 type=Path,
249 default=None,
250 help="Output directory (path), default: current directory.",
251 )
252 astronaut_parser.add_argument(
253 "--format",
254 "-f",
255 choices=IMAGE_FORMATS,
256 default="tiff",
257 help="Output image format (str), default: %(default)s.",
258 )
259 astronaut_parser.set_defaults(func=_astronaut_create)
261 return parser
264def main(argv: list[str] | None = None) -> int:
265 """Entry point for the `dictk` console script; returns the process exit code."""
266 parser = build_parser()
267 args = parser.parse_args(argv)
268 return args.func(args)
271if __name__ == "__main__":
272 sys.exit(main())