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