Mercurial > repos > rv43 > tomo_setup
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 #%%============================================================================ |