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