Mercurial > repos > rv43 > tomo
view tomo.py @ 18:b0a816c8f66c draft
"planemo upload for repository https://github.com/rolfverberg/galaxytools commit 1743a2a14a40c3a9ea949c36dc53e5d448646e19-dirty"
author | rv43 |
---|---|
date | Mon, 18 Apr 2022 15:13:20 +0000 |
parents | 7f723407beb3 |
children | 4eba03628711 |
line wrap: on
line source
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Fri Dec 10 09:54:37 2021 @author: rv43 """ import logging import os import sys import getopt import re import io try: import pyinputplus as pyip except: pass import argparse import numpy as np import numexpr as ne import multiprocessing as mp import scipy.ndimage as spi import tomopy from time import time from skimage.transform import iradon from skimage.restoration import denoise_tv_chambolle import msnc_tools as msnc class set_numexpr_threads: def __init__(self, num_core): cpu_count = mp.cpu_count() logging.debug(f'start: num_core={num_core} cpu_count={cpu_count}') if num_core is None or num_core < 1 or num_core > cpu_count: self.num_core = cpu_count else: self.num_core = num_core logging.debug(f'self.num_core={self.num_core}') def __enter__(self): self.num_core_org = ne.set_num_threads(self.num_core) logging.debug(f'self.num_core={self.num_core}') def __exit__(self, exc_type, exc_value, traceback): ne.set_num_threads(self.num_core_org) class ConfigTomo(msnc.Config): """Class for processing a config file. """ def __init__(self, config_file=None, config_dict=None): super().__init__(config_file, config_dict) def _validate_txt(self): """Returns False if any required config parameter is illegal or missing. """ is_valid = True # Check for required first-level keys pars_required = ['tdf_data_path', 'tbf_data_path', 'detector_id'] pars_missing = [] is_valid = super().validate(pars_required, pars_missing) if len(pars_missing) > 0: logging.error(f'Missing item(s) in config file: {", ".join(pars_missing)}') self.detector_id = self.config.get('detector_id') # Find tomography dark field images file/folder self.tdf_data_path = self.config.get('tdf_data_path') # Find tomography bright field images file/folder self.tbf_data_path = self.config.get('tbf_data_path') # Check number of tomography image stacks self.num_tomo_stacks = self.config.get('num_tomo_stacks', 1) if not msnc.is_int(self.num_tomo_stacks, 1): self.num_tomo_stacks = None msnc.illegal_value('num_tomo_stacks', self.num_tomo_stacks, 'config file') return False logging.info(f'num_tomo_stacks = {self.num_tomo_stacks}') # Find tomography images file/folders and stack parameters tomo_data_paths_indices = sorted({key:value for key,value in self.config.items() if 'tomo_data_path' in key}.items()) if len(tomo_data_paths_indices) != self.num_tomo_stacks: logging.error(f'Incorrect number of tomography data path names in config file') is_valid = False self.tomo_data_paths = [tomo_data_paths_indices[i][1] for i in range(self.num_tomo_stacks)] self.tomo_data_indices = [msnc.get_trailing_int(tomo_data_paths_indices[i][0]) if msnc.get_trailing_int(tomo_data_paths_indices[i][0]) else None for i in range(self.num_tomo_stacks)] tomo_ref_height_indices = sorted({key:value for key,value in self.config.items() if 'z_pos' in key}.items()) if self.num_tomo_stacks > 1 and len(tomo_ref_height_indices) != self.num_tomo_stacks: logging.error(f'Incorrect number of tomography reference heights in config file') is_valid = False if len(tomo_ref_height_indices): self.tomo_ref_heights = [ tomo_ref_height_indices[i][1] for i in range(self.num_tomo_stacks)] else: self.tomo_ref_heights = [0.0]*self.num_tomo_stacks # Check tomo angle (theta) range self.start_theta = self.config.get('start_theta', 0.) if not msnc.is_num(self.start_theta, 0.): msnc.illegal_value('start_theta', self.start_theta, 'config file') is_valid = False logging.debug(f'start_theta = {self.start_theta}') self.end_theta = self.config.get('end_theta', 180.) if not msnc.is_num(self.end_theta, self.start_theta): msnc.illegal_value('end_theta', self.end_theta, 'config file') is_valid = False logging.debug(f'end_theta = {self.end_theta}') self.num_thetas = self.config.get('num_thetas') if not (self.num_thetas is None or msnc.is_int(self.num_thetas, 1)): msnc.illegal_value('num_thetas', self.num_thetas, 'config file') self.num_thetas = None is_valid = False logging.debug(f'num_thetas = {self.num_thetas}') return is_valid def _validate_yaml(self): """Returns False if any required config parameter is illegal or missing. """ is_valid = True # Check for required first-level keys pars_required = ['dark_field', 'bright_field', 'stack_info', 'detector'] pars_missing = [] is_valid = super().validate(pars_required, pars_missing) if len(pars_missing) > 0: logging.error(f'Missing item(s) in config file: {", ".join(pars_missing)}') self.detector_id = self.config['detector'].get('id') # Find tomography dark field images file/folder self.tdf_data_path = self.config['dark_field'].get('data_path') # Find tomography bright field images file/folder self.tbf_data_path = self.config['bright_field'].get('data_path') # Check number of tomography image stacks stack_info = self.config['stack_info'] self.num_tomo_stacks = stack_info.get('num', 1) if not msnc.is_int(self.num_tomo_stacks, 1): self.num_tomo_stacks = None msnc.illegal_value('stack_info:num', self.num_tomo_stacks, 'config file') return False logging.info(f'num_tomo_stacks = {self.num_tomo_stacks}') # Find tomography images file/folders and stack parameters stacks = stack_info.get('stacks') if stacks is None or len(stacks) is not self.num_tomo_stacks: msnc.illegal_value('stack_info:stacks', stacks, 'config file') return False self.tomo_data_paths = [] self.tomo_data_indices = [] self.tomo_ref_heights = [] for stack in stacks: self.tomo_data_paths.append(stack.get('data_path')) self.tomo_data_indices.append(stack.get('index')) self.tomo_ref_heights.append(stack.get('ref_height')) # Check tomo angle (theta) range theta_range = self.config.get('theta_range') if theta_range is None: self.start_theta = 0. self.end_theta = 180. self.num_thetas = None else: self.start_theta = theta_range.get('start', 0.) if not msnc.is_num(self.start_theta, 0.): msnc.illegal_value('theta_range:start', self.start_theta, 'config file') is_valid = False logging.debug(f'start_theta = {self.start_theta}') self.end_theta = theta_range.get('end', 180.) if not msnc.is_num(self.end_theta, self.start_theta): msnc.illegal_value('theta_range:end', self.end_theta, 'config file') is_valid = False logging.debug(f'end_theta = {self.end_theta}') self.num_thetas = theta_range.get('num') if self.num_thetas and not msnc.is_int(self.num_thetas, 1): msnc.illegal_value('theta_range:num', self.num_thetas, 'config file') self.num_thetas = None is_valid = False logging.debug(f'num_thetas = {self.num_thetas}') return is_valid def validate(self): """Returns False if any required config parameter is illegal or missing. """ is_valid = True # Check work_folder (shared by both file formats) work_folder = os.path.abspath(self.config.get('work_folder', '')) if not os.path.isdir(work_folder): msnc.illegal_value('work_folder', work_folder, 'config file') is_valid = False logging.info(f'work_folder: {work_folder}') # Check data filetype (shared by both file formats) self.data_filetype = self.config.get('data_filetype', 'tif') if not isinstance(self.data_filetype, str) or (self.data_filetype != 'tif' and self.data_filetype != 'h5'): msnc.illegal_value('data_filetype', self.data_filetype, 'config file') if self.suffix == '.yml' or self.suffix == '.yaml': is_valid = self._validate_yaml() elif self.suffix == '.txt': is_valid = self._validate_txt() else: logging.error(f'Undefined or illegal config file extension: {self.suffix}') is_valid = False # Find tomography bright field images file/folder if self.tdf_data_path: if self.data_filetype == 'h5': if isinstance(self.tdf_data_path, str): if not os.path.isabs(self.tdf_data_path): self.tdf_data_path = os.path.abspath( f'{work_folder}/{self.tdf_data_path}') else: msnc.illegal_value('tdf_data_path', tdf_data_fil, 'config file') is_valid = False else: if isinstance(self.tdf_data_path, int): self.tdf_data_path = os.path.abspath( f'{work_folder}/{self.tdf_data_path}/nf') elif isinstance(self.tdf_data_path, str): if not os.path.isabs(self.tdf_data_path): self.tdf_data_path = os.path.abspath( f'{work_folder}/{self.tdf_data_path}') else: msnc.illegal_value('tdf_data_path', self.tdf_data_path, 'config file') is_valid = False logging.info(f'dark field images path = {self.tdf_data_path}') # Find tomography bright field images file/folder if self.tbf_data_path: if self.data_filetype == 'h5': if isinstance(self.tbf_data_path, str): if not os.path.isabs(self.tbf_data_path): self.tbf_data_path = os.path.abspath( f'{work_folder}/{self.tbf_data_path}') else: msnc.illegal_value('tbf_data_path', tbf_data_fil, 'config file') is_valid = False else: if isinstance(self.tbf_data_path, int): self.tbf_data_path = os.path.abspath( f'{work_folder}/{self.tbf_data_path}/nf') elif isinstance(self.tbf_data_path, str): if not os.path.isabs(self.tbf_data_path): self.tbf_data_path = os.path.abspath( f'{work_folder}/{self.tbf_data_path}') else: msnc.illegal_value('tbf_data_path', self.tbf_data_path, 'config file') is_valid = False logging.info(f'bright field images path = {self.tbf_data_path}') # Find tomography images file/folders and stack parameters tomo_data_paths = [] tomo_data_indices = [] tomo_ref_heights = [] for data_path, index, ref_height in zip(self.tomo_data_paths, self.tomo_data_indices, self.tomo_ref_heights): if self.data_filetype == 'h5': if isinstance(data_path, str): if not os.path.isabs(data_path): data_path = os.path.abspath(f'{work_folder}/{data_path}') else: msnc.illegal_value(f'stack_info:stacks:data_path', data_path, 'config file') is_valid = False data_path = None else: if isinstance(data_path, int): data_path = os.path.abspath(f'{work_folder}/{data_path}/nf') elif isinstance(data_path, str): if not os.path.isabs(data_path): data_path = os.path.abspath(f'{work_folder}/{data_path}') else: msnc.illegal_value(f'stack_info:stacks:data_path', data_path, 'config file') is_valid = False data_path = None tomo_data_paths.append(data_path) if index is None: if self.num_tomo_stacks > 1: logging.error('Missing stack_info:stacks:index in config file') is_valid = False index = None else: index = 1 elif not isinstance(index, int): msnc.illegal_value(f'stack_info:stacks:index', index, 'config file') is_valid = False index = None tomo_data_indices.append(index) if ref_height is None: if self.num_tomo_stacks > 1: logging.error('Missing stack_info:stacks:ref_height in config file') is_valid = False ref_height = None else: ref_height = 0. elif not msnc.is_num(ref_height): msnc.illegal_value(f'stack_info:stacks:ref_height', ref_height, 'config file') is_valid = False ref_height = None # Set reference heights relative to first stack if (len(tomo_ref_heights) and msnc.is_num(ref_height) and msnc.is_num(tomo_ref_heights[0])): ref_height = (round(ref_height-tomo_ref_heights[0], 3)) tomo_ref_heights.append(ref_height) tomo_ref_heights[0] = 0.0 logging.info('tomography data paths:') for i in range(self.num_tomo_stacks): logging.info(f' {tomo_data_paths[i]}') logging.info(f'tomography data path indices: {tomo_data_indices}') logging.info(f'tomography reference heights: {tomo_ref_heights}') # Update config in memory if self.suffix == '.txt': self.config = {} dark_field = self.config.get('dark_field') if dark_field is None: self.config['dark_field'] = {'data_path' : self.tdf_data_path} else: self.config['dark_field']['data_path'] = self.tdf_data_path bright_field = self.config.get('bright_field') if bright_field is None: self.config['bright_field'] = {'data_path' : self.tbf_data_path} else: self.config['bright_field']['data_path'] = self.tbf_data_path detector = self.config.get('detector') if detector is None: self.config['detector'] = {'id' : self.detector_id} else: detector['id'] = self.detector_id self.config['work_folder'] = work_folder self.config['data_filetype'] = self.data_filetype stack_info = self.config.get('stack_info') if stack_info is None: stacks = [] for i in range(self.num_tomo_stacks): stacks.append({'data_path' : tomo_data_paths[i], 'index' : tomo_data_indices[i], 'ref_height' : tomo_ref_heights[i]}) self.config['stack_info'] = {'num' : self.num_tomo_stacks, 'stacks' : stacks} else: stack_info['num'] = self.num_tomo_stacks stacks = stack_info.get('stacks') for i,stack in enumerate(stacks): stack['data_path'] = tomo_data_paths[i] stack['index'] = tomo_data_indices[i] stack['ref_height'] = tomo_ref_heights[i] if self.num_thetas: theta_range = {'start' : self.start_theta, 'end' : self.end_theta, 'num' : self.num_thetas} else: theta_range = {'start' : self.start_theta, 'end' : self.end_theta} self.config['theta_range'] = theta_range # Cleanup temporary validation variables del self.tdf_data_path del self.tbf_data_path del self.detector_id del self.data_filetype del self.num_tomo_stacks del self.tomo_data_paths del self.tomo_data_indices del self.tomo_ref_heights del self.start_theta del self.end_theta del self.num_thetas return is_valid class Tomo: """Processing tomography data with misalignment. """ def __init__(self, config_file=None, config_dict=None, config_out=None, output_folder='.', log_level='INFO', log_stream='tomo.log', galaxy_flag=False, test_mode=False, num_core=-1): """Initialize with optional config input file or dictionary """ self.num_core = None self.config_out = config_out self.output_folder = output_folder self.galaxy_flag = galaxy_flag self.test_mode = test_mode self.save_plots = True # Make input argument? self.save_plots_only = True # Make input argument? self.cf = None self.config = None self.is_valid = True self.tdf = np.array([]) self.tbf = np.array([]) self.tomo_stacks = [] self.tomo_recon_stacks = [] # Validate input parameters if config_file is not None and not os.path.isfile(config_file): raise OSError(f'Invalid config_file input {config_file} {type(config_file)}') if config_dict is not None and not isinstance(config_dict, dict): raise ValueError(f'Invalid config_dict input {config_dict} {type(config_dict)}') if config_out is not None: if isinstance(config_out, str): if isinstance(log_stream, str): path = os.path.split(log_stream)[0] if path and not os.path.isdir(path): raise OSError(f'Invalid log_stream path') else: raise OSError(f'Invalid config_out input {config_out} {type(config_out)}') if not os.path.isdir(output_folder): raise OSError(f'Invalid output_folder input {output_folder} {type(output_folder)}') if isinstance(log_stream, str): path = os.path.split(log_stream)[0] if path and not os.path.isdir(path): raise OSError(f'Invalid log_stream path') if not os.path.isabs(path): log_stream = f'{output_folder}/{log_stream}' if not isinstance(galaxy_flag, bool): raise ValueError(f'Invalid galaxy_flag input {galaxy_flag} {type(galaxy_flag)}') if not isinstance(test_mode, bool): raise ValueError(f'Invalid test_mode input {test_mode} {type(test_mode)}') if not isinstance(num_core, int) or num_core < -1 or num_core == 0: raise ValueError(f'Invalid num_core input {num_core} {type(num_core)}') if num_core == -1: self.num_core = mp.cpu_count() else: self.num_core = num_core # Set log configuration logging_format = '%(asctime)s : %(levelname)s - %(module)s : %(funcName)s - %(message)s' if self.test_mode: self.save_plots_only = True if isinstance(log_stream, str): logging.basicConfig(filename=f'{log_stream}', filemode='w', format=logging_format, level=logging.INFO, force=True) #format=logging_format, level=logging.WARNING, force=True) elif isinstance(log_stream, io.TextIOWrapper): #logging.basicConfig(filemode='w', format=logging_format, level=logging.WARNING, logging.basicConfig(filemode='w', format=logging_format, level=logging.INFO, stream=log_stream, force=True) else: raise ValueError(f'Invalid log_stream: {log_stream}') logging.warning('Ignoring log_level argument in test mode') else: level = getattr(logging, log_level.upper(), None) if not isinstance(level, int): raise ValueError(f'Invalid log_level: {log_level}') if log_stream is sys.stdout: logging.basicConfig(format=logging_format, level=level, force=True, handlers=[logging.StreamHandler()]) else: if isinstance(log_stream, str): logging.basicConfig(filename=f'{log_stream}', filemode='w', format=logging_format, level=level, force=True) elif isinstance(log_stream, io.TextIOWrapper): logging.basicConfig(filemode='w', format=logging_format, level=level, stream=log_stream, force=True) else: raise ValueError(f'Invalid log_stream: {log_stream}') stream_handler = logging.StreamHandler() logging.getLogger().addHandler(stream_handler) stream_handler.setLevel(logging.WARNING) stream_handler.setFormatter(logging.Formatter(logging_format)) # Check/set output config file name if self.config_out is None: self.config_out = f'{self.output_folder}/config.yaml' elif (self.config_out is os.path.basename(self.config_out) and not os.path.isabs(self.config_out)): self.config_out = f'{self.output_folder}/{self.config_out}' # Create config object and load config file self.cf = ConfigTomo(config_file, config_dict) if not self.cf.load_flag: self.is_valid = False return if self.galaxy_flag: assert(self.output_folder == '.') assert(self.test_mode is False) self.save_plots = True self.save_plots_only = True else: # Input validation is already performed during link_data_to_galaxy # Check config file parameters self.is_valid = self.cf.validate() # Load detector info file df = msnc.Detector(self.cf.config['detector']['id']) # Check detector info file parameters if df.validate(): pixel_size = df.getPixelSize() num_rows, num_columns = df.getDimensions() if not pixel_size or not num_rows or not num_columns: self.is_valid = False else: pixel_size = None num_rows = None num_columns = None self.is_valid = False # Update config self.cf.config['detector']['pixel_size'] = pixel_size self.cf.config['detector']['rows'] = num_rows self.cf.config['detector']['columns'] = num_columns logging.debug(f'pixel_size = self.cf.config["detector"]["pixel_size"]') logging.debug(f'num_rows: {self.cf.config["detector"]["rows"]}') logging.debug(f'num_columns: {self.cf.config["detector"]["columns"]}') # Safe config to file if self.is_valid: self.cf.saveFile(self.config_out) # Initialize shortcut to config self.config = self.cf.config # Initialize tomography stack num_tomo_stacks = self.config['stack_info']['num'] if num_tomo_stacks: self.tomo_stacks = [np.array([]) for _ in range(num_tomo_stacks)] self.tomo_recon_stacks = [np.array([]) for _ in range(num_tomo_stacks)] logging.debug(f'num_core = {self.num_core}') logging.debug(f'config_file = {config_file}') logging.debug(f'config_dict = {config_dict}') logging.debug(f'config_out = {self.config_out}') logging.debug(f'output_folder = {self.output_folder}') logging.debug(f'log_stream = {log_stream}') logging.debug(f'log_level = {log_level}') logging.debug(f'galaxy_flag = {self.galaxy_flag}') logging.debug(f'test_mode = {self.test_mode}') logging.debug(f'save_plots = {self.save_plots}') logging.debug(f'save_plots_only = {self.save_plots_only}') def _selectImageRanges(self, available_stacks=None): """Select image files to be included in analysis. """ self.is_valid = True stack_info = self.config['stack_info'] if available_stacks is None: available_stacks = [False]*stack_info['num'] elif len(available_stacks) != stack_info['num']: logging.warning('Illegal dimension of available_stacks in getImageFiles '+ f'({len(available_stacks)}'); available_stacks = [False]*stack_info['num'] # Check number of tomography angles/thetas num_thetas = self.config['theta_range'].get('num') if num_thetas is None: num_thetas = pyip.inputInt('\nEnter the number of thetas (>0): ', greaterThan=0) elif not msnc.is_int(num_thetas, 0): msnc.illegal_value('num_thetas', num_thetas, 'config file') self.is_valid = False return self.config['theta_range']['num'] = num_thetas logging.debug(f'num_thetas = {self.config["theta_range"]["num"]}') # Find tomography dark field images dark_field = self.config['dark_field'] img_start = dark_field.get('img_start', -1) img_offset = dark_field.get('img_offset', -1) num_imgs = dark_field.get('num', 0) if not self.test_mode: img_start, img_offset, num_imgs = msnc.selectImageRange(img_start, img_offset, num_imgs, 'dark field') if img_start < 0 or num_imgs < 1: logging.error('Unable to find suitable dark field images') if dark_field['data_path']: self.is_valid = False dark_field['img_start'] = img_start dark_field['img_offset'] = img_offset dark_field['num'] = num_imgs logging.debug(f'Dark field image start index: {dark_field["img_start"]}') logging.debug(f'Dark field image offset: {dark_field["img_offset"]}') logging.debug(f'Number of dark field images: {dark_field["num"]}') # Find tomography bright field images bright_field = self.config['bright_field'] img_start = bright_field.get('img_start', -1) img_offset = bright_field.get('img_offset', -1) num_imgs = bright_field.get('num', 0) if not self.test_mode: img_start, img_offset, num_imgs = msnc.selectImageRange(img_start, img_offset, num_imgs, 'bright field') if img_start < 0 or num_imgs < 1: logging.error('Unable to find suitable bright field images') self.is_valid = False bright_field['img_start'] = img_start bright_field['img_offset'] = img_offset bright_field['num'] = num_imgs logging.debug(f'Bright field image start index: {bright_field["img_start"]}') logging.debug(f'Bright field image offset: {bright_field["img_offset"]}') logging.debug(f'Number of bright field images: {bright_field["num"]}') # Find tomography images for i,stack in enumerate(stack_info['stacks']): # Check if stack is already loaded or available if self.tomo_stacks[i].size or available_stacks[i]: continue index = stack['index'] img_start = stack.get('img_start', -1) img_offset = stack.get('img_offset', -1) num_imgs = stack.get('num', 0) if not self.test_mode: img_start, img_offset, num_imgs = msnc.selectImageRange(img_start, img_offset, num_imgs, f'tomography stack {index}', num_thetas) if img_start < 0 or num_imgs != num_thetas: logging.error('Unable to find suitable tomography images') self.is_valid = False stack['img_start'] = img_start stack['img_offset'] = img_offset stack['num'] = num_imgs logging.debug(f'Tomography stack {index} image start index: {stack["img_start"]}') logging.debug(f'Tomography stack {index} image offset: {stack["img_offset"]}') logging.debug(f'Number of tomography images for stack {index}: {stack["num"]}') # Safe updated config to file if self.is_valid: self.cf.saveFile(self.config_out) return def _genDark(self, tdf_files, dark_field_pngname): """Generate dark field. """ # Load the dark field images logging.debug('Loading dark field...') dark_field = self.config['dark_field'] tdf_stack = msnc.loadImageStack(tdf_files, self.config['data_filetype'], dark_field['img_offset'], dark_field['num']) # Take median self.tdf = np.median(tdf_stack, axis=0) del tdf_stack # RV make input of some kind (not always needed) tdf_cutoff = 21 self.tdf[self.tdf > tdf_cutoff] = np.nan tdf_mean = np.nanmean(self.tdf) logging.debug(f'tdf_cutoff = {tdf_cutoff}') logging.debug(f'tdf_mean = {tdf_mean}') np.nan_to_num(self.tdf, copy=False, nan=tdf_mean, posinf=tdf_mean, neginf=0.) if self.galaxy_flag: msnc.quickImshow(self.tdf, title='dark field', name=dark_field_pngname, save_fig=True, save_only=True) elif not self.test_mode: msnc.quickImshow(self.tdf, title='dark field', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) def _genBright(self, tbf_files, bright_field_pngname): """Generate bright field. """ # Load the bright field images logging.debug('Loading bright field...') bright_field = self.config['bright_field'] tbf_stack = msnc.loadImageStack(tbf_files, self.config['data_filetype'], bright_field['img_offset'], bright_field['num']) # Take median """Median or mean: It may be best to try the median because of some image artifacts that arise due to crinkles in the upstream kapton tape windows causing some phase contrast images to appear on the detector. One thing that also may be useful in a future implementation is to do a brightfield adjustment on EACH frame of the tomo based on a ROI in the corner of the frame where there is no sample but there is the direct X-ray beam because there is frame to frame fluctuations from the incoming beam. We don’t typically account for them but potentially could. """ self.tbf = np.median(tbf_stack, axis=0) del tbf_stack # Subtract dark field if self.tdf.size: self.tbf -= self.tdf else: logging.warning('Dark field unavailable') if self.galaxy_flag: msnc.quickImshow(self.tbf, title='bright field', name=bright_field_pngname, save_fig=True, save_only=True) elif not self.test_mode: msnc.quickImshow(self.tbf, title='bright field', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) def _setDetectorBounds(self, tomo_stack_files, tomo_field_pngname, detectorbounds_pngname): """Set vertical detector bounds for image stack. """ preprocess = self.config.get('preprocess') if preprocess is None: img_x_bounds = [None, None] else: img_x_bounds = preprocess.get('img_x_bounds', [0, self.tbf.shape[0]]) if img_x_bounds[0] is not None and img_x_bounds[1] is not None: if img_x_bounds[0] < 0: msnc.illegal_value('img_x_bounds[0]', img_x_bounds[0], 'config file') img_x_bounds[0] = 0 if not msnc.is_index_range(img_x_bounds, 0, self.tbf.shape[0]): msnc.illegal_value('img_x_bounds[1]', img_x_bounds[1], 'config file') img_x_bounds[1] = self.tbf.shape[0] if self.test_mode: # Update config and save to file if preprocess is None: self.cf.config['preprocess'] = {'img_x_bounds' : [0, self.tbf.shape[0]]} else: preprocess['img_x_bounds'] = img_x_bounds self.cf.saveFile(self.config_out) return # Check reference heights pixel_size = self.config['detector']['pixel_size'] if pixel_size is None: raise ValueError('Detector pixel size unavailable') if not self.tbf.size: raise ValueError('Bright field unavailable') num_x_min = None num_tomo_stacks = self.config['stack_info']['num'] stacks = self.config['stack_info']['stacks'] if num_tomo_stacks > 1: delta_z = stacks[1]['ref_height']-stacks[0]['ref_height'] for i in range(2, num_tomo_stacks): delta_z = min(delta_z, stacks[i]['ref_height']-stacks[i-1]['ref_height']) logging.debug(f'delta_z = {delta_z}') num_x_min = int(delta_z/pixel_size)+1 logging.debug(f'num_x_min = {num_x_min}') if num_x_min > self.tbf.shape[0]: logging.warning('Image bounds and pixel size prevent seamless stacking') num_x_min = self.tbf.shape[0] # Select image bounds if self.galaxy_flag: if num_x_min is None or num_x_min >= self.tbf.shape[0]: img_x_bounds = [0, self.tbf.shape[0]] else: margin = int(num_x_min/10) offset = min(0, int((self.tbf.shape[0]-num_x_min)/2-margin)) img_x_bounds = [offset, num_x_min+offset+2*margin] tomo_stack = msnc.loadImageStack(tomo_stack_files[0], self.config['data_filetype'], stacks[0]['img_offset'], 1) x_sum = np.sum(tomo_stack[0,:,:], 1) x_sum_min = x_sum.min() x_sum_max = x_sum.max() title = f'tomography image at theta={self.config["theta_range"]["start"]}' msnc.quickImshow(tomo_stack[0,:,:], title=title, name=tomo_field_pngname, save_fig=True, save_only=True, show_grid=True) msnc.quickPlot((range(x_sum.size), x_sum), ([img_x_bounds[0], img_x_bounds[0]], [x_sum_min, x_sum_max], 'r-'), ([img_x_bounds[1]-1, img_x_bounds[1]-1], [x_sum_min, x_sum_max], 'r-'), title='sum over theta and y', name=detectorbounds_pngname, save_fig=True, save_only=True, show_grid=True) # Update config and save to file if preprocess is None: self.cf.config['preprocess'] = {'img_x_bounds' : img_x_bounds} else: preprocess['img_x_bounds'] = img_x_bounds self.cf.saveFile(self.config_out) del x_sum return # For one tomography stack only: load the first image title = None msnc.quickImshow(self.tbf, title='bright field') if num_tomo_stacks == 1: tomo_stack = msnc.loadImageStack(tomo_stack_files[0], self.config['data_filetype'], stacks[0]['img_offset'], 1) title = f'tomography image at theta={self.config["theta_range"]["start"]}' msnc.quickImshow(tomo_stack[0,:,:], title=title) tomo_or_bright = pyip.inputNum('\nSelect image bounds from bright field (0) or '+ 'from first tomography image (1): ', min=0, max=1) else: print('\nSelect image bounds from bright field') tomo_or_bright = 0 if tomo_or_bright: x_sum = np.sum(tomo_stack[0,:,:], 1) use_bounds = 'no' if img_x_bounds[0] is not None and img_x_bounds[1] is not None: tmp = np.copy(tomo_stack[0,:,:]) tmp_max = tmp.max() tmp[img_x_bounds[0],:] = tmp_max tmp[img_x_bounds[1]-1,:] = tmp_max msnc.quickImshow(tmp, title=title) del tmp x_sum_min = x_sum.min() x_sum_max = x_sum.max() msnc.quickPlot((range(x_sum.size), x_sum), ([img_x_bounds[0], img_x_bounds[0]], [x_sum_min, x_sum_max], 'r-'), ([img_x_bounds[1]-1, img_x_bounds[1]-1], [x_sum_min, x_sum_max], 'r-'), title='sum over theta and y') print(f'lower bound = {img_x_bounds[0]} (inclusive)\n'+ f'upper bound = {img_x_bounds[1]} (exclusive)]') use_bounds = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) if use_bounds == 'no': img_x_bounds = msnc.selectImageBounds(tomo_stack[0,:,:], 0, img_x_bounds[0], img_x_bounds[1], num_x_min, f'tomography image at theta={self.config["theta_range"]["start"]}') if num_x_min is not None and img_x_bounds[1]-img_x_bounds[0]+1 < num_x_min: logging.warning('Image bounds and pixel size prevent seamless stacking') title = f'tomography image at theta={self.config["theta_range"]["start"]}' msnc.quickImshow(tomo_stack[0,:,:], title=title, path=self.output_folder, save_fig=self.save_plots, save_only=True) msnc.quickPlot(range(img_x_bounds[0], img_x_bounds[1]), x_sum[img_x_bounds[0]:img_x_bounds[1]], title='sum over theta and y', path=self.output_folder, save_fig=self.save_plots, save_only=True) else: x_sum = np.sum(self.tbf, 1) x_sum_min = x_sum.min() x_sum_max = x_sum.max() use_bounds = 'no' if img_x_bounds[0] is not None and img_x_bounds[1] is not None: tmp = np.copy(self.tbf) tmp_max = tmp.max() tmp[img_x_bounds[0],:] = tmp_max tmp[img_x_bounds[1]-1,:] = tmp_max title = 'Bright field' msnc.quickImshow(tmp, title=title) del tmp msnc.quickPlot((range(x_sum.size), x_sum), ([img_x_bounds[0], img_x_bounds[0]], [x_sum_min, x_sum_max], 'r-'), ([img_x_bounds[1]-1, img_x_bounds[1]-1], [x_sum_min, x_sum_max], 'r-'), title='sum over theta and y') print(f'lower bound = {img_x_bounds[0]} (inclusive)\n'+ f'upper bound = {img_x_bounds[1]} (exclusive)]') use_bounds = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) if use_bounds == 'no': fit = msnc.fitStep(y=x_sum, model='rectangle', form='atan') x_low = fit.get('center1', None) x_upp = fit.get('center2', None) if (x_low is not None and x_low >= 0 and x_upp is not None and x_low < x_upp < x_sum.size): x_low = int(x_low-(x_upp-x_low)/10) if x_low < 0: x_low = 0 x_upp = int(x_upp+(x_upp-x_low)/10) if x_upp >= x_sum.size: x_upp = x_sum.size tmp = np.copy(self.tbf) tmp_max = tmp.max() tmp[x_low,:] = tmp_max tmp[x_upp-1,:] = tmp_max title = 'Bright field' msnc.quickImshow(tmp, title=title) del tmp msnc.quickPlot((range(x_sum.size), x_sum), ([x_low, x_low], [x_sum_min, x_sum_max], 'r-'), ([x_upp, x_upp], [x_sum_min, x_sum_max], 'r-'), title='sum over theta and y') print(f'lower bound = {x_low} (inclusive)\nupper bound = {x_upp} (exclusive)]') use_fit = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) if use_fit == 'no': img_x_bounds = msnc.selectArrayBounds(x_sum, img_x_bounds[0], img_x_bounds[1], num_x_min, 'sum over theta and y') else: img_x_bounds = [x_low, x_upp] if num_x_min is not None and img_x_bounds[1]-img_x_bounds[0]+1 < num_x_min: logging.warning('Image bounds and pixel size prevent seamless stacking') msnc.quickPlot(range(img_x_bounds[0], img_x_bounds[1]), x_sum[img_x_bounds[0]:img_x_bounds[1]], title='sum over theta and y', path=self.output_folder, save_fig=self.save_plots, save_only=True) del x_sum logging.debug(f'img_x_bounds: {img_x_bounds}') if self.save_plots_only: msnc.clearFig('bright field') msnc.clearFig('sum over theta and y') if title: msnc.clearFig(title) # Update config and save to file if preprocess is None: self.cf.config['preprocess'] = {'img_x_bounds' : img_x_bounds} else: preprocess['img_x_bounds'] = img_x_bounds self.cf.saveFile(self.config_out) def _setZoomOrSkip(self): """Set zoom and/or theta skip to reduce memory the requirement for the analysis. """ preprocess = self.config.get('preprocess') zoom_perc = 100 if not self.galaxy_flag: if preprocess is None or 'zoom_perc' not in preprocess: if pyip.inputYesNo( '\nDo you want to zoom in to reduce memory requirement (y/[n])? ', blank=True) == 'yes': zoom_perc = pyip.inputInt(' Enter zoom percentage [1, 100]: ', min=1, max=100) else: zoom_perc = preprocess['zoom_perc'] if msnc.is_num(zoom_perc, 1., 100.): zoom_perc = int(zoom_perc) else: msnc.illegal_value('zoom_perc', zoom_perc, 'config file') zoom_perc = 100 num_theta_skip = 0 if not self.galaxy_flag: if preprocess is None or 'num_theta_skip' not in preprocess: if pyip.inputYesNo( 'Do you want to skip thetas to reduce memory requirement (y/[n])? ', blank=True) == 'yes': num_theta_skip = pyip.inputInt(' Enter the number skip theta interval'+ f' [0, {self.num_thetas-1}]: ', min=0, max=self.num_thetas-1) else: num_theta_skip = preprocess['num_theta_skip'] if not msnc.is_int(num_theta_skip, 0): msnc.illegal_value('num_theta_skip', num_theta_skip, 'config file') num_theta_skip = 0 logging.info(f'zoom_perc = {zoom_perc}') logging.info(f'num_theta_skip = {num_theta_skip}') # Update config and save to file if preprocess is None: self.cf.config['preprocess'] = {'zoom_perc' : zoom_perc, 'num_theta_skip' : num_theta_skip} else: preprocess['zoom_perc'] = zoom_perc preprocess['num_theta_skip'] = num_theta_skip self.cf.saveFile(self.config_out) def _loadTomo(self, base_name, index, required=False): """Load a tomography stack. """ # stack order: row,theta,column zoom_perc = None preprocess = self.config.get('preprocess') if preprocess: zoom_perc = preprocess.get('zoom_perc') if zoom_perc is None or zoom_perc == 100: title = f'{base_name} fullres' else: title = f'{base_name} {zoom_perc}p' title += f'_{index}' tomo_file = re.sub(r"\s+", '_', f'{self.output_folder}/{title}.npy') load_flag = 'no' available = False if os.path.isfile(tomo_file): available = True if required: load_flag = 'yes' else: load_flag = pyip.inputYesNo(f'\nDo you want to load {tomo_file} (y/n)? ') stack = np.array([]) if load_flag == 'yes': t0 = time() logging.info(f'Loading {tomo_file} ...') try: stack = np.load(tomo_file) except IOError or ValueError: stack = np.array([]) logging.error(f'Error loading {tomo_file}') logging.info(f'... done in {time()-t0:.2f} seconds!') if stack.size: msnc.quickImshow(stack[:,0,:], title=title, path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) return stack, available def _saveTomo(self, base_name, stack, index=None): """Save a tomography stack. """ zoom_perc = None preprocess = self.config.get('preprocess') if preprocess: zoom_perc = preprocess.get('zoom_perc') if zoom_perc is None or zoom_perc == 100: title = f'{base_name} fullres' else: title = f'{base_name} {zoom_perc}p' if index: title += f'_{index}' tomo_file = re.sub(r"\s+", '_', f'{self.output_folder}/{title}.npy') t0 = time() logging.info(f'Saving {tomo_file} ...') np.save(tomo_file, stack) logging.info(f'... done in {time()-t0:.2f} seconds!') def _genTomo(self, tomo_stack_files, available_stacks, num_core=None): """Generate tomography fields. """ if num_core is None: num_core = self.num_core stacks = self.config['stack_info']['stacks'] assert(len(self.tomo_stacks) == self.config['stack_info']['num']) assert(len(self.tomo_stacks) == len(stacks)) if len(available_stacks) != len(stacks): logging.warning('Illegal dimension of available_stacks in _genTomo'+ f'({len(available_stacks)}'); available_stacks = [False]*self.num_tomo_stacks preprocess = self.config.get('preprocess') if preprocess is None: img_x_bounds = [0, self.tbf.shape[0]] img_y_bounds = [0, self.tbf.shape[1]] zoom_perc = 100 num_theta_skip = 0 else: img_x_bounds = preprocess.get('img_x_bounds', [0, self.tbf.shape[0]]) img_y_bounds = preprocess.get('img_y_bounds', [0, self.tbf.shape[1]]) zoom_perc = preprocess.get('zoom_perc', 100) num_theta_skip = preprocess.get('num_theta_skip', 0) if self.tdf.size: tdf = self.tdf[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]] else: logging.warning('Dark field unavailable') if not self.tbf.size: raise ValueError('Bright field unavailable') tbf = self.tbf[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]] for i,stack in enumerate(stacks): # Check if stack is already loaded or available if self.tomo_stacks[i].size or available_stacks[i]: continue # Load a stack of tomography images index = stack['index'] t0 = time() tomo_stack = msnc.loadImageStack(tomo_stack_files[i], self.config['data_filetype'], stack['img_offset'], self.config['theta_range']['num'], num_theta_skip, img_x_bounds, img_y_bounds) tomo_stack = tomo_stack.astype('float64') logging.debug(f'loading stack {index} took {time()-t0:.2f} seconds!') # Subtract dark field if self.tdf.size: t0 = time() with set_numexpr_threads(self.num_core): ne.evaluate('tomo_stack-tdf', out=tomo_stack) logging.debug(f'subtracting dark field took {time()-t0:.2f} seconds!') # Normalize t0 = time() with set_numexpr_threads(self.num_core): ne.evaluate('tomo_stack/tbf', out=tomo_stack, truediv=True) logging.debug(f'normalizing took {time()-t0:.2f} seconds!') # Remove non-positive values and linearize data t0 = time() cutoff = 1.e-6 with set_numexpr_threads(self.num_core): ne.evaluate('where(tomo_stack<cutoff, cutoff, tomo_stack)', out=tomo_stack) with set_numexpr_threads(self.num_core): ne.evaluate('-log(tomo_stack)', out=tomo_stack) logging.debug('removing non-positive values and linearizing data took '+ f'{time()-t0:.2f} seconds!') # Get rid of nans/infs that may be introduced by normalization t0 = time() np.where(np.isfinite(tomo_stack), tomo_stack, 0.) logging.debug(f'remove nans/infs took {time()-t0:.2f} seconds!') # Downsize tomography stack to smaller size tomo_stack = tomo_stack.astype('float32') if not self.galaxy_flag: title = f'red stack fullres {index}' if not self.test_mode: msnc.quickImshow(tomo_stack[0,:,:], title=title, path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) if zoom_perc != 100: t0 = time() logging.info(f'Zooming in ...') tomo_zoom_list = [] for j in range(tomo_stack.shape[0]): tomo_zoom = spi.zoom(tomo_stack[j,:,:], 0.01*zoom_perc) tomo_zoom_list.append(tomo_zoom) tomo_stack = np.stack([tomo_zoom for tomo_zoom in tomo_zoom_list]) logging.info(f'... done in {time()-t0:.2f} seconds!') del tomo_zoom_list if not self.galaxy_flag: title = f'red stack {zoom_perc}p {index}' if not self.test_mode: msnc.quickImshow(tomo_stack[0,:,:], title=title, path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) # Convert tomography stack from theta,row,column to row,theta,column tomo_stack = np.swapaxes(tomo_stack, 0, 1) # Save tomography stack to file if not self.galaxy_flag: if not self.test_mode: self._saveTomo('red stack', tomo_stack, index) else: np.savetxt(f'{self.output_folder}/red_stack_{index}.txt', tomo_stack[0,:,:], fmt='%.6e') # Combine stacks t0 = time() self.tomo_stacks[i] = tomo_stack logging.debug(f'combining nstack took {time()-t0:.2f} seconds!') # Update config and save to file stack['preprocessed'] = True stack.pop('reconstructed', 'reconstructed not found') find_center = self.config.get('find_center') if find_center: find_center.pop('completed', 'completed not found') if self.test_mode: find_center.pop('lower_center_offset', 'lower_center_offset not found') find_center.pop('upper_center_offset', 'upper_center_offset not found') self.cf.saveFile(self.config_out) if self.tdf.size: del tdf del tbf def _reconstructOnePlane(self, tomo_plane_T, center, thetas_deg, eff_pixel_size, cross_sectional_dim, plot_sinogram=True, num_core=1): """Invert the sinogram for a single tomography plane. """ # tomo_plane_T index order: column,theta assert(0 <= center < tomo_plane_T.shape[0]) center_offset = center-tomo_plane_T.shape[0]/2 two_offset = 2*int(np.round(center_offset)) two_offset_abs = np.abs(two_offset) max_rad = int(0.5*(cross_sectional_dim/eff_pixel_size)*1.1) # 10% slack to avoid edge effects dist_from_edge = max(1, int(np.floor((tomo_plane_T.shape[0]-two_offset_abs)/2.)-max_rad)) if two_offset >= 0: logging.debug(f'sinogram range = [{two_offset+dist_from_edge}, {-dist_from_edge}]') sinogram = tomo_plane_T[two_offset+dist_from_edge:-dist_from_edge,:] else: logging.debug(f'sinogram range = [{dist_from_edge}, {two_offset-dist_from_edge}]') sinogram = tomo_plane_T[dist_from_edge:two_offset-dist_from_edge,:] if plot_sinogram and not self.test_mode: msnc.quickImshow(sinogram.T, f'sinogram center offset{center_offset:.2f}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only, aspect='auto') # Inverting sinogram t0 = time() recon_sinogram = iradon(sinogram, theta=thetas_deg, circle=True) logging.debug(f'inverting sinogram took {time()-t0:.2f} seconds!') del sinogram # Removing ring artifacts # RV parameters for the denoise, gaussian, and ring removal will be different for different feature sizes t0 = time() # recon_sinogram = filters.gaussian(recon_sinogram, 3.0) recon_sinogram = spi.gaussian_filter(recon_sinogram, 0.5) recon_clean = np.expand_dims(recon_sinogram, axis=0) del recon_sinogram recon_clean = tomopy.misc.corr.remove_ring(recon_clean, rwidth=17, ncore=num_core) logging.debug(f'filtering and removing ring artifact took {time()-t0:.2f} seconds!') return recon_clean def _plotEdgesOnePlane(self, recon_plane, title, path=None, weight=0.001): # RV parameters for the denoise, gaussian, and ring removal will be different for different feature sizes edges = denoise_tv_chambolle(recon_plane, weight = weight) vmax = np.max(edges[0,:,:]) vmin = -vmax if self.galaxy_flag: msnc.quickImshow(edges[0,:,:], title, path=path, save_fig=True, save_only=True, cmap='gray', vmin=vmin, vmax=vmax) else: if path is None: path=self.output_folder msnc.quickImshow(edges[0,:,:], f'{title} coolwarm', path=path, save_fig=self.save_plots, save_only=self.save_plots_only, cmap='coolwarm') msnc.quickImshow(edges[0,:,:], f'{title} gray', path=path, save_fig=self.save_plots, save_only=self.save_plots_only, cmap='gray', vmin=vmin, vmax=vmax) del edges def _findCenterOnePlane(self, sinogram, row, thetas_deg, eff_pixel_size, cross_sectional_dim, tol=0.1, num_core=1, galaxy_param=None): """Find center for a single tomography plane. """ if self.galaxy_flag: assert(galaxy_param) if not os.path.exists('find_center_pngs'): os.mkdir('find_center_pngs') # sinogram index order: theta,column # need index order column,theta for iradon, so take transpose sinogram_T = sinogram.T center = sinogram.shape[1]/2 # try automatic center finding routines for initial value tomo_center = tomopy.find_center_vo(sinogram, ncore=num_core) center_offset_vo = tomo_center-center if self.test_mode: logging.info(f'Center at row {row} using Nghia Vo’s method = {center_offset_vo:.2f}') del sinogram_T return float(center_offset_vo) elif self.galaxy_flag: logging.info(f'Center at row {row} using Nghia Vo’s method = {center_offset_vo:.2f}') recon_plane = self._reconstructOnePlane(sinogram_T, tomo_center, thetas_deg, eff_pixel_size, cross_sectional_dim, False, num_core) title = f'edges row{row} center offset{center_offset_vo:.2f} Vo' self._plotEdgesOnePlane(recon_plane, title, path='find_center_pngs') del recon_plane if not galaxy_param['center_type_selector']: del sinogram_T return float(center_offset_vo) else: print(f'Center at row {row} using Nghia Vo’s method = {center_offset_vo:.2f}') recon_plane = self._reconstructOnePlane(sinogram_T, tomo_center, thetas_deg, eff_pixel_size, cross_sectional_dim, False, num_core) title = f'edges row{row} center offset{center_offset_vo:.2f} Vo' self._plotEdgesOnePlane(recon_plane, title) if not self.galaxy_flag: if (pyip.inputYesNo('Try finding center using phase correlation '+ '(y/[n])? ', blank=True) == 'yes'): tomo_center = tomopy.find_center_pc(sinogram, sinogram, tol=0.1, rotc_guess=tomo_center) error = 1. while error > tol: prev = tomo_center tomo_center = tomopy.find_center_pc(sinogram, sinogram, tol=tol, rotc_guess=tomo_center) error = np.abs(tomo_center-prev) center_offset = tomo_center-center print(f'Center at row {row} using phase correlation = {center_offset:.2f}') recon_plane = self._reconstructOnePlane(sinogram_T, tomo_center, thetas_deg, eff_pixel_size, cross_sectional_dim, False, num_core) title = f'edges row{row} center_offset{center_offset:.2f} PC' self._plotEdgesOnePlane(recon_plane, title) if (pyip.inputYesNo('Accept a center location ([y]) or continue '+ 'search (n)? ', blank=True) != 'no'): center_offset = pyip.inputNum( f' Enter chosen center offset [{-int(center)}, {int(center)}] '+ f'([{center_offset_vo:.2f}])): ', blank=True) if center_offset == '': center_offset = center_offset_vo del sinogram_T del recon_plane return float(center_offset) # perform center finding search while True: if self.galaxy_flag and galaxy_param and galaxy_param['center_type_selector']: set_center = center_offset_vo if galaxy_param['center_type_selector'] == 'user': set_center = galaxy_param['set_center'] set_range = galaxy_param['set_range'] center_offset_step = galaxy_param['set_step'] if (not msnc.is_num(set_range, 0) or not msnc.is_num(center_offset_step) or center_offset_step <= 0): logging.warning('Illegal center finding search parameter, skip search') del sinogram_T return float(center_offset_vo) set_range = center_offset_step*max(1, int(set_range/center_offset_step)) center_offset_low = set_center-set_range center_offset_upp = set_center+set_range else: center_offset_low = pyip.inputInt('\nEnter lower bound for center offset '+ f'[{-int(center)}, {int(center)}]: ', min=-int(center), max=int(center)) center_offset_upp = pyip.inputInt('Enter upper bound for center offset '+ f'[{center_offset_low}, {int(center)}]: ', min=center_offset_low, max=int(center)) if center_offset_upp == center_offset_low: center_offset_step = 1 else: center_offset_step = pyip.inputInt('Enter step size for center offset search '+ f'[1, {center_offset_upp-center_offset_low}]: ', min=1, max=center_offset_upp-center_offset_low) num_center_offset = 1+int((center_offset_upp-center_offset_low)/center_offset_step) center_offsets = np.linspace(center_offset_low, center_offset_upp, num_center_offset) for center_offset in center_offsets: if center_offset == center_offset_vo: continue logging.info(f'center_offset = {center_offset:.2f}') recon_plane = self._reconstructOnePlane(sinogram_T, center_offset+center, thetas_deg, eff_pixel_size, cross_sectional_dim, False, num_core) title = f'edges row{row} center_offset{center_offset:.2f}' if self.galaxy_flag: self._plotEdgesOnePlane(recon_plane, title, path='find_center_pngs') else: self._plotEdgesOnePlane(recon_plane, title) if self.galaxy_flag or pyip.inputInt('\nContinue (0) or end the search (1): ', min=0, max=1): break del sinogram_T del recon_plane if self.galaxy_flag: center_offset = center_offset_vo else: center_offset = pyip.inputNum(f' Enter chosen center offset '+ f'[{-int(center)}, {int(center)}]: ', min=-int(center), max=int(center)) return float(center_offset) def _reconstructOneTomoStack(self, tomo_stack, thetas, row_bounds=None, center_offsets=[], sigma=0.1, rwidth=30, num_core=1, algorithm='gridrec', run_secondary_sirt=False, secondary_iter=100): """reconstruct a single tomography stack. """ # stack order: row,theta,column # thetas must be in radians # centers_offset: tomography axis shift in pixels relative to column center # RV should we remove stripes? # https://tomopy.readthedocs.io/en/latest/api/tomopy.prep.stripe.html # RV should we remove rings? # https://tomopy.readthedocs.io/en/latest/api/tomopy.misc.corr.html # RV: Add an option to do (extra) secondary iterations later or to do some sort of convergence test? if row_bounds is None: row_bounds = [0, tomo_stack.shape[0]] else: if not msnc.is_index_range(row_bounds, 0, tomo_stack.shape[0]): raise ValueError('Illegal row bounds in reconstructOneTomoStack') if thetas.size != tomo_stack.shape[1]: raise ValueError('theta dimension mismatch in reconstructOneTomoStack') if not len(center_offsets): centers = np.zeros((row_bounds[1]-row_bounds[0])) elif len(center_offsets) == 2: centers = np.linspace(center_offsets[0], center_offsets[1], row_bounds[1]-row_bounds[0]) else: if center_offsets.size != row_bounds[1]-row_bounds[0]: raise ValueError('center_offsets dimension mismatch in reconstructOneTomoStack') centers = center_offsets centers += tomo_stack.shape[2]/2 if True: tomo_stack = tomopy.prep.stripe.remove_stripe_fw( tomo_stack[row_bounds[0]:row_bounds[1]], sigma=sigma, ncore=num_core) else: tomo_stack = tomo_stack[row_bounds[0]:row_bounds[1]] tomo_recon_stack = tomopy.recon(tomo_stack, thetas, centers, sinogram_order=True, algorithm=algorithm, ncore=num_core) if run_secondary_sirt and secondary_iter > 0: #options = {'method':'SIRT_CUDA', 'proj_type':'cuda', 'num_iter':secondary_iter} #RV: doesn't work for me: "Error: CUDA error 803: system has unsupported display driver / # cuda driver combination." #options = {'method':'SIRT', 'proj_type':'linear', 'MinConstraint': 0, 'num_iter':secondary_iter} #SIRT did not finish while running overnight #options = {'method':'SART', 'proj_type':'linear', 'num_iter':secondary_iter} options = {'method':'SART', 'proj_type':'linear', 'MinConstraint': 0, 'num_iter':secondary_iter} tomo_recon_stack = tomopy.recon(tomo_stack, thetas, centers, init_recon=tomo_recon_stack, options=options, sinogram_order=True, algorithm=tomopy.astra, ncore=num_core) if True: tomopy.misc.corr.remove_ring(tomo_recon_stack, rwidth=rwidth, out=tomo_recon_stack, ncore=num_core) return tomo_recon_stack def findImageFiles(self): """Find all available image files. """ self.is_valid = True # Find dark field images dark_field = self.config['dark_field'] img_start, num_imgs, dark_files = msnc.findImageFiles( dark_field['data_path'], self.config['data_filetype'], 'dark field') if img_start < 0 or num_imgs < 1: logging.error('Unable to find suitable dark field images') if dark_field['data_path']: self.is_valid = False img_start_old = dark_field.get('img_start') num_imgs_old = dark_field.get('num') if num_imgs_old is None: dark_field['num'] = num_imgs else: if num_imgs_old > num_imgs: logging.error('Inconsistent number of availaible dark field images') if dark_field['data_path']: self.is_valid = False if img_start_old is None: dark_field['img_start'] = img_start else: if img_start_old < img_start: logging.error('Inconsistent image start index for dark field images') if dark_field['data_path']: self.is_valid = False logging.info(f'Number of dark field images = {dark_field["num"]}') logging.info(f'Dark field image start index = {dark_field["img_start"]}') # Find bright field images bright_field = self.config['bright_field'] img_start, num_imgs, bright_files = msnc.findImageFiles( bright_field['data_path'], self.config['data_filetype'], 'bright field') if img_start < 0 or num_imgs < 1: logging.error('Unable to find suitable bright field images') self.is_valid = False img_start_old = bright_field.get('img_start') num_imgs_old = bright_field.get('num') if num_imgs_old is None: bright_field['num'] = num_imgs else: if num_imgs_old > num_imgs: logging.error('Inconsistent number of availaible bright field images') self.is_valid = False if img_start_old is None: bright_field['img_start'] = img_start else: if img_start_old < img_start: logging.warning('Inconsistent image start index for bright field images') self.is_valid = False logging.info(f'Number of bright field images = {bright_field["num"]}') logging.info(f'Bright field image start index = {bright_field["img_start"]}') # Find tomography images tomo_stack_files = [] for stack in self.config['stack_info']['stacks']: index = stack['index'] img_start, num_imgs, tomo_files = msnc.findImageFiles( stack['data_path'], self.config['data_filetype'], f'tomography set {index}') if img_start < 0 or num_imgs < 1: logging.error('Unable to find suitable tomography images') self.is_valid = False img_start_old = stack.get('img_start') num_imgs_old = stack.get('num') if num_imgs_old is None: stack['num'] = num_imgs else: if num_imgs_old > num_imgs: logging.error('Inconsistent number of availaible tomography images') self.is_valid = False if img_start_old is None: stack['img_start'] = img_start else: if img_start_old < img_start: logging.warning('Inconsistent image start index for tomography images') self.is_valid = False logging.info(f'Number of tomography images for set {index} = {stack["num"]}') logging.info(f'Tomography set {index} image start index = {stack["img_start"]}') tomo_stack_files.append(tomo_files) del tomo_files # Safe updated config if self.is_valid: self.cf.saveFile(self.config_out) return dark_files, bright_files, tomo_stack_files def loadTomoStacks(self, input_name): """Load tomography stacks (only for Galaxy). """ assert(self.galaxy_flag) t0 = time() logging.info(f'Loading preprocessed tomography stack from {input_name} ...') stack_info = self.config['stack_info'] stacks = stack_info.get('stacks') assert(len(self.tomo_stacks) == stack_info['num']) with np.load(input_name) as f: for i,stack in enumerate(stacks): self.tomo_stacks[i] = f[f'set_{stack["index"]}'] logging.info(f'loaded stack {i}: index = {stack["index"]}, shape = '+ f'{self.tomo_stacks[i].shape}') logging.info(f'... done in {time()-t0:.2f} seconds!') def genTomoStacks(self, galaxy_param=None, num_core=None): """Preprocess tomography images. """ if num_core is None: num_core = self.num_core # Try loading any already preprocessed stacks (skip in Galaxy) # preprocessed stack order for each one in stack: row,theta,column stack_info = self.config['stack_info'] stacks = stack_info['stacks'] num_tomo_stacks = stack_info['num'] assert(num_tomo_stacks == len(self.tomo_stacks)) available_stacks = [False]*num_tomo_stacks if self.galaxy_flag: assert(isinstance(galaxy_param, dict)) tdf_files = galaxy_param['tdf_files'] tbf_files = galaxy_param['tbf_files'] tomo_stack_files = galaxy_param['tomo_stack_files'] assert(num_tomo_stacks == len(tomo_stack_files)) else: if galaxy_param: logging.warning('Ignoring galaxy_param in findCenters (only for Galaxy)') galaxy_param = None tdf_files, tbf_files, tomo_stack_files = self.findImageFiles() if not self.is_valid: return for i,stack in enumerate(stacks): if not self.tomo_stacks[i].size and stack.get('preprocessed', False): self.tomo_stacks[i], available_stacks[i] = \ self._loadTomo('red stack', stack['index']) # Preprocess any unloaded stacks if False in available_stacks: logging.debug('Preprocessing tomography images') # Check required image files (skip in Galaxy) if not self.galaxy_flag: self._selectImageRanges(available_stacks) if not self.is_valid: return # Generate dark field if tdf_files: self._genDark(tdf_files, galaxy_param['dark_field_pngname']) # Generate bright field self._genBright(tbf_files, galaxy_param['bright_field_pngname']) # Set vertical detector bounds for image stack self._setDetectorBounds(tomo_stack_files, galaxy_param['tomo_field_pngname'], galaxy_param['detectorbounds_pngname']) # Set zoom and/or theta skip to reduce memory the requirement self._setZoomOrSkip() # Generate tomography fields self._genTomo(tomo_stack_files, available_stacks, num_core) # Save tomography stack to file if self.galaxy_flag: t0 = time() output_name = galaxy_param['output_name'] logging.info(f'Saving preprocessed tomography stack to {output_name} ...') save_stacks = {f'set_{stack["index"]}':tomo_stack for stack,tomo_stack in zip(stacks,self.tomo_stacks)} logging.info(f'output_name = {output_name}') logging.info(f'save_stacks = {save_stacks}') np.savez(output_name, **save_stacks) logging.info(f'... done in {time()-t0:.2f} seconds!') del available_stacks # Adjust sample reference height, update config and save to file preprocess = self.config.get('preprocess') if preprocess is None: img_x_bounds = [0, self.tbf.shape[0]] else: img_x_bounds = preprocess.get('img_x_bounds', [0, self.tbf.shape[0]]) pixel_size = self.config['detector']['pixel_size'] if pixel_size is None: raise ValueError('Detector pixel size unavailable') pixel_size *= img_x_bounds[0] for stack in stacks: stack['ref_height'] = stack['ref_height']+pixel_size self.cf.saveFile(self.config_out) def findCenters(self, galaxy_param=None, num_core=None): """Find rotation axis centers for the tomography stacks. """ if num_core is None: num_core = self.num_core logging.debug('Find centers for tomography stacks') stacks = self.config['stack_info']['stacks'] available_stacks = [stack['index'] for stack in stacks if stack.get('preprocessed', False)] logging.debug('Available stacks: {available_stacks}') if self.galaxy_flag: assert(isinstance(galaxy_param, dict)) row_bounds = galaxy_param['row_bounds'] center_rows = galaxy_param['center_rows'] if center_rows is None: logging.error('Missing center_rows entry in galaxy_param') return center_type_selector = galaxy_param['center_type_selector'] if center_type_selector: if center_type_selector == 'vo': set_center = None elif center_type_selector == 'user': set_center = galaxy_param['set_center'] else: logging.error('Illegal center_type_selector entry in galaxy_param '+ f'({center_type_selector})') galaxy_param['center_type_selector'] = None else: if galaxy_param: logging.warning('Ignoring galaxy_param in findCenters (only for Galaxy)') galaxy_param = None # Check for valid available center stack index find_center = self.config.get('find_center') center_stack_index = None if find_center and 'center_stack_index' in find_center: center_stack_index = find_center['center_stack_index'] if (not isinstance(center_stack_index, int) or center_stack_index not in available_stacks): msnc.illegal_value('center_stack_index', center_stack_index, 'config file') else: if self.test_mode: assert(find_center.get('completed', False) == False) else: print('\nFound calibration center offset info for stack '+ f'{center_stack_index}') if (pyip.inputYesNo('Do you want to use this again ([y]/n)? ', blank=True) != 'no' and find_center.get('completed', False) == True): return # Load the required preprocessed stack # preprocessed stack order: row,theta,column num_tomo_stacks = self.config['stack_info']['num'] assert(len(stacks) == num_tomo_stacks) assert(len(self.tomo_stacks) == num_tomo_stacks) if num_tomo_stacks == 1: if not center_stack_index: center_stack_index = stacks[0]['index'] elif center_stack_index != stacks[0]['index']: raise ValueError(f'Inconsistent center_stack_index {center_stack_index}') if not self.tomo_stacks[0].size: self.tomo_stacks[0], available = self._loadTomo('red stack', center_stack_index, required=True) center_stack = self.tomo_stacks[0] if not center_stack.size: stacks[0]['preprocessed'] = False raise OSError('Unable to load the required preprocessed tomography stack') assert(stacks[0].get('preprocessed', False) == True) elif self.galaxy_flag: logging.error('CHECK/FIX FOR GALAXY') #center_stack_index = stacks[int(num_tomo_stacks/2)]['index'] else: while True: if not center_stack_index: center_stack_index = pyip.inputInt('\nEnter tomography stack index to get ' f'rotation axis centers {available_stacks}: ') while center_stack_index not in available_stacks: center_stack_index = pyip.inputInt('\nEnter tomography stack index to get ' f'rotation axis centers {available_stacks}: ') tomo_stack_index = available_stacks.index(center_stack_index) if not self.tomo_stacks[tomo_stack_index].size: self.tomo_stacks[tomo_stack_index], available = self._loadTomo( 'red stack', center_stack_index, required=True) center_stack = self.tomo_stacks[tomo_stack_index] if not center_stack.size: stacks[tomo_stack_index]['preprocessed'] = False logging.error(f'Unable to load the {center_stack_index}th '+ 'preprocessed tomography stack, pick another one') else: break assert(stacks[tomo_stack_index].get('preprocessed', False) == True) if find_center is None: self.config['find_center'] = {'center_stack_index' : center_stack_index} find_center = self.config['find_center'] else: find_center['center_stack_index'] = center_stack_index if not self.galaxy_flag: row_bounds = find_center.get('row_bounds', None) center_rows = [find_center.get('lower_row', None), find_center.get('upper_row', None)] if row_bounds is None: row_bounds = [0, center_stack.shape[0]] if row_bounds[0] == -1: row_bounds[0] = 0 if row_bounds[1] == -1: row_bounds[1] = center_stack.shape[0] if not msnc.is_index_range(row_bounds, 0, center_stack.shape[0]): msnc.illegal_value('row_bounds', row_bounds) return # Set thetas (in degrees) theta_range = self.config['theta_range'] theta_start = theta_range['start'] theta_end = theta_range['end'] num_theta = theta_range['num'] num_theta_skip = self.config['preprocess'].get('num_theta_skip', 0) thetas_deg = np.linspace(theta_start, theta_end, int(num_theta/(num_theta_skip+1)), endpoint=False) # Get non-overlapping sample row boundaries zoom_perc = self.config['preprocess'].get('zoom_perc', 100) pixel_size = self.config['detector']['pixel_size'] if pixel_size is None: raise ValueError('Detector pixel size unavailable') eff_pixel_size = 100.*pixel_size/zoom_perc logging.debug(f'eff_pixel_size = {eff_pixel_size}') if num_tomo_stacks == 1: accept = 'yes' if not self.test_mode and not self.galaxy_flag: accept = 'no' print('\nSelect bounds for image reconstruction') if msnc.is_index_range(row_bounds, 0, center_stack.shape[0]): a_tmp = np.copy(center_stack[:,0,:]) a_tmp_max = a_tmp.max() a_tmp[row_bounds[0],:] = a_tmp_max a_tmp[row_bounds[1]-1,:] = a_tmp_max print(f'lower bound = {row_bounds[0]} (inclusive)') print(f'upper bound = {row_bounds[1]} (exclusive)') msnc.quickImshow(a_tmp, title=f'center stack theta={theta_start}', aspect='auto') del a_tmp accept = pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) if accept == 'no': (n1, n2) = msnc.selectImageBounds(center_stack[:,0,:], 0, title=f'center stack theta={theta_start}') else: n1 = row_bounds[0] n2 = row_bounds[1] else: logging.error('CHECK/FIX FOR GALAXY') tomo_ref_heights = [stack['ref_height'] for stack in stacks] n1 = int((1.+(tomo_ref_heights[0]+center_stack.shape[0]*eff_pixel_size- tomo_ref_heights[1])/eff_pixel_size)/2) n2 = center_stack.shape[0]-n1 logging.info(f'n1 = {n1}, n2 = {n2} (n2-n1) = {(n2-n1)*eff_pixel_size:.3f} mm') if not center_stack.size: RuntimeError('Center stack not loaded') if not self.test_mode and not self.galaxy_flag: tmp = center_stack[:,0,:] tmp_max = tmp.max() tmp[n1,:] = tmp_max tmp[n2-1,:] = tmp_max if msnc.is_index_range(center_rows, 0, tmp.shape[0]): tmp[center_rows[0],:] = tmp_max tmp[center_rows[1]-1,:] = tmp_max extent = [0, tmp.shape[1], tmp.shape[0], 0] msnc.quickImshow(tmp, title=f'center stack theta={theta_start}', path=self.output_folder, extent=extent, save_fig=self.save_plots, save_only=self.save_plots_only, aspect='auto') del tmp #extent = [0, center_stack.shape[2], n2, n1] #msnc.quickImshow(center_stack[n1:n2,0,:], title=f'center stack theta={theta_start}', # path=self.output_folder, extent=extent, save_fig=self.save_plots, # save_only=self.save_plots_only, show_grid=True, aspect='auto') # Get cross sectional diameter in mm cross_sectional_dim = center_stack.shape[2]*eff_pixel_size logging.debug(f'cross_sectional_dim = {cross_sectional_dim}') # Determine center offset at sample row boundaries logging.info('Determine center offset at sample row boundaries') # Lower row center use_row = 'no' use_center = 'no' row = center_rows[0] if self.test_mode or self.galaxy_flag: assert(msnc.is_int(row, n1, n2-2)) if msnc.is_int(row, n1, n2-2): if self.test_mode or self.galaxy_flag: use_row = 'yes' else: msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto') use_row = pyip.inputYesNo('\nCurrent row index for lower center = ' f'{row}, use this value ([y]/n)? ', blank=True) if self.save_plots_only: msnc.clearFig(f'theta={theta_start}') if use_row != 'no': center_offset = find_center.get('lower_center_offset') if msnc.is_num(center_offset): use_center = pyip.inputYesNo('Current lower center offset = '+ f'{center_offset}, use this value ([y]/n)? ', blank=True) if use_center == 'no': if use_row == 'no': if not self.test_mode: msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto') row = pyip.inputInt('\nEnter row index to find lower center '+ f'[[{n1}], {n2-2}]: ', min=n1, max=n2-2, blank=True) if row == '': row = n1 if self.save_plots_only: msnc.clearFig(f'theta={theta_start}') # center_stack order: row,theta,column center_offset = self._findCenterOnePlane(center_stack[row,:,:], row, thetas_deg, eff_pixel_size, cross_sectional_dim, num_core=num_core, galaxy_param=galaxy_param) logging.info(f'lower_center_offset = {center_offset:.2f} {type(center_offset)}') print(center_offset) # Update config and save to file find_center['row_bounds'] = [n1, n2] find_center['lower_row'] = row find_center['lower_center_offset'] = center_offset self.cf.saveFile(self.config_out) lower_row = row # Upper row center use_row = 'no' use_center = 'no' row = center_rows[1] if self.test_mode or self.galaxy_flag: assert(msnc.is_int(row, lower_row+1, n2-1)) if msnc.is_int(row, lower_row+1, n2-1): if self.test_mode or self.galaxy_flag: use_row = 'yes' else: msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto') use_row = pyip.inputYesNo('\nCurrent row index for upper center = ' f'{row}, use this value ([y]/n)? ', blank=True) if self.save_plots_only: msnc.clearFig(f'theta={theta_start}') if use_row != 'no': center_offset = find_center.get('upper_center_offset') if msnc.is_num(center_offset): use_center = pyip.inputYesNo('Current upper center offset = '+ f'{center_offset}, use this value ([y]/n)? ', blank=True) if use_center == 'no': if use_row == 'no': if not self.test_mode: msnc.quickImshow(center_stack[:,0,:], title=f'theta={theta_start}', aspect='auto') row = pyip.inputInt('\nEnter row index to find upper center '+ f'[{lower_row+1}, [{n2-1}]]: ', min=lower_row+1, max=n2-1, blank=True) if row == '': row = n2-1 if self.save_plots_only: msnc.clearFig(f'theta={theta_start}') # center_stack order: row,theta,column center_offset = self._findCenterOnePlane(center_stack[row,:,:], row, thetas_deg, eff_pixel_size, cross_sectional_dim, num_core=num_core, galaxy_param=galaxy_param) logging.info(f'upper_center_offset = {center_offset:.2f}') del center_stack # Update config and save to file find_center['upper_row'] = row find_center['upper_center_offset'] = center_offset find_center['completed'] = True self.cf.saveFile(self.config_out) def checkCenters(self): """Check centers for the tomography stacks. """ #RV TODO load all stacks and check at all stack boundaries return logging.debug('Check centers for tomography stacks') center_stack_index = self.config.get('center_stack_index') if center_stack_index is None: raise ValueError('Unable to read center_stack_index from config') center_stack_index = self.tomo_stacks[self.tomo_data_indices.index(center_stack_index)] lower_row = self.config.get('lower_row') if lower_row is None: raise ValueError('Unable to read lower_row from config') lower_center_offset = self.config.get('lower_center_offset') if lower_center_offset is None: raise ValueError('Unable to read lower_center_offset from config') upper_row = self.config.get('upper_row') if upper_row is None: raise ValueError('Unable to read upper_row from config') upper_center_offset = self.config.get('upper_center_offset') if upper_center_offset is None: raise ValueError('Unable to read upper_center_offset from config') center_slope = (upper_center_offset-lower_center_offset)/(upper_row-lower_row) shift = upper_center_offset-lower_center_offset if lower_row == 0: logging.warning(f'lower_row == 0: one row offset between both planes') else: lower_row -= 1 lower_center_offset -= center_slope # stack order: stack,row,theta,column if center_stack_index: stack1 = self.tomo_stacks[center_stack_index-1] stack2 = self.tomo_stacks[center_stack_index] if not stack1.size: logging.error(f'Unable to load required tomography stack {stack1}') elif not stack2.size: logging.error(f'Unable to load required tomography stack {stack1}') else: assert(0 <= lower_row < stack2.shape[0]) assert(0 <= upper_row < stack1.shape[0]) plane1 = stack1[upper_row,:] plane2 = stack2[lower_row,:] for i in range(-2, 3): shift_i = shift+2*i plane1_shifted = spi.shift(plane2, [0, shift_i]) msnc.quickPlot((plane1[0,:],), (plane1_shifted[0,:],), title=f'stacks {stack1} {stack2} shifted {2*i} theta={self.start_theta}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) if center_stack_index < self.num_tomo_stacks-1: stack1 = self.tomo_stacks[center_stack_index] stack2 = self.tomo_stacks[center_stack_index+1] if not stack1.size: logging.error(f'Unable to load required tomography stack {stack1}') elif not stack2.size: logging.error(f'Unable to load required tomography stack {stack1}') else: assert(0 <= lower_row < stack2.shape[0]) assert(0 <= upper_row < stack1.shape[0]) plane1 = stack1[upper_row,:] plane2 = stack2[lower_row,:] for i in range(-2, 3): shift_i = -shift+2*i plane1_shifted = spi.shift(plane2, [0, shift_i]) msnc.quickPlot((plane1[0,:],), (plane1_shifted[0,:],), title=f'stacks {stack1} {stack2} shifted {2*i} theta={start_theta}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) del plane1, plane2, plane1_shifted # Update config file self.config = msnc.update('config.txt', 'check_centers', True, 'find_centers') def reconstructTomoStacks(self, galaxy_param=None, num_core=None): """Reconstruct tomography stacks. """ print('OK1') if num_core is None: num_core = self.num_core logging.debug('Reconstruct tomography stacks') stacks = self.config['stack_info']['stacks'] assert(len(self.tomo_stacks) == self.config['stack_info']['num']) assert(len(self.tomo_stacks) == len(stacks)) assert(len(self.tomo_recon_stacks) == len(stacks)) print('OK2') if self.galaxy_flag: assert(isinstance(galaxy_param, dict)) # Get rotation axis centers center_offsets = galaxy_param['center_offsets'] assert(isinstance(center_offsets, list) and len(center_offsets) == 2) lower_center_offset = center_offsets[0] assert(msnc.is_num(lower_center_offset)) upper_center_offset = center_offsets[1] assert(msnc.is_num(upper_center_offset)) else: if galaxy_param: logging.warning('Ignoring galaxy_param in reconstructTomoStacks (only for Galaxy)') galaxy_param = None lower_center_offset = None upper_center_offset = None print('OK3') # Get rotation axis rows and centers find_center = self.config['find_center'] lower_row = find_center.get('lower_row') if lower_row is None: logging.error('Unable to read lower_row from config') return upper_row = find_center.get('upper_row') if upper_row is None: logging.error('Unable to read upper_row from config') return logging.debug(f'lower_row = {lower_row} upper_row = {upper_row}') assert(lower_row < upper_row) if lower_center_offset is None: lower_center_offset = find_center.get('lower_center_offset') if lower_center_offset is None: logging.error('Unable to read lower_center_offset from config') return if upper_center_offset is None: upper_center_offset = find_center.get('upper_center_offset') if upper_center_offset is None: logging.error('Unable to read upper_center_offset from config') return center_slope = (upper_center_offset-lower_center_offset)/(upper_row-lower_row) # Set thetas (in radians) print('OK4') theta_range = self.config['theta_range'] theta_start = theta_range['start'] theta_end = theta_range['end'] num_theta = theta_range['num'] num_theta_skip = self.config['preprocess'].get('num_theta_skip', 0) thetas = np.radians(np.linspace(theta_start, theta_end, int(num_theta/(num_theta_skip+1)), endpoint=False)) print('OK5') # Reconstruct tomo stacks zoom_perc = self.config['preprocess'].get('zoom_perc', 100) if zoom_perc == 100: basetitle = 'recon stack full' else: basetitle = f'recon stack {zoom_perc}p' load_error = False stacks = self.config['stack_info']['stacks'] for i,stack in enumerate(stacks): # Check if stack can be loaded # reconstructed stack order for each one in stack : row/z,x,y # preprocessed stack order for each one in stack: row,theta,column index = stack['index'] if not self.galaxy_flag: available = False if stack.get('reconstructed', False): self.tomo_recon_stacks[i], available = self._loadTomo('recon stack', index) if self.tomo_recon_stacks[i].size or available: if self.tomo_stacks[i].size: self.tomo_stacks[i] = np.array([]) assert(stack.get('preprocessed', False) == True) assert(stack.get('reconstructed', False) == True) continue stack['reconstructed'] = False if not self.tomo_stacks[i].size: self.tomo_stacks[i], available = self._loadTomo('red stack', index, required=True) print(f'self.tomo_stacks.shape = {self.tomo_stacks[i].shape}') if not self.tomo_stacks[i].size: logging.error(f'Unable to load tomography stack {index} for reconstruction') stack[i]['preprocessed'] = False load_error = True continue assert(0 <= lower_row < upper_row < self.tomo_stacks[i].shape[0]) center_offsets = [lower_center_offset-lower_row*center_slope, upper_center_offset+(self.tomo_stacks[i].shape[0]-1-upper_row)*center_slope] t0 = time() self.tomo_recon_stacks[i]= self._reconstructOneTomoStack(self.tomo_stacks[i], thetas, center_offsets=center_offsets, sigma=0.1, num_core=num_core, algorithm='gridrec', run_secondary_sirt=True, secondary_iter=25) logging.info(f'Reconstruction of stack {index} took {time()-t0:.2f} seconds!') if not self.test_mode and not self.galaxy_flag: row_slice = int(self.tomo_stacks[i].shape[0]/2) title = f'{basetitle} {index} slice{row_slice}' msnc.quickImshow(self.tomo_recon_stacks[i][row_slice,:,:], title=title, path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) msnc.quickPlot(self.tomo_recon_stacks[i] [row_slice,int(self.tomo_recon_stacks[i].shape[2]/2),:], title=f'{title} cut{int(self.tomo_recon_stacks[i].shape[2]/2)}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) self._saveTomo('recon stack', self.tomo_recon_stacks[i], index) # else: # np.savetxt(self.output_folder+f'recon_stack_{index}.txt', # self.tomo_recon_stacks[i][row_slice,:,:], fmt='%.6e') self.tomo_stacks[i] = np.array([]) print('OK6') # Update config and save to file stack['reconstructed'] = True combine_stacks = self.config.get('combine_stacks') if combine_stacks and index in combine_stacks.get('stacks', []): combine_stacks['stacks'].remove(index) self.cf.saveFile(self.config_out) print('OK7') # Save reconstructed tomography stack to file if self.galaxy_flag: t0 = time() output_name = galaxy_param['output_name'] logging.info(f'Saving reconstructed tomography stack to {output_name} ...') save_stacks = {f'set_{stack["index"]}':tomo_stack for stack,tomo_stack in zip(stacks,self.tomo_recon_stacks)} np.savez(output_name, **save_stacks) logging.info(f'... done in {time()-t0:.2f} seconds!') def combineTomoStacks(self): """Combine the reconstructed tomography stacks. """ # stack order: stack,row(z),x,y logging.debug('Combine reconstructed tomography stacks') # Load any unloaded reconstructed stacks stack_info = self.config['stack_info'] stacks = stack_info['stacks'] for i,stack in enumerate(stacks): available = False if not self.tomo_recon_stacks[i].size and stack.get('reconstructed', False): self.tomo_recon_stacks[i], available = self._loadTomo('recon stack', stack['index'], required=True) if not self.tomo_recon_stacks[i].size: logging.error(f'Unable to load reconstructed stack {stack["index"]}') stack['reconstructed'] = False return if i: if (self.tomo_recon_stacks[i].shape[1] != self.tomo_recon_stacks[0].shape[1] or self.tomo_recon_stacks[i].shape[1] != self.tomo_recon_stacks[0].shape[1]): logging.error('Incompatible reconstructed tomography stack dimensions') return # Get center stack boundaries row_bounds = self.config['find_center']['row_bounds'] if not msnc.is_index_range(row_bounds, 0, self.tomo_recon_stacks[0].shape[0]): msnc.illegal_value('row_bounds', row_bounds, 'config file') return # Selecting x bounds (in yz-plane) tomosum = 0 #RV FIX := [tomosum := tomosum+np.sum(tomo_recon_stack, axis=(0,2)) for tomo_recon_stack in self.tomo_recon_stacks] combine_stacks = self.config.get('combine_stacks') if combine_stacks and 'x_bounds' in combine_stacks: x_bounds = combine_stacks['x_bounds'] if not msnc.is_index_range(x_bounds, 0, self.tomo_recon_stacks[0].shape[1]): msnc.illegal_value('x_bounds', x_bounds, 'config file') elif not self.test_mode: msnc.quickPlot(tomosum, title='recon stack sum yz') if pyip.inputYesNo('\nCurrent image x-bounds: '+ f'[{x_bounds[0]}, {x_bounds[1]}], use these values ([y]/n)? ', blank=True) == 'no': if pyip.inputYesNo( 'Do you want to change the image x-bounds ([y]/n)? ', blank=True) == 'no': x_bounds = [0, self.tomo_recon_stacks[0].shape[1]] else: x_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum yz') else: msnc.quickPlot(tomosum, title='recon stack sum yz') if pyip.inputYesNo('\nDo you want to change the image x-bounds (y/n)? ') == 'no': x_bounds = [0, self.tomo_recon_stacks[0].shape[1]] else: x_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum yz') if False and self.test_mode: np.savetxt(f'{self.output_folder}/recon_stack_sum_yz.txt', tomosum[x_bounds[0]:x_bounds[1]], fmt='%.6e') if self.save_plots_only: msnc.clearFig('recon stack sum yz') # Selecting y bounds (in xz-plane) tomosum = 0 #RV FIX := [tomosum := tomosum+np.sum(tomo_recon_stack, axis=(0,1)) for tomo_recon_stack in self.tomo_recon_stacks] if combine_stacks and 'y_bounds' in combine_stacks: y_bounds = combine_stacks['y_bounds'] if not msnc.is_index_range(y_bounds, 0, self.tomo_recon_stacks[0].shape[1]): msnc.illegal_value('y_bounds', y_bounds, 'config file') elif not self.test_mode: msnc.quickPlot(tomosum, title='recon stack sum xz') if pyip.inputYesNo('\nCurrent image y-bounds: '+ f'[{y_bounds[0]}, {y_bounds[1]}], use these values ([y]/n)? ', blank=True) == 'no': if pyip.inputYesNo( 'Do you want to change the image y-bounds ([y]/n)? ', blank=True) == 'no': y_bounds = [0, self.tomo_recon_stacks[0].shape[1]] else: y_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum yz') else: msnc.quickPlot(tomosum, title='recon stack sum xz') if pyip.inputYesNo('\nDo you want to change the image y-bounds (y/n)? ') == 'no': y_bounds = [0, self.tomo_recon_stacks[0].shape[1]] else: y_bounds = msnc.selectArrayBounds(tomosum, title='recon stack sum xz') if False and self.test_mode: np.savetxt(f'{self.output_folder}/recon_stack_sum_xz.txt', tomosum[y_bounds[0]:y_bounds[1]], fmt='%.6e') if self.save_plots_only: msnc.clearFig('recon stack sum xz') # Combine reconstructed tomography stacks logging.info(f'Combining reconstructed stacks ...') t0 = time() num_tomo_stacks = stack_info['num'] if num_tomo_stacks == 1: low_bound = row_bounds[0] else: low_bound = 0 tomo_recon_combined = self.tomo_recon_stacks[0][low_bound:row_bounds[1]:, x_bounds[0]:x_bounds[1],y_bounds[0]:y_bounds[1]] if num_tomo_stacks > 2: tomo_recon_combined = np.concatenate([tomo_recon_combined]+ [self.tomo_recon_stacks[i][row_bounds[0]:row_bounds[1], x_bounds[0]:x_bounds[1],y_bounds[0]:y_bounds[1]] for i in range(1, num_tomo_stacks-1)]) if num_tomo_stacks > 1: tomo_recon_combined = np.concatenate([tomo_recon_combined]+ [self.tomo_recon_stacks[num_tomo_stacks-1][row_bounds[0]:, x_bounds[0]:x_bounds[1],y_bounds[0]:y_bounds[1]]]) logging.info(f'... done in {time()-t0:.2f} seconds!') combined_stacks = [stack['index'] for stack in stacks] # Wrap up if in test_mode tomosum = np.sum(tomo_recon_combined, axis=(1,2)) if self.test_mode: zoom_perc = self.config['preprocess'].get('zoom_perc', 100) filename = 'recon combined sum xy' if zoom_perc is None or zoom_perc == 100: filename += ' fullres.dat' else: filename += f' {zoom_perc}p.dat' msnc.quickPlot(tomosum, title='recon combined sum xy', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) if False: np.savetxt(f'{self.output_folder}/recon_combined_sum_xy.txt', tomosum, fmt='%.6e') np.savetxt(f'{self.output_folder}/recon_combined.txt', tomo_recon_combined[int(tomo_recon_combined.shape[0]/2),:,:], fmt='%.6e') # Update config and save to file if combine_stacks: combine_stacks['x_bounds'] = x_bounds combine_stacks['y_bounds'] = y_bounds combine_stacks['stacks'] = combined_stacks else: self.config['combine_stacks'] = {'x_bounds' : x_bounds, 'y_bounds' : y_bounds, 'stacks' : combined_stacks} self.cf.saveFile(self.config_out) return # Selecting z bounds (in xy-plane) msnc.quickPlot(tomosum, title='recon combined sum xy') if pyip.inputYesNo( '\nDo you want to change the image z-bounds (y/[n])? ', blank=True) != 'yes': z_bounds = [0, tomo_recon_combined.shape[0]] else: z_bounds = msnc.selectArrayBounds(tomosum, title='recon combined sum xy') if z_bounds[0] != 0 or z_bounds[1] != tomo_recon_combined.shape[0]: tomo_recon_combined = tomo_recon_combined[z_bounds[0]:z_bounds[1],:,:] logging.info(f'tomo_recon_combined.shape = {tomo_recon_combined.shape}') if self.save_plots_only: msnc.clearFig('recon combined sum xy') # Plot center slices msnc.quickImshow(tomo_recon_combined[int(tomo_recon_combined.shape[0]/2),:,:], title=f'recon combined xslice{int(tomo_recon_combined.shape[0]/2)}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) msnc.quickImshow(tomo_recon_combined[:,int(tomo_recon_combined.shape[1]/2),:], title=f'recon combined yslice{int(tomo_recon_combined.shape[1]/2)}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) msnc.quickImshow(tomo_recon_combined[:,:,int(tomo_recon_combined.shape[2]/2)], title=f'recon combined zslice{int(tomo_recon_combined.shape[2]/2)}', path=self.output_folder, save_fig=self.save_plots, save_only=self.save_plots_only) # Save combined reconstructed tomo stacks base_name = 'recon combined' for stack in stacks: base_name += f' {stack["index"]}' self._saveTomo(base_name, tomo_recon_combined) # Update config and save to file if combine_stacks: combine_stacks['x_bounds'] = x_bounds combine_stacks['y_bounds'] = y_bounds combine_stacks['z_bounds'] = z_bounds combine_stacks['stacks'] = combined_stacks else: self.config['combine_stacks'] = {'x_bounds' : x_bounds, 'y_bounds' : y_bounds, 'z_bounds' : z_bounds, 'stacks' : combined_stacks} self.cf.saveFile(self.config_out) def runTomo(config_file=None, config_dict=None, output_folder='.', log_level='INFO', test_mode=False, num_core=-1): """Run a tomography analysis. """ # Instantiate Tomo object tomo = Tomo(config_file=config_file, output_folder=output_folder, log_level=log_level, test_mode=test_mode, num_core=num_core) if not tomo.is_valid: raise ValueError('Invalid config and/or detector file provided.') # Preprocess the image files assert(tomo.config['stack_info']) num_tomo_stacks = tomo.config['stack_info']['num'] assert(num_tomo_stacks == len(tomo.tomo_stacks)) preprocessed_stacks = [] if not tomo.test_mode: preprocess = tomo.config.get('preprocess', None) if preprocess: preprocessed_stacks = [stack['index'] for stack in tomo.config['stack_info']['stacks'] if stack.get('preprocessed', False)] if len(preprocessed_stacks) != num_tomo_stacks: tomo.genTomoStacks() if not tomo.is_valid: IOError('Unable to load all required image files.') tomo.cf.saveFile(tomo.config_out) # Find centers find_center = tomo.config.get('find_center') if find_center is None or not find_center.get('completed', False): tomo.findCenters() # Check centers #if num_tomo_stacks > 1 and not tomo.config.get('check_centers', False): # tomo.checkCenters() # Reconstruct tomography stacks assert(tomo.config['stack_info']['stacks']) reconstructed_stacks = [stack['index'] for stack in tomo.config['stack_info']['stacks'] if stack.get('reconstructed', False)] if len(reconstructed_stacks) != num_tomo_stacks: tomo.reconstructTomoStacks() # Combine reconstructed tomography stacks reconstructed_stacks = [stack['index'] for stack in tomo.config['stack_info']['stacks'] if stack.get('reconstructed', False)] combine_stacks = tomo.config.get('combine_stacks') if len(reconstructed_stacks) and (combine_stacks is None or combine_stacks.get('stacks') != reconstructed_stacks): tomo.combineTomoStacks() #%%============================================================================ if __name__ == '__main__': # Parse command line arguments parser = argparse.ArgumentParser( description='Tomography reconstruction') parser.add_argument('-c', '--config', default=None, help='Input config') parser.add_argument('-o', '--output_folder', default='.', help='Output folder') parser.add_argument('-l', '--log_level', default='INFO', help='Log level') parser.add_argument('-t', '--test_mode', action='store_true', default=False, help='Test mode flag') parser.add_argument('--num_core', type=int, default=-1, help='Number of cores') args = parser.parse_args() if args.config is None: if os.path.isfile('config.yaml'): args.config = 'config.yaml' else: args.config = 'config.txt' # Set basic log configuration logging_format = '%(asctime)s : %(levelname)s - %(module)s : %(funcName)s - %(message)s' if not args.test_mode: level = getattr(logging, args.log_level.upper(), None) if not isinstance(level, int): raise ValueError(f'Invalid log_level: {args.log_level}') logging.basicConfig(format=logging_format, level=level, force=True, handlers=[logging.StreamHandler()]) logging.debug(f'config = {args.config}') logging.debug(f'output_folder = {args.output_folder}') logging.debug(f'log_level = {args.log_level}') logging.debug(f'test_mode = {args.test_mode}') logging.debug(f'num_core = {args.num_core}') # Run tomography analysis runTomo(config_file=args.config, output_folder=args.output_folder, log_level=args.log_level, test_mode=args.test_mode, num_core=args.num_core) #%%============================================================================ # input('Press any key to continue') #%%============================================================================