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 |
