comparison tomo.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 Fri Dec 10 09:54:37 2021
6
7 @author: rv43
8 """
9
10 import logging
11
12 import os
13 import sys
14 import getopt
15 import re
16 import io
17 try:
18 import pyinputplus as pyip
19 except:
20 pass
21 import numpy as np
22 import numexpr as ne
23 import multiprocessing as mp
24 import scipy.ndimage as spi
25 import tomopy
26 from time import time
27 from skimage.transform import iradon
28 from skimage.restoration import denoise_tv_chambolle
29
30 import msnc_tools as msnc
31
32 class set_numexpr_threads:
33
34 def __init__(self, nthreads):
35 cpu_count = mp.cpu_count()
36 if nthreads is None or nthreads > cpu_count:
37 self.n = cpu_count
38 else:
39 self.n = nthreads
40
41 def __enter__(self):
42 self.oldn = ne.set_num_threads(self.n)
43
44 def __exit__(self, exc_type, exc_value, traceback):
45 ne.set_num_threads(self.oldn)
46
47 class ConfigTomo(msnc.Config):
48 """Class for processing a config file.
49 """
50
51 def __init__(self, config_file=None, config_dict=None):
52 super().__init__(config_file, config_dict)
53
54 def _validate_txt(self):
55 """Returns False if any required config parameter is illegal or missing.
56 """
57 is_valid = True
58
59 # Check for required first-level keys
60 pars_required = ['tdf_data_path', 'tbf_data_path', 'detector_id']
61 pars_missing = []
62 is_valid = super().validate(pars_required, pars_missing)
63 if len(pars_missing) > 0:
64 logging.error(f'Missing item(s) in config file: {", ".join(pars_missing)}')
65 self.detector_id = self.config.get('detector_id')
66
67 # Find tomography dark field images file/folder
68 self.tdf_data_path = self.config.get('tdf_data_path')
69
70 # Find tomography bright field images file/folder
71 self.tbf_data_path = self.config.get('tbf_data_path')
72
73 # Check number of tomography image stacks
74 self.num_tomo_stacks = self.config.get('num_tomo_stacks', 1)
75 if not msnc.is_int(self.num_tomo_stacks, 1):
76 self.num_tomo_stacks = None
77 msnc.illegal_value('num_tomo_stacks', self.num_tomo_stacks, 'config file')
78 return False
79 logging.info(f'num_tomo_stacks = {self.num_tomo_stacks}')
80
81 # Find tomography images file/folders and stack parameters
82 tomo_data_paths_indices = sorted({key:value for key,value in self.config.items()
83 if 'tomo_data_path' in key}.items())
84 if len(tomo_data_paths_indices) != self.num_tomo_stacks:
85 logging.error(f'Incorrect number of tomography data path names in config file')
86 is_valid = False
87 self.tomo_data_paths = [tomo_data_paths_indices[i][1] for i in range(self.num_tomo_stacks)]
88 self.tomo_data_indices = [msnc.get_trailing_int(tomo_data_paths_indices[i][0])
89 if msnc.get_trailing_int(tomo_data_paths_indices[i][0]) else None
90 for i in range(self.num_tomo_stacks)]
91 tomo_ref_height_indices = sorted({key:value for key,value in self.config.items()
92 if 'z_pos' in key}.items())
93 if self.num_tomo_stacks > 1 and len(tomo_ref_height_indices) != self.num_tomo_stacks:
94 logging.error(f'Incorrect number of tomography reference heights in config file')
95 is_valid = False
96 if len(tomo_ref_height_indices):
97 self.tomo_ref_heights = [
98 tomo_ref_height_indices[i][1] for i in range(self.num_tomo_stacks)]
99 else:
100 self.tomo_ref_heights = [0.0]*self.num_tomo_stacks
101
102 # Check tomo angle (theta) range
103 self.start_theta = self.config.get('start_theta', 0.)
104 if not msnc.is_num(self.start_theta, 0.):
105 msnc.illegal_value('start_theta', self.start_theta, 'config file')
106 is_valid = False
107 logging.debug(f'start_theta = {self.start_theta}')
108 self.end_theta = self.config.get('end_theta', 180.)
109 if not msnc.is_num(self.end_theta, self.start_theta):
110 msnc.illegal_value('end_theta', self.end_theta, 'config file')
111 is_valid = False
112 logging.debug(f'end_theta = {self.end_theta}')
113 self.num_thetas = self.config.get('num_thetas')
114 if not (self.num_thetas is None or msnc.is_int(self.num_thetas, 1)):
115 msnc.illegal_value('num_thetas', self.num_thetas, 'config file')
116 self.num_thetas = None
117 is_valid = False
118 logging.debug(f'num_thetas = {self.num_thetas}')
119
120 return is_valid
121
122 def _validate_yaml(self):
123 """Returns False if any required config parameter is illegal or missing.
124 """
125 is_valid = True
126
127 # Check for required first-level keys
128 pars_required = ['dark_field', 'bright_field', 'stack_info', 'detector']
129 pars_missing = []
130 is_valid = super().validate(pars_required, pars_missing)
131 if len(pars_missing) > 0:
132 logging.error(f'Missing item(s) in config file: {", ".join(pars_missing)}')
133 self.detector_id = self.config['detector'].get('id')
134
135 # Find tomography dark field images file/folder
136 self.tdf_data_path = self.config['dark_field'].get('data_path')
137
138 # Find tomography bright field images file/folder
139 self.tbf_data_path = self.config['bright_field'].get('data_path')
140
141 # Check number of tomography image stacks
142 stack_info = self.config['stack_info']
143 self.num_tomo_stacks = stack_info.get('num', 1)
144 if not msnc.is_int(self.num_tomo_stacks, 1):
145 self.num_tomo_stacks = None
146 msnc.illegal_value('stack_info:num', self.num_tomo_stacks, 'config file')
147 return False
148 logging.info(f'num_tomo_stacks = {self.num_tomo_stacks}')
149
150 # Find tomography images file/folders and stack parameters
151 stacks = stack_info.get('stacks')
152 if stacks is None or len(stacks) is not self.num_tomo_stacks:
153 msnc.illegal_value('stack_info:stacks', stacks, 'config file')
154 return False
155 self.tomo_data_paths = []
156 self.tomo_data_indices = []
157 self.tomo_ref_heights = []
158 for stack in stacks:
159 self.tomo_data_paths.append(stack.get('data_path'))
160 self.tomo_data_indices.append(stack.get('index'))
161 self.tomo_ref_heights.append(stack.get('ref_height'))
162
163 # Check tomo angle (theta) range
164 theta_range = self.config.get('theta_range')
165 if theta_range is None:
166 self.start_theta = 0.
167 self.end_theta = 180.
168 self.num_thetas = None
169 else:
170 self.start_theta = theta_range.get('start', 0.)
171 if not msnc.is_num(self.start_theta, 0.):
172 msnc.illegal_value('theta_range:start', self.start_theta, 'config file')
173 is_valid = False
174 logging.debug(f'start_theta = {self.start_theta}')
175 self.end_theta = theta_range.get('end', 180.)
176 if not msnc.is_num(self.end_theta, self.start_theta):
177 msnc.illegal_value('theta_range:end', self.end_theta, 'config file')
178 is_valid = False
179 logging.debug(f'end_theta = {self.end_theta}')
180 self.num_thetas = theta_range.get('num')
181 if self.num_thetas and not msnc.is_int(self.num_thetas, 1):
182 msnc.illegal_value('theta_range:num', self.num_thetas, 'config file')
183 self.num_thetas = None
184 is_valid = False
185 logging.debug(f'num_thetas = {self.num_thetas}')
186
187 return is_valid
188
189 def validate(self):
190 """Returns False if any required config parameter is illegal or missing.
191 """
192 is_valid = True
193
194 # Check work_folder (shared by both file formats)
195 work_folder = os.path.abspath(self.config.get('work_folder', ''))
196 if not os.path.isdir(work_folder):
197 msnc.illegal_value('work_folder', work_folder, 'config file')
198 is_valid = False
199 logging.info(f'work_folder: {work_folder}')
200
201 # Check data filetype (shared by both file formats)
202 self.data_filetype = self.config.get('data_filetype', 'tif')
203 if not isinstance(self.data_filetype, str) or (self.data_filetype != 'tif' and
204 self.data_filetype != 'h5'):
205 msnc.illegal_value('data_filetype', self.data_filetype, 'config file')
206
207 if self.suffix == '.yml' or self.suffix == '.yaml':
208 is_valid = self._validate_yaml()
209 elif self.suffix == '.txt':
210 is_valid = self._validate_txt()
211 else:
212 logging.error(f'Undefined or illegal config file extension: {self.suffix}')
213
214 # Find tomography bright field images file/folder
215 if self.tdf_data_path:
216 if self.data_filetype == 'h5':
217 if isinstance(self.tdf_data_path, str):
218 if not os.path.isabs(self.tdf_data_path):
219 self.tdf_data_path = os.path.abspath(
220 f'{work_folder}/{self.tdf_data_path}')
221 else:
222 msnc.illegal_value('tdf_data_path', tdf_data_fil, 'config file')
223 is_valid = False
224 else:
225 if isinstance(self.tdf_data_path, int):
226 self.tdf_data_path = os.path.abspath(
227 f'{work_folder}/{self.tdf_data_path}/nf')
228 elif isinstance(self.tdf_data_path, str):
229 if not os.path.isabs(self.tdf_data_path):
230 self.tdf_data_path = os.path.abspath(
231 f'{work_folder}/{self.tdf_data_path}')
232 else:
233 msnc.illegal_value('tdf_data_path', self.tdf_data_path, 'config file')
234 is_valid = False
235 logging.info(f'dark field images path = {self.tdf_data_path}')
236
237 # Find tomography bright field images file/folder
238 if self.tbf_data_path:
239 if self.data_filetype == 'h5':
240 if isinstance(self.tbf_data_path, str):
241 if not os.path.isabs(self.tbf_data_path):
242 self.tbf_data_path = os.path.abspath(
243 f'{work_folder}/{self.tbf_data_path}')
244 else:
245 msnc.illegal_value('tbf_data_path', tbf_data_fil, 'config file')
246 is_valid = False
247 else:
248 if isinstance(self.tbf_data_path, int):
249 self.tbf_data_path = os.path.abspath(
250 f'{work_folder}/{self.tbf_data_path}/nf')
251 elif isinstance(self.tbf_data_path, str):
252 if not os.path.isabs(self.tbf_data_path):
253 self.tbf_data_path = os.path.abspath(
254 f'{work_folder}/{self.tbf_data_path}')
255 else:
256 msnc.illegal_value('tbf_data_path', self.tbf_data_path, 'config file')
257 is_valid = False
258 logging.info(f'bright field images path = {self.tbf_data_path}')
259
260 # Find tomography images file/folders and stack parameters
261 tomo_data_paths = []
262 tomo_data_indices = []
263 tomo_ref_heights = []
264 for data_path, index, ref_height in zip(self.tomo_data_paths, self.tomo_data_indices,
265 self.tomo_ref_heights):
266 if self.data_filetype == 'h5':
267 if isinstance(data_path, str):
268 if not os.path.isabs(data_path):
269 data_path = os.path.abspath(f'{work_folder}/{data_path}')
270 else:
271 msnc.illegal_value(f'stack_info:stacks:data_path', data_path, 'config file')
272 is_valid = False
273 data_path = None
274 else:
275 if isinstance(data_path, int):
276 data_path = os.path.abspath(f'{work_folder}/{data_path}/nf')
277 elif isinstance(data_path, str):
278 if not os.path.isabs(data_path):
279 data_path = os.path.abspath(f'{work_folder}/{data_path}')
280 else:
281 msnc.illegal_value(f'stack_info:stacks:data_path', data_path, 'config file')
282 is_valid = False
283 data_path = None
284 tomo_data_paths.append(data_path)
285 if index is None:
286 if self.num_tomo_stacks > 1:
287 logging.error('Missing stack_info:stacks:index in config file')
288 is_valid = False
289 index = None
290 else:
291 index = 1
292 elif not isinstance(index, int):
293 msnc.illegal_value(f'stack_info:stacks:index', index, 'config file')
294 is_valid = False
295 index = None
296 tomo_data_indices.append(index)
297 if ref_height is None:
298 if self.num_tomo_stacks > 1:
299 logging.error('Missing stack_info:stacks:ref_height in config file')
300 is_valid = False
301 ref_height = None
302 else:
303 ref_height = 0.
304 elif not msnc.is_num(ref_height):
305 msnc.illegal_value(f'stack_info:stacks:ref_height', ref_height, 'config file')
306 is_valid = False
307 ref_height = None
308 # Set reference heights relative to first stack
309 if (len(tomo_ref_heights) and msnc.is_num(ref_height) and
310 msnc.is_num(tomo_ref_heights[0])):
311 ref_height = (round(ref_height-tomo_ref_heights[0], 3))
312 tomo_ref_heights.append(ref_height)
313 tomo_ref_heights[0] = 0.0
314 logging.info('tomography data paths:')
315 for i in range(self.num_tomo_stacks):
316 logging.info(f' {tomo_data_paths[i]}')
317 logging.info(f'tomography data path indices: {tomo_data_indices}')
318 logging.info(f'tomography reference heights: {tomo_ref_heights}')
319
320 # Update config in memory
321 if self.suffix == '.txt':
322 self.config = {}
323 dark_field = self.config.get('dark_field')
324 if dark_field is None:
325 self.config['dark_field'] = {'data_path' : self.tdf_data_path}
326 else:
327 self.config['dark_field']['data_path'] = self.tdf_data_path
328 bright_field = self.config.get('bright_field')
329 if bright_field is None:
330 self.config['bright_field'] = {'data_path' : self.tbf_data_path}
331 else:
332 self.config['bright_field']['data_path'] = self.tbf_data_path
333 detector = self.config.get('detector')
334 if detector is None:
335 self.config['detector'] = {'id' : self.detector_id}
336 else:
337 detector['id'] = self.detector_id
338 self.config['work_folder'] = work_folder
339 self.config['data_filetype'] = self.data_filetype
340 stack_info = self.config.get('stack_info')
341 if stack_info is None:
342 stacks = []
343 for i in range(self.num_tomo_stacks):
344 stacks.append({'data_path' : tomo_data_paths[i], 'index' : tomo_data_indices[i],
345 'ref_height' : tomo_ref_heights[i]})
346 self.config['stack_info'] = {'num' : self.num_tomo_stacks, 'stacks' : stacks}
347 else:
348 stack_info['num'] = self.num_tomo_stacks
349 stacks = stack_info.get('stacks')
350 for i,stack in enumerate(stacks):
351 stack['data_path'] = tomo_data_paths[i]
352 stack['index'] = tomo_data_indices[i]
353 stack['ref_height'] = tomo_ref_heights[i]
354 if self.num_thetas:
355 theta_range = {'start' : self.start_theta, 'end' : self.end_theta,
356 'num' : self.num_thetas}
357 else:
358 theta_range = {'start' : self.start_theta, 'end' : self.end_theta}
359 self.config['theta_range'] = theta_range
360
361 # Cleanup temporary validation variables
362 del self.tdf_data_path
363 del self.tbf_data_path
364 del self.detector_id
365 del self.data_filetype
366 del self.num_tomo_stacks
367 del self.tomo_data_paths
368 del self.tomo_data_indices
369 del self.tomo_ref_heights
370 del self.start_theta
371 del self.end_theta
372 del self.num_thetas
373
374 return is_valid
375
376 class Tomo:
377 """Processing tomography data with misalignment.
378 """
379
380 def __init__(self, config_file=None, config_dict=None, config_out=None, output_folder='.',
381 log_level='INFO', log_stream='tomo.log', galaxy_flag=False, test_mode=False):
382 """Initialize with optional config input file or dictionary
383 """
384 self.ncore = mp.cpu_count()
385 self.config_out = config_out
386 self.output_folder = output_folder
387 self.galaxy_flag = galaxy_flag
388 self.test_mode = test_mode
389 self.save_plots = True # Make input argument?
390 self.save_plots_only = True # Make input argument?
391 self.cf = None
392 self.config = None
393 self.is_valid = True
394 self.tdf = np.array([])
395 self.tbf = np.array([])
396 self.tomo_stacks = []
397 self.tomo_recon_stacks = []
398
399 # Validate input parameters
400 if config_file is not None and not os.path.isfile(config_file):
401 raise OSError(f'Invalid config_file input {config_file} {type(config_file)}')
402 if config_dict is not None and not isinstance(config_dict, dict):
403 raise ValueError(f'Invalid config_dict input {config_dict} {type(config_dict)}')
404 if config_out is not None:
405 if isinstance(config_out, str):
406 if isinstance(log_stream, str):
407 path = os.path.split(log_stream)[0]
408 if path and not os.path.isdir(path):
409 raise OSError(f'Invalid log_stream path')
410 else:
411 raise OSError(f'Invalid config_out input {config_out} {type(config_out)}')
412 if not os.path.isdir(output_folder):
413 raise OSError(f'Invalid output_folder input {output_folder} {type(output_folder)}')
414 if isinstance(log_stream, str):
415 path = os.path.split(log_stream)[0]
416 if path and not os.path.isdir(path):
417 raise OSError(f'Invalid log_stream path')
418 if not os.path.isabs(path):
419 log_stream = f'{output_folder}/{log_stream}'
420 if not isinstance(galaxy_flag, bool):
421 raise ValueError(f'Invalid galaxy_flag input {galaxy_flag} {type(galaxy_flag)}')
422 if not isinstance(test_mode, bool):
423 raise ValueError(f'Invalid test_mode input {test_mode} {type(test_mode)}')
424
425 # Set log configuration
426 logging_format = '%(asctime)s : %(levelname)s - %(module)s : %(funcName)s - %(message)s'
427 if self.test_mode:
428 self.save_plots_only = True
429 if isinstance(log_stream, str):
430 logging.basicConfig(filename=f'{log_stream}', filemode='w',
431 format=logging_format, level=logging.WARNING, force=True)
432 elif isinstance(log_stream, io.TextIOWrapper):
433 logging.basicConfig(filemode='w', format=logging_format, level=logging.WARNING,
434 stream=log_stream, force=True)
435 else:
436 raise ValueError(f'Invalid log_stream: {log_stream}')
437 logging.warning('Ignoring log_level argument in test mode')
438 else:
439 level = getattr(logging, log_level.upper(), None)
440 if not isinstance(level, int):
441 raise ValueError(f'Invalid log_level: {log_level}')
442 if log_stream is sys.stdout:
443 logging.basicConfig(format=logging_format, level=level, force=True,
444 handlers=[logging.StreamHandler()])
445 else:
446 if isinstance(log_stream, str):
447 logging.basicConfig(filename=f'{log_stream}', filemode='w',
448 format=logging_format, level=level, force=True)
449 elif isinstance(log_stream, io.TextIOWrapper):
450 logging.basicConfig(filemode='w', format=logging_format, level=level,
451 stream=log_stream, force=True)
452 else:
453 raise ValueError(f'Invalid log_stream: {log_stream}')
454 stream_handler = logging.StreamHandler()
455 logging.getLogger().addHandler(stream_handler)
456 stream_handler.setLevel(logging.WARNING)
457 stream_handler.setFormatter(logging.Formatter(logging_format))
458
459 # Check/set output config file name
460 if self.config_out is None:
461 self.config_out = f'{self.output_folder}/config.yaml'
462 elif (self.config_out is os.path.basename(self.config_out) and
463 not os.path.isabs(self.config_out)):
464 self.config_out = f'{self.output_folder}/{self.config_out}'
465
466 logging.info(f'ncore = {self.ncore}')
467 logging.debug(f'config_file = {config_file}')
468 logging.debug(f'config_dict = {config_dict}')
469 logging.debug(f'config_out = {self.config_out}')
470 logging.debug(f'output_folder = {self.output_folder}')
471 logging.debug(f'log_stream = {log_stream}')
472 logging.debug(f'log_level = {log_level}')
473 logging.debug(f'galaxy_flag = {self.galaxy_flag}')
474 logging.debug(f'test_mode = {self.test_mode}')
475
476 # Create config object and load config file
477 self.cf = ConfigTomo(config_file, config_dict)
478 if not self.cf.load_flag:
479 self.is_valid = False
480 return
481
482 if self.galaxy_flag:
483 self.ncore = 1 #RV can I set this? mp.cpu_count()
484 assert(self.output_folder == '.')
485 assert(self.test_mode is False)
486 self.save_plots = True
487 self.save_plots_only = True
488 else:
489 # Input validation is already performed during link_data_to_galaxy
490
491 # Check config file parameters
492 self.is_valid = self.cf.validate()
493
494 # Load detector info file
495 df = msnc.Detector(self.cf.config['detector']['id'])
496
497 # Check detector info file parameters
498 if df.validate():
499 pixel_size = df.getPixelSize()
500 num_rows, num_columns = df.getDimensions()
501 if not pixel_size or not num_rows or not num_columns:
502 self.is_valid = False
503 else:
504 pixel_size = None
505 num_rows = None
506 num_columns = None
507 self.is_valid = False
508
509 # Update config
510 self.cf.config['detector']['pixel_size'] = pixel_size
511 self.cf.config['detector']['rows'] = num_rows
512 self.cf.config['detector']['columns'] = num_columns
513 logging.debug(f'pixel_size = self.cf.config["detector"]["pixel_size"]')
514 logging.debug(f'num_rows: {self.cf.config["detector"]["rows"]}')
515 logging.debug(f'num_columns: {self.cf.config["detector"]["columns"]}')
516
517 # Safe config to file
518 if self.is_valid:
519 self.cf.saveFile(self.config_out)
520
521 # Initialize shortcut to config
522 self.config = self.cf.config
523
524 # Initialize tomography stack
525 num_tomo_stacks = self.config['stack_info']['num']
526 if num_tomo_stacks:
527 self.tomo_stacks = [np.array([]) for _ in range(num_tomo_stacks)]
528 self.tomo_recon_stacks = [np.array([]) for _ in range(num_tomo_stacks)]
529
530 logging.debug(f'save_plots = {self.save_plots}')
531 logging.debug(f'save_plots_only = {self.save_plots_only}')
532
533 def _selectImageRanges(self, available_stacks=None):
534 """Select image files to be included in analysis.
535 """
536 self.is_valid = True
537 stack_info = self.config['stack_info']
538 if available_stacks is None:
539 available_stacks = [False]*stack_info['num']
540 elif len(available_stacks) != stack_info['num']:
541 logging.warning('Illegal dimension of available_stacks in getImageFiles '+
542 f'({len(available_stacks)}');
543 available_stacks = [False]*stack_info['num']
544
545 # Check number of tomography angles/thetas
546 num_thetas = self.config['theta_range'].get('num')
547 if num_thetas is None:
548 num_thetas = pyip.inputInt('\nEnter the number of thetas (>0): ', greaterThan=0)
549 elif not msnc.is_int(num_thetas, 0):
550 msnc.illegal_value('num_thetas', num_thetas, 'config file')
551 self.is_valid = False
552 return
553 self.config['theta_range']['num'] = num_thetas
554 logging.debug(f'num_thetas = {self.config["theta_range"]["num"]}')
555
556 # Find tomography dark field images
557 dark_field = self.config['dark_field']
558 img_start = dark_field.get('img_start', -1)
559 img_offset = dark_field.get('img_offset', -1)
560 num_imgs = dark_field.get('num', 0)
561 if not self.test_mode:
562 img_start, img_offset, num_imgs = msnc.selectImageRange(img_start, img_offset,
563 num_imgs, 'dark field')
564 if img_start < 0 or num_imgs < 1:
565 logging.error('Unable to find suitable dark field images')
566 if dark_field['data_path']:
567 self.is_valid = False
568 dark_field['img_start'] = img_start
569 dark_field['img_offset'] = img_offset
570 dark_field['num'] = num_imgs
571 logging.debug(f'Dark field image start index: {dark_field["img_start"]}')
572 logging.debug(f'Dark field image offset: {dark_field["img_offset"]}')
573 logging.debug(f'Number of dark field images: {dark_field["num"]}')
574
575 # Find tomography bright field images
576 bright_field = self.config['bright_field']
577 img_start = bright_field.get('img_start', -1)
578 img_offset = bright_field.get('img_offset', -1)
579 num_imgs = bright_field.get('num', 0)
580 if not self.test_mode:
581 img_start, img_offset, num_imgs = msnc.selectImageRange(img_start, img_offset,
582 num_imgs, 'bright field')
583 if img_start < 0 or num_imgs < 1:
584 logging.error('Unable to find suitable bright field images')
585 if bright_field['data_path']:
586 self.is_valid = False
587 bright_field['img_start'] = img_start
588 bright_field['img_offset'] = img_offset
589 bright_field['num'] = num_imgs
590 logging.debug(f'Bright field image start index: {bright_field["img_start"]}')
591 logging.debug(f'Bright field image offset: {bright_field["img_offset"]}')
592 logging.debug(f'Number of bright field images: {bright_field["num"]}')
593
594 # Find tomography images
595 for i,stack in enumerate(stack_info['stacks']):
596 # Check if stack is already loaded or available
597 if self.tomo_stacks[i].size or available_stacks[i]:
598 continue
599 index = stack['index']
600 img_start = stack.get('img_start', -1)
601 img_offset = stack.get('img_offset', -1)
602 num_imgs = stack.get('num', 0)
603 if not self.test_mode:
604 img_start, img_offset, num_imgs = msnc.selectImageRange(img_start, img_offset,
605 num_imgs, f'tomography stack {index}', num_thetas)
606 if img_start < 0 or num_imgs != num_thetas:
607 logging.error('Unable to find suitable tomography images')
608 self.is_valid = False
609 stack['img_start'] = img_start
610 stack['img_offset'] = img_offset
611 stack['num'] = num_imgs
612 logging.debug(f'Tomography stack {index} image start index: {stack["img_start"]}')
613 logging.debug(f'Tomography stack {index} image offset: {stack["img_offset"]}')
614 logging.debug(f'Number of tomography images for stack {index}: {stack["num"]}')
615 available_stacks[i] = True
616
617 # Safe updated config to file
618 if self.is_valid:
619 self.cf.saveFile(self.config_out)
620
621 return
622
623 def _genDark(self, tdf_files, dark_field_pngname):
624 """Generate dark field.
625 """
626 # Load the dark field images
627 logging.debug('Loading dark field...')
628 dark_field = self.config['dark_field']
629 tdf_stack = msnc.loadImageStack(tdf_files, self.config['data_filetype'],
630 dark_field['img_offset'], dark_field['num'])
631
632 # Take median
633 self.tdf = np.median(tdf_stack, axis=0)
634 del tdf_stack
635
636 # RV make input of some kind (not always needed)
637 tdf_cutoff = 21
638 self.tdf[self.tdf > tdf_cutoff] = np.nan
639 tdf_mean = np.nanmean(self.tdf)
640 logging.debug(f'tdf_cutoff = {tdf_cutoff}')
641 logging.debug(f'tdf_mean = {tdf_mean}')
642 np.nan_to_num(self.tdf, copy=False, nan=tdf_mean, posinf=tdf_mean, neginf=0.)
643 if not self.test_mode and not self.galaxy_flag:
644 msnc.quickImshow(self.tdf, title='dark field', path=self.output_folder,
645 save_fig=self.save_plots, save_only=self.save_plots_only)
646 elif self.galaxy_flag:
647 msnc.quickImshow(self.tdf, title='dark field', name=dark_field_pngname,
648 save_fig=True, save_only=True)
649
650 def _genBright(self, tbf_files, bright_field_pngname):
651 """Generate bright field.
652 """
653 # Load the bright field images
654 logging.debug('Loading bright field...')
655 bright_field = self.config['bright_field']
656 tbf_stack = msnc.loadImageStack(tbf_files, self.config['data_filetype'],
657 bright_field['img_offset'], bright_field['num'])
658
659 # Take median
660 """Median or mean: It may be best to try the median because of some image
661 artifacts that arise due to crinkles in the upstream kapton tape windows
662 causing some phase contrast images to appear on the detector.
663 One thing that also may be useful in a future implementation is to do a
664 brightfield adjustment on EACH frame of the tomo based on a ROI in the
665 corner of the frame where there is no sample but there is the direct X-ray
666 beam because there is frame to frame fluctuations from the incoming beam.
667 We don’t typically account for them but potentially could.
668 """
669 self.tbf = np.median(tbf_stack, axis=0)
670 del tbf_stack
671
672 # Subtract dark field
673 if self.tdf.size:
674 self.tbf -= self.tdf
675 else:
676 logging.warning('Dark field unavailable')
677 if not self.test_mode and not self.galaxy_flag:
678 msnc.quickImshow(self.tbf, title='bright field', path=self.output_folder,
679 save_fig=self.save_plots, save_only=self.save_plots_only)
680 elif self.galaxy_flag:
681 msnc.quickImshow(self.tbf, title='bright field', name=bright_field_pngname,
682 save_fig=True, save_only=True)
683
684 def _setDetectorBounds(self, tomo_stack_files, tomo_field_pngname, detectorbounds_pngname):
685 """Set vertical detector bounds for image stack.
686 """
687 preprocess = self.config.get('preprocess')
688 if preprocess is None:
689 img_x_bounds = [None, None]
690 else:
691 img_x_bounds = preprocess.get('img_x_bounds', [0, self.tbf.shape[0]])
692 if self.test_mode:
693 # Update config and save to file
694 if preprocess is None:
695 self.cf.config['preprocess'] = {'img_x_bounds' : [0, self.tbf.shape[0]]}
696 else:
697 preprocess['img_x_bounds'] = img_x_bounds
698 self.cf.saveFile(self.config_out)
699 return
700
701 # Check reference heights
702 pixel_size = self.config['detector']['pixel_size']
703 if pixel_size is None:
704 raise ValueError('Detector pixel size unavailable')
705 if not self.tbf.size:
706 raise ValueError('Bright field unavailable')
707 num_x_min = None
708 num_tomo_stacks = self.config['stack_info']['num']
709 stacks = self.config['stack_info']['stacks']
710 if num_tomo_stacks > 1:
711 delta_z = stacks[1]['ref_height']-stacks[0]['ref_height']
712 for i in range(2, num_tomo_stacks):
713 delta_z = min(delta_z, stacks[i]['ref_height']-stacks[i-1]['ref_height'])
714 logging.debug(f'delta_z = {delta_z}')
715 num_x_min = int(delta_z/pixel_size)+1
716 logging.debug(f'num_x_min = {num_x_min}')
717 if num_x_min > self.tbf.shape[0]:
718 logging.warning('Image bounds and pixel size prevent seamless stacking')
719 num_x_min = self.tbf.shape[0]
720
721 # Select image bounds
722 if self.galaxy_flag:
723 if num_x_min is None or num_x_min >= self.tbf.shape[0]:
724 img_x_bounds = [0, self.tbf.shape[0]]
725 else:
726 margin = int(num_x_min/10)
727 offset = min(0, int((self.tbf.shape[0]-num_x_min)/2-margin))
728 img_x_bounds = [offset, num_x_min+offset+2*margin]
729 tomo_stack = msnc.loadImageStack(tomo_stack_files[0], self.config['data_filetype'],
730 stacks[0]['img_offset'], 1)
731 x_sum = np.sum(tomo_stack[0,:,:], 1)
732 title = f'tomography image at theta={self.config["theta_range"]["start"]}'
733 msnc.quickImshow(tomo_stack[0,:,:], title=title, name=tomo_field_pngname,
734 save_fig=True, save_only=True)
735 msnc.quickPlot((range(x_sum.size), x_sum),
736 ([img_x_bounds[0], img_x_bounds[0]], [x_sum.min(), x_sum.max()], 'r-'),
737 ([img_x_bounds[1]-1, img_x_bounds[1]-1], [x_sum.min(), x_sum.max()], 'r-'),
738 title='sum over theta and y', name=detectorbounds_pngname,
739 save_fig=True, save_only=True)
740
741 # Update config and save to file
742 if preprocess is None:
743 self.cf.config['preprocess'] = {'img_x_bounds' : img_x_bounds}
744 else:
745 preprocess['img_x_bounds'] = img_x_bounds
746 self.cf.saveFile(self.config_out)
747 return
748
749 # For one tomography stack only: load the first image
750 title = None
751 msnc.quickImshow(self.tbf, title='bright field')
752 if num_tomo_stacks == 1:
753 tomo_stack = msnc.loadImageStack(tomo_stack_files[0], self.config['data_filetype'],
754 stacks[0]['img_offset'], 1)
755 title = f'tomography image at theta={self.config["theta_range"]["start"]}'
756 msnc.quickImshow(tomo_stack[0,:,:], title=title)
757 tomo_or_bright = pyip.inputNum('\nSelect image bounds from bright field (0) or '+
758 'from first tomography image (1): ', min=0, max=1)
759 else:
760 print('\nSelect image bounds from bright field')
761 tomo_or_bright = 0
762 if tomo_or_bright:
763 x_sum = np.sum(tomo_stack[0,:,:], 1)
764 use_bounds = 'no'
765 if img_x_bounds[0] is not None and img_x_bounds[1] is not None:
766 if img_x_bounds[0] < 0:
767 msnc.illegal_value('img_x_bounds[0]', img_x_bounds[0], 'config file')
768 img_x_bounds[0] = 0
769 if not img_x_bounds[0] < img_x_bounds[1] <= x_sum.size:
770 msnc.illegal_value('img_x_bounds[1]', img_x_bounds[1], 'config file')
771 img_x_bounds[1] = x_sum.size
772 tomo_tmp = tomo_stack[0,:,:]
773 tomo_tmp[img_x_bounds[0],:] = tomo_stack[0,:,:].max()
774 tomo_tmp[img_x_bounds[1]-1,:] = tomo_stack[0,:,:].max()
775 title = f'tomography image at theta={self.config["theta_range"]["start"]}'
776 msnc.quickImshow(tomo_stack[0,:,:], title=title)
777 msnc.quickPlot((range(x_sum.size), x_sum),
778 ([img_x_bounds[0], img_x_bounds[0]], [x_sum.min(), x_sum.max()], 'r-'),
779 ([img_x_bounds[1]-1, img_x_bounds[1]-1], [x_sum.min(), x_sum.max()], 'r-'),
780 title='sum over theta and y')
781 print(f'lower bound = {img_x_bounds[0]} (inclusive)\n'+
782 f'upper bound = {img_x_bounds[1]} (exclusive)]')
783 use_bounds = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True)
784 if use_bounds == 'no':
785 img_x_bounds = msnc.selectImageBounds(tomo_stack[0,:,:], 0,
786 img_x_bounds[0], img_x_bounds[1], num_x_min,
787 f'tomography image at theta={self.config["theta_range"]["start"]}')
788 if num_x_min is not None and img_x_bounds[1]-img_x_bounds[0]+1 < num_x_min:
789 logging.warning('Image bounds and pixel size prevent seamless stacking')
790 tomo_tmp = tomo_stack[0,:,:]
791 tomo_tmp[img_x_bounds[0],:] = tomo_stack[0,:,:].max()
792 tomo_tmp[img_x_bounds[1]-1,:] = tomo_stack[0,:,:].max()
793 title = f'tomography image at theta={self.config["theta_range"]["start"]}'
794 msnc.quickImshow(tomo_stack[0,:,:], title=title, path=self.output_folder,
795 save_fig=self.save_plots, save_only=True)
796 msnc.quickPlot(range(img_x_bounds[0], img_x_bounds[1]),
797 x_sum[img_x_bounds[0]:img_x_bounds[1]],
798 title='sum over theta and y', path=self.output_folder,
799 save_fig=self.save_plots, save_only=True)
800 else:
801 x_sum = np.sum(self.tbf, 1)
802 use_bounds = 'no'
803 if img_x_bounds[0] is not None and img_x_bounds[1] is not None:
804 if img_x_bounds[0] < 0:
805 msnc.illegal_value('img_x_bounds[0]', img_x_bounds[0], 'config file')
806 img_x_bounds[0] = 0
807 if not img_x_bounds[0] < img_x_bounds[1] <= x_sum.size:
808 msnc.illegal_value('img_x_bounds[1]', img_x_bounds[1], 'config file')
809 img_x_bounds[1] = x_sum.size
810 msnc.quickPlot((range(x_sum.size), x_sum),
811 ([img_x_bounds[0], img_x_bounds[0]], [x_sum.min(), x_sum.max()], 'r-'),
812 ([img_x_bounds[1]-1, img_x_bounds[1]-1], [x_sum.min(), x_sum.max()], 'r-'),
813 title='sum over theta and y')
814 print(f'lower bound = {img_x_bounds[0]} (inclusive)\n'+
815 f'upper bound = {img_x_bounds[1]} (exclusive)]')
816 use_bounds = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True)
817 if use_bounds == 'no':
818 fit = msnc.fitStep(y=x_sum, model='rectangle', form='atan')
819 x_low = fit.get('center1', None)
820 x_upp = fit.get('center2', None)
821 if (x_low is not None and x_low >= 0 and x_upp is not None and
822 x_low < x_upp < x_sum.size):
823 x_low = int(x_low-(x_upp-x_low)/10)
824 if x_low < 0:
825 x_low = 0
826 x_upp = int(x_upp+(x_upp-x_low)/10)
827 if x_upp >= x_sum.size:
828 x_upp = x_sum.size
829 msnc.quickPlot((range(x_sum.size), x_sum),
830 ([x_low, x_low], [x_sum.min(), x_sum.max()], 'r-'),
831 ([x_upp, x_upp], [x_sum.min(), x_sum.max()], 'r-'),
832 title='sum over theta and y')
833 print(f'lower bound = {x_low} (inclusive)\nupper bound = {x_upp} (exclusive)]')
834 use_fit = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True)
835 if use_fit == 'no':
836 img_x_bounds = msnc.selectArrayBounds(x_sum, img_x_bounds[0], img_x_bounds[1],
837 num_x_min, 'sum over theta and y')
838 else:
839 img_x_bounds = [x_low, x_upp]
840 if num_x_min is not None and img_x_bounds[1]-img_x_bounds[0]+1 < num_x_min:
841 logging.warning('Image bounds and pixel size prevent seamless stacking')
842 msnc.quickPlot(range(img_x_bounds[0], img_x_bounds[1]),
843 x_sum[img_x_bounds[0]:img_x_bounds[1]],
844 title='sum over theta and y', path=self.output_folder,
845 save_fig=self.save_plots, save_only=True)
846 logging.debug(f'img_x_bounds: {img_x_bounds}')
847
848 if self.save_plots_only:
849 msnc.clearFig('bright field')
850 msnc.clearFig('sum over theta and y')
851 if title:
852 msnc.clearFig(title)
853
854 # Update config and save to file
855 if preprocess is None:
856 self.cf.config['preprocess'] = {'img_x_bounds' : img_x_bounds}
857 else:
858 preprocess['img_x_bounds'] = img_x_bounds
859 self.cf.saveFile(self.config_out)
860
861 def _setZoomOrSkip(self):
862 """Set zoom and/or theta skip to reduce memory the requirement for the analysis.
863 """
864 preprocess = self.config.get('preprocess')
865 zoom_perc = 100
866 if not self.galaxy_flag:
867 if preprocess is None or 'zoom_perc' not in preprocess:
868 if pyip.inputYesNo(
869 '\nDo you want to zoom in to reduce memory requirement (y/[n])? ',
870 blank=True) == 'yes':
871 zoom_perc = pyip.inputInt(' Enter zoom percentage [1, 100]: ',
872 min=1, max=100)
873 else:
874 zoom_perc = preprocess['zoom_perc']
875 if msnc.is_num(zoom_perc, 1., 100.):
876 zoom_perc = int(zoom_perc)
877 else:
878 msnc.illegal_value('zoom_perc', zoom_perc, 'config file')
879 zoom_perc = 100
880 num_theta_skip = 0
881 if not self.galaxy_flag:
882 if preprocess is None or 'num_theta_skip' not in preprocess:
883 if pyip.inputYesNo(
884 'Do you want to skip thetas to reduce memory requirement (y/[n])? ',
885 blank=True) == 'yes':
886 num_theta_skip = pyip.inputInt(' Enter the number skip theta interval'+
887 f' [0, {self.num_thetas-1}]: ', min=0, max=self.num_thetas-1)
888 else:
889 num_theta_skip = preprocess['num_theta_skip']
890 if not msnc.is_int(num_theta_skip, 0):
891 msnc.illegal_value('num_theta_skip', num_theta_skip, 'config file')
892 num_theta_skip = 0
893 logging.info(f'zoom_perc = {zoom_perc}')
894 logging.info(f'num_theta_skip = {num_theta_skip}')
895
896 # Update config and save to file
897 if preprocess is None:
898 self.cf.config['preprocess'] = {'zoom_perc' : zoom_perc,
899 'num_theta_skip' : num_theta_skip}
900 else:
901 preprocess['zoom_perc'] = zoom_perc
902 preprocess['num_theta_skip'] = num_theta_skip
903 self.cf.saveFile(self.config_out)
904
905 def _loadTomo(self, base_name, index, required=False):
906 """Load a tomography stack.
907 """
908 # stack order: row,theta,column
909 zoom_perc = None
910 preprocess = self.config.get('preprocess')
911 if preprocess:
912 zoom_perc = preprocess.get('zoom_perc')
913 if zoom_perc is None or zoom_perc == 100:
914 title = f'{base_name} fullres'
915 else:
916 title = f'{base_name} {zoom_perc}p'
917 title += f'_{index}'
918 tomo_file = re.sub(r"\s+", '_', f'{self.output_folder}/{title}.npy')
919 load_flag = 'no'
920 available = False
921 if os.path.isfile(tomo_file):
922 available = True
923 if required:
924 load_flag = 'yes'
925 else:
926 load_flag = pyip.inputYesNo(f'\nDo you want to load {tomo_file} (y/n)? ')
927 stack = np.array([])
928 if load_flag == 'yes':
929 t0 = time()
930 logging.info(f'Loading {tomo_file} ...')
931 try:
932 stack = np.load(tomo_file)
933 except IOError or ValueError:
934 stack = np.array([])
935 logging.error(f'Error loading {tomo_file}')
936 logging.info(f'... done in {time()-t0:.2f} seconds!')
937 if stack.size:
938 msnc.quickImshow(stack[:,0,:], title=title, path=self.output_folder,
939 save_fig=self.save_plots, save_only=self.save_plots_only)
940 return stack, available
941
942 def _saveTomo(self, base_name, stack, index=None):
943 """Save a tomography stack.
944 """
945 zoom_perc = None
946 preprocess = self.config.get('preprocess')
947 if preprocess:
948 zoom_perc = preprocess.get('zoom_perc')
949 if zoom_perc is None or zoom_perc == 100:
950 title = f'{base_name} fullres'
951 else:
952 title = f'{base_name} {zoom_perc}p'
953 if index:
954 title += f'_{index}'
955 tomo_file = re.sub(r"\s+", '_', f'{self.output_folder}/{title}.npy')
956 t0 = time()
957 logging.info(f'Saving {tomo_file} ...')
958 np.save(tomo_file, stack)
959 logging.info(f'... done in {time()-t0:.2f} seconds!')
960
961 def _genTomo(self, tomo_stack_files, available_stacks):
962 """Generate tomography fields.
963 """
964 stacks = self.config['stack_info']['stacks']
965 assert(len(self.tomo_stacks) == self.config['stack_info']['num'])
966 assert(len(self.tomo_stacks) == len(stacks))
967 if len(available_stacks) != len(stacks):
968 logging.warning('Illegal dimension of available_stacks in _genTomo'+
969 f'({len(available_stacks)}');
970 available_stacks = [False]*self.num_tomo_stacks
971
972 preprocess = self.config.get('preprocess')
973 if preprocess is None:
974 img_x_bounds = [0, self.tbf.shape[0]]
975 img_y_bounds = [0, self.tbf.shape[1]]
976 zoom_perc = preprocess['zoom_perc']
977 num_theta_skip = preprocess['num_theta_skip']
978 else:
979 img_x_bounds = preprocess.get('img_x_bounds', [0, self.tbf.shape[0]])
980 img_y_bounds = preprocess.get('img_y_bounds', [0, self.tbf.shape[1]])
981 zoom_perc = 100
982 num_theta_skip = 0
983
984 if self.tdf.size:
985 tdf = self.tdf[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
986 else:
987 logging.warning('Dark field unavailable')
988 if not self.tbf.size:
989 raise ValueError('Bright field unavailable')
990 tbf = self.tbf[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
991
992 for i,stack in enumerate(stacks):
993 # Check if stack is already loaded or available
994 if self.tomo_stacks[i].size or available_stacks[i]:
995 continue
996
997 # Load a stack of tomography images
998 t0 = time()
999 tomo_stack = msnc.loadImageStack(tomo_stack_files[i], self.config['data_filetype'],
1000 stack['img_offset'], self.config['theta_range']['num'], num_theta_skip,
1001 img_x_bounds, img_y_bounds)
1002 tomo_stack = tomo_stack.astype('float64')
1003 logging.debug(f'loading took {time()-t0:.2f} seconds!')
1004
1005 # Subtract dark field
1006 if self.tdf.size:
1007 t0 = time()
1008 with set_numexpr_threads(self.ncore):
1009 ne.evaluate('tomo_stack-tdf', out=tomo_stack)
1010 logging.debug(f'subtracting dark field took {time()-t0:.2f} seconds!')
1011
1012 # Normalize
1013 t0 = time()
1014 with set_numexpr_threads(self.ncore):
1015 ne.evaluate('tomo_stack/tbf', out=tomo_stack, truediv=True)
1016 logging.debug(f'normalizing took {time()-t0:.2f} seconds!')
1017
1018 # Remove non-positive values and linearize data
1019 t0 = time()
1020 cutoff = 1.e-6
1021 with set_numexpr_threads(self.ncore):
1022 ne.evaluate('where(tomo_stack<cutoff, cutoff, tomo_stack)', out=tomo_stack)
1023 with set_numexpr_threads(self.ncore):
1024 ne.evaluate('-log(tomo_stack)', out=tomo_stack)
1025 logging.debug('removing non-positive values and linearizing data took '+
1026 f'{time()-t0:.2f} seconds!')
1027
1028 # Get rid of nans/infs that may be introduced by normalization
1029 t0 = time()
1030 np.where(np.isfinite(tomo_stack), tomo_stack, 0.)
1031 logging.debug(f'remove nans/infs took {time()-t0:.2f} seconds!')
1032
1033 # Downsize tomography stack to smaller size
1034 tomo_stack = tomo_stack.astype('float32')
1035 if not self.galaxy_flag:
1036 index = stack['index']
1037 title = f'red stack fullres {index}'
1038 if not self.test_mode:
1039 msnc.quickImshow(tomo_stack[0,:,:], title=title, path=self.output_folder,
1040 save_fig=self.save_plots, save_only=self.save_plots_only)
1041 if zoom_perc != 100:
1042 t0 = time()
1043 logging.info(f'Zooming in ...')
1044 tomo_zoom_list = []
1045 for j in range(tomo_stack.shape[0]):
1046 tomo_zoom = spi.zoom(tomo_stack[j,:,:], 0.01*zoom_perc)
1047 tomo_zoom_list.append(tomo_zoom)
1048 tomo_stack = np.stack([tomo_zoom for tomo_zoom in tomo_zoom_list])
1049 logging.info(f'... done in {time()-t0:.2f} seconds!')
1050 del tomo_zoom_list
1051 if not self.galaxy_flag:
1052 title = f'red stack {zoom_perc}p {index}'
1053 if not self.test_mode:
1054 msnc.quickImshow(tomo_stack[0,:,:], title=title, path=self.output_folder,
1055 save_fig=self.save_plots, save_only=self.save_plots_only)
1056
1057 # Convert tomography stack from theta,row,column to row,theta,column
1058 tomo_stack = np.swapaxes(tomo_stack, 0, 1)
1059
1060 # Save tomography stack to file
1061 if not self.galaxy_flag:
1062 if not self.test_mode:
1063 self._saveTomo('red stack', tomo_stack, index)
1064 else:
1065 np.savetxt(self.output_folder+f'red_stack_{index}.txt',
1066 tomo_stack[0,:,:], fmt='%.6e')
1067
1068 # Combine stacks
1069 t0 = time()
1070 self.tomo_stacks[i] = tomo_stack
1071 logging.debug(f'combining nstack took {time()-t0:.2f} seconds!')
1072
1073 # Update config and save to file
1074 stack['preprocessed'] = True
1075 self.cf.saveFile(self.config_out)
1076
1077 if self.tdf.size:
1078 del tdf
1079 del tbf
1080
1081 def _reconstructOnePlane(self, tomo_plane_T, center, thetas_deg, eff_pixel_size,
1082 cross_sectional_dim, plot_sinogram=True):
1083 """Invert the sinogram for a single tomography plane.
1084 """
1085 # tomo_plane_T index order: column,theta
1086 assert(0 <= center < tomo_plane_T.shape[0])
1087 center_offset = center-tomo_plane_T.shape[0]/2
1088 two_offset = 2*int(np.round(center_offset))
1089 two_offset_abs = np.abs(two_offset)
1090 max_rad = int(0.5*(cross_sectional_dim/eff_pixel_size)*1.1) # 10% slack to avoid edge effects
1091 dist_from_edge = max(1, int(np.floor((tomo_plane_T.shape[0]-two_offset_abs)/2.)-max_rad))
1092 if two_offset >= 0:
1093 logging.debug(f'sinogram range = [{two_offset+dist_from_edge}, {-dist_from_edge}]')
1094 sinogram = tomo_plane_T[two_offset+dist_from_edge:-dist_from_edge,:]
1095 else:
1096 logging.debug(f'sinogram range = [{dist_from_edge}, {two_offset-dist_from_edge}]')
1097 sinogram = tomo_plane_T[dist_from_edge:two_offset-dist_from_edge,:]
1098 if plot_sinogram:
1099 msnc.quickImshow(sinogram.T, f'sinogram center offset{center_offset:.2f}',
1100 path=self.output_folder, save_fig=self.save_plots,
1101 save_only=self.save_plots_only, aspect='auto')
1102
1103 # Inverting sinogram
1104 t0 = time()
1105 recon_sinogram = iradon(sinogram, theta=thetas_deg, circle=True)
1106 logging.debug(f'inverting sinogram took {time()-t0:.2f} seconds!')
1107 del sinogram
1108
1109 # Removing ring artifacts
1110 # RV parameters for the denoise, gaussian, and ring removal will be different for different feature sizes
1111 t0 = time()
1112 # recon_sinogram = filters.gaussian(recon_sinogram, 3.0)
1113 recon_sinogram = spi.gaussian_filter(recon_sinogram, 0.5)
1114 recon_clean = np.expand_dims(recon_sinogram, axis=0)
1115 del recon_sinogram
1116 recon_clean = tomopy.misc.corr.remove_ring(recon_clean, rwidth=17)
1117 logging.debug(f'filtering and removing ring artifact took {time()-t0:.2f} seconds!')
1118 return recon_clean
1119
1120 def _plotEdgesOnePlane(self, recon_plane, base_name, weight=0.001):
1121 # RV parameters for the denoise, gaussian, and ring removal will be different for different feature sizes
1122 edges = denoise_tv_chambolle(recon_plane, weight = weight)
1123 vmax = np.max(edges[0,:,:])
1124 vmin = -vmax
1125 msnc.quickImshow(edges[0,:,:], f'{base_name} coolwarm', path=self.output_folder,
1126 save_fig=self.save_plots, save_only=self.save_plots_only, cmap='coolwarm')
1127 msnc.quickImshow(edges[0,:,:], f'{base_name} gray', path=self.output_folder,
1128 save_fig=self.save_plots, save_only=self.save_plots_only, cmap='gray',
1129 vmin=vmin, vmax=vmax)
1130 del edges
1131
1132 def _findCenterOnePlane(self, sinogram, row, thetas_deg, eff_pixel_size, cross_sectional_dim,
1133 tol=0.1):
1134 """Find center for a single tomography plane.
1135 """
1136 # sinogram index order: theta,column
1137 # need index order column,theta for iradon, so take transpose
1138 sinogram_T = sinogram.T
1139 center = sinogram.shape[1]/2
1140
1141 # try automatic center finding routines for initial value
1142 tomo_center = tomopy.find_center_vo(sinogram)
1143 center_offset_vo = tomo_center-center
1144 print(f'Center at row {row} using Nghia Vo’s method = {center_offset_vo:.2f}')
1145 recon_plane = self._reconstructOnePlane(sinogram_T, tomo_center, thetas_deg,
1146 eff_pixel_size, cross_sectional_dim, False)
1147 base_name=f'edges row{row} center_offset_vo{center_offset_vo:.2f}'
1148 self._plotEdgesOnePlane(recon_plane, base_name)
1149 if pyip.inputYesNo('Try finding center using phase correlation (y/[n])? ',
1150 blank=True) == 'yes':
1151 tomo_center = tomopy.find_center_pc(sinogram, sinogram, tol=0.1,
1152 rotc_guess=tomo_center)
1153 error = 1.
1154 while error > tol:
1155 prev = tomo_center
1156 tomo_center = tomopy.find_center_pc(sinogram, sinogram, tol=tol,
1157 rotc_guess=tomo_center)
1158 error = np.abs(tomo_center-prev)
1159 center_offset = tomo_center-center
1160 print(f'Center at row {row} using phase correlation = {center_offset:.2f}')
1161 recon_plane = self._reconstructOnePlane(sinogram_T, tomo_center, thetas_deg,
1162 eff_pixel_size, cross_sectional_dim, False)
1163 base_name=f'edges row{row} center_offset{center_offset:.2f}'
1164 self._plotEdgesOnePlane(recon_plane, base_name)
1165 if pyip.inputYesNo('Accept a center location ([y]) or continue search (n)? ',
1166 blank=True) != 'no':
1167 del sinogram_T
1168 del recon_plane
1169 center_offset = pyip.inputNum(
1170 f' Enter chosen center offset [{-int(center)}, {int(center)}] '+
1171 f'([{center_offset_vo}])): ', blank=True)
1172 if center_offset == '':
1173 center_offset = center_offset_vo
1174 return float(center_offset)
1175
1176 while True:
1177 center_offset_low = pyip.inputInt('\nEnter lower bound for center offset '+
1178 f'[{-int(center)}, {int(center)}]: ', min=-int(center), max=int(center))
1179 center_offset_upp = pyip.inputInt('Enter upper bound for center offset '+
1180 f'[{center_offset_low}, {int(center)}]: ',
1181 min=center_offset_low, max=int(center))
1182 if center_offset_upp == center_offset_low:
1183 center_offset_step = 1
1184 else:
1185 center_offset_step = pyip.inputInt('Enter step size for center offset search '+
1186 f'[1, {center_offset_upp-center_offset_low}]: ',
1187 min=1, max=center_offset_upp-center_offset_low)
1188 for center_offset in range(center_offset_low, center_offset_upp+center_offset_step,
1189 center_offset_step):
1190 logging.info(f'center_offset = {center_offset}')
1191 recon_plane = self._reconstructOnePlane(sinogram_T, center_offset+center,
1192 thetas_deg, eff_pixel_size, cross_sectional_dim, False)
1193 base_name=f'edges row{row} center_offset{center_offset}'
1194 self._plotEdgesOnePlane(recon_plane, base_name)
1195 if pyip.inputInt('\nContinue (0) or end the search (1): ', min=0, max=1):
1196 break
1197
1198 del sinogram_T
1199 del recon_plane
1200 center_offset = pyip.inputNum(f' Enter chosen center offset '+
1201 f'[{-int(center)}, {int(center)}]: ', min=-int(center), max=int(center))
1202 return float(center_offset)
1203
1204 def _reconstructOneTomoStack(self, tomo_stack, thetas, row_bounds=None,
1205 center_offsets=[], sigma=0.1, rwidth=30, ncore=1, algorithm='gridrec',
1206 run_secondary_sirt=False, secondary_iter=100):
1207 """reconstruct a single tomography stack.
1208 """
1209 # stack order: row,theta,column
1210 # thetas must be in radians
1211 # centers_offset: tomography axis shift in pixels relative to column center
1212 # RV should we remove stripes?
1213 # https://tomopy.readthedocs.io/en/latest/api/tomopy.prep.stripe.html
1214 # RV should we remove rings?
1215 # https://tomopy.readthedocs.io/en/latest/api/tomopy.misc.corr.html
1216 # RV: Add an option to do (extra) secondary iterations later or to do some sort of convergence test?
1217 if row_bounds is None:
1218 row_bounds = [0, tomo_stack.shape[0]]
1219 else:
1220 if not (0 <= row_bounds[0] <= row_bounds[1] <= tomo_stack.shape[0]):
1221 raise ValueError('Illegal row bounds in reconstructOneTomoStack')
1222 if thetas.size != tomo_stack.shape[1]:
1223 raise ValueError('theta dimension mismatch in reconstructOneTomoStack')
1224 if not len(center_offsets):
1225 centers = np.zeros((row_bounds[1]-row_bounds[0]))
1226 elif len(center_offsets) == 2:
1227 centers = np.linspace(center_offsets[0], center_offsets[1],
1228 row_bounds[1]-row_bounds[0])
1229 else:
1230 if center_offsets.size != row_bounds[1]-row_bounds[0]:
1231 raise ValueError('center_offsets dimension mismatch in reconstructOneTomoStack')
1232 centers = center_offsets
1233 centers += tomo_stack.shape[2]/2
1234 if True:
1235 tomo_stack = tomopy.prep.stripe.remove_stripe_fw(tomo_stack[row_bounds[0]:row_bounds[1]],
1236 sigma=sigma, ncore=ncore)
1237 else:
1238 tomo_stack = tomo_stack[row_bounds[0]:row_bounds[1]]
1239 tomo_recon_stack = tomopy.recon(tomo_stack, thetas, centers, sinogram_order=True,
1240 algorithm=algorithm, ncore=ncore)
1241 if run_secondary_sirt and secondary_iter > 0:
1242 #options = {'method':'SIRT_CUDA', 'proj_type':'cuda', 'num_iter':secondary_iter}
1243 #RV: doesn't work for me: "Error: CUDA error 803: system has unsupported display driver /
1244 # cuda driver combination."
1245 #options = {'method':'SIRT', 'proj_type':'linear', 'MinConstraint': 0, 'num_iter':secondary_iter}
1246 #SIRT did not finish while running overnight
1247 #options = {'method':'SART', 'proj_type':'linear', 'num_iter':secondary_iter}
1248 options = {'method':'SART', 'proj_type':'linear', 'MinConstraint': 0, 'num_iter':secondary_iter}
1249 tomo_recon_stack = tomopy.recon(tomo_stack, thetas, centers, init_recon=tomo_recon_stack,
1250 options=options, sinogram_order=True, algorithm=tomopy.astra, ncore=ncore)
1251 if True:
1252 tomopy.misc.corr.remove_ring(tomo_recon_stack, rwidth=rwidth, out=tomo_recon_stack)
1253 return tomo_recon_stack
1254
1255 def findImageFiles(self):
1256 """Find all available image files.
1257 """
1258 self.is_valid = True
1259
1260 # Find dark field images
1261 dark_field = self.config['dark_field']
1262 img_start, num_imgs, dark_files = msnc.findImageFiles(
1263 dark_field['data_path'], self.config['data_filetype'], 'dark field')
1264 if img_start < 0 or num_imgs < 1:
1265 logging.error('Unable to find suitable dark field images')
1266 if dark_field['data_path']:
1267 self.is_valid = False
1268 dark_field['num'] = num_imgs
1269 dark_field['img_start'] = img_start
1270 logging.info(f'Number of dark field images = {dark_field["num"]}')
1271 logging.info(f'Dark field image start index = {dark_field["img_start"]}')
1272
1273 # Find bright field images
1274 bright_field = self.config['bright_field']
1275 img_start, num_imgs, bright_files = msnc.findImageFiles(
1276 bright_field['data_path'], self.config['data_filetype'], 'bright field')
1277 if img_start < 0 or num_imgs < 1:
1278 logging.error('Unable to find suitable bright field images')
1279 self.is_valid = False
1280 bright_field['num'] = num_imgs
1281 bright_field['img_start'] = img_start
1282 logging.info(f'Number of bright field images = {bright_field["num"]}')
1283 logging.info(f'Bright field image start index = {bright_field["img_start"]}')
1284
1285 # Find tomography images
1286 tomo_stack_files = []
1287 for stack in self.config['stack_info']['stacks']:
1288 index = stack['index']
1289 img_start, num_imgs, tomo_files = msnc.findImageFiles(
1290 stack['data_path'], self.config['data_filetype'], f'tomography set {index}')
1291 if img_start < 0 or num_imgs < 1:
1292 logging.error('Unable to find suitable tomography images')
1293 self.is_valid = False
1294 stack['num'] = num_imgs
1295 stack['img_start'] = img_start
1296 logging.info(f'Number of tomography images for set {index} = {stack["num"]}')
1297 logging.info(f'Tomography set {index} image start index = {stack["img_start"]}')
1298 tomo_stack_files.append(tomo_files)
1299 del tomo_files
1300
1301 # Safe updated config
1302 if self.is_valid:
1303 self.cf.saveFile(self.config_out)
1304
1305 return dark_files, bright_files, tomo_stack_files
1306
1307 def genTomoStacks(self, tdf_files=None, tbf_files=None, tomo_stack_files=None,
1308 dark_field_pngname=None, bright_field_pngname=None, tomo_field_pngname=None,
1309 detectorbounds_pngname=None, output_name=None):
1310 """Preprocess tomography images.
1311 """
1312 # Try loading any already preprocessed stacks (skip in Galaxy)
1313 # preprocessed stack order for each one in stack: row,theta,column
1314 stack_info = self.config['stack_info']
1315 stacks = stack_info['stacks']
1316 num_tomo_stacks = stack_info['num']
1317 assert(num_tomo_stacks == len(self.tomo_stacks))
1318 available_stacks = [False]*num_tomo_stacks
1319 if self.galaxy_flag:
1320 assert(tdf_files is None or isinstance(tdf_files, list))
1321 assert(isinstance(tbf_files, list))
1322 assert(isinstance(tomo_stack_files, list))
1323 assert(num_tomo_stacks == len(tomo_stack_files))
1324 assert(isinstance(dark_field_pngname, str))
1325 assert(isinstance(bright_field_pngname, str))
1326 assert(isinstance(tomo_field_pngname, str))
1327 assert(isinstance(detectorbounds_pngname, str))
1328 assert(isinstance(output_name, str))
1329 else:
1330 if tdf_files:
1331 logging.warning('Ignoring tdf_files in genTomoStacks (only for Galaxy)')
1332 if tbf_files:
1333 logging.warning('Ignoring tbf_files in genTomoStacks (only for Galaxy)')
1334 if tomo_stack_files:
1335 logging.warning('Ignoring tomo_stack_files in genTomoStacks (only for Galaxy)')
1336 tdf_files, tbf_files, tomo_stack_files = self.findImageFiles()
1337 if not self.is_valid:
1338 return
1339 for i,stack in enumerate(stacks):
1340 if not self.tomo_stacks[i].size and stack.get('preprocessed', False):
1341 self.tomo_stacks[i], available_stacks[i] = \
1342 self._loadTomo('red stack', stack['index'])
1343 if dark_field_pngname:
1344 logging.warning('Ignoring dark_field_pngname in genTomoStacks (only for Galaxy)')
1345 if bright_field_pngname:
1346 logging.warning('Ignoring bright_field_pngname in genTomoStacks (only for Galaxy)')
1347 if tomo_field_pngname:
1348 logging.warning('Ignoring tomo_field_pngname in genTomoStacks (only for Galaxy)')
1349 if detectorbounds_pngname:
1350 logging.warning('Ignoring detectorbounds_pngname in genTomoStacks '+
1351 '(only used in Galaxy)')
1352 if output_name:
1353 logging.warning('Ignoring output_name in genTomoStacks '+
1354 '(only used in Galaxy)')
1355
1356 # Preprocess any unloaded stacks
1357 if False in available_stacks:
1358 logging.debug('Preprocessing tomography images')
1359
1360 # Check required image files (skip in Galaxy)
1361 if not self.galaxy_flag:
1362 self._selectImageRanges(available_stacks)
1363 if not self.is_valid:
1364 return
1365
1366 # Generate dark field
1367 if tdf_files:
1368 self._genDark(tdf_files, dark_field_pngname)
1369
1370 # Generate bright field
1371 self._genBright(tbf_files, bright_field_pngname)
1372
1373 # Set vertical detector bounds for image stack
1374 self._setDetectorBounds(tomo_stack_files, tomo_field_pngname, detectorbounds_pngname)
1375
1376 # Set zoom and/or theta skip to reduce memory the requirement
1377 self._setZoomOrSkip()
1378
1379 # Generate tomography fields
1380 self._genTomo(tomo_stack_files, available_stacks)
1381
1382 # Save tomography stack to file
1383 if self.galaxy_flag:
1384 t0 = time()
1385 logging.info(f'Saving preprocessed tomography stack to file ...')
1386 save_stacks = {f'set_{stack["index"]}':tomo_stack
1387 for stack,tomo_stack in zip(stacks,self.tomo_stacks)}
1388 np.savez(output_name, **save_stacks)
1389 logging.info(f'... done in {time()-t0:.2f} seconds!')
1390
1391 del available_stacks
1392
1393 # Adjust sample reference height, update config and save to file
1394 preprocess = self.config.get('preprocess')
1395 if preprocess is None:
1396 img_x_bounds = [0, self.tbf.shape[0]]
1397 else:
1398 img_x_bounds = preprocess.get('img_x_bounds', [0, self.tbf.shape[0]])
1399 pixel_size = self.config['detector']['pixel_size']
1400 if pixel_size is None:
1401 raise ValueError('Detector pixel size unavailable')
1402 pixel_size *= img_x_bounds[0]
1403 for stack in stacks:
1404 stack['ref_height'] = stack['ref_height']+pixel_size
1405 self.cf.saveFile(self.config_out)
1406
1407 def findCenters(self):
1408 """Find rotation axis centers for the tomography stacks.
1409 """
1410 logging.debug('Find centers for tomography stacks')
1411 stacks = self.config['stack_info']['stacks']
1412 available_stacks = [stack['index'] for stack in stacks if stack.get('preprocessed', False)]
1413 logging.debug('Avaliable stacks: {available_stacks}')
1414
1415 # Check for valid available center stack index
1416 find_center = self.config.get('find_center')
1417 if find_center and 'center_stack_index' in find_center:
1418 center_stack_index = find_center['center_stack_index']
1419 if (not isinstance(center_stack_index, int) or
1420 center_stack_index not in available_stacks):
1421 msnc.illegal_value('center_stack_index', center_stack_index, 'config file')
1422 else:
1423 if self.test_mode:
1424 find_center['completed'] = True
1425 self.cf.saveFile(self.config_out)
1426 return
1427 print('\nFound calibration center offset info for stack '+
1428 f'{center_stack_index}')
1429 if pyip.inputYesNo('Do you want to use this again (y/n)? ') == 'yes':
1430 find_center['completed'] = True
1431 self.cf.saveFile(self.config_out)
1432 return
1433
1434 # Load the required preprocessed stack
1435 # preprocessed stack order: row,theta,column
1436 num_tomo_stacks = self.config['stack_info']['num']
1437 assert(len(stacks) == num_tomo_stacks)
1438 assert(len(self.tomo_stacks) == num_tomo_stacks)
1439 if num_tomo_stacks == 1:
1440 center_stack_index = stacks[0]['index']
1441 if not self.tomo_stacks[0].size:
1442 self.tomo_stacks[0], available = self._loadTomo('red stack', center_stack_index,
1443 required=True)
1444 center_stack = self.tomo_stacks[0]
1445 if not center_stack.size:
1446 logging.error('Unable to load the required preprocessed tomography stack')
1447 return
1448 assert(stacks[0].get('preprocessed', False) == True)
1449 else:
1450 while True:
1451 center_stack_index = pyip.inputInt('\nEnter tomography stack index to get '
1452 f'rotation axis centers {available_stacks}: ')
1453 while center_stack_index not in available_stacks:
1454 center_stack_index = pyip.inputInt('\nEnter tomography stack index to get '
1455 f'rotation axis centers {available_stacks}: ')
1456 tomo_stack_index = available_stacks.index(center_stack_index)
1457 if not self.tomo_stacks[tomo_stack_index].size:
1458 self.tomo_stacks[tomo_stack_index], available = self._loadTomo(
1459 'red stack', center_stack_index, required=True)
1460 center_stack = self.tomo_stacks[tomo_stack_index]
1461 if not center_stack.size:
1462 logging.error(f'Unable to load the {center_stack_index}th '+
1463 'preprocessed tomography stack, pick another one')
1464 else:
1465 break
1466 assert(stacks[tomo_stack_index].get('preprocessed', False) == True)
1467 if find_center is None:
1468 self.config['find_center'] = {'center_stack_index' : center_stack_index}
1469 find_center = self.config['find_center']
1470
1471 # Set thetas (in degrees)
1472 theta_range = self.config['theta_range']
1473 theta_start = theta_range['start']
1474 theta_end = theta_range['end']
1475 num_theta = theta_range['num']
1476 num_theta_skip = self.config['preprocess'].get('num_theta_skip', 0)
1477 thetas_deg = np.linspace(theta_start, theta_end, int(num_theta/(num_theta_skip+1)),
1478 endpoint=False)
1479
1480 # Get non-overlapping sample row boundaries
1481 zoom_perc = self.config['preprocess'].get('zoom_perc', 100)
1482 pixel_size = self.config['detector']['pixel_size']
1483 if pixel_size is None:
1484 raise ValueError('Detector pixel size unavailable')
1485 eff_pixel_size = 100.*pixel_size/zoom_perc
1486 logging.debug(f'eff_pixel_size = {eff_pixel_size}')
1487 tomo_ref_heights = [stack['ref_height'] for stack in stacks]
1488 if num_tomo_stacks == 1:
1489 n1 = 0
1490 height = center_stack.shape[0]*eff_pixel_size
1491 if pyip.inputYesNo('\nDo you want to reconstruct the full samply height '+
1492 f'({height:.3f} mm) (y/n)? ') == 'no':
1493 height = pyip.inputNum('\nEnter the desired reconstructed sample height '+
1494 f'in mm [0, {height:.3f}]: ', min=0, max=height)
1495 n1 = int(0.5*(center_stack.shape[0]-height/eff_pixel_size))
1496 else:
1497 n1 = int((1.+(tomo_ref_heights[0]+center_stack.shape[0]*eff_pixel_size-
1498 tomo_ref_heights[1])/eff_pixel_size)/2)
1499 n2 = center_stack.shape[0]-n1
1500 logging.info(f'n1 = {n1}, n2 = {n2} (n2-n1) = {(n2-n1)*eff_pixel_size:.3f} mm')
1501 if not center_stack.size:
1502 RuntimeError('Center stack not loaded')
1503 msnc.quickImshow(center_stack[:,0,:], title=f'center stack theta={theta_start}',
1504 path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only)
1505
1506 # Get cross sectional diameter in mm
1507 cross_sectional_dim = center_stack.shape[2]*eff_pixel_size
1508 logging.debug(f'cross_sectional_dim = {cross_sectional_dim}')
1509
1510 # Determine center offset at sample row boundaries
1511 logging.info('Determine center offset at sample row boundaries')
1512
1513 # Lower row center
1514 use_row = False
1515 use_center = False
1516 row = find_center.get('lower_row')
1517 if msnc.is_int(row, n1, n2-2):
1518 msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto')
1519 use_row = pyip.inputYesNo('\nCurrent row index for lower center = '
1520 f'{row}, use this value (y/n)? ')
1521 if self.save_plots_only:
1522 msnc.clearFig(f'theta={theta_start}')
1523 if use_row:
1524 center_offset = find_center.get('lower_center_offset')
1525 if msnc.is_num(center_offset):
1526 use_center = pyip.inputYesNo('Current lower center offset = '+
1527 f'{center_offset}, use this value (y/n)? ')
1528 if not use_center:
1529 if not use_row:
1530 msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto')
1531 row = pyip.inputInt('\nEnter row index to find lower center '+
1532 f'[[{n1}], {n2-2}]: ', min=n1, max=n2-2, blank=True)
1533 if row == '':
1534 row = n1
1535 if self.save_plots_only:
1536 msnc.clearFig(f'theta={theta_start}')
1537 # center_stack order: row,theta,column
1538 center_offset = self._findCenterOnePlane(center_stack[row,:,:], row, thetas_deg,
1539 eff_pixel_size, cross_sectional_dim)
1540 logging.info(f'Lower center offset = {center_offset}')
1541
1542 # Update config and save to file
1543 find_center['row_bounds'] = [n1, n2]
1544 find_center['lower_row'] = row
1545 find_center['lower_center_offset'] = center_offset
1546 self.cf.saveFile(self.config_out)
1547 lower_row = row
1548
1549 # Upper row center
1550 use_row = False
1551 use_center = False
1552 row = find_center.get('upper_row')
1553 if msnc.is_int(row, lower_row+1, n2-1):
1554 msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto')
1555 use_row = pyip.inputYesNo('\nCurrent row index for upper center = '
1556 f'{row}, use this value (y/n)? ')
1557 if self.save_plots_only:
1558 msnc.clearFig(f'theta={theta_start}')
1559 if use_row:
1560 center_offset = find_center.get('upper_center_offset')
1561 if msnc.is_num(center_offset):
1562 use_center = pyip.inputYesNo('Current upper center offset = '+
1563 f'{center_offset}, use this value (y/n)? ')
1564 if not use_center:
1565 if not use_row:
1566 msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto')
1567 row = pyip.inputInt('\nEnter row index to find upper center '+
1568 f'[{lower_row+1}, [{n2-1}]]: ', min=lower_row+1, max=n2-1, blank=True)
1569 if row == '':
1570 row = n2-1
1571 if self.save_plots_only:
1572 msnc.clearFig(f'theta={theta_start}')
1573 # center_stack order: row,theta,column
1574 center_offset = self._findCenterOnePlane(center_stack[row,:,:], row, thetas_deg,
1575 eff_pixel_size, cross_sectional_dim)
1576 logging.info(f'upper_center_offset = {center_offset}')
1577 del center_stack
1578
1579 # Update config and save to file
1580 find_center['upper_row'] = row
1581 find_center['upper_center_offset'] = center_offset
1582 find_center['completed'] = True
1583 self.cf.saveFile(self.config_out)
1584
1585 def checkCenters(self):
1586 """Check centers for the tomography stacks.
1587 """
1588 #RV TODO load all stacks and check at all stack boundaries
1589 return
1590 logging.debug('Check centers for tomography stacks')
1591 center_stack_index = self.config.get('center_stack_index')
1592 if center_stack_index is None:
1593 raise ValueError('Unable to read center_stack_index from config')
1594 center_stack_index = self.tomo_stacks[self.tomo_data_indices.index(center_stack_index)]
1595 lower_row = self.config.get('lower_row')
1596 if lower_row is None:
1597 raise ValueError('Unable to read lower_row from config')
1598 lower_center_offset = self.config.get('lower_center_offset')
1599 if lower_center_offset is None:
1600 raise ValueError('Unable to read lower_center_offset from config')
1601 upper_row = self.config.get('upper_row')
1602 if upper_row is None:
1603 raise ValueError('Unable to read upper_row from config')
1604 upper_center_offset = self.config.get('upper_center_offset')
1605 if upper_center_offset is None:
1606 raise ValueError('Unable to read upper_center_offset from config')
1607 center_slope = (upper_center_offset-lower_center_offset)/(upper_row-lower_row)
1608 shift = upper_center_offset-lower_center_offset
1609 if lower_row == 0:
1610 logging.warning(f'lower_row == 0: one row offset between both planes')
1611 else:
1612 lower_row -= 1
1613 lower_center_offset -= center_slope
1614
1615 # stack order: stack,row,theta,column
1616 if center_stack_index:
1617 stack1 = self.tomo_stacks[center_stack_index-1]
1618 stack2 = self.tomo_stacks[center_stack_index]
1619 if not stack1.size:
1620 logging.error(f'Unable to load required tomography stack {stack1}')
1621 elif not stack2.size:
1622 logging.error(f'Unable to load required tomography stack {stack1}')
1623 else:
1624 assert(0 <= lower_row < stack2.shape[0])
1625 assert(0 <= upper_row < stack1.shape[0])
1626 plane1 = stack1[upper_row,:]
1627 plane2 = stack2[lower_row,:]
1628 for i in range(-2, 3):
1629 shift_i = shift+2*i
1630 plane1_shifted = spi.shift(plane2, [0, shift_i])
1631 msnc.quickPlot((plane1[0,:],), (plane1_shifted[0,:],),
1632 title=f'stacks {stack1} {stack2} shifted {2*i} theta={self.start_theta}',
1633 path=self.output_folder, save_fig=self.save_plots,
1634 save_only=self.save_plots_only)
1635 if center_stack_index < self.num_tomo_stacks-1:
1636 stack1 = self.tomo_stacks[center_stack_index]
1637 stack2 = self.tomo_stacks[center_stack_index+1]
1638 if not stack1.size:
1639 logging.error(f'Unable to load required tomography stack {stack1}')
1640 elif not stack2.size:
1641 logging.error(f'Unable to load required tomography stack {stack1}')
1642 else:
1643 assert(0 <= lower_row < stack2.shape[0])
1644 assert(0 <= upper_row < stack1.shape[0])
1645 plane1 = stack1[upper_row,:]
1646 plane2 = stack2[lower_row,:]
1647 for i in range(-2, 3):
1648 shift_i = -shift+2*i
1649 plane1_shifted = spi.shift(plane2, [0, shift_i])
1650 msnc.quickPlot((plane1[0,:],), (plane1_shifted[0,:],),
1651 title=f'stacks {stack1} {stack2} shifted {2*i} theta={start_theta}',
1652 path=self.output_folder, save_fig=self.save_plots,
1653 save_only=self.save_plots_only)
1654 del plane1, plane2, plane1_shifted
1655
1656 # Update config file
1657 self.config = msnc.update('config.txt', 'check_centers', True, 'find_centers')
1658
1659 def reconstructTomoStacks(self):
1660 """Reconstruct tomography stacks.
1661 """
1662 logging.debug('Reconstruct tomography stacks')
1663
1664 # Get rotation axis rows and centers
1665 find_center = self.config['find_center']
1666 lower_row = find_center.get('lower_row')
1667 if lower_row is None:
1668 logging.error('Unable to read lower_row from config')
1669 return
1670 lower_center_offset = find_center.get('lower_center_offset')
1671 if lower_center_offset is None:
1672 logging.error('Unable to read lower_center_offset from config')
1673 return
1674 upper_row = find_center.get('upper_row')
1675 if upper_row is None:
1676 logging.error('Unable to read upper_row from config')
1677 return
1678 upper_center_offset = find_center.get('upper_center_offset')
1679 if upper_center_offset is None:
1680 logging.error('Unable to read upper_center_offset from config')
1681 return
1682 logging.debug(f'lower_row = {lower_row} upper_row = {upper_row}')
1683 assert(lower_row < upper_row)
1684 center_slope = (upper_center_offset-lower_center_offset)/(upper_row-lower_row)
1685
1686 # Set thetas (in radians)
1687 theta_range = self.config['theta_range']
1688 theta_start = theta_range['start']
1689 theta_end = theta_range['end']
1690 num_theta = theta_range['num']
1691 num_theta_skip = self.config['preprocess'].get('num_theta_skip', 0)
1692 thetas = np.radians(np.linspace(theta_start, theta_end,
1693 int(num_theta/(num_theta_skip+1)), endpoint=False))
1694
1695 # Reconstruct tomo stacks
1696 zoom_perc = self.config['preprocess'].get('zoom_perc', 100)
1697 if zoom_perc == 100:
1698 basetitle = 'recon stack full'
1699 else:
1700 basetitle = f'recon stack {zoom_perc}p'
1701 load_error = False
1702 stacks = self.config['stack_info']['stacks']
1703 for i,stack in enumerate(stacks):
1704 # Check if stack can be loaded
1705 # reconstructed stack order for each one in stack : row/z,x,y
1706 # preprocessed stack order for each one in stack: row,theta,column
1707 index = stack['index']
1708 available = False
1709 if stack.get('reconstructed', False):
1710 self.tomo_recon_stacks[i], available = self._loadTomo('recon stack', index)
1711 if self.tomo_recon_stacks[i].size or available:
1712 if self.tomo_stacks[i].size:
1713 self.tomo_stacks[i] = np.array([])
1714 assert(stack.get('reconstructed', False) == True)
1715 continue
1716 else:
1717 stack['reconstructed'] = False
1718 if not self.tomo_stacks[i].size:
1719 self.tomo_stacks[i], available = self._loadTomo('red stack', index,
1720 required=True)
1721 if not self.tomo_stacks[i].size:
1722 logging.error(f'Unable to load tomography stack {index} for reconstruction')
1723 load_error = True
1724 continue
1725 assert(0 <= lower_row < upper_row < self.tomo_stacks[i].shape[0])
1726 center_offsets = [lower_center_offset-lower_row*center_slope,
1727 upper_center_offset+(self.tomo_stacks[i].shape[0]-1-upper_row)*center_slope]
1728 t0 = time()
1729 self.tomo_recon_stacks[i]= self._reconstructOneTomoStack(self.tomo_stacks[i],
1730 thetas, center_offsets=center_offsets, sigma=0.1, ncore=self.ncore,
1731 algorithm='gridrec', run_secondary_sirt=True, secondary_iter=25)
1732 logging.info(f'Reconstruction of stack {index} took {time()-t0:.2f} seconds!')
1733 if not self.test_mode:
1734 row_slice = int(self.tomo_stacks[i].shape[0]/2)
1735 title = f'{basetitle} {index} slice{row_slice}'
1736 msnc.quickImshow(self.tomo_recon_stacks[i][row_slice,:,:], title=title,
1737 path=self.output_folder, save_fig=self.save_plots,
1738 save_only=self.save_plots_only)
1739 msnc.quickPlot(self.tomo_recon_stacks[i]
1740 [row_slice,int(self.tomo_recon_stacks[i].shape[2]/2),:],
1741 title=f'{title} cut{int(self.tomo_recon_stacks[i].shape[2]/2)}',
1742 path=self.output_folder, save_fig=self.save_plots,
1743 save_only=self.save_plots_only)
1744 self._saveTomo('recon stack', self.tomo_recon_stacks[i], index)
1745 # else:
1746 # np.savetxt(self.output_folder+f'recon_stack_{index}.txt',
1747 # self.tomo_recon_stacks[i][row_slice,:,:], fmt='%.6e')
1748 self.tomo_stacks[i] = np.array([])
1749
1750 # Update config and save to file
1751 stack['reconstructed'] = True
1752 self.cf.saveFile(self.config_out)
1753
1754 def combineTomoStacks(self):
1755 """Combine the reconstructed tomography stacks.
1756 """
1757 # stack order: stack,row(z),x,y
1758 logging.debug('Combine reconstructed tomography stacks')
1759 # Load any unloaded reconstructed stacks
1760 stacks = self.config['stack_info']['stacks']
1761 for i,stack in enumerate(stacks):
1762 if not self.tomo_recon_stacks[i].size and stack.get('reconstructed', False):
1763 self.tomo_recon_stacks[i], available = self._loadTomo('recon stack',
1764 stack['index'], required=True)
1765 if not self.tomo_recon_stacks[i].size or not available:
1766 logging.error(f'Unable to load reconstructed stack {stack["index"]}')
1767 return
1768 if i:
1769 if (self.tomo_recon_stacks[i].shape[1] != self.tomo_recon_stacks[0].shape[1] or
1770 self.tomo_recon_stacks[i].shape[1] != self.tomo_recon_stacks[0].shape[1]):
1771 logging.error('Incompatible reconstructed tomography stack dimensions')
1772 return
1773
1774 # Get center stack boundaries
1775 row_bounds = self.config['find_center']['row_bounds']
1776 if not msnc.is_index_range(row_bounds, 0, self.tomo_recon_stacks[0].shape[0]):
1777 msnc.illegal_value('row_bounds', row_bounds, 'config file')
1778 return
1779
1780 # Selecting xy bounds
1781 tomosum = 0
1782 #RV FIX :=
1783 #[tomosum := tomosum+np.sum(tomo_recon_stack, axis=(0,2)) for tomo_recon_stack in
1784 # self.tomo_recon_stacks]
1785 combine_stacks =self.config.get('combine_stacks')
1786 if combine_stacks and 'x_bounds' in combine_stacks:
1787 x_bounds = combine_stacks['x_bounds']
1788 if not msnc.is_index_range(x_bounds, 0, self.tomo_recon_stacks[0].shape[1]):
1789 msnc.illegal_value('x_bounds', x_bounds, 'config file')
1790 elif not self.test_mode:
1791 msnc.quickPlot(tomosum, title='recon stack sum yz')
1792 if pyip.inputYesNo('\nCurrent image x-bounds: '+
1793 f'[{x_bounds[0]}, {x_bounds[1]}], use these values ([y]/n)? ',
1794 blank=True) == 'no':
1795 if pyip.inputYesNo(
1796 'Do you want to change the image x-bounds ([y]/n)? ',
1797 blank=True) == 'no':
1798 x_bounds = [0, self.tomo_recon_stacks[0].shape[1]]
1799 else:
1800 x_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum yz')
1801 else:
1802 msnc.quickPlot(tomosum, title='recon stack sum yz')
1803 if pyip.inputYesNo('\nDo you want to change the image x-bounds (y/n)? ') == 'no':
1804 x_bounds = [0, self.tomo_recon_stacks[0].shape[1]]
1805 else:
1806 x_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum yz')
1807 if False and self.test_mode:
1808 np.savetxt(self.output_folder+'recon_stack_sum_yz.txt',
1809 tomosum[x_bounds[0]:x_bounds[1]], fmt='%.6e')
1810 if self.save_plots_only:
1811 msnc.clearFig('recon stack sum yz')
1812 tomosum = 0
1813 #RV FIX :=
1814 #[tomosum := tomosum+np.sum(tomo_recon_stack, axis=(0,1)) for tomo_recon_stack in
1815 # self.tomo_recon_stacks]
1816 if combine_stacks and 'y_bounds' in combine_stacks:
1817 y_bounds = combine_stacks['y_bounds']
1818 if not msnc.is_index_range(x_bounds, 0, self.tomo_recon_stacks[0].shape[1]):
1819 msnc.illegal_value('y_bounds', y_bounds, 'config file')
1820 elif not self.test_mode:
1821 msnc.quickPlot(tomosum, title='recon stack sum xz')
1822 if pyip.inputYesNo('\nCurrent image y-bounds: '+
1823 f'[{y_bounds[0]}, {y_bounds[1]}], use these values ([y]/n)? ',
1824 blank=True) == 'no':
1825 if pyip.inputYesNo(
1826 'Do you want to change the image y-bounds ([y]/n)? ',
1827 blank=True) == 'no':
1828 y_bounds = [0, self.tomo_recon_stacks[0].shape[1]]
1829 else:
1830 y_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum yz')
1831 else:
1832 msnc.quickPlot(tomosum, title='recon stack sum xz')
1833 if pyip.inputYesNo('\nDo you want to change the image y-bounds (y/n)? ') == 'no':
1834 y_bounds = [0, self.tomo_recon_stacks[0].shape[1]]
1835 else:
1836 y_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum xz')
1837 if False and self.test_mode:
1838 np.savetxt(self.output_folder+'recon_stack_sum_xz.txt',
1839 tomosum[y_bounds[0]:y_bounds[1]], fmt='%.6e')
1840 if self.save_plots_only:
1841 msnc.clearFig('recon stack sum xz')
1842
1843 # Combine reconstructed tomography stacks
1844 logging.info(f'Combining reconstructed stacks ...')
1845 t0 = time()
1846 num_tomo_stacks = self.config['stack_info']['num']
1847 if num_tomo_stacks == 1:
1848 low_bound = row_bounds[0]
1849 else:
1850 low_bound = 0
1851 tomo_recon_combined = self.tomo_recon_stacks[0][low_bound:row_bounds[1]:,
1852 x_bounds[0]:x_bounds[1],y_bounds[0]:y_bounds[1]]
1853 if num_tomo_stacks > 2:
1854 tomo_recon_combined = np.concatenate([tomo_recon_combined]+
1855 [self.tomo_recon_stacks[i][row_bounds[0]:row_bounds[1],
1856 x_bounds[0]:x_bounds[1],y_bounds[0]:y_bounds[1]]
1857 for i in range(1, num_tomo_stacks-1)])
1858 if num_tomo_stacks > 1:
1859 tomo_recon_combined = np.concatenate([tomo_recon_combined]+
1860 [self.tomo_recon_stacks[num_tomo_stacks-1][row_bounds[0]:,
1861 x_bounds[0]:x_bounds[1],y_bounds[0]:y_bounds[1]]])
1862 logging.info(f'... done in {time()-t0:.2f} seconds!')
1863 tomosum = np.sum(tomo_recon_combined, axis=(1,2))
1864 if self.test_mode:
1865 zoom_perc = self.config['preprocess'].get('zoom_perc', 100)
1866 filename = 'recon combined sum xy'
1867 if zoom_perc is None or zoom_perc == 100:
1868 filename += ' fullres.dat'
1869 else:
1870 filename += f' {zoom_perc}p.dat'
1871 msnc.quickPlot(tomosum, title='recon combined sum xy',
1872 path=self.output_folder, save_fig=self.save_plots,
1873 save_only=self.save_plots_only)
1874 if False:
1875 np.savetxt(self.output_folder+'recon_combined_sum_xy.txt',
1876 tomosum, fmt='%.6e')
1877 np.savetxt(self.output_folder+'recon_combined.txt',
1878 tomo_recon_combined[int(tomo_recon_combined.shape[0]/2),:,:], fmt='%.6e')
1879 combine_stacks =self.config.get('combine_stacks')
1880
1881 # Update config and save to file
1882 if combine_stacks:
1883 combine_stacks['x_bounds'] = x_bounds
1884 combine_stacks['y_bounds'] = y_bounds
1885 else:
1886 self.config['combine_stacks'] = {'x_bounds' : x_bounds, 'y_bounds' : y_bounds}
1887 self.cf.saveFile(self.config_out)
1888 return
1889 msnc.quickPlot(tomosum, title='recon combined sum xy')
1890 if pyip.inputYesNo(
1891 '\nDo you want to change the image z-bounds (y/[n])? ',
1892 blank=True) != 'yes':
1893 z_bounds = [0, tomo_recon_combined.shape[0]]
1894 else:
1895 z_bounds = msnc.selectArrayBounds(tomosum, title='recon combined sum xy')
1896 if z_bounds[0] != 0 or z_bounds[1] != tomo_recon_combined.shape[0]:
1897 tomo_recon_combined = tomo_recon_combined[z_bounds[0]:z_bounds[1],:,:]
1898 logging.info(f'tomo_recon_combined.shape = {tomo_recon_combined.shape}')
1899 if self.save_plots_only:
1900 msnc.clearFig('recon combined sum xy')
1901
1902 # Plot center slices
1903 msnc.quickImshow(tomo_recon_combined[int(tomo_recon_combined.shape[0]/2),:,:],
1904 title=f'recon combined xslice{int(tomo_recon_combined.shape[0]/2)}',
1905 path=self.output_folder, save_fig=self.save_plots,
1906 save_only=self.save_plots_only)
1907 msnc.quickImshow(tomo_recon_combined[:,int(tomo_recon_combined.shape[1]/2),:],
1908 title=f'recon combined yslice{int(tomo_recon_combined.shape[1]/2)}',
1909 path=self.output_folder, save_fig=self.save_plots,
1910 save_only=self.save_plots_only)
1911 msnc.quickImshow(tomo_recon_combined[:,:,int(tomo_recon_combined.shape[2]/2)],
1912 title=f'recon combined zslice{int(tomo_recon_combined.shape[2]/2)}',
1913 path=self.output_folder, save_fig=self.save_plots,
1914 save_only=self.save_plots_only)
1915
1916 # Save combined reconstructed tomo stacks
1917 base_name = 'recon combined'
1918 combined_stacks = []
1919 for stack in stacks:
1920 base_name += f' {stack["index"]}'
1921 combined_stacks.append(stack['index'])
1922 self._saveTomo(base_name, tomo_recon_combined)
1923
1924 # Update config and save to file
1925 if combine_stacks:
1926 combine_stacks['x_bounds'] = x_bounds
1927 combine_stacks['y_bounds'] = y_bounds
1928 combine_stacks['stacks'] = combined_stacks
1929 else:
1930 self.config['combine_stacks'] = {'x_bounds' : x_bounds, 'y_bounds' : y_bounds,
1931 'stacks' : combined_stacks}
1932 self.cf.saveFile(self.config_out)
1933
1934 def runTomo(config_file=None, config_dict=None, output_folder='.', log_level='INFO',
1935 test_mode=False):
1936 """Run a tomography analysis.
1937 """
1938 # Instantiate Tomo object
1939 tomo = Tomo(config_file=config_file, output_folder=output_folder, log_level=log_level,
1940 test_mode=test_mode)
1941 if not tomo.is_valid:
1942 raise ValueError('Invalid config and/or detector file provided.')
1943
1944 # Preprocess the image files
1945 num_tomo_stacks = tomo.config['stack_info']['num']
1946 assert(num_tomo_stacks == len(tomo.tomo_stacks))
1947 preprocess = tomo.config.get('preprocess', None)
1948 preprocessed_stacks = []
1949 if preprocess:
1950 preprocessed_stacks = [stack['index'] for stack in tomo.config['stack_info']['stacks']
1951 if stack.get('preprocessed', False)]
1952 if not len(preprocessed_stacks):
1953 tomo.genTomoStacks()
1954 if not tomo.is_valid:
1955 IOError('Unable to load all required image files.')
1956 find_center = tomo.config.get('find_center')
1957 if find_center and find_center.get('completed', False):
1958 center_stack_index = find_center['center_stack_index']
1959 if not center_stack_index in preprocessed_stacks:
1960 find_center['completed'] = False
1961 #RV FIX
1962 # tomo.config.pop('check_center', 'check_center not found')
1963 # combined_stacks = tomo.config.get('combined_stacks')
1964 # if combined_stacks:
1965 # combined_stacks['completed'] = False
1966 tomo.cf.saveFile(tomo.config_out)
1967
1968 # Find centers
1969 find_center = tomo.config.get('find_center')
1970 if find_center is None or not find_center.get('completed', False):
1971 tomo.findCenters()
1972
1973 # Check centers
1974 #if num_tomo_stacks > 1 and not tomo.config.get('check_centers', False):
1975 # tomo.checkCenters()
1976
1977 # Reconstruct tomography stacks
1978 if len(tomo.config.get('reconstructed_stacks', [])) != tomo.config['stack_info']['num']:
1979 tomo.reconstructTomoStacks()
1980
1981 # Combine reconstructed tomography stacks
1982 combined_stacks = tomo.config.get('combined_stacks')
1983 if combined_stacks is None or not combined_stacks.get('completed', False):
1984 tomo.combineTomoStacks()
1985
1986 #%%============================================================================
1987 if __name__ == '__main__':
1988 # Parse command line arguments
1989 arguments = sys.argv[1:]
1990 config_file = None
1991 output_folder = '.'
1992 log_level = 'INFO'
1993 test_mode = False
1994 try:
1995 opts, args = getopt.getopt(arguments,"hc:o:l:t")
1996 except getopt.GetoptError:
1997 print('usage: tomo.py -c <config_file> -o <output_folder> -l <log_level> -t')
1998 sys.exit(2)
1999 for opt, arg in opts:
2000 if opt == '-h':
2001 print('usage: tomo.py -c <config_file> -o <output_folder> -l <log_level> -t')
2002 sys.exit()
2003 elif opt in ("-c"):
2004 config_file = arg
2005 elif opt in ("-o"):
2006 output_folder = arg
2007 elif opt in ("-l"):
2008 log_level = arg
2009 elif opt in ("-t"):
2010 test_mode = True
2011 if config_file is None:
2012 if os.path.isfile('config.yaml'):
2013 config_file = 'config.yaml'
2014 else:
2015 config_file = 'config.txt'
2016
2017 # Set basic log configuration
2018 logging_format = '%(asctime)s : %(levelname)s - %(module)s : %(funcName)s - %(message)s'
2019 if not test_mode:
2020 level = getattr(logging, log_level.upper(), None)
2021 if not isinstance(level, int):
2022 raise ValueError(f'Invalid log_level: {log_level}')
2023 logging.basicConfig(format=logging_format, level=level, force=True,
2024 handlers=[logging.StreamHandler()])
2025
2026 logging.debug(f'config_file = {config_file}')
2027 logging.debug(f'output_folder = {output_folder}')
2028 logging.debug(f'log_level = {log_level}')
2029 logging.debug(f'test_mode = {test_mode}')
2030
2031 # Run tomography analysis
2032 runTomo(config_file=config_file, output_folder=output_folder, log_level=log_level,
2033 test_mode=test_mode)
2034
2035 #%%============================================================================
2036 input('Press any key to continue')
2037 #%%============================================================================