comparison tomo.py @ 4:7405057bcb29 draft default tip

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