Mercurial > repos > rv43 > tomo
comparison msnc_tools.py @ 0:cb1b0d757704 draft
"planemo upload for repository https://github.com/rolfverberg/galaxytools commit 2da52c7db6def807073a1d437a00e0e2a8e7e72e"
author | rv43 |
---|---|
date | Tue, 29 Mar 2022 16:10:16 +0000 |
parents | |
children | e4778148df6b |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:cb1b0d757704 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # -*- coding: utf-8 -*- | |
4 """ | |
5 Created on Mon Dec 6 15:36:22 2021 | |
6 | |
7 @author: rv43 | |
8 """ | |
9 | |
10 import logging | |
11 | |
12 import os | |
13 import sys | |
14 import re | |
15 import yaml | |
16 import h5py | |
17 try: | |
18 import pyinputplus as pyip | |
19 except: | |
20 pass | |
21 import numpy as np | |
22 import imageio as img | |
23 import matplotlib.pyplot as plt | |
24 from time import time | |
25 from ast import literal_eval | |
26 try: | |
27 from lmfit.models import StepModel, RectangleModel | |
28 except: | |
29 pass | |
30 | |
31 def depth_list(L): return isinstance(L, list) and max(map(depth_list, L))+1 | |
32 def depth_tuple(T): return isinstance(T, tuple) and max(map(depth_tuple, T))+1 | |
33 | |
34 def is_int(v, v_min=None, v_max=None): | |
35 """Value is an integer in range v_min <= v <= v_max. | |
36 """ | |
37 if not isinstance(v, int): | |
38 return False | |
39 if (v_min != None and v < v_min) or (v_max != None and v > v_max): | |
40 return False | |
41 return True | |
42 | |
43 def is_num(v, v_min=None, v_max=None): | |
44 """Value is a number in range v_min <= v <= v_max. | |
45 """ | |
46 if not isinstance(v, (int,float)): | |
47 return False | |
48 if (v_min != None and v < v_min) or (v_max != None and v > v_max): | |
49 return False | |
50 return True | |
51 | |
52 def is_index(v, v_min=0, v_max=None): | |
53 """Value is an array index in range v_min <= v < v_max. | |
54 """ | |
55 if not isinstance(v, int): | |
56 return False | |
57 if v < v_min or (v_max != None and v >= v_max): | |
58 return False | |
59 return True | |
60 | |
61 def is_index_range(v, v_min=0, v_max=None): | |
62 """Value is an array index range in range v_min <= v[0] <= v[1] < v_max. | |
63 """ | |
64 if not (isinstance(v, list) and len(v) == 2 and isinstance(v[0], int) and | |
65 isinstance(v[1], int)): | |
66 return False | |
67 if not 0 <= v[0] < v[1] or (v_max != None and v[1] >= v_max): | |
68 return False | |
69 return True | |
70 | |
71 def illegal_value(name, value, location=None, exit_flag=False): | |
72 if not isinstance(location, str): | |
73 location = '' | |
74 else: | |
75 location = f'in {location} ' | |
76 if isinstance(name, str): | |
77 logging.error(f'Illegal value for {name} {location}({value}, {type(value)})') | |
78 else: | |
79 logging.error(f'Illegal value {location}({value}, {type(value)})') | |
80 if exit_flag: | |
81 exit(1) | |
82 | |
83 def get_trailing_int(string): | |
84 indexRegex = re.compile(r'\d+$') | |
85 mo = indexRegex.search(string) | |
86 if mo == None: | |
87 return None | |
88 else: | |
89 return int(mo.group()) | |
90 | |
91 def findImageFiles(path, filetype, name=None): | |
92 if isinstance(name, str): | |
93 name = f' {name} ' | |
94 else: | |
95 name = ' ' | |
96 # Find available index range | |
97 if filetype == 'tif': | |
98 if not isinstance(path, str) and not os.path.isdir(path): | |
99 illegal_value('path', path, 'findImageRange') | |
100 return -1, 0, [] | |
101 indexRegex = re.compile(r'\d+') | |
102 # At this point only tiffs | |
103 files = sorted([f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and | |
104 f.endswith('.tif') and indexRegex.search(f)]) | |
105 num_imgs = len(files) | |
106 if num_imgs < 1: | |
107 logging.warning('No available'+name+'files') | |
108 return -1, 0, [] | |
109 first_index = indexRegex.search(files[0]).group() | |
110 last_index = indexRegex.search(files[-1]).group() | |
111 if first_index == None or last_index == None: | |
112 logging.error('Unable to find correctly indexed'+name+'images') | |
113 return -1, 0, [] | |
114 first_index = int(first_index) | |
115 last_index = int(last_index) | |
116 if num_imgs != last_index-first_index+1: | |
117 logging.error('Non-consecutive set of indices for'+name+'images') | |
118 return -1, 0, [] | |
119 paths = [os.path.join(path, f) for f in files] | |
120 elif filetype == 'h5': | |
121 if not isinstance(path, str) or not os.path.isfile(path): | |
122 illegal_value('path', path, 'findImageRange') | |
123 return -1, 0, [] | |
124 # At this point only h5 in alamo2 detector style | |
125 first_index = 0 | |
126 with h5py.File(path, 'r') as f: | |
127 num_imgs = f['entry/instrument/detector/data'].shape[0] | |
128 last_index = num_imgs-1 | |
129 paths = [path] | |
130 else: | |
131 illegal_value('filetype', filetype, 'findImageRange') | |
132 return -1, 0, [] | |
133 logging.debug('\nNumber of available'+name+f'images: {num_imgs}') | |
134 logging.debug('Index range of available'+name+f'images: [{first_index}, '+ | |
135 f'{last_index}]') | |
136 | |
137 return first_index, num_imgs, paths | |
138 | |
139 def selectImageRange(first_index, offset, num_imgs, name=None, num_required=None): | |
140 if isinstance(name, str): | |
141 name = f' {name} ' | |
142 else: | |
143 name = ' ' | |
144 # Check existing values | |
145 use_input = 'no' | |
146 if (is_int(first_index, 0) and is_int(offset, 0) and is_int(num_imgs, 1)): | |
147 if offset < 0: | |
148 use_input = pyip.inputYesNo('\nCurrent'+name+f'first index = {first_index}, '+ | |
149 'use this value ([y]/n)? ', blank=True) | |
150 else: | |
151 use_input = pyip.inputYesNo('\nCurrent'+name+'first index/offset = '+ | |
152 f'{first_index}/{offset}, use these values ([y]/n)? ', | |
153 blank=True) | |
154 if use_input != 'no': | |
155 use_input = pyip.inputYesNo('Current number of'+name+'images = '+ | |
156 f'{num_imgs}, use this value ([y]/n)? ', | |
157 blank=True) | |
158 if use_input == 'yes': | |
159 return first_index, offset, num_imgs | |
160 | |
161 # Check range against requirements | |
162 if num_imgs < 1: | |
163 logging.warning('No available'+name+'images') | |
164 return -1, -1, 0 | |
165 if num_required == None: | |
166 if num_imgs == 1: | |
167 return first_index, 0, 1 | |
168 else: | |
169 if not is_int(num_required, 1): | |
170 illegal_value('num_required', num_required, 'selectImageRange') | |
171 return -1, -1, 0 | |
172 if num_imgs < num_required: | |
173 logging.error('Unable to find the required'+name+ | |
174 f'images ({num_imgs} out of {num_required})') | |
175 return -1, -1, 0 | |
176 | |
177 # Select index range | |
178 if num_required == None: | |
179 last_index = first_index+num_imgs | |
180 use_all = f'Use all ([{first_index}, {last_index}])' | |
181 pick_offset = 'Pick a first index offset and a number of images' | |
182 pick_bounds = 'Pick the first and last index' | |
183 menuchoice = pyip.inputMenu([use_all, pick_offset, pick_bounds], numbered=True) | |
184 if menuchoice == use_all: | |
185 offset = 0 | |
186 elif menuchoice == pick_offset: | |
187 offset = pyip.inputInt('Enter the first index offset'+ | |
188 f' [0, {last_index-first_index}]: ', min=0, max=last_index-first_index) | |
189 first_index += offset | |
190 if first_index == last_index: | |
191 num_imgs = 1 | |
192 else: | |
193 num_imgs = pyip.inputInt(f'Enter the number of images [1, {num_imgs-offset}]: ', | |
194 min=1, max=num_imgs-offset) | |
195 else: | |
196 offset = pyip.inputInt(f'Enter the first index [{first_index}, {last_index}]: ', | |
197 min=first_index, max=last_index)-first_index | |
198 first_index += offset | |
199 num_imgs = pyip.inputInt(f'Enter the last index [{first_index}, {last_index}]: ', | |
200 min=first_index, max=last_index)-first_index+1 | |
201 else: | |
202 use_all = f'Use ([{first_index}, {first_index+num_required-1}])' | |
203 pick_offset = 'Pick the first index offset' | |
204 menuchoice = pyip.inputMenu([use_all, pick_offset], numbered=True) | |
205 offset = 0 | |
206 if menuchoice == pick_offset: | |
207 offset = pyip.inputInt('Enter the first index offset'+ | |
208 f'[0, {num_imgs-num_required}]: ', min=0, max=num_imgs-num_required) | |
209 first_index += offset | |
210 num_imgs = num_required | |
211 | |
212 return first_index, offset, num_imgs | |
213 | |
214 def loadImage(f, img_x_bounds=None, img_y_bounds=None): | |
215 """Load a single image from file. | |
216 """ | |
217 if not os.path.isfile(f): | |
218 logging.error(f'Unable to load {f}') | |
219 return None | |
220 img_read = img.imread(f) | |
221 if not img_x_bounds: | |
222 img_x_bounds = [0, img_read.shape[0]] | |
223 else: | |
224 if (not isinstance(img_x_bounds, list) or len(img_x_bounds) != 2 or | |
225 not (0 <= img_x_bounds[0] < img_x_bounds[1] <= img_read.shape[0])): | |
226 logging.error(f'inconsistent row dimension in {f}') | |
227 return None | |
228 if not img_y_bounds: | |
229 img_y_bounds = [0, img_read.shape[1]] | |
230 else: | |
231 if (not isinstance(img_y_bounds, list) or len(img_y_bounds) != 2 or | |
232 not (0 <= img_y_bounds[0] < img_y_bounds[1] <= img_read.shape[0])): | |
233 logging.error(f'inconsistent column dimension in {f}') | |
234 return None | |
235 return img_read[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]] | |
236 | |
237 def loadImageStack(files, filetype, img_offset, num_imgs, num_img_skip=0, | |
238 img_x_bounds=None, img_y_bounds=None): | |
239 """Load a set of images and return them as a stack. | |
240 """ | |
241 logging.debug(f'img_offset = {img_offset}') | |
242 logging.debug(f'num_imgs = {num_imgs}') | |
243 logging.debug(f'num_img_skip = {num_img_skip}') | |
244 logging.debug(f'\nfiles:\n{files}\n') | |
245 img_stack = np.array([]) | |
246 if filetype == 'tif': | |
247 img_read_stack = [] | |
248 i = 1 | |
249 t0 = time() | |
250 for f in files[img_offset:img_offset+num_imgs:num_img_skip+1]: | |
251 if not i%20: | |
252 logging.info(f' loading {i}/{num_imgs}: {f}') | |
253 else: | |
254 logging.debug(f' loading {i}/{num_imgs}: {f}') | |
255 img_read = loadImage(f, img_x_bounds, img_y_bounds) | |
256 img_read_stack.append(img_read) | |
257 i += num_img_skip+1 | |
258 img_stack = np.stack([img_read for img_read in img_read_stack]) | |
259 logging.info(f'... done in {time()-t0:.2f} seconds!') | |
260 logging.debug(f'img_stack shape = {np.shape(img_stack)}') | |
261 del img_read_stack, img_read | |
262 elif filetype == 'h5': | |
263 if not isinstance(files[0], str) and not os.path.isfile(files[0]): | |
264 illegal_value('files[0]', files[0], 'loadImageStack') | |
265 return img_stack | |
266 t0 = time() | |
267 with h5py.File(files[0], 'r') as f: | |
268 shape = f['entry/instrument/detector/data'].shape | |
269 if len(shape) != 3: | |
270 logging.error(f'inconsistent dimensions in {files[0]}') | |
271 if not img_x_bounds: | |
272 img_x_bounds = [0, shape[1]] | |
273 else: | |
274 if (not isinstance(img_x_bounds, list) or len(img_x_bounds) != 2 or | |
275 not (0 <= img_x_bounds[0] < img_x_bounds[1] <= shape[1])): | |
276 logging.error(f'inconsistent row dimension in {files[0]}') | |
277 if not img_y_bounds: | |
278 img_y_bounds = [0, shape[2]] | |
279 else: | |
280 if (not isinstance(img_y_bounds, list) or len(img_y_bounds) != 2 or | |
281 not (0 <= img_y_bounds[0] < img_y_bounds[1] <= shape[2])): | |
282 logging.error(f'inconsistent column dimension in {files[0]}') | |
283 img_stack = f.get('entry/instrument/detector/data')[ | |
284 img_offset:img_offset+num_imgs:num_img_skip+1, | |
285 img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]] | |
286 logging.info(f'... done in {time()-t0:.2f} seconds!') | |
287 else: | |
288 illegal_value('filetype', filetype, 'findImageRange') | |
289 return img_stack | |
290 | |
291 def clearFig(title): | |
292 if not isinstance(title, str): | |
293 illegal_value('title', title, 'clearFig') | |
294 return | |
295 plt.close(fig=re.sub(r"\s+", '_', title)) | |
296 | |
297 def quickImshow(a, title=None, path=None, name=None, save_fig=False, save_only=False, | |
298 clear=True, **kwargs): | |
299 if title != None and not isinstance(title, str): | |
300 illegal_value('title', title, 'quickImshow') | |
301 return | |
302 if path is not None and not isinstance(path, str): | |
303 illegal_value('path', path, 'quickImshow') | |
304 return | |
305 if not isinstance(save_fig, bool): | |
306 illegal_value('save_fig', save_fig, 'quickImshow') | |
307 return | |
308 if not isinstance(save_only, bool): | |
309 illegal_value('save_only', save_only, 'quickImshow') | |
310 return | |
311 if not isinstance(clear, bool): | |
312 illegal_value('clear', clear, 'quickImshow') | |
313 return | |
314 if not title: | |
315 title='quick_imshow' | |
316 else: | |
317 title = re.sub(r"\s+", '_', title) | |
318 if name is None: | |
319 if path is None: | |
320 path = f'{title}.png' | |
321 else: | |
322 path = f'{path}/{title}.png' | |
323 else: | |
324 if path is None: | |
325 path = name | |
326 else: | |
327 path = f'{path}/{name}' | |
328 if clear: | |
329 plt.close(fig=title) | |
330 if save_only: | |
331 plt.figure(title) | |
332 plt.imshow(a, **kwargs) | |
333 plt.savefig(path) | |
334 plt.close(fig=title) | |
335 #plt.imsave(f'{title}.png', a, **kwargs) | |
336 else: | |
337 plt.ion() | |
338 plt.figure(title) | |
339 plt.imshow(a, **kwargs) | |
340 if save_fig: | |
341 plt.savefig(path) | |
342 plt.pause(1) | |
343 | |
344 def quickPlot(*args, title=None, path=None, name=None, save_fig=False, save_only=False, | |
345 clear=True, **kwargs): | |
346 if title != None and not isinstance(title, str): | |
347 illegal_value('title', title, 'quickPlot') | |
348 return | |
349 if path is not None and not isinstance(path, str): | |
350 illegal_value('path', path, 'quickPlot') | |
351 return | |
352 if not isinstance(save_fig, bool): | |
353 illegal_value('save_fig', save_fig, 'quickPlot') | |
354 return | |
355 if not isinstance(save_only, bool): | |
356 illegal_value('save_only', save_only, 'quickPlot') | |
357 return | |
358 if not isinstance(clear, bool): | |
359 illegal_value('clear', clear, 'quickPlot') | |
360 return | |
361 if not title: | |
362 title = 'quick_plot' | |
363 else: | |
364 title = re.sub(r"\s+", '_', title) | |
365 if name is None: | |
366 if path is None: | |
367 path = f'{title}.png' | |
368 else: | |
369 path = f'{path}/{title}.png' | |
370 else: | |
371 if path is None: | |
372 path = name | |
373 else: | |
374 path = f'{path}/{name}' | |
375 if clear: | |
376 plt.close(fig=title) | |
377 if save_only: | |
378 plt.figure(title) | |
379 if depth_tuple(args) > 1: | |
380 for y in args: | |
381 plt.plot(*y, **kwargs) | |
382 else: | |
383 plt.plot(*args, **kwargs) | |
384 plt.savefig(path) | |
385 plt.close(fig=title) | |
386 else: | |
387 plt.ion() | |
388 plt.figure(title) | |
389 if depth_tuple(args) > 1: | |
390 for y in args: | |
391 plt.plot(*y, **kwargs) | |
392 else: | |
393 plt.plot(*args, **kwargs) | |
394 if save_fig: | |
395 plt.savefig(path) | |
396 plt.pause(1) | |
397 | |
398 def selectArrayBounds(a, x_low=None, x_upp=None, num_x_min=None, | |
399 title='select array bounds'): | |
400 """Interactively select the lower and upper data bounds for a numpy array. | |
401 """ | |
402 if not isinstance(a, np.ndarray) or a.ndim != 1: | |
403 logging.error('Illegal array type or dimension in selectArrayBounds') | |
404 return None | |
405 if num_x_min == None: | |
406 num_x_min = 1 | |
407 else: | |
408 if num_x_min < 2 or num_x_min > a.size: | |
409 logging.warning('Illegal input for num_x_min in selectArrayBounds, input ignored') | |
410 num_x_min = 1 | |
411 if x_low == None: | |
412 x_min = 0 | |
413 x_max = a.size | |
414 x_low_max = a.size-num_x_min | |
415 while True: | |
416 quickPlot(range(x_min, x_max), a[x_min:x_max], title=title) | |
417 zoom_flag = pyip.inputInt('Set lower data bound (0) or zoom in (1)?: ', | |
418 min=0, max=1) | |
419 if zoom_flag: | |
420 x_min = pyip.inputInt(f' Set lower zoom index [0, {x_low_max}]: ', | |
421 min=0, max=x_low_max) | |
422 x_max = pyip.inputInt(f' Set upper zoom index [{x_min+1}, {x_low_max+1}]: ', | |
423 min=x_min+1, max=x_low_max+1) | |
424 else: | |
425 x_low = pyip.inputInt(f' Set lower data bound [0, {x_low_max}]: ', | |
426 min=0, max=x_low_max) | |
427 break | |
428 else: | |
429 if not is_int(x_low, 0, a.size-num_x_min): | |
430 illegal_value('x_low', x_low, 'selectArrayBounds') | |
431 return None | |
432 if x_upp == None: | |
433 x_min = x_low+num_x_min | |
434 x_max = a.size | |
435 x_upp_min = x_min | |
436 while True: | |
437 quickPlot(range(x_min, x_max), a[x_min:x_max], title=title) | |
438 zoom_flag = pyip.inputInt('Set upper data bound (0) or zoom in (1)?: ', | |
439 min=0, max=1) | |
440 if zoom_flag: | |
441 x_min = pyip.inputInt(f' Set upper zoom index [{x_upp_min}, {a.size-1}]: ', | |
442 min=x_upp_min, max=a.size-1) | |
443 x_max = pyip.inputInt(f' Set upper zoom index [{x_min+1}, {a.size}]: ', | |
444 min=x_min+1, max=a.size) | |
445 else: | |
446 x_upp = pyip.inputInt(f' Set upper data bound [{x_upp_min}, {a.size}]: ', | |
447 min=x_upp_min, max=a.size) | |
448 break | |
449 else: | |
450 if not is_int(x_upp, x_low+num_x_min, a.size): | |
451 illegal_value('x_upp', x_upp, 'selectArrayBounds') | |
452 return None | |
453 bounds = [x_low, x_upp] | |
454 print(f'lower bound = {x_low} (inclusive)\nupper bound = {x_upp} (exclusive)]') | |
455 #quickPlot(range(bounds[0], bounds[1]), a[bounds[0]:bounds[1]], title=title) | |
456 quickPlot((range(a.size), a), ([bounds[0], bounds[0]], [a.min(), a.max()], 'r-'), | |
457 ([bounds[1], bounds[1]], [a.min(), a.max()], 'r-'), title=title) | |
458 if pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) == 'no': | |
459 bounds = selectArrayBounds(a, title=title) | |
460 return bounds | |
461 | |
462 def selectImageBounds(a, axis, low=None, upp=None, num_min=None, | |
463 title='select array bounds'): | |
464 """Interactively select the lower and upper data bounds for a 2D numpy array. | |
465 """ | |
466 if not isinstance(a, np.ndarray) or a.ndim != 2: | |
467 logging.error('Illegal array type or dimension in selectImageBounds') | |
468 return None | |
469 if axis < 0 or axis >= a.ndim: | |
470 illegal_value('axis', axis, 'selectImageBounds') | |
471 return None | |
472 if num_min == None: | |
473 num_min = 1 | |
474 else: | |
475 if num_min < 2 or num_min > a.shape[axis]: | |
476 logging.warning('Illegal input for num_min in selectImageBounds, input ignored') | |
477 num_min = 1 | |
478 if low == None: | |
479 min_ = 0 | |
480 max_ = a.shape[axis] | |
481 low_max = a.shape[axis]-num_min | |
482 while True: | |
483 if axis: | |
484 quickImshow(a[:,min_:max_], title=title, aspect='auto', | |
485 extent=[min_,max_,a.shape[0],0]) | |
486 else: | |
487 quickImshow(a[min_:max_,:], title=title, aspect='auto', | |
488 extent=[0,a.shape[1], max_,min_]) | |
489 zoom_flag = pyip.inputInt('Set lower data bound (0) or zoom in (1)?: ', | |
490 min=0, max=1) | |
491 if zoom_flag: | |
492 min_ = pyip.inputInt(f' Set lower zoom index [0, {low_max}]: ', | |
493 min=0, max=low_max) | |
494 max_ = pyip.inputInt(f' Set upper zoom index [{min_+1}, {low_max+1}]: ', | |
495 min=min_+1, max=low_max+1) | |
496 else: | |
497 low = pyip.inputInt(f' Set lower data bound [0, {low_max}]: ', | |
498 min=0, max=low_max) | |
499 break | |
500 else: | |
501 if not is_int(low, 0, a.shape[axis]-num_min): | |
502 illegal_value('low', low, 'selectImageBounds') | |
503 return None | |
504 if upp == None: | |
505 min_ = low+num_min | |
506 max_ = a.shape[axis] | |
507 upp_min = min_ | |
508 while True: | |
509 if axis: | |
510 quickImshow(a[:,min_:max_], title=title, aspect='auto', | |
511 extent=[min_,max_,a.shape[0],0]) | |
512 else: | |
513 quickImshow(a[min_:max_,:], title=title, aspect='auto', | |
514 extent=[0,a.shape[1], max_,min_]) | |
515 zoom_flag = pyip.inputInt('Set upper data bound (0) or zoom in (1)?: ', | |
516 min=0, max=1) | |
517 if zoom_flag: | |
518 min_ = pyip.inputInt(f' Set upper zoom index [{upp_min}, {a.shape[axis]-1}]: ', | |
519 min=upp_min, max=a.shape[axis]-1) | |
520 max_ = pyip.inputInt(f' Set upper zoom index [{min_+1}, {a.shape[axis]}]: ', | |
521 min=min_+1, max=a.shape[axis]) | |
522 else: | |
523 upp = pyip.inputInt(f' Set upper data bound [{upp_min}, {a.shape[axis]}]: ', | |
524 min=upp_min, max=a.shape[axis]) | |
525 break | |
526 else: | |
527 if not is_int(upp, low+num_min, a.shape[axis]): | |
528 illegal_value('upp', upp, 'selectImageBounds') | |
529 return None | |
530 bounds = [low, upp] | |
531 a_tmp = a | |
532 if axis: | |
533 a_tmp[:,bounds[0]] = a.max() | |
534 a_tmp[:,bounds[1]] = a.max() | |
535 else: | |
536 a_tmp[bounds[0],:] = a.max() | |
537 a_tmp[bounds[1],:] = a.max() | |
538 print(f'lower bound = {low} (inclusive)\nupper bound = {upp} (exclusive)') | |
539 quickImshow(a_tmp, title=title) | |
540 if pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) == 'no': | |
541 bounds = selectImageBounds(a, title=title) | |
542 return bounds | |
543 | |
544 def fitStep(x=None, y=None, model='step', form='arctan'): | |
545 if not isinstance(y, np.ndarray) or y.ndim != 1: | |
546 logging.error('Illegal array type or dimension for y in fitStep') | |
547 return | |
548 if isinstance(x, type(None)): | |
549 x = np.array(range(y.size)) | |
550 elif not isinstance(x, np.ndarray) or x.ndim != 1 or x.size != y.size: | |
551 logging.error('Illegal array type or dimension for x in fitStep') | |
552 return | |
553 if not isinstance(model, str) or not model in ('step', 'rectangle'): | |
554 illegal_value('model', model, 'fitStepModel') | |
555 return | |
556 if not isinstance(form, str) or not form in ('linear', 'atan', 'arctan', 'erf', 'logistic'): | |
557 illegal_value('form', form, 'fitStepModel') | |
558 return | |
559 | |
560 if model == 'step': | |
561 mod = StepModel(form=form) | |
562 else: | |
563 mod = RectangleModel(form=form) | |
564 pars = mod.guess(y, x=x) | |
565 out = mod.fit(y, pars, x=x) | |
566 #print(out.fit_report()) | |
567 #quickPlot((x,y),(x,out.best_fit)) | |
568 return out.best_values | |
569 | |
570 class Config: | |
571 """Base class for processing a config file or dictionary. | |
572 """ | |
573 def __init__(self, config_file=None, config_dict=None): | |
574 self.config = {} | |
575 self.load_flag = False | |
576 self.suffix = None | |
577 | |
578 # Load config file | |
579 if config_file is not None and config_dict is not None: | |
580 logging.warning('Ignoring config_dict (both config_file and config_dict are specified)') | |
581 if config_file is not None: | |
582 self.loadFile(config_file) | |
583 elif config_dict is not None: | |
584 self.loadDict(config_dict) | |
585 | |
586 def loadFile(self, config_file): | |
587 """Load a config file. | |
588 """ | |
589 if self.load_flag: | |
590 logging.warning('Overwriting any previously loaded config file') | |
591 self.config = {} | |
592 | |
593 # Ensure config file exists | |
594 if not os.path.isfile(config_file): | |
595 logging.error(f'Unable to load {config_file}') | |
596 return | |
597 | |
598 # Load config file | |
599 self.suffix = os.path.splitext(config_file)[1] | |
600 if self.suffix == '.yml' or self.suffix == '.yaml': | |
601 with open(config_file, 'r') as f: | |
602 self.config = yaml.safe_load(f) | |
603 elif self.suffix == '.txt': | |
604 with open(config_file, 'r') as f: | |
605 lines = f.read().splitlines() | |
606 self.config = {item[0].strip():literal_eval(item[1].strip()) for item in | |
607 [line.split('#')[0].split('=') for line in lines if '=' in line.split('#')[0]]} | |
608 else: | |
609 logging.error(f'Illegal config file extension: {self.suffix}') | |
610 | |
611 # Make sure config file was correctly loaded | |
612 if isinstance(self.config, dict): | |
613 self.load_flag = True | |
614 else: | |
615 logging.error(f'Unable to load dictionary from config file: {config_file}') | |
616 self.config = {} | |
617 | |
618 def loadDict(self, config_dict): | |
619 """Takes a dictionary and places it into self.config. | |
620 """ | |
621 exit('loadDict not tested yet, what format do we follow: txt or yaml?') | |
622 if self.load_flag: | |
623 logging.warning('Overwriting the previously loaded config file') | |
624 | |
625 if isinstance(config_dict, dict): | |
626 self.config = config_dict | |
627 self.load_flag = True | |
628 else: | |
629 logging.error(f'Illegal dictionary config object: {config_dict}') | |
630 self.config = {} | |
631 | |
632 def saveFile(self, config_file): | |
633 """Save the config file (as a yaml file only right now). | |
634 """ | |
635 suffix = os.path.splitext(config_file)[1] | |
636 if suffix != '.yml' and suffix != '.yaml': | |
637 logging.error(f'Illegal config file extension: {suffix}') | |
638 | |
639 # Check if config file exists | |
640 if os.path.isfile(config_file): | |
641 logging.info(f'Updating {config_file}') | |
642 else: | |
643 logging.info(f'Saving {config_file}') | |
644 | |
645 # Save config file | |
646 with open(config_file, 'w') as f: | |
647 yaml.dump(self.config, f) | |
648 | |
649 def validate(self, pars_required, pars_missing=None): | |
650 """Returns False if any required first level keys are missing. | |
651 """ | |
652 if not self.load_flag: | |
653 logging.error('Load a config file prior to calling Config.validate') | |
654 pars = [p for p in pars_required if p not in self.config] | |
655 if isinstance(pars_missing, list): | |
656 pars_missing.extend(pars) | |
657 elif pars_missing is not None: | |
658 illegal_value('pars_missing', pars_missing, 'Config.validate') | |
659 if len(pars) > 0: | |
660 return False | |
661 return True | |
662 | |
663 #RV FIX this is for a txt file, obsolete? | |
664 # def update_txt(self, config_file, key, value, search_string=None, header=None): | |
665 # if not self.load_flag: | |
666 # logging.error('Load a config file prior to calling Config.update') | |
667 # | |
668 # if not os.path.isfile(config_file): | |
669 # logging.error(f'Unable to load {config_file}') | |
670 # lines = [] | |
671 # else: | |
672 # with open(config_file, 'r') as f: | |
673 # lines = f.read().splitlines() | |
674 # config = {item[0].strip():literal_eval(item[1].strip()) for item in | |
675 # [line.split('#')[0].split('=') for line in lines if '=' in line.split('#')[0]]} | |
676 # if not isinstance(key, str): | |
677 # illegal_value('key', key, 'Config.update') | |
678 # return config | |
679 # if isinstance(value, str): | |
680 # newline = f"{key} = '{value}'" | |
681 # else: | |
682 # newline = f'{key} = {value}' | |
683 # if key in config.keys(): | |
684 # # Update key with value | |
685 # for index,line in enumerate(lines): | |
686 # if '=' in line: | |
687 # item = line.split('#')[0].split('=') | |
688 # if item[0].strip() == key: | |
689 # lines[index] = newline | |
690 # break | |
691 # else: | |
692 # # Insert new key/value pair | |
693 # if search_string != None: | |
694 # if isinstance(search_string, str): | |
695 # search_string = [search_string] | |
696 # elif not isinstance(search_string, (tuple, list)): | |
697 # illegal_value('search_string', search_string, 'Config.update') | |
698 # search_string = None | |
699 # update_flag = False | |
700 # if search_string != None: | |
701 # indices = [[index for index,line in enumerate(lines) if item in line] | |
702 # for item in search_string] | |
703 # for i,index in enumerate(indices): | |
704 # if index: | |
705 # if len(search_string) > 1 and key < search_string[i]: | |
706 # lines.insert(index[0], newline) | |
707 # else: | |
708 # lines.insert(index[0]+1, newline) | |
709 # update_flag = True | |
710 # break | |
711 # if not update_flag: | |
712 # if isinstance(header, str): | |
713 # lines += ['', header, newline] | |
714 # else: | |
715 # lines += ['', newline] | |
716 # # Write updated config file | |
717 # with open(config_file, 'w') as f: | |
718 # for line in lines: | |
719 # f.write(f'{line}\n') | |
720 # # Update loaded config | |
721 # config['key'] = value | |
722 # | |
723 #RV update and bring into Config if needed again | |
724 #def search(config_file, search_string): | |
725 # if not os.path.isfile(config_file): | |
726 # logging.error(f'Unable to load {config_file}') | |
727 # return False | |
728 # with open(config_file, 'r') as f: | |
729 # lines = f.read() | |
730 # if search_string in lines: | |
731 # return True | |
732 # return False | |
733 | |
734 class Detector: | |
735 """Class for processing a detector info file or dictionary. | |
736 """ | |
737 def __init__(self, detector_id): | |
738 self.detector = {} | |
739 self.load_flag = False | |
740 self.validate_flag = False | |
741 | |
742 # Load detector file | |
743 self.loadFile(detector_id) | |
744 | |
745 def loadFile(self, detector_id): | |
746 """Load a detector file. | |
747 """ | |
748 if self.load_flag: | |
749 logging.warning('Overwriting the previously loaded detector file') | |
750 self.detector = {} | |
751 | |
752 # Ensure detector file exists | |
753 if not isinstance(detector_id, str): | |
754 illegal_value('detector_id', detector_id, 'Detector.loadFile') | |
755 return | |
756 detector_file = f'{detector_id}.yaml' | |
757 if not os.path.isfile(detector_file): | |
758 detector_file = self.config['detector_id']+'.yaml' | |
759 if not os.path.isfile(detector_file): | |
760 logging.error(f'Unable to load detector info file for {detector_id}') | |
761 return | |
762 | |
763 # Load detector file | |
764 with open(detector_file, 'r') as f: | |
765 self.detector = yaml.safe_load(f) | |
766 | |
767 # Make sure detector file was correctly loaded | |
768 if isinstance(self.detector, dict): | |
769 self.load_flag = True | |
770 else: | |
771 logging.error(f'Unable to load dictionary from detector file: {detector_file}') | |
772 self.detector = {} | |
773 | |
774 def validate(self): | |
775 """Returns False if any config parameters is illegal or missing. | |
776 """ | |
777 if not self.load_flag: | |
778 logging.error('Load a detector file prior to calling Detector.validate') | |
779 | |
780 # Check for required first-level keys | |
781 pars_required = ['detector', 'lens_magnification'] | |
782 pars_missing = [p for p in pars_required if p not in self.detector] | |
783 if len(pars_missing) > 0: | |
784 logging.error(f'Missing item(s) in detector file: {", ".join(pars_missing)}') | |
785 return False | |
786 | |
787 is_valid = True | |
788 | |
789 # Check detector pixel config keys | |
790 pixels = self.detector['detector'].get('pixels') | |
791 if not pixels: | |
792 pars_missing.append('detector:pixels') | |
793 else: | |
794 rows = pixels.get('rows') | |
795 if not rows: | |
796 pars_missing.append('detector:pixels:rows') | |
797 columns = pixels.get('columns') | |
798 if not columns: | |
799 pars_missing.append('detector:pixels:columns') | |
800 size = pixels.get('size') | |
801 if not size: | |
802 pars_missing.append('detector:pixels:size') | |
803 | |
804 if not len(pars_missing): | |
805 self.validate_flag = True | |
806 else: | |
807 is_valid = False | |
808 | |
809 return is_valid | |
810 | |
811 def getPixelSize(self): | |
812 """Returns the detector pixel size. | |
813 """ | |
814 if not self.validate_flag: | |
815 logging.error('Validate detector file info prior to calling Detector.getPixelSize') | |
816 | |
817 lens_magnification = self.detector.get('lens_magnification') | |
818 if not isinstance(lens_magnification, (int,float)) or lens_magnification <= 0.: | |
819 illegal_value('lens_magnification', lens_magnification, 'detector file') | |
820 return 0 | |
821 pixel_size = self.detector['detector'].get('pixels').get('size') | |
822 if isinstance(pixel_size, (int,float)): | |
823 if pixel_size <= 0.: | |
824 illegal_value('pixel_size', pixel_size, 'detector file') | |
825 return 0 | |
826 pixel_size /= lens_magnification | |
827 elif isinstance(pixel_size, list): | |
828 if ((len(pixel_size) > 2) or | |
829 (len(pixel_size) == 2 and pixel_size[0] != pixel_size[1])): | |
830 illegal_value('pixel size', pixel_size, 'detector file') | |
831 return 0 | |
832 elif not is_num(pixel_size[0], 0.): | |
833 illegal_value('pixel size', pixel_size, 'detector file') | |
834 return 0 | |
835 else: | |
836 pixel_size = pixel_size[0]/lens_magnification | |
837 else: | |
838 illegal_value('pixel size', pixel_size, 'detector file') | |
839 return 0 | |
840 | |
841 return pixel_size | |
842 | |
843 def getDimensions(self): | |
844 """Returns the detector pixel dimensions. | |
845 """ | |
846 if not self.validate_flag: | |
847 logging.error('Validate detector file info prior to calling Detector.getDimensions') | |
848 | |
849 pixels = self.detector['detector'].get('pixels') | |
850 num_rows = pixels.get('rows') | |
851 if not is_int(num_rows, 1): | |
852 illegal_value('rows', num_rows, 'detector file') | |
853 return (0, 0) | |
854 num_columns = pixels.get('columns') | |
855 if not is_int(num_columns, 1): | |
856 illegal_value('columns', num_columns, 'detector file') | |
857 return (0, 0) | |
858 | |
859 return num_rows, num_columns |