Coverage for src/dictk/cli.py: 99%

82 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-24 22:28 +0000

1"""Command-line interface for dictk. 

2 

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""" 

7 

8import argparse 

9from pathlib import Path 

10import sys 

11 

12import numpy as np 

13 

14from dictk.imaging import astronaut, checkerboard, write_image 

15from dictk.rosta import rosta 

16 

17IMAGE_FORMATS = ("tiff", "png", "jpg", "svg") 

18 

19 

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 ) 

32 

33 

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}" 

38 

39 

40def _astronaut_filename(width: int, height: int, image_format: str) -> str: 

41 return f"astronaut_{width}w_by_{height}h.{image_format}" 

42 

43 

44def _write_output(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_image(arr, 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 

55 

56 

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 

70 

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 _write_output(arr, args.output, filename) 

80 

81 

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 

93 

94 filename = _checkerboard_filename( 

95 args.width, args.height, args.count_x, args.count_y, args.format 

96 ) 

97 return _write_output(arr, args.output, filename) 

98 

99 

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 

106 

107 filename = _astronaut_filename(args.width, args.height, args.format) 

108 return _write_output(arr, args.output, filename) 

109 

110 

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) 

117 

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) 

178 

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) 

226 

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) 

260 

261 return parser 

262 

263 

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) 

269 

270 

271if __name__ == "__main__": 

272 sys.exit(main())