ChessAnalysisPipeline 0.0.13__py3-none-any.whl → 0.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ChessAnalysisPipeline might be problematic. Click here for more details.

CHAP/tomo/processor.py CHANGED
@@ -8,7 +8,6 @@ Description: Module for Processors used only by tomography experiments
8
8
  """
9
9
 
10
10
  # System modules
11
- from os import mkdir
12
11
  from os import path as os_path
13
12
  from sys import exit as sys_exit
14
13
  from time import time
@@ -19,16 +18,18 @@ import numpy as np
19
18
  # Local modules
20
19
  from CHAP.utils.general import (
21
20
  is_num,
21
+ is_num_series,
22
22
  is_int_pair,
23
23
  input_int,
24
24
  input_num,
25
+ input_num_list,
25
26
  input_yesno,
26
27
  select_image_indices,
27
28
  select_roi_1d,
28
29
  select_roi_2d,
29
30
  quick_imshow,
31
+ nxcopy,
30
32
  )
31
- from CHAP.utils.fit import Fit
32
33
  from CHAP.processor import Processor
33
34
  from CHAP.reader import main
34
35
 
@@ -111,6 +112,7 @@ class TomoCHESSMapConverter(Processor):
111
112
  NXdetector,
112
113
  NXentry,
113
114
  NXinstrument,
115
+ NXlink,
114
116
  NXroot,
115
117
  NXsample,
116
118
  NXsource,
@@ -125,12 +127,19 @@ class TomoCHESSMapConverter(Processor):
125
127
  tomofields = get_nxroot(data, 'tomofields')
126
128
  detector_config = self.get_config(data, 'tomo.models.Detector')
127
129
 
128
- if darkfield is not None and not isinstance(darkfield, NXentry):
129
- raise ValueError('Invalid parameter darkfield ({darkfield})')
130
+ if darkfield is not None:
131
+ if isinstance(darkfield, NXroot):
132
+ darkfield = darkfield[darkfield.default]
133
+ if not isinstance(darkfield, NXentry):
134
+ raise ValueError(f'Invalid parameter darkfield ({darkfield})')
135
+ if isinstance(brightfield, NXroot):
136
+ brightfield = brightfield[brightfield.default]
130
137
  if not isinstance(brightfield, NXentry):
131
- raise ValueError('Invalid parameter brightfield ({brightfield})')
138
+ raise ValueError(f'Invalid parameter brightfield ({brightfield})')
139
+ if isinstance(tomofields, NXroot):
140
+ tomofields = tomofields[tomofields.default]
132
141
  if not isinstance(tomofields, NXentry):
133
- raise ValueError('Invalid parameter tomofields {tomofields})')
142
+ raise ValueError(f'Invalid parameter tomofields {tomofields})')
134
143
 
135
144
  # Construct NXroot
136
145
  nxroot = NXroot()
@@ -148,7 +157,7 @@ class TomoCHESSMapConverter(Processor):
148
157
  '(available independent dimensions: '
149
158
  f'{independent_dimensions})')
150
159
  rotation_angles_index = \
151
- tomofields.data.attrs['rotation_angles_indices']
160
+ tomofields.data.axes.index('rotation_angles')
152
161
  rotation_angle_data_type = \
153
162
  tomofields.data.rotation_angles.attrs['data_type']
154
163
  if rotation_angle_data_type != 'scan_column':
@@ -157,7 +166,7 @@ class TomoCHESSMapConverter(Processor):
157
166
  matched_dimensions.pop(matched_dimensions.index('rotation_angles'))
158
167
  if 'x_translation' in independent_dimensions:
159
168
  x_translation_index = \
160
- tomofields.data.attrs['x_translation_indices']
169
+ tomofields.data.axes.index('x_translation')
161
170
  x_translation_data_type = \
162
171
  tomofields.data.x_translation.attrs['data_type']
163
172
  x_translation_name = \
@@ -170,7 +179,7 @@ class TomoCHESSMapConverter(Processor):
170
179
  x_translation_data_type = None
171
180
  if 'z_translation' in independent_dimensions:
172
181
  z_translation_index = \
173
- tomofields.data.attrs['z_translation_indices']
182
+ tomofields.data.axes.index('z_translation')
174
183
  z_translation_data_type = \
175
184
  tomofields.data.z_translation.attrs['data_type']
176
185
  z_translation_name = \
@@ -188,9 +197,11 @@ class TomoCHESSMapConverter(Processor):
188
197
  '"rotation_angles"}')
189
198
 
190
199
  # Construct base NXentry and add to NXroot
191
- nxentry = NXentry()
192
- nxroot[map_config.title] = nxentry
193
- nxroot.attrs['default'] = map_config.title
200
+ nxentry = NXentry(name=map_config.title)
201
+ nxroot[nxentry.nxname] = nxentry
202
+ nxentry.set_default()
203
+
204
+ # Add configuration fields
194
205
  nxentry.definition = 'NXtomo'
195
206
  nxentry.map_config = tomofields.map_config
196
207
 
@@ -214,12 +225,12 @@ class TomoCHESSMapConverter(Processor):
214
225
  # Add an NXdetector to the NXinstrument
215
226
  # (do not fill in data fields yet)
216
227
  detector_prefix = detector_config.prefix
217
- detectors = list(set(tomofields.data.entries)
218
- - set(independent_dimensions))
228
+ detectors = list(
229
+ set(tomofields.data.entries) - set(independent_dimensions))
219
230
  if detector_prefix not in detectors:
220
231
  raise ValueError(f'Data for detector {detector_prefix} is '
221
232
  f'unavailable (available detectors: {detectors})')
222
- tomo_stacks = np.asarray(tomofields.data[detector_prefix])
233
+ tomo_stacks = tomofields.data[detector_prefix]
223
234
  tomo_stack_shape = tomo_stacks.shape
224
235
  assert len(tomo_stack_shape) == 2+len(independent_dimensions)
225
236
  assert tomo_stack_shape[-2] == detector_config.rows
@@ -272,8 +283,8 @@ class TomoCHESSMapConverter(Processor):
272
283
  num_image = data_shape[0]
273
284
  image_keys += num_image*[2]
274
285
  sequence_numbers += list(range(num_image))
275
- image_stacks.append(np.asarray(
276
- nxcollection.data[detector_prefix]))
286
+ image_stacks.append(
287
+ nxcollection.data[detector_prefix])
277
288
  rotation_angles += num_image*[0.0]
278
289
  if (x_translation_data_type == 'spec_motor' or
279
290
  z_translation_data_type == 'spec_motor'):
@@ -312,8 +323,8 @@ class TomoCHESSMapConverter(Processor):
312
323
  num_image = data_shape[0]
313
324
  image_keys += num_image*[1]
314
325
  sequence_numbers += list(range(num_image))
315
- image_stacks.append(np.asarray(
316
- nxcollection.data[detector_prefix]))
326
+ image_stacks.append(
327
+ nxcollection.data[detector_prefix])
317
328
  rotation_angles += num_image*[0.0]
318
329
  if (x_translation_data_type == 'spec_motor' or
319
330
  z_translation_data_type == 'spec_motor'):
@@ -347,10 +358,11 @@ class TomoCHESSMapConverter(Processor):
347
358
  z_trans = [0.0]
348
359
  tomo_stacks = np.reshape(tomo_stacks, (1,1,*tomo_stacks.shape))
349
360
  else:
350
- if len(list(tomofields.data.z_translation)):
351
- z_trans = list(tomofields.data.z_translation)
352
- else:
353
- z_trans = [float(tomofields.data.z_translation)]
361
+ z_trans = tomofields.data.z_translation.nxdata
362
+ # if len(list(tomofields.data.z_translation)):
363
+ # z_trans = list(tomofields.data.z_translation)
364
+ # else:
365
+ # z_trans = [float(tomofields.data.z_translation)]
354
366
  if rotation_angles_index < z_translation_index:
355
367
  tomo_stacks = np.swapaxes(
356
368
  tomo_stacks, rotation_angles_index,
@@ -363,14 +375,16 @@ class TomoCHESSMapConverter(Processor):
363
375
  tomo_stacks, rotation_angles_index, x_translation_index)
364
376
  tomo_stacks = np.expand_dims(tomo_stacks, 0)
365
377
  else:
366
- if len(list(tomofields.data.x_translation)):
367
- x_trans = list(tomofields.data.x_translation)
368
- else:
369
- x_trans = [float(tomofields.data.x_translation)]
370
- if len(list(tomofields.data.z_translation)):
371
- z_trans = list(tomofields.data.z_translation)
372
- else:
373
- z_trans = [float(tomofields.data.z_translation)]
378
+ x_trans = tomofields.data.x_translation.nxdata
379
+ z_trans = tomofields.data.z_translation.nxdata
380
+ #if tomofields.data.x_translation.size > 1:
381
+ # x_trans = list(tomofields.data.x_translation)
382
+ #else:
383
+ # x_trans = [float(tomofields.data.x_translation)]
384
+ #if len(list(tomofields.data.z_translation)):
385
+ # z_trans = list(tomofields.data.z_translation)
386
+ #else:
387
+ # z_trans = [float(tomofields.data.z_translation)]
374
388
  if (rotation_angles_index
375
389
  < max(x_translation_index, z_translation_index)):
376
390
  tomo_stacks = np.swapaxes(
@@ -380,8 +394,7 @@ class TomoCHESSMapConverter(Processor):
380
394
  tomo_stacks = np.swapaxes(
381
395
  tomo_stacks, x_translation_index, z_translation_index)
382
396
  # Restrict to 180 degrees set of data for now to match old code
383
- thetas = np.asarray(tomofields.data.rotation_angles)
384
- #RV num_image = len(tomofields.data.rotation_angles)
397
+ thetas = tomofields.data.rotation_angles.nxdata
385
398
  assert len(thetas) > 2
386
399
  delta_theta = thetas[1]-thetas[0]
387
400
  if thetas[-1]-thetas[0] > 180-delta_theta:
@@ -394,10 +407,8 @@ class TomoCHESSMapConverter(Processor):
394
407
  for j, x in enumerate(x_trans):
395
408
  image_keys += num_image*[0]
396
409
  sequence_numbers += list(range(num_image))
397
- image_stacks.append(np.asarray(
398
- tomo_stacks[i,j][:image_end,:,:]))
410
+ image_stacks.append(tomo_stacks[i,j,:image_end,:,:])
399
411
  rotation_angles += list(thetas)
400
- #RV rotation_angles += list(tomofields.data.rotation_angles)
401
412
  x_translations += num_image*[x]
402
413
  z_translations += num_image*[z]
403
414
 
@@ -415,18 +426,12 @@ class TomoCHESSMapConverter(Processor):
415
426
  nxsample.z_translation.units = 'mm'
416
427
 
417
428
  # Add an NXdata to NXentry
418
- nxdata = NXdata()
419
- nxentry.data = nxdata
420
- nxdata.makelink(nxentry.instrument.detector.data, name='data')
421
- nxdata.makelink(nxentry.instrument.detector.image_key)
422
- nxdata.makelink(nxentry.sample.rotation_angle)
423
- nxdata.makelink(nxentry.sample.x_translation)
424
- nxdata.makelink(nxentry.sample.z_translation)
425
- nxdata.attrs['signal'] = 'data'
426
- # nxdata.attrs['axes'] = ['field', 'row', 'column']
427
- # nxdata.attrs['field_indices'] = 0
428
- # nxdata.attrs['row_indices'] = 1
429
- # nxdata.attrs['column_indices'] = 2
429
+ nxentry.data = NXdata(NXlink(nxentry.instrument.detector.data))
430
+ nxentry.data.makelink(nxentry.instrument.detector.image_key)
431
+ nxentry.data.makelink(nxentry.sample.rotation_angle)
432
+ nxentry.data.makelink(nxentry.sample.x_translation)
433
+ nxentry.data.makelink(nxentry.sample.z_translation)
434
+ nxentry.data.set_default()
430
435
 
431
436
  return nxroot
432
437
 
@@ -439,9 +444,9 @@ class TomoDataProcessor(Processor):
439
444
  """
440
445
 
441
446
  def process(
442
- self, data, interactive=False, reduce_data=False,
447
+ self, data, outputdir='.', interactive=False, reduce_data=False,
443
448
  find_center=False, calibrate_center=False, reconstruct_data=False,
444
- combine_data=False, output_folder='.', save_figs='no', **kwargs):
449
+ combine_data=False, save_figs='no'):
445
450
  """
446
451
  Process the input map or configuration with the step specific
447
452
  instructions and return either a dictionary or a
@@ -450,6 +455,8 @@ class TomoDataProcessor(Processor):
450
455
  :param data: Input configuration and specific step instructions
451
456
  for tomographic image reduction.
452
457
  :type data: list[PipelineData]
458
+ :param outputdir: Output folder name, defaults to '.'.
459
+ :type outputdir:: str, optional
453
460
  :param interactive: Allows for user interactions,
454
461
  defaults to False.
455
462
  :type interactive: bool, optional
@@ -468,8 +475,6 @@ class TomoDataProcessor(Processor):
468
475
  :param combine_data: Combine the reconstructed tomography
469
476
  stacks, defaults to False.
470
477
  :type combine_data: bool, optional
471
- :param output_folder: Output folder name, defaults to '.'.
472
- :type output_folder:: str, optional
473
478
  :param save_figs: Safe figures to file ('yes' or 'only') and/or
474
479
  display figures ('yes' or 'no'), defaults to 'no'.
475
480
  :type save_figs: Literal['yes', 'no', 'only'], optional
@@ -480,10 +485,7 @@ class TomoDataProcessor(Processor):
480
485
  :rtype: Union[dict, nexusformat.nexus.NXroot]
481
486
  """
482
487
  # Local modules
483
- from nexusformat.nexus import (
484
- nxsetconfig,
485
- NXroot,
486
- )
488
+ from nexusformat.nexus import nxsetconfig
487
489
  from CHAP.pipeline import PipelineItem
488
490
  from CHAP.tomo.models import (
489
491
  TomoReduceConfig,
@@ -529,8 +531,8 @@ class TomoDataProcessor(Processor):
529
531
  nxroot = get_nxroot(data)
530
532
 
531
533
  tomo = Tomo(
532
- interactive=interactive, output_folder=output_folder,
533
- save_figs=save_figs)
534
+ logger=self.logger, interactive=interactive,
535
+ outputdir=outputdir, save_figs=save_figs)
534
536
 
535
537
  nxsetconfig(memory=100000)
536
538
 
@@ -539,7 +541,7 @@ class TomoDataProcessor(Processor):
539
541
  if (reduce_data or find_center
540
542
  or reconstruct_data or reconstruct_data_config is not None
541
543
  or combine_data or combine_data_config is not None):
542
- self._logger.warning('Ignoring any step specific instructions '
544
+ self.logger.warning('Ignoring any step specific instructions '
543
545
  'during center calibration')
544
546
  if nxroot is None:
545
547
  raise RuntimeError('Map info required to calibrate the '
@@ -588,7 +590,7 @@ class TomoDataProcessor(Processor):
588
590
 
589
591
  # Reconstruct tomography stacks
590
592
  # RV pass reconstruct_data_config and center_config directly to
591
- # tomo.reconstruct_data?
593
+ # tomo.reconstruct_data?
592
594
  if reconstruct_data or reconstruct_data_config is not None:
593
595
  if reconstruct_data_config is None:
594
596
  reconstruct_data_config = TomoReconstructConfig()
@@ -606,49 +608,6 @@ class TomoDataProcessor(Processor):
606
608
  return center_config
607
609
  return nxroot
608
610
 
609
- def nxcopy(nxobject, exclude_nxpaths=None, nxpath_prefix=''):
610
- """
611
- Function that returns a copy of a nexus object, optionally exluding
612
- certain child items.
613
-
614
- :param nxobject: The input nexus object to "copy".
615
- :type nxobject: nexusformat.nexus.NXobject
616
- :param exlude_nxpaths: A list of paths to child nexus objects that
617
- should be excluded from the returned "copy", defaults to `[]`.
618
- :type exclude_nxpaths: list[str], optional
619
- :param nxpath_prefix: For use in recursive calls from inside this
620
- function only.
621
- :type nxpath_prefix: str
622
- :return: Copy of the input `nxobject` with some children optionally
623
- exluded.
624
- :rtype: nexusformat.nexus.NXobject
625
- """
626
- # Third party modules
627
- from nexusformat.nexus import NXgroup
628
-
629
- nxobject_copy = nxobject.__class__()
630
- if not nxpath_prefix:
631
- if 'default' in nxobject.attrs:
632
- nxobject_copy.attrs['default'] = nxobject.attrs['default']
633
- else:
634
- for k, v in nxobject.attrs.items():
635
- nxobject_copy.attrs[k] = v
636
-
637
- if exclude_nxpaths is None:
638
- exclude_nxpaths = []
639
- for k, v in nxobject.items():
640
- nxpath = os_path.join(nxpath_prefix, k)
641
- if nxpath in exclude_nxpaths:
642
- continue
643
- if isinstance(v, NXgroup):
644
- nxobject_copy[k] = nxcopy(
645
- v, exclude_nxpaths=exclude_nxpaths,
646
- nxpath_prefix=os_path.join(nxpath_prefix, k))
647
- else:
648
- nxobject_copy[k] = v
649
-
650
- return nxobject_copy
651
-
652
611
 
653
612
  class SetNumexprThreads:
654
613
  """
@@ -694,7 +653,7 @@ class Tomo:
694
653
  """Reconstruct a set of tomographic images."""
695
654
 
696
655
  def __init__(
697
- self, interactive=False, num_core=-1, output_folder='.',
656
+ self, logger=None, outputdir='.', interactive=False, num_core=-1,
698
657
  save_figs='no'):
699
658
  """
700
659
  Initialize Tomo.
@@ -704,28 +663,31 @@ class Tomo:
704
663
  :type interactive: bool, optional
705
664
  :param num_core: Number of processors.
706
665
  :type num_core: int
707
- :param output_folder: Output folder name, defaults to '.'.
708
- :type output_folder:: str, optional
666
+ :param outputdir: Output folder name, defaults to '.'.
667
+ :type outputdir:: str, optional
709
668
  :param save_figs: Safe figures to file ('yes' or 'only') and/or
710
669
  display figures ('yes' or 'no'), defaults to 'no'.
711
670
  :type save_figs: Literal['yes', 'no', 'only'], optional
712
671
  :raises ValueError: Invalid input parameter.
713
672
  """
714
673
  # System modules
715
- from logging import getLogger
716
674
  from multiprocessing import cpu_count
717
675
 
718
676
  self.__name__ = self.__class__.__name__
719
- self._logger = getLogger(self.__name__)
720
- self._logger.propagate = False
677
+ if logger is None:
678
+ # System modules
679
+ from logging import getLogger
680
+
681
+ self._logger = getLogger(self.__name__)
682
+ self._logger.propagate = False
683
+ else:
684
+ self._logger = logger
721
685
 
722
686
  if not isinstance(interactive, bool):
723
687
  raise ValueError(f'Invalid parameter interactive ({interactive})')
688
+ self._outputdir = outputdir
724
689
  self._interactive = interactive
725
690
  self._num_core = num_core
726
- self._output_folder = os_path.abspath(output_folder)
727
- if not os_path.isdir(self._output_folder):
728
- mkdir(self._output_folder)
729
691
  self._test_config = {}
730
692
  if save_figs == 'only':
731
693
  self._save_only = True
@@ -751,6 +713,12 @@ class Tomo:
751
713
  f'num_core = {self._num_core} is larger than the number '
752
714
  f'of available processors and reduced to {cpu_count()}')
753
715
  self._num_core = cpu_count()
716
+ # Tompy py uses numexpr with NUMEXPR_MAX_THREADS = 64
717
+ if self._num_core > 64:
718
+ self._logger.warning(
719
+ f'num_core = {self._num_core} is larger than the number '
720
+ f'of processors suitable to Tomopy and reduced to 64')
721
+ self._num_core = 64
754
722
 
755
723
  def reduce_data(
756
724
  self, nxroot, tool_config=None, calibrate_center_rows=False):
@@ -775,8 +743,9 @@ class Tomo:
775
743
 
776
744
  self._logger.info('Generate the reduced tomography images')
777
745
 
746
+ # Validate input parameter
778
747
  if isinstance(nxroot, NXroot):
779
- nxentry = nxroot[nxroot.attrs['default']]
748
+ nxentry = nxroot[nxroot.default]
780
749
  else:
781
750
  raise ValueError(
782
751
  f'Invalid parameter nxroot {type(nxroot)}:\n{nxroot}')
@@ -796,13 +765,11 @@ class Tomo:
796
765
  self._logger.warning('Ignoring parameter img_row_bounds '
797
766
  'during rotation axis calibration')
798
767
  img_row_bounds = None
799
-
800
768
  image_key = nxentry.instrument.detector.get('image_key', None)
801
769
  if image_key is None or 'data' not in nxentry.instrument.detector:
802
770
  raise ValueError(f'Unable to find image_key or data in '
803
771
  'instrument.detector '
804
772
  f'({nxentry.instrument.detector.tree})')
805
- image_key = np.asarray(image_key)
806
773
 
807
774
  # Create an NXprocess to store data reduction (meta)data
808
775
  reduced_data = NXprocess()
@@ -813,7 +780,7 @@ class Tomo:
813
780
  # Generate bright field
814
781
  reduced_data = self._gen_bright(nxentry, reduced_data, image_key)
815
782
 
816
- # Get rotation angles for image stacks
783
+ # Get rotation angles for image stacks (in degrees)
817
784
  thetas = self._gen_thetas(nxentry, image_key)
818
785
 
819
786
  # Get the image stack mask to remove bad images from stack
@@ -829,7 +796,7 @@ class Tomo:
829
796
  len(thetas)) < drop_fraction/100, 0, 1).astype(bool)
830
797
 
831
798
  # Set zoom and/or rotation angle interval to reduce memory
832
- # requirement
799
+ # requirement
833
800
  if image_mask is None:
834
801
  zoom_perc, delta_theta = self._set_zoom_or_delta_theta(
835
802
  thetas, delta_theta)
@@ -851,12 +818,12 @@ class Tomo:
851
818
  img_row_bounds = self._set_detector_bounds(
852
819
  nxentry, reduced_data, image_key, thetas[0],
853
820
  img_row_bounds, calibrate_center_rows)
854
- self._logger.info(f'img_row_bounds = {img_row_bounds}')
821
+ self._logger.debug(f'img_row_bounds = {img_row_bounds}')
855
822
  if calibrate_center_rows:
856
823
  calibrate_center_rows = tuple(sorted(img_row_bounds))
857
824
  img_row_bounds = None
858
825
  if img_row_bounds is None:
859
- tbf_shape = np.asarray(reduced_data.data.bright_field).shape
826
+ tbf_shape = reduced_data.data.bright_field.shape
860
827
  img_row_bounds = (0, tbf_shape[0])
861
828
  reduced_data.img_row_bounds = img_row_bounds
862
829
  reduced_data.img_row_bounds.units = 'pixels'
@@ -873,34 +840,33 @@ class Tomo:
873
840
  nxentry, reduced_data, image_key, calibrate_center_rows)
874
841
 
875
842
  # Create a copy of the input Nexus object and remove raw and
876
- # any existing reduced data
877
- if isinstance(nxroot, NXroot):
878
- exclude_items = [
879
- f'{nxentry.nxname}/reduced_data/data',
880
- f'{nxentry.nxname}/instrument/detector/data',
881
- f'{nxentry.nxname}/instrument/detector/image_key',
882
- f'{nxentry.nxname}/instrument/detector/sequence_number',
883
- f'{nxentry.nxname}/sample/rotation_angle',
884
- f'{nxentry.nxname}/sample/x_translation',
885
- f'{nxentry.nxname}/sample/z_translation',
886
- f'{nxentry.nxname}/data/data',
887
- f'{nxentry.nxname}/data/image_key',
888
- f'{nxentry.nxname}/data/rotation_angle',
889
- f'{nxentry.nxname}/data/x_translation',
890
- f'{nxentry.nxname}/data/z_translation',
891
- ]
892
- nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
893
- nxentry = nxroot[nxroot.attrs['default']]
843
+ # any existing reduced data
844
+ exclude_items = [
845
+ f'{nxentry.nxname}/reduced_data/data',
846
+ f'{nxentry.nxname}/instrument/detector/data',
847
+ f'{nxentry.nxname}/instrument/detector/image_key',
848
+ f'{nxentry.nxname}/instrument/detector/sequence_number',
849
+ f'{nxentry.nxname}/sample/rotation_angle',
850
+ f'{nxentry.nxname}/sample/x_translation',
851
+ f'{nxentry.nxname}/sample/z_translation',
852
+ f'{nxentry.nxname}/data/data',
853
+ f'{nxentry.nxname}/data/image_key',
854
+ f'{nxentry.nxname}/data/rotation_angle',
855
+ f'{nxentry.nxname}/data/x_translation',
856
+ f'{nxentry.nxname}/data/z_translation',
857
+ ]
858
+ nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
894
859
 
895
860
  # Add the reduced data NXprocess
861
+ nxentry = nxroot[nxroot.default]
896
862
  nxentry.reduced_data = reduced_data
897
863
 
898
864
  if 'data' not in nxentry:
899
865
  nxentry.data = NXdata()
866
+ nxentry.data.set_default()
900
867
  nxentry.data.makelink(
901
868
  nxentry.reduced_data.data.tomo_fields, name='reduced_data')
902
- nxentry.data.makelink(
903
- nxentry.reduced_data.rotation_angle, name='rotation_angle')
869
+ nxentry.data.makelink(nxentry.reduced_data.rotation_angle)
904
870
  nxentry.data.attrs['signal'] = 'reduced_data'
905
871
 
906
872
  return nxroot, calibrate_center_rows
@@ -920,51 +886,48 @@ class Tomo:
920
886
  :rtype: dict
921
887
  """
922
888
  # Third party modules
923
- from nexusformat.nexus import (
924
- NXentry,
925
- NXroot,
926
- )
889
+ from nexusformat.nexus import NXroot
927
890
  from yaml import safe_dump
928
891
 
929
892
  self._logger.info('Find the calibrated center axis info')
930
893
 
931
894
  if isinstance(nxroot, NXroot):
932
- nxentry = nxroot[nxroot.attrs['default']]
895
+ nxentry = nxroot[nxroot.default]
933
896
  else:
934
897
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
935
898
 
936
899
  # Check if reduced data is available
937
- if ('reduced_data' not in nxentry
938
- or 'reduced_data' not in nxentry.data):
900
+ if 'reduced_data' not in nxentry:
939
901
  raise ValueError(f'Unable to find valid reduced data in {nxentry}.')
940
902
 
941
903
  # Select the image stack to find the calibrated center axis
942
- # reduced data axes order: stack,theta,row,column
904
+ # reduced data axes order: stack,theta,row,column
943
905
  # Note: Nexus can't follow a link if the data it points to is
944
- # too big get the data from the actual place, not from
945
- # nxentry.data
906
+ # too big get the data from the actual place, not from
907
+ # nxentry.data
946
908
  num_tomo_stacks = nxentry.reduced_data.data.tomo_fields.shape[0]
909
+ self._logger.debug(f'num_tomo_stacks = {num_tomo_stacks}')
947
910
  if num_tomo_stacks == 1:
948
911
  center_stack_index = 0
949
912
  else:
950
913
  center_stack_index = tool_config.center_stack_index
951
914
  if calibrate_center_rows:
952
- center_stack_index = int(num_tomo_stacks/2)
915
+ center_stack_index = num_tomo_stacks//2
953
916
  elif self._interactive:
954
917
  if center_stack_index is None:
955
918
  center_stack_index = input_int(
956
919
  '\nEnter tomography stack index to calibrate the '
957
920
  'center axis', ge=0, lt=num_tomo_stacks,
958
- default=int(num_tomo_stacks/2))
921
+ default=num_tomo_stacks//2)
959
922
  else:
960
923
  if center_stack_index is None:
961
- center_stack_index = int(num_tomo_stacks/2)
924
+ center_stack_index = num_tomo_stacks//2
962
925
  self._logger.warning(
963
926
  'center_stack_index unspecified, use stack '
964
927
  f'{center_stack_index} to find center axis info')
965
928
 
966
929
  # Get thetas (in degrees)
967
- thetas = np.asarray(nxentry.reduced_data.rotation_angle)
930
+ thetas = nxentry.reduced_data.rotation_angle.nxdata
968
931
 
969
932
  # Select center rows
970
933
  if calibrate_center_rows:
@@ -975,7 +938,7 @@ class Tomo:
975
938
  import matplotlib.pyplot as plt
976
939
 
977
940
  # Get full bright field
978
- tbf = np.asarray(nxentry.reduced_data.data.bright_field)
941
+ tbf = nxentry.reduced_data.data.bright_field.nxdata
979
942
  tbf_shape = tbf.shape
980
943
 
981
944
  # Get image bounds
@@ -1022,8 +985,7 @@ class Tomo:
1022
985
  # Plot results
1023
986
  if self._save_figs:
1024
987
  fig.savefig(
1025
- os_path.join(
1026
- self._output_folder, 'center_finding_rows.png'))
988
+ os_path.join(self._outputdir, 'center_finding_rows.png'))
1027
989
  plt.close()
1028
990
 
1029
991
  # Get effective pixel_size
@@ -1047,12 +1009,13 @@ class Tomo:
1047
1009
  t0 = time()
1048
1010
  center_offsets.append(
1049
1011
  self._find_center_one_plane(
1050
- nxentry.reduced_data.data.tomo_fields[
1051
- center_stack_index,:,offset_row,:],
1052
- row, thetas, eff_pixel_size, cross_sectional_dim,
1053
- path=self._output_folder, num_core=self._num_core,
1012
+ nxentry.reduced_data.data.tomo_fields, center_stack_index,
1013
+ row, offset_row, np.radians(thetas), eff_pixel_size,
1014
+ cross_sectional_dim, path=self._outputdir,
1015
+ num_core=self._num_core,
1054
1016
  center_offset_min=tool_config.center_offset_min,
1055
1017
  center_offset_max=tool_config.center_offset_max,
1018
+ center_search_range=tool_config.center_search_range,
1056
1019
  gaussian_sigma=tool_config.gaussian_sigma,
1057
1020
  ring_width=tool_config.ring_width,
1058
1021
  prev_center_offset=prev_center_offset))
@@ -1097,9 +1060,8 @@ class Tomo:
1097
1060
  """
1098
1061
  # Third party modules
1099
1062
  from nexusformat.nexus import (
1100
- nxgetconfig,
1101
1063
  NXdata,
1102
- NXentry,
1064
+ NXfield,
1103
1065
  NXprocess,
1104
1066
  NXroot,
1105
1067
  )
@@ -1107,15 +1069,14 @@ class Tomo:
1107
1069
  self._logger.info('Reconstruct the tomography data')
1108
1070
 
1109
1071
  if isinstance(nxroot, NXroot):
1110
- nxentry = nxroot[nxroot.attrs['default']]
1072
+ nxentry = nxroot[nxroot.default]
1111
1073
  else:
1112
1074
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
1113
1075
  if not isinstance(center_info, dict):
1114
1076
  raise ValueError(f'Invalid parameter center_info ({center_info})')
1115
1077
 
1116
1078
  # Check if reduced data is available
1117
- if ('reduced_data' not in nxentry
1118
- or 'reduced_data' not in nxentry.data):
1079
+ if 'reduced_data' not in nxentry:
1119
1080
  raise ValueError(f'Unable to find valid reduced data in {nxentry}.')
1120
1081
 
1121
1082
  # Create an NXprocess to store image reconstruction (meta)data
@@ -1132,38 +1093,28 @@ class Tomo:
1132
1093
  / (center_rows[1]-center_rows[0])
1133
1094
 
1134
1095
  # Get thetas (in degrees)
1135
- thetas = np.asarray(nxentry.reduced_data.rotation_angle)
1096
+ thetas = nxentry.reduced_data.rotation_angle.nxdata
1136
1097
 
1137
1098
  # Reconstruct tomography data
1138
- # reduced data axes order: stack,theta,row,column
1139
- # reconstructed data: row/-z,y,x
1099
+ # - reduced data axes order: stack,theta,row,column
1100
+ # - reconstructed data axes order: row/-z,y,x
1140
1101
  # Note: Nexus can't follow a link if the data it points to is
1141
- # too big get the data from the actual place, not from
1142
- # nxentry.data
1102
+ # too big get the data from the actual place, not from
1103
+ # nxentry.data
1143
1104
  if 'zoom_perc' in nxentry.reduced_data:
1144
1105
  res_title = f'{nxentry.reduced_data.attrs["zoom_perc"]}p'
1145
1106
  else:
1146
1107
  res_title = 'fullres'
1147
- tomo_stacks = np.asarray(nxentry.reduced_data.data.tomo_fields)
1108
+ tomo_stacks = nxentry.reduced_data.data.tomo_fields
1148
1109
  num_tomo_stacks = tomo_stacks.shape[0]
1149
- tomo_recon_stacks = num_tomo_stacks*[np.array([])]
1110
+ tomo_recon_stacks = []
1150
1111
  img_row_bounds = tuple(nxentry.reduced_data.get(
1151
1112
  'img_row_bounds', (0, tomo_stacks.shape[2])))
1152
1113
  center_rows -= img_row_bounds[0]
1153
1114
  for i in range(num_tomo_stacks):
1154
1115
  # Convert reduced data stack from theta,row,column to
1155
- # row,theta,column
1156
- t0 = time()
1157
- tomo_stack = tomo_stacks[i]
1158
- self._logger.info(
1159
- f'Reading reduced data stack {i} took {time()-t0:.2f} '
1160
- 'seconds')
1161
- if (len(tomo_stack.shape) != 3
1162
- or any(True for dim in tomo_stack.shape if not dim)):
1163
- raise RuntimeError(
1164
- f'Unable to load tomography stack {i} for '
1165
- 'reconstruction')
1166
- tomo_stack = np.swapaxes(tomo_stack, 0, 1)
1116
+ # row,theta,column
1117
+ tomo_stack = np.swapaxes(tomo_stacks[i,:,:,:], 0, 1)
1167
1118
  assert len(thetas) == tomo_stack.shape[1]
1168
1119
  assert 0 <= center_rows[0] < center_rows[1] < tomo_stack.shape[0]
1169
1120
  center_offsets = [
@@ -1173,41 +1124,42 @@ class Tomo:
1173
1124
  ]
1174
1125
  t0 = time()
1175
1126
  tomo_recon_stack = self._reconstruct_one_tomo_stack(
1176
- tomo_stack, thetas, center_offsets=center_offsets,
1127
+ tomo_stack, np.radians(thetas), center_offsets=center_offsets,
1177
1128
  num_core=self._num_core, algorithm='gridrec',
1178
1129
  secondary_iters=tool_config.secondary_iters,
1130
+ gaussian_sigma=tool_config.gaussian_sigma,
1179
1131
  remove_stripe_sigma=tool_config.remove_stripe_sigma,
1180
1132
  ring_width=tool_config.ring_width)
1181
1133
  self._logger.info(
1182
1134
  f'Reconstruction of stack {i} took {time()-t0:.2f} seconds')
1183
1135
 
1184
1136
  # Combine stacks
1185
- tomo_recon_stacks[i] = tomo_recon_stack
1137
+ tomo_recon_stacks.append(tomo_recon_stack)
1186
1138
 
1187
1139
  # Resize the reconstructed tomography data
1188
- # reconstructed data order in each stack: row/-z,y,x
1140
+ # - reconstructed axis data order in each stack: row/-z,y,x
1189
1141
  tomo_recon_shape = tomo_recon_stacks[0].shape
1190
1142
  x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1191
1143
  tomo_recon_stacks, x_bounds=tool_config.x_bounds,
1192
1144
  y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
1193
1145
  if x_bounds is None:
1194
1146
  x_range = (0, tomo_recon_shape[2])
1195
- x_slice = int(x_range[1]/2)
1147
+ x_slice = x_range[1]//2
1196
1148
  else:
1197
1149
  x_range = (min(x_bounds), max(x_bounds))
1198
- x_slice = int((x_bounds[0]+x_bounds[1]) / 2)
1150
+ x_slice = (x_bounds[0]+x_bounds[1])//2
1199
1151
  if y_bounds is None:
1200
1152
  y_range = (0, tomo_recon_shape[1])
1201
- y_slice = int(y_range[1] / 2)
1153
+ y_slice = y_range[1]//2
1202
1154
  else:
1203
1155
  y_range = (min(y_bounds), max(y_bounds))
1204
- y_slice = int((y_bounds[0]+y_bounds[1]) / 2)
1156
+ y_slice = (y_bounds[0]+y_bounds[1])//2
1205
1157
  if z_bounds is None:
1206
1158
  z_range = (0, tomo_recon_shape[0])
1207
- z_slice = int(z_range[1] / 2)
1159
+ z_slice = z_range[1]//2
1208
1160
  else:
1209
1161
  z_range = (min(z_bounds), max(z_bounds))
1210
- z_slice = int((z_bounds[0]+z_bounds[1]) / 2)
1162
+ z_slice = (z_bounds[0]+z_bounds[1])//2
1211
1163
  z_dim_org = tomo_recon_shape[0]
1212
1164
  for i, stack in enumerate(tomo_recon_stacks):
1213
1165
  tomo_recon_stacks[i] = stack[
@@ -1215,18 +1167,17 @@ class Tomo:
1215
1167
  x_range[0]:x_range[1]]
1216
1168
  tomo_recon_stacks = np.asarray(tomo_recon_stacks)
1217
1169
 
1218
- row_pixel_size = float(
1219
- nxentry.instrument.detector.row_pixel_size)
1220
- column_pixel_size = float(
1221
- nxentry.instrument.detector.column_pixel_size)
1170
+ detector = nxentry.instrument.detector
1171
+ row_pixel_size = float(detector.row_pixel_size)
1172
+ column_pixel_size = float(detector.column_pixel_size)
1222
1173
  if num_tomo_stacks == 1:
1223
1174
  # Convert the reconstructed tomography data from internal
1224
- # coordinate frame row/-z,y,x with the origin on the
1225
- # near-left-top corner to an z,y,x coordinate frame
1226
- # with the origin on the par file x,z values, halfway
1227
- # in the y-dimension.
1228
- # Here x is to the right, y along the beam direction
1229
- # and z upwards in the lab frame of reference
1175
+ # coordinate frame row/-z,y,x with the origin on the
1176
+ # near-left-top corner to an z,y,x coordinate frame with
1177
+ # the origin on the par file x,z values, halfway in the
1178
+ # y-dimension.
1179
+ # Here x is to the right, y along the beam direction and
1180
+ # z upwards in the lab frame of reference
1230
1181
  tomo_recon_stack = np.flip(tomo_recon_stacks[0], 0)
1231
1182
  z_range = (z_dim_org-z_range[1], z_dim_org-z_range[0])
1232
1183
 
@@ -1234,19 +1185,17 @@ class Tomo:
1234
1185
  x = column_pixel_size * (
1235
1186
  np.linspace(
1236
1187
  x_range[0], x_range[1], x_range[1]-x_range[0], False)
1237
- - 0.5*nxentry.instrument.detector.columns
1238
- + 0.5)
1188
+ - 0.5*detector.columns + 0.5)
1239
1189
  x = np.asarray(x + nxentry.reduced_data.x_translation[0])
1240
1190
  y = np.asarray(
1241
1191
  column_pixel_size * (
1242
1192
  np.linspace(
1243
1193
  y_range[0], y_range[1], y_range[1]-y_range[0], False)
1244
- - 0.5*nxentry.instrument.detector.columns
1245
- + 0.5))
1194
+ - 0.5*detector.columns + 0.5))
1246
1195
  z = row_pixel_size*(
1247
1196
  np.linspace(
1248
1197
  z_range[0], z_range[1], z_range[1]-z_range[0], False)
1249
- + nxentry.instrument.detector.rows
1198
+ + detector.rows
1250
1199
  - int(nxentry.reduced_data.img_row_bounds[1])
1251
1200
  + 0.5)
1252
1201
  z = np.asarray(z + nxentry.reduced_data.z_translation[0])
@@ -1262,7 +1211,7 @@ class Tomo:
1262
1211
  quick_imshow(
1263
1212
  tomo_recon_stack[:,:,x_index],
1264
1213
  title=f'recon {res_title} x={x[x_index]:.4f}',
1265
- origin='lower', extent=extent, path=self._output_folder,
1214
+ origin='lower', extent=extent, path=self._outputdir,
1266
1215
  save_fig=True, save_only=True)
1267
1216
  y_index = y_slice-y_range[0]
1268
1217
  extent = (
@@ -1273,7 +1222,7 @@ class Tomo:
1273
1222
  quick_imshow(
1274
1223
  tomo_recon_stack[:,y_index,:],
1275
1224
  title=f'recon {res_title} y={y[y_index]:.4f}',
1276
- origin='lower', extent=extent, path=self._output_folder,
1225
+ origin='lower', extent=extent, path=self._outputdir,
1277
1226
  save_fig=True, save_only=True)
1278
1227
  z_index = z_slice-z_range[0]
1279
1228
  extent = (
@@ -1284,7 +1233,7 @@ class Tomo:
1284
1233
  quick_imshow(
1285
1234
  tomo_recon_stack[z_index,:,:],
1286
1235
  title=f'recon {res_title} z={z[z_index]:.4f}',
1287
- origin='lower', extent=extent, path=self._output_folder,
1236
+ origin='lower', extent=extent, path=self._outputdir,
1288
1237
  save_fig=True, save_only=True)
1289
1238
  else:
1290
1239
  # Plot a few reconstructed image slices
@@ -1294,25 +1243,23 @@ class Tomo:
1294
1243
  title = f'{basetitle} {res_title} xslice{x_slice}'
1295
1244
  quick_imshow(
1296
1245
  tomo_recon_stacks[i,:,:,x_slice-x_range[0]],
1297
- title=title, path=self._output_folder,
1298
- save_fig=True, save_only=True)
1246
+ title=title, path=self._outputdir, save_fig=True,
1247
+ save_only=True)
1299
1248
  title = f'{basetitle} {res_title} yslice{y_slice}'
1300
1249
  quick_imshow(
1301
1250
  tomo_recon_stacks[i,:,y_slice-y_range[0],:],
1302
- title=title, path=self._output_folder,
1303
- save_fig=True, save_only=True)
1251
+ title=title, path=self._outputdir, save_fig=True,
1252
+ save_only=True)
1304
1253
  title = f'{basetitle} {res_title} zslice{z_slice}'
1305
1254
  quick_imshow(
1306
1255
  tomo_recon_stacks[i,z_slice-z_range[0],:,:],
1307
- title=title, path=self._output_folder,
1308
- save_fig=True, save_only=True)
1256
+ title=title, path=self._outputdir, save_fig=True,
1257
+ save_only=True)
1309
1258
 
1310
1259
  # Add image reconstruction to reconstructed data NXprocess
1311
- # reconstructed data order:
1312
- # - for one stack: z,y,x
1313
- # - for multiple stacks: row/-z,y,x
1314
- nxprocess.data = NXdata()
1315
- nxprocess.attrs['default'] = 'data'
1260
+ # reconstructed axis data order:
1261
+ # - for one stack: z,y,x
1262
+ # - for multiple stacks: row/-z,y,x
1316
1263
  for k, v in center_info.items():
1317
1264
  nxprocess[k] = v
1318
1265
  if k == 'center_rows' or k == 'center_offsets':
@@ -1335,53 +1282,45 @@ class Tomo:
1335
1282
  nxprocess.z_bounds.units = 'pixels'
1336
1283
  nxprocess.z_bounds.attrs['long_name'] = \
1337
1284
  'z range indices in reduced data frame of reference'
1338
- nxprocess.data.attrs['signal'] = 'reconstructed_data'
1339
1285
  if num_tomo_stacks == 1:
1340
- nxprocess.data.reconstructed_data = tomo_recon_stack
1341
- nxprocess.data.attrs['axes'] = ['z', 'y', 'x']
1342
- nxprocess.data.attrs['x_indices'] = 2
1343
- nxprocess.data.attrs['y_indices'] = 1
1344
- nxprocess.data.attrs['z_indices'] = 0
1345
- nxprocess.data.x = x
1346
- nxprocess.data.x.units = \
1347
- nxentry.instrument.detector.column_pixel_size.units
1348
- nxprocess.data.y = y
1349
- nxprocess.data.y.units = \
1350
- nxentry.instrument.detector.column_pixel_size.units
1351
- nxprocess.data.z = z
1352
- nxprocess.data.z.units = \
1353
- nxentry.instrument.detector.row_pixel_size.units
1286
+ nxprocess.data = NXdata(
1287
+ NXfield(tomo_recon_stack, 'reconstructed_data'),
1288
+ (NXfield(
1289
+ z, 'z', attrs={'units': detector.row_pixel_size.units}),
1290
+ NXfield(
1291
+ y, 'y',
1292
+ attrs={'units': detector.column_pixel_size.units}),
1293
+ NXfield(
1294
+ x, 'x',
1295
+ attrs={'units': detector.column_pixel_size.units}),))
1354
1296
  else:
1355
- nxprocess.data.reconstructed_data = tomo_recon_stacks
1297
+ nxprocess.data = NXdata(
1298
+ NXfield(tomo_recon_stacks, 'reconstructed_data'))
1356
1299
 
1357
1300
  # Create a copy of the input Nexus object and remove reduced
1358
- # data
1301
+ # data
1359
1302
  exclude_items = [
1360
1303
  f'{nxentry.nxname}/reduced_data/data',
1361
1304
  f'{nxentry.nxname}/data/reduced_data',
1362
1305
  f'{nxentry.nxname}/data/rotation_angle',
1363
1306
  ]
1364
- nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1307
+ nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1365
1308
 
1366
1309
  # Add the reconstructed data NXprocess to the new Nexus object
1367
- nxentry_copy = nxroot_copy[nxroot_copy.attrs['default']]
1368
- nxentry_copy.reconstructed_data = nxprocess
1369
- if 'data' not in nxentry_copy:
1370
- nxentry_copy.data = NXdata()
1371
- nxentry_copy.attrs['default'] = 'data'
1372
- nxentry_copy.data.makelink(
1373
- nxprocess.data.reconstructed_data, name='reconstructed_data')
1374
- nxentry_copy.data.attrs['signal'] = 'reconstructed_data'
1310
+ nxentry = nxroot[nxroot.default]
1311
+ nxentry.reconstructed_data = nxprocess
1312
+ if 'data' not in nxentry:
1313
+ nxentry.data = NXdata()
1314
+ nxentry.data.set_default()
1315
+ nxentry.data.makelink(nxprocess.data.reconstructed_data)
1375
1316
  if num_tomo_stacks == 1:
1376
- nxentry_copy.data.attrs['axes'] = ['z', 'y', 'x']
1377
- nxentry_copy.data.attrs['x_indices'] = 2
1378
- nxentry_copy.data.attrs['y_indices'] = 1
1379
- nxentry_copy.data.attrs['z_indices'] = 0
1380
- nxentry_copy.data.makelink(nxprocess.data.x, name='x')
1381
- nxentry_copy.data.makelink(nxprocess.data.y, name='y')
1382
- nxentry_copy.data.makelink(nxprocess.data.z, name='z')
1317
+ nxentry.data.attrs['axes'] = ['z', 'y', 'x']
1318
+ nxentry.data.makelink(nxprocess.data.x)
1319
+ nxentry.data.makelink(nxprocess.data.y)
1320
+ nxentry.data.makelink(nxprocess.data.z)
1321
+ nxentry.data.attrs['signal'] = 'reconstructed_data'
1383
1322
 
1384
- return nxroot_copy
1323
+ return nxroot
1385
1324
 
1386
1325
  def combine_data(self, nxroot, tool_config):
1387
1326
  """Combine the reconstructed tomography stacks.
@@ -1399,7 +1338,7 @@ class Tomo:
1399
1338
  # Third party modules
1400
1339
  from nexusformat.nexus import (
1401
1340
  NXdata,
1402
- NXentry,
1341
+ NXfield,
1403
1342
  NXprocess,
1404
1343
  NXroot,
1405
1344
  )
@@ -1407,18 +1346,17 @@ class Tomo:
1407
1346
  self._logger.info('Combine the reconstructed tomography stacks')
1408
1347
 
1409
1348
  if isinstance(nxroot, NXroot):
1410
- nxentry = nxroot[nxroot.attrs['default']]
1349
+ nxentry = nxroot[nxroot.default]
1411
1350
  else:
1412
1351
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
1413
1352
 
1414
1353
  # Check if reconstructed image data is available
1415
- if ('reconstructed_data' not in nxentry
1416
- or 'reconstructed_data' not in nxentry.data):
1354
+ if 'reconstructed_data' not in nxentry:
1417
1355
  raise KeyError(
1418
1356
  f'Unable to find valid reconstructed image data in {nxentry}')
1419
1357
 
1420
1358
  # Create an NXprocess to store combined image reconstruction
1421
- # (meta)data
1359
+ # (meta)data
1422
1360
  nxprocess = NXprocess()
1423
1361
 
1424
1362
  if nxentry.reconstructed_data.data.reconstructed_data.ndim == 3:
@@ -1431,105 +1369,106 @@ class Tomo:
1431
1369
  return nxroot
1432
1370
 
1433
1371
  # Get and combine the reconstructed stacks
1434
- # reconstructed data order: stack,row/-z,y,x
1372
+ # - reconstructed axis data order: stack,row/-z,y,x
1435
1373
  # Note: Nexus can't follow a link if the data it points to is
1436
- # too big. So get the data from the actual place, not from
1437
- # nxentry.data
1438
- # (load one stack at a time to reduce risk of hitting Nexus
1439
- # data access limit)
1374
+ # too big. So get the data from the actual place, not from
1375
+ # nxentry.data
1376
+ # Also load one stack at a time to reduce risk of hitting Nexus
1377
+ # data access limit
1440
1378
  t0 = time()
1441
1379
  tomo_recon_combined = \
1442
1380
  nxentry.reconstructed_data.data.reconstructed_data[0,:,:,:]
1381
+ # RV check this out more
1382
+ # tomo_recon_combined = np.concatenate(
1383
+ # [tomo_recon_combined]
1384
+ # + [nxentry.reconstructed_data.data.reconstructed_data[i,:,:,:]
1385
+ # for i in range(1, num_tomo_stacks)])
1443
1386
  tomo_recon_combined = np.concatenate(
1444
- [tomo_recon_combined]
1445
- + [nxentry.reconstructed_data.data.reconstructed_data[i,:,:,:]
1446
- for i in range(1, num_tomo_stacks)])
1387
+ [nxentry.reconstructed_data.data.reconstructed_data[i,:,:,:]
1388
+ for i in range(num_tomo_stacks-1, 0, -1)]
1389
+ + [tomo_recon_combined])
1447
1390
  self._logger.info(
1448
1391
  f'Combining the reconstructed stacks took {time()-t0:.2f} seconds')
1449
1392
  tomo_shape = tomo_recon_combined.shape
1450
1393
 
1451
1394
  # Resize the combined tomography data stacks
1452
- # combined data order: row/-z,y,x
1453
- if self._interactive:
1395
+ # - combined axis data order: row/-z,y,x
1396
+ if self._interactive or self._save_figs:
1454
1397
  x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1455
1398
  tomo_recon_combined, combine_data=True)
1456
1399
  else:
1457
1400
  x_bounds = tool_config.x_bounds
1458
1401
  if x_bounds is None:
1459
1402
  self._logger.warning(
1460
- 'x_bounds unspecified, reconstruct data for full x-range')
1403
+ 'x_bounds unspecified, combine data for full x-range')
1461
1404
  elif not is_int_pair(
1462
1405
  x_bounds, ge=0, le=tomo_shape[2]):
1463
1406
  raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
1464
1407
  y_bounds = tool_config.y_bounds
1465
1408
  if y_bounds is None:
1466
1409
  self._logger.warning(
1467
- 'y_bounds unspecified, reconstruct data for full y-range')
1410
+ 'y_bounds unspecified, combine data for full y-range')
1468
1411
  elif not is_int_pair(
1469
1412
  y_bounds, ge=0, le=tomo_shape[1]):
1470
1413
  raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
1471
1414
  z_bounds = tool_config.z_bounds
1472
1415
  if z_bounds is None:
1473
1416
  self._logger.warning(
1474
- 'z_bounds unspecified, reconstruct data for full z-range')
1417
+ 'z_bounds unspecified, combine data for full z-range')
1475
1418
  elif not is_int_pair(
1476
1419
  z_bounds, ge=0, le=tomo_shape[0]):
1477
1420
  raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
1478
1421
  if x_bounds is None:
1479
1422
  x_range = (0, tomo_shape[2])
1480
- x_slice = int(x_range[1]/2)
1423
+ x_slice = x_range[1]//2
1481
1424
  else:
1482
1425
  x_range = (min(x_bounds), max(x_bounds))
1483
- x_slice = int((x_bounds[0]+x_bounds[1]) / 2)
1426
+ x_slice = (x_bounds[0]+x_bounds[1])//2
1484
1427
  if y_bounds is None:
1485
1428
  y_range = (0, tomo_shape[1])
1486
- y_slice = int(y_range[1]/2)
1429
+ y_slice = y_range[1]//2
1487
1430
  else:
1488
1431
  y_range = (min(y_bounds), max(y_bounds))
1489
- y_slice = int((y_bounds[0]+y_bounds[1]) / 2)
1432
+ y_slice = (y_bounds[0]+y_bounds[1])//2
1490
1433
  if z_bounds is None:
1491
1434
  z_range = (0, tomo_shape[0])
1492
- z_slice = int(z_range[1]/2)
1435
+ z_slice = z_range[1]//2
1493
1436
  else:
1494
1437
  z_range = (min(z_bounds), max(z_bounds))
1495
- z_slice = int((z_bounds[0]+z_bounds[1]) / 2)
1438
+ z_slice = (z_bounds[0]+z_bounds[1])//2
1496
1439
  z_dim_org = tomo_shape[0]
1497
1440
  tomo_recon_combined = tomo_recon_combined[
1498
1441
  z_range[0]:z_range[1],y_range[0]:y_range[1],x_range[0]:x_range[1]]
1499
1442
 
1500
1443
  # Convert the reconstructed tomography data from internal
1501
- # coordinate frame row/-z,y,x with the origin on the
1502
- # near-left-top corner to an z,y,x coordinate frame.
1503
- # Here x is to the right, y along the beam direction
1504
- # and z upwards in the lab frame of reference
1444
+ # coordinate frame row/-z,y,x with the origin on the
1445
+ # near-left-top corner to an z,y,x coordinate frame.
1446
+ # Here x is to the right, y along the beam direction and
1447
+ # z upwards in the lab frame of reference
1505
1448
  tomo_recon_combined = np.flip(tomo_recon_combined, 0)
1506
1449
  tomo_shape = tomo_recon_combined.shape
1507
1450
  z_range = (z_dim_org-z_range[1], z_dim_org-z_range[0])
1508
1451
 
1509
1452
  # Get coordinate axes
1510
- row_pixel_size = float(
1511
- nxentry.instrument.detector.row_pixel_size)
1512
- column_pixel_size = float(
1513
- nxentry.instrument.detector.column_pixel_size)
1453
+ detector = nxentry.instrument.detector
1454
+ row_pixel_size = float(detector.row_pixel_size)
1455
+ column_pixel_size = float(detector.column_pixel_size)
1514
1456
  x = column_pixel_size * (
1515
1457
  np.linspace(x_range[0], x_range[1], x_range[1]-x_range[0], False)
1516
- - 0.5*nxentry.instrument.detector.columns
1517
- + 0.5)
1458
+ - 0.5*detector.columns + 0.5)
1518
1459
  if nxentry.reconstructed_data.get('x_bounds', None) is not None:
1519
1460
  x += column_pixel_size*nxentry.reconstructed_data.x_bounds[0]
1520
1461
  x = np.asarray(x + nxentry.reduced_data.x_translation[0])
1521
1462
  y = column_pixel_size * (
1522
1463
  np.linspace(y_range[0], y_range[1], y_range[1]-y_range[0], False)
1523
- - 0.5*nxentry.instrument.detector.columns
1524
- + 0.5)
1464
+ - 0.5*detector.columns + 0.5)
1525
1465
  if nxentry.reconstructed_data.get('y_bounds', None) is not None:
1526
1466
  y += column_pixel_size*nxentry.reconstructed_data.y_bounds[0]
1527
1467
  y = np.asarray(y)
1528
1468
  z = row_pixel_size*(
1529
1469
  np.linspace(z_range[0], z_range[1], z_range[1]-z_range[0], False)
1530
1470
  - int(nxentry.reduced_data.img_row_bounds[0])
1531
- + 0.5*(nxentry.instrument.detector.rows)
1532
- -0.5)
1471
+ + 0.5*detector.rows - 0.5)
1533
1472
  z = np.asarray(z + nxentry.reduced_data.z_translation[0])
1534
1473
 
1535
1474
  # Plot a few combined image slices
@@ -1539,39 +1478,37 @@ class Tomo:
1539
1478
  y[-1],
1540
1479
  z[0],
1541
1480
  z[-1])
1542
- x_slice = int(tomo_shape[2]/2)
1481
+ x_slice = tomo_shape[2]//2
1543
1482
  quick_imshow(
1544
1483
  tomo_recon_combined[:,:,x_slice],
1545
1484
  title=f'recon combined x={x[x_slice]:.4f}', origin='lower',
1546
- extent=extent, path=self._output_folder, save_fig=True,
1485
+ extent=extent, path=self._outputdir, save_fig=True,
1547
1486
  save_only=True)
1548
1487
  extent = (
1549
1488
  x[0],
1550
1489
  x[-1],
1551
1490
  z[0],
1552
1491
  z[-1])
1553
- y_slice = int(tomo_shape[1]/2)
1492
+ y_slice = tomo_shape[1]//2
1554
1493
  quick_imshow(
1555
1494
  tomo_recon_combined[:,y_slice,:],
1556
1495
  title=f'recon combined y={y[y_slice]:.4f}', origin='lower',
1557
- extent=extent, path=self._output_folder, save_fig=True,
1496
+ extent=extent, path=self._outputdir, save_fig=True,
1558
1497
  save_only=True)
1559
1498
  extent = (
1560
1499
  x[0],
1561
1500
  x[-1],
1562
1501
  y[0],
1563
1502
  y[-1])
1564
- z_slice = int(tomo_shape[0]/2)
1503
+ z_slice = tomo_shape[0]//2
1565
1504
  quick_imshow(
1566
1505
  tomo_recon_combined[z_slice,:,:],
1567
1506
  title=f'recon combined z={z[z_slice]:.4f}', origin='lower',
1568
- extent=extent, path=self._output_folder, save_fig=True,
1507
+ extent=extent, path=self._outputdir, save_fig=True,
1569
1508
  save_only=True)
1570
1509
 
1571
1510
  # Add image reconstruction to reconstructed data NXprocess
1572
- # combined data order: z,y,x
1573
- nxprocess.data = NXdata()
1574
- nxprocess.attrs['default'] = 'data'
1511
+ # - combined axis data order: z,y,x
1575
1512
  if x_bounds is not None and x_bounds != (0, tomo_shape[2]):
1576
1513
  nxprocess.x_bounds = x_bounds
1577
1514
  nxprocess.x_bounds.units = 'pixels'
@@ -1587,48 +1524,36 @@ class Tomo:
1587
1524
  nxprocess.z_bounds.units = 'pixels'
1588
1525
  nxprocess.z_bounds.attrs['long_name'] = \
1589
1526
  'z range indices in reconstructed data frame of reference'
1590
- nxprocess.data.combined_data = tomo_recon_combined
1591
- nxprocess.data.attrs['signal'] = 'combined_data'
1592
- nxprocess.data.attrs['axes'] = ['z', 'y', 'x']
1593
- nxprocess.data.attrs['x_indices'] = 2
1594
- nxprocess.data.attrs['y_indices'] = 1
1595
- nxprocess.data.attrs['z_indices'] = 0
1596
- nxprocess.data.x = x
1597
- nxprocess.data.x.units = \
1598
- nxentry.instrument.detector.column_pixel_size.units
1599
- nxprocess.data.y = y
1600
- nxprocess.data.y.units = \
1601
- nxentry.instrument.detector.column_pixel_size.units
1602
- nxprocess.data.z = z
1603
- nxprocess.data.z.units = \
1604
- nxentry.instrument.detector.row_pixel_size.units
1527
+ nxprocess.data = NXdata(
1528
+ NXfield(tomo_recon_combined, 'combined_data'),
1529
+ (NXfield(z, 'z', attrs={'units': detector.row_pixel_size.units}),
1530
+ NXfield(
1531
+ y, 'y', attrs={'units': detector.column_pixel_size.units}),
1532
+ NXfield(
1533
+ x, 'x', attrs={'units': detector.column_pixel_size.units}),))
1605
1534
 
1606
1535
  # Create a copy of the input Nexus object and remove
1607
- # reconstructed data
1536
+ # reconstructed data
1608
1537
  exclude_items = [
1609
1538
  f'{nxentry.nxname}/reconstructed_data/data',
1610
1539
  f'{nxentry.nxname}/data/reconstructed_data',
1611
1540
  ]
1612
- nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1541
+ nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1613
1542
 
1614
1543
  # Add the combined data NXprocess to the new Nexus object
1615
- nxentry_copy = nxroot_copy[nxroot_copy.attrs['default']]
1616
- nxentry_copy.combined_data = nxprocess
1617
- if 'data' not in nxentry_copy:
1618
- nxentry_copy.data = NXdata()
1619
- nxentry_copy.attrs['default'] = 'data'
1620
- nxentry_copy.data.makelink(
1621
- nxprocess.data.combined_data, name='combined_data')
1622
- nxentry_copy.data.attrs['signal'] = 'combined_data'
1623
- nxentry_copy.data.attrs['axes'] = ['z', 'y', 'x']
1624
- nxentry_copy.data.attrs['x_indices'] = 2
1625
- nxentry_copy.data.attrs['y_indices'] = 1
1626
- nxentry_copy.data.attrs['z_indices'] = 0
1627
- nxentry_copy.data.makelink(nxprocess.data.x, name='x')
1628
- nxentry_copy.data.makelink(nxprocess.data.y, name='y')
1629
- nxentry_copy.data.makelink(nxprocess.data.z, name='z')
1630
-
1631
- return nxroot_copy
1544
+ nxentry = nxroot[nxroot.default]
1545
+ nxentry.combined_data = nxprocess
1546
+ if 'data' not in nxentry:
1547
+ nxentry.data = NXdata()
1548
+ nxentry.data.set_default()
1549
+ nxentry.data.makelink(nxprocess.data.combined_data)
1550
+ nxentry.data.attrs['axes'] = ['z', 'y', 'x']
1551
+ nxentry.data.makelink(nxprocess.data.x)
1552
+ nxentry.data.makelink(nxprocess.data.y)
1553
+ nxentry.data.makelink(nxprocess.data.z)
1554
+ nxentry.data.attrs['signal'] = 'combined_data'
1555
+
1556
+ return nxroot
1632
1557
 
1633
1558
  def _gen_dark(self, nxentry, reduced_data, image_key):
1634
1559
  """Generate dark field."""
@@ -1639,8 +1564,7 @@ class Tomo:
1639
1564
  field_indices = [
1640
1565
  index for index, key in enumerate(image_key) if key == 2]
1641
1566
  if field_indices:
1642
- tdf_stack = np.asarray(
1643
- nxentry.instrument.detector.data[field_indices,:,:])
1567
+ tdf_stack = nxentry.instrument.detector.data[field_indices,:,:]
1644
1568
  else:
1645
1569
  self._logger.warning('Dark field unavailable')
1646
1570
  return reduced_data
@@ -1655,7 +1579,6 @@ class Tomo:
1655
1579
  raise RuntimeError(f'Invalid tdf_stack shape ({tdf_stack.shape})')
1656
1580
 
1657
1581
  # Remove dark field intensities above the cutoff
1658
- # tdf_cutoff = None
1659
1582
  tdf_cutoff = tdf.min() + 2 * (np.median(tdf)-tdf.min())
1660
1583
  self._logger.debug(f'tdf_cutoff = {tdf_cutoff}')
1661
1584
  if tdf_cutoff is not None:
@@ -1676,7 +1599,7 @@ class Tomo:
1676
1599
  if self._save_figs:
1677
1600
  quick_imshow(
1678
1601
  tdf, title='Dark field', name='dark_field',
1679
- path=self._output_folder, save_fig=True, save_only=True)
1602
+ path=self._outputdir, save_fig=True, save_only=True)
1680
1603
 
1681
1604
  # Add dark field to reduced data NXprocess
1682
1605
  reduced_data.data = NXdata()
@@ -1693,8 +1616,7 @@ class Tomo:
1693
1616
  field_indices = [
1694
1617
  index for index, key in enumerate(image_key) if key == 1]
1695
1618
  if field_indices:
1696
- tbf_stack = np.asarray(
1697
- nxentry.instrument.detector.data[field_indices,:,:])
1619
+ tbf_stack = nxentry.instrument.detector.data[field_indices,:,:]
1698
1620
  else:
1699
1621
  raise ValueError('Bright field unavailable')
1700
1622
 
@@ -1727,7 +1649,7 @@ class Tomo:
1727
1649
  if self._save_figs:
1728
1650
  quick_imshow(
1729
1651
  tbf, title='Bright field', name='bright_field',
1730
- path=self._output_folder, save_fig=True, save_only=True)
1652
+ path=self._outputdir, save_fig=True, save_only=True)
1731
1653
 
1732
1654
  # Add bright field to reduced data NXprocess
1733
1655
  if 'data' not in reduced_data:
@@ -1754,48 +1676,74 @@ class Tomo:
1754
1676
  if image_mask is None:
1755
1677
  first_image_index = 0
1756
1678
  else:
1757
- image_mask = np.asarray(image_mask)
1758
1679
  first_image_index = int(np.argmax(image_mask))
1759
1680
  field_indices_all = [
1760
1681
  index for index, key in enumerate(image_key) if key == 0]
1761
1682
  if not field_indices_all:
1762
1683
  raise ValueError('Tomography field(s) unavailable')
1763
- z_translation_all = np.asarray(
1764
- nxentry.sample.z_translation)[field_indices_all]
1684
+ z_translation_all = nxentry.sample.z_translation[field_indices_all]
1765
1685
  z_translation_levels = sorted(list(set(z_translation_all)))
1766
1686
  num_tomo_stacks = len(z_translation_levels)
1767
- center_stack_index = int(num_tomo_stacks/2)
1687
+ center_stack_index = num_tomo_stacks//2
1768
1688
  z_translation = z_translation_levels[center_stack_index]
1769
1689
  try:
1770
1690
  field_indices = [
1771
1691
  field_indices_all[index]
1772
1692
  for index, z in enumerate(z_translation_all)
1773
1693
  if z == z_translation]
1774
- first_image = np.asarray(nxentry.instrument.detector.data[
1775
- field_indices[first_image_index]])
1694
+ first_image = nxentry.instrument.detector.data[
1695
+ field_indices[first_image_index]]
1776
1696
  except:
1777
1697
  raise RuntimeError('Unable to load the tomography images '
1778
1698
  f'for stack {i}')
1779
1699
 
1780
1700
  # Set initial image bounds or rotation calibration rows
1781
- tbf = np.asarray(reduced_data.data.bright_field)
1701
+ tbf = reduced_data.data.bright_field.nxdata
1782
1702
  if (not isinstance(calibrate_center_rows, bool)
1783
1703
  and is_int_pair(calibrate_center_rows)):
1784
1704
  img_row_bounds = calibrate_center_rows
1785
1705
  else:
1786
1706
  if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1707
+ # System modules
1708
+ from sys import float_info
1709
+
1710
+ # Third party modules
1711
+ from nexusformat.nexus import (
1712
+ NXdata,
1713
+ NXfield,
1714
+ )
1715
+
1716
+ # Local modules
1717
+ from CHAP.utils.fit import FitProcessor
1718
+
1787
1719
  pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1788
1720
  # Try to get a fit from the bright field
1789
1721
  row_sum = np.sum(tbf, 1)
1790
- fit = Fit.fit_data(
1791
- row_sum, 'rectangle', x=np.array(range(len(row_sum))),
1792
- form='atan', guess=True)
1793
- parameters = fit.best_values
1722
+ num = len(row_sum)
1723
+ fit = FitProcessor()
1724
+ model = {'model': 'rectangle',
1725
+ 'parameters': [
1726
+ {'name': 'amplitude',
1727
+ 'value': row_sum.max()-row_sum.min(),
1728
+ 'min': 0.0},
1729
+ {'name': 'center1', 'value': 0.25*num,
1730
+ 'min': 0.0, 'max': num},
1731
+ {'name': 'sigma1', 'value': num/7.0,
1732
+ 'min': float_info.min},
1733
+ {'name': 'center2', 'value': 0.75*num,
1734
+ 'min': 0.0, 'max': num},
1735
+ {'name': 'sigma2', 'value': num/7.0,
1736
+ 'min': float_info.min}]}
1737
+ bounds_fit = fit.process(
1738
+ NXdata(NXfield(row_sum, 'y'),
1739
+ NXfield(np.array(range(num)), 'x')),
1740
+ {'models': [model], 'method': 'trf'})
1741
+ parameters = bounds_fit.best_values
1794
1742
  row_low_fit = parameters.get('center1', None)
1795
1743
  row_upp_fit = parameters.get('center2', None)
1796
1744
  sig_low = parameters.get('sigma1', None)
1797
1745
  sig_upp = parameters.get('sigma2', None)
1798
- have_fit = (fit.success and row_low_fit is not None
1746
+ have_fit = (bounds_fit.success and row_low_fit is not None
1799
1747
  and row_upp_fit is not None and sig_low is not None
1800
1748
  and sig_upp is not None
1801
1749
  and 0 <= row_low_fit < row_upp_fit <= row_sum.size
@@ -1829,12 +1777,12 @@ class Tomo:
1829
1777
  self._logger.debug(f'num_row_min = {num_row_min}')
1830
1778
  if have_fit:
1831
1779
  # Center the default range relative to the fitted
1832
- # window
1833
- row_low = int((row_low_fit+row_upp_fit-num_row_min) / 2)
1780
+ # window
1781
+ row_low = int((row_low_fit+row_upp_fit-num_row_min)/2)
1834
1782
  row_upp = row_low+num_row_min
1835
1783
  else:
1836
1784
  # Center the default range
1837
- row_low = int((tbf.shape[0]-num_row_min) / 2)
1785
+ row_low = int((tbf.shape[0]-num_row_min)/2)
1838
1786
  row_upp = row_low+num_row_min
1839
1787
  img_row_bounds = (row_low, row_upp)
1840
1788
  if calibrate_center_rows:
@@ -1885,21 +1833,20 @@ class Tomo:
1885
1833
  if self._save_figs:
1886
1834
  if calibrate_center_rows:
1887
1835
  fig.savefig(os_path.join(
1888
- self._output_folder, 'rotation_calibration_rows.png'))
1836
+ self._outputdir, 'rotation_calibration_rows.png'))
1889
1837
  else:
1890
1838
  fig.savefig(os_path.join(
1891
- self._output_folder, 'detector_image_bounds.png'))
1839
+ self._outputdir, 'detector_image_bounds.png'))
1892
1840
  plt.close()
1893
1841
 
1894
1842
  return img_row_bounds
1895
1843
 
1896
1844
  def _gen_thetas(self, nxentry, image_key):
1897
1845
  """Get the rotation angles for the image stacks."""
1898
- # Get the rotation angles
1846
+ # Get the rotation angles (in degrees)
1899
1847
  field_indices_all = [
1900
1848
  index for index, key in enumerate(image_key) if key == 0]
1901
- z_translation_all = np.asarray(
1902
- nxentry.sample.z_translation)[field_indices_all]
1849
+ z_translation_all = nxentry.sample.z_translation[field_indices_all]
1903
1850
  z_translation_levels = sorted(list(set(z_translation_all)))
1904
1851
  thetas = None
1905
1852
  for i, z_translation in enumerate(z_translation_levels):
@@ -1907,22 +1854,20 @@ class Tomo:
1907
1854
  field_indices_all[index]
1908
1855
  for index, z in enumerate(z_translation_all)
1909
1856
  if z == z_translation]
1910
- sequence_numbers = np.asarray(
1911
- nxentry.instrument.detector.sequence_number)[field_indices]
1857
+ sequence_numbers = \
1858
+ nxentry.instrument.detector.sequence_number[field_indices]
1912
1859
  assert (list(sequence_numbers)
1913
1860
  == list(range((len(sequence_numbers)))))
1914
1861
  if thetas is None:
1915
- thetas = np.asarray(
1916
- nxentry.sample.rotation_angle)[
1917
- field_indices][sequence_numbers]
1862
+ thetas = nxentry.sample.rotation_angle[
1863
+ field_indices][sequence_numbers]
1918
1864
  else:
1919
1865
  assert all(
1920
- thetas[i] == np.asarray(
1921
- nxentry.sample.rotation_angle)[
1922
- field_indices[index]]
1866
+ thetas[i] == nxentry.sample.rotation_angle[
1867
+ field_indices[index]]
1923
1868
  for i, index in enumerate(sequence_numbers))
1924
1869
 
1925
- return thetas
1870
+ return np.asarray(thetas)
1926
1871
 
1927
1872
  def _set_zoom_or_delta_theta(self, thetas, delta_theta=None):
1928
1873
  """
@@ -1949,7 +1894,7 @@ class Tomo:
1949
1894
  if self._interactive:
1950
1895
  if delta_theta is None:
1951
1896
  delta_theta = thetas[1]-thetas[0]
1952
- print(f'Available \u03b8 range: [{thetas[0]}, {thetas[-1]}]')
1897
+ print(f'\nAvailable \u03b8 range: [{thetas[0]}, {thetas[-1]}]')
1953
1898
  print(f'Current \u03b8 interval: {delta_theta}')
1954
1899
  if input_yesno(
1955
1900
  'Do you want to change the \u03b8 interval to reduce the '
@@ -1973,13 +1918,13 @@ class Tomo:
1973
1918
 
1974
1919
  # Get dark field
1975
1920
  if 'dark_field' in reduced_data.data:
1976
- tdf = np.asarray(reduced_data.data.dark_field)
1921
+ tdf = reduced_data.data.dark_field.nxdata
1977
1922
  else:
1978
1923
  self._logger.warning('Dark field unavailable')
1979
1924
  tdf = None
1980
1925
 
1981
1926
  # Get bright field
1982
- tbf = np.asarray(reduced_data.data.bright_field)
1927
+ tbf = reduced_data.data.bright_field.nxdata
1983
1928
  tbf_shape = tbf.shape
1984
1929
 
1985
1930
  # Subtract dark field
@@ -2018,26 +1963,26 @@ class Tomo:
2018
1963
  img_column_bounds[0]:img_column_bounds[1]]
2019
1964
 
2020
1965
  # Get thetas (in degrees)
2021
- thetas = np.asarray(reduced_data.rotation_angle)
1966
+ thetas = reduced_data.rotation_angle.nxdata
2022
1967
 
2023
1968
  # Get or create image mask
2024
1969
  image_mask = reduced_data.get('image_mask')
2025
1970
  if image_mask is None:
2026
- image_mask = np.ones(len(thetas), dtype=bool)
1971
+ image_mask = [True]*len(thetas)
2027
1972
  else:
2028
- image_mask = np.asarray(image_mask)
1973
+ image_mask = list(image_mask)
2029
1974
 
2030
1975
  # Get the tomography images
2031
1976
  field_indices_all = [
2032
1977
  index for index, key in enumerate(image_key) if key == 0]
2033
1978
  if not field_indices_all:
2034
1979
  raise ValueError('Tomography field(s) unavailable')
2035
- z_translation_all = np.asarray(
2036
- nxentry.sample.z_translation)[field_indices_all]
1980
+ z_translation_all = nxentry.sample.z_translation[
1981
+ field_indices_all]
2037
1982
  z_translation_levels = sorted(list(set(z_translation_all)))
2038
1983
  num_tomo_stacks = len(z_translation_levels)
2039
1984
  if calibrate_center_rows:
2040
- center_stack_index = int(num_tomo_stacks/2)
1985
+ center_stack_index = num_tomo_stacks//2
2041
1986
  tomo_stacks = num_tomo_stacks*[np.array([])]
2042
1987
  horizontal_shifts = []
2043
1988
  vertical_shifts = []
@@ -2046,25 +1991,25 @@ class Tomo:
2046
1991
  continue
2047
1992
  try:
2048
1993
  field_indices = [
2049
- field_indices_all[index]
2050
- for index, z in enumerate(z_translation_all)
1994
+ field_indices_all[i]
1995
+ for i, z in enumerate(z_translation_all)
2051
1996
  if z == z_translation]
2052
- field_indices_masked = np.asarray(field_indices)[image_mask]
2053
- horizontal_shift = list(set(np.asarray(
2054
- nxentry.sample.x_translation)[field_indices_masked]))
1997
+ field_indices_masked = [
1998
+ v for i, v in enumerate(field_indices) if image_mask[i]]
1999
+ horizontal_shift = list(
2000
+ set(nxentry.sample.x_translation[field_indices_masked]))
2055
2001
  assert len(horizontal_shift) == 1
2056
2002
  horizontal_shifts += horizontal_shift
2057
- vertical_shift = list(set(np.asarray(
2058
- nxentry.sample.z_translation)[field_indices_masked]))
2003
+ vertical_shift = list(
2004
+ set(nxentry.sample.z_translation[field_indices_masked]))
2059
2005
  assert len(vertical_shift) == 1
2060
2006
  vertical_shifts += vertical_shift
2061
- sequence_numbers = np.asarray(
2062
- nxentry.instrument.detector.sequence_number)[
2063
- field_indices]
2007
+ sequence_numbers = \
2008
+ nxentry.instrument.detector.sequence_number[field_indices]
2064
2009
  assert (list(sequence_numbers)
2065
2010
  == list(range((len(sequence_numbers)))))
2066
- tomo_stack = np.asarray(
2067
- nxentry.instrument.detector.data)[field_indices_masked]
2011
+ tomo_stack = nxentry.instrument.detector.data[
2012
+ field_indices_masked]
2068
2013
  except:
2069
2014
  raise RuntimeError('Unable to load the tomography images '
2070
2015
  f'for stack {i}')
@@ -2147,7 +2092,7 @@ class Tomo:
2147
2092
  name = f'reduced_data_stack_{i}_theta_{theta}'
2148
2093
  quick_imshow(
2149
2094
  tomo_stack[0,:,:], title=title, name=name,
2150
- path=self._output_folder, save_fig=self._save_figs,
2095
+ path=self._outputdir, save_fig=self._save_figs,
2151
2096
  save_only=self._save_only, block=self._block)
2152
2097
  zoom_perc = 100
2153
2098
  if zoom_perc != 100:
@@ -2163,7 +2108,7 @@ class Tomo:
2163
2108
  f'{round(thetas[0], 2)+0}'
2164
2109
  quick_imshow(
2165
2110
  tomo_stack[0,:,:], title=title,
2166
- path=self._output_folder, save_fig=self._save_figs,
2111
+ path=self._outputdir, save_fig=self._save_figs,
2167
2112
  save_only=self._save_only, block=self._block)
2168
2113
  del tomo_zoom_list
2169
2114
 
@@ -2179,11 +2124,11 @@ class Tomo:
2179
2124
  reduced_tomo_stacks[i] = np.zeros(tomo_stack_shape)
2180
2125
 
2181
2126
  # Add tomo field info to reduced data NXprocess
2182
- reduced_data.x_translation = np.asarray(horizontal_shifts)
2127
+ reduced_data.x_translation = horizontal_shifts
2183
2128
  reduced_data.x_translation.units = 'mm'
2184
- reduced_data.z_translation = np.asarray(vertical_shifts)
2129
+ reduced_data.z_translation = vertical_shifts
2185
2130
  reduced_data.z_translation.units = 'mm'
2186
- reduced_data.data.tomo_fields = np.asarray(reduced_tomo_stacks)
2131
+ reduced_data.data.tomo_fields = reduced_tomo_stacks
2187
2132
  reduced_data.data.attrs['signal'] = 'tomo_fields'
2188
2133
 
2189
2134
  if tdf is not None:
@@ -2193,22 +2138,35 @@ class Tomo:
2193
2138
  return reduced_data
2194
2139
 
2195
2140
  def _find_center_one_plane(
2196
- self, sinogram, row, thetas, eff_pixel_size, cross_sectional_dim,
2197
- path=None, num_core=1, center_offset_min=-50, center_offset_max=50,
2198
- gaussian_sigma=None, ring_width=None, prev_center_offset=None):
2199
- """Find center for a single tomography plane."""
2141
+ self, tomo_stacks, stack_index, row, offset_row, thetas,
2142
+ eff_pixel_size, cross_sectional_dim, path=None, num_core=1,
2143
+ center_offset_min=-50, center_offset_max=50,
2144
+ center_search_range=None, gaussian_sigma=None, ring_width=None,
2145
+ prev_center_offset=None):
2146
+ """
2147
+ Find center for a single tomography plane.
2148
+
2149
+ tomo_stacks data axes order: stack,theta,row,column
2150
+ thetas in radians
2151
+ """
2200
2152
  # Third party modules
2201
2153
  import matplotlib.pyplot as plt
2202
- from tomopy import find_center_vo
2154
+ from tomopy import (
2155
+ # find_center,
2156
+ find_center_vo,
2157
+ find_center_pc,
2158
+ )
2203
2159
 
2204
- # sinogram index order: theta,column
2205
- sinogram = np.asarray(sinogram)
2206
2160
  if not gaussian_sigma:
2207
2161
  gaussian_sigma = None
2208
2162
  if not ring_width:
2209
2163
  ring_width = None
2210
2164
 
2211
- # Try Nghia Vo's method to get the default center offset
2165
+ # Get the sinogram for the selected plane
2166
+ sinogram = tomo_stacks[stack_index,:,offset_row,:]
2167
+ center_offset_range = sinogram.shape[1]/2
2168
+
2169
+ # Try Nghia Vo's method to find the center
2212
2170
  t0 = time()
2213
2171
  if center_offset_min is None:
2214
2172
  center_offset_min = -50
@@ -2228,298 +2186,420 @@ class Tomo:
2228
2186
  self._logger.info(
2229
2187
  f'Finding center using Nghia Vo\'s method took {time()-t0:.2f} '
2230
2188
  'seconds')
2231
- center_offset_range = sinogram.shape[1]/2
2232
2189
  center_offset_vo = float(tomo_center-center_offset_range)
2233
2190
  self._logger.info(
2234
2191
  f'Center at row {row} using Nghia Vo\'s method = '
2235
2192
  f'{center_offset_vo:.2f}')
2236
2193
 
2237
- center_offset_default = None
2194
+ selected_center_offset = center_offset_vo
2238
2195
  if self._interactive or self._save_figs:
2239
2196
 
2240
- # Need column,theta for iradon, so take transpose
2241
- sinogram_t = sinogram.T
2242
-
2243
- # Reconstruct the plane for the Nghia Vo's center offset
2197
+ # Try Guizar-Sicairos's phase correlation method to find
2198
+ # the center
2244
2199
  t0 = time()
2245
- recon_plane = self._reconstruct_one_plane(
2246
- sinogram_t, center_offset_vo, thetas, eff_pixel_size,
2247
- cross_sectional_dim, False, num_core, gaussian_sigma,
2248
- ring_width)
2200
+ tomo_center = find_center_pc(
2201
+ tomo_stacks[stack_index,0,:,:],
2202
+ tomo_stacks[stack_index,-1,:,:])
2203
+ self._logger.info(
2204
+ 'Finding center using Guizar-Sicairos\'s phase correlation '
2205
+ f'method took {time()-t0:.2f} seconds')
2206
+ center_offset_pc = float(tomo_center-center_offset_range)
2207
+ self._logger.info(
2208
+ f'Center at row {row} using Guizar-Sicairos\'s image entropy '
2209
+ f'method = {center_offset_pc:.2f}')
2210
+
2211
+ # Try Donath's image entropy method to find the center
2212
+ # Skip this method, it seems flawed somehow or I'm doing something wrong
2213
+ # t0 = time()
2214
+ # tomo_center = find_center(
2215
+ # tomo_stacks[stack_index,:,:,:], thetas,
2216
+ # ind=offset_row)
2217
+ # self._logger.info(
2218
+ # 'Finding center using Donath\'s image entropy method took '
2219
+ # f'{time()-t0:.2f} seconds')
2220
+ # center_offset_ie = float(tomo_center-center_offset_range)
2221
+ # self._logger.info(
2222
+ # f'Center at row {row} using Donath\'s image entropy method = '
2223
+ # f'{center_offset_ie:.2f}')
2224
+
2225
+ # Reconstruct the plane for the Nghia Vo's center
2226
+ t0 = time()
2227
+ center_offsets = [center_offset_vo]
2228
+ fig_titles = [f'Vo\'s method: center offset = '
2229
+ f'{center_offset_vo:.2f}']
2230
+ recon_planes = [self._reconstruct_planes(
2231
+ sinogram, center_offset_vo, thetas, num_core=num_core,
2232
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width)]
2249
2233
  self._logger.info(
2250
2234
  f'Reconstructing row {row} with center at '
2251
2235
  f'{center_offset_vo} took {time()-t0:.2f} seconds')
2252
2236
 
2253
- recon_edges = [self._get_edges_one_plane(recon_plane)]
2254
- if (not self._interactive or prev_center_offset is None
2255
- or prev_center_offset == center_offset_vo):
2256
- fig, accept, center_offset_default = \
2257
- self._select_center_offset(
2258
- recon_edges, row, center_offset_vo, vo=True)
2259
- # Plot results
2260
- if self._save_figs:
2261
- fig.savefig(
2262
- os_path.join(
2263
- self._output_folder,
2264
- f'edges_center_row_{row}_vo.png'))
2265
- plt.close()
2266
-
2267
- if self._interactive:
2268
-
2269
- if center_offset_default is None:
2270
- # Reconstruct the plane at the previous center offset
2237
+ # Reconstruct the plane for the Guizar-Sicairos's center
2238
+ t0 = time()
2239
+ center_offsets.append(center_offset_pc)
2240
+ fig_titles.append(f'Guizar-Sicairos\'s method: center offset = '
2241
+ f'{center_offset_pc:.2f}')
2242
+ recon_planes.append(self._reconstruct_planes(
2243
+ sinogram, center_offset_pc, thetas, num_core=num_core,
2244
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2245
+ self._logger.info(
2246
+ f'Reconstructing row {row} with center at '
2247
+ f'{center_offset_pc} took {time()-t0:.2f} seconds')
2248
+
2249
+ # Reconstruct the plane for the Donath's center
2250
+ # t0 = time()
2251
+ # center_offsets.append(center_offset_ie)
2252
+ # fig_titles.append(f'Donath\'s method: center offset = '
2253
+ # f'{center_offset_ie:.2f}')
2254
+ # recon_planes.append(self._reconstruct_planes(
2255
+ # sinogram, center_offset_ie, thetas, num_core=num_core,
2256
+ # gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2257
+ # self._logger.info(
2258
+ # f'Reconstructing row {row} with center at '
2259
+ # f'{center_offset_ie} took {time()-t0:.2f} seconds')
2260
+
2261
+ # Reconstruct the plane at the previous row's center
2262
+ if (prev_center_offset is not None
2263
+ and prev_center_offset not in center_offsets):
2271
2264
  t0 = time()
2272
- recon_plane = self._reconstruct_one_plane(
2273
- sinogram_t, prev_center_offset, thetas, eff_pixel_size,
2274
- cross_sectional_dim, False, num_core, gaussian_sigma,
2275
- ring_width)
2265
+ center_offsets.append(prev_center_offset)
2266
+ fig_titles.append(f'Previous row\'s: center offset = '
2267
+ f'{prev_center_offset:.2f}')
2268
+ recon_planes.append(self._reconstruct_planes(
2269
+ sinogram, prev_center_offset, thetas, num_core=num_core,
2270
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2276
2271
  self._logger.info(
2277
2272
  f'Reconstructing row {row} with center at '
2278
2273
  f'{prev_center_offset} took {time()-t0:.2f} seconds')
2279
2274
 
2280
- recon_edges.insert(0, self._get_edges_one_plane(recon_plane))
2281
- fig, accept, center_offset_default = \
2282
- self._select_center_offset(
2283
- recon_edges, row,
2284
- (prev_center_offset, center_offset_vo),
2285
- vo=True, include_all_bad=True)
2286
- # Plot results
2287
- if self._save_figs:
2288
- fig.savefig(
2289
- os_path.join(
2290
- self._output_folder,
2291
- f'edges_center_row_{row}_'
2292
- f'{prev_center_offset}_{center_offset_vo}.png'))
2293
- plt.close()
2275
+ # t0 = time()
2276
+ # recon_edges = []
2277
+ # for recon_plane in recon_planes:
2278
+ # recon_edges.append(self._get_edges_one_plane(recon_plane))
2279
+ # print(f'\nGetting edges for row {row} with centers at '
2280
+ # f'{center_offsets} took {time()-t0:.2f} seconds\n')
2294
2281
 
2295
- if center_offset_default == center_offset_vo:
2296
- recon_edges.pop(0)
2297
- fig, accept, _ = self._select_center_offset(
2298
- recon_edges, row, center_offset_vo, vo=True)
2299
- # Plot results
2300
- if self._save_figs:
2301
- fig.savefig(
2302
- os_path.join(
2303
- self._output_folder,
2304
- f'edges_center_row_{row}_vo.png'))
2305
- plt.close()
2306
- else:
2307
- recon_edges.pop()
2308
- if center_offset_default == prev_center_offset:
2309
- fig, accept, _ = self._select_center_offset(
2310
- recon_edges, row, prev_center_offset, vo=False)
2311
- # Plot results
2312
- if self._save_figs:
2313
- fig.savefig(
2314
- os_path.join(
2315
- self._output_folder,
2316
- f'edges_center_row_{row}_'
2317
- f'{prev_center_offset}.png'))
2318
- plt.close()
2319
- else:
2320
- center_offset_default = prev_center_offset
2282
+ # Select the best center
2283
+ fig, accept, selected_center_offset = \
2284
+ self._select_center_offset(
2285
+ recon_planes, row, center_offsets, default_offset_index=0,
2286
+ fig_titles=fig_titles, search_button=False,
2287
+ include_all_bad=True)
2321
2288
 
2322
- # Perform center finding search
2323
- prev_index = None
2324
- up = True
2325
- include_all_bad = True
2326
- selected_center_offset = center_offset_default
2327
- center_offsets = [center_offset_default]
2328
- step_size = 4
2329
- if not accept:
2330
- max_step_size = min(
2331
- center_offset_range+center_offset_default,
2332
- center_offset_range-center_offset_default-1)
2333
- max_step_size = 1 << int(np.log2(max_step_size))-1
2334
- step_size = input_int(
2335
- '\nEnter the intial step size in the center '
2336
- 'calibration search (will be truncated to the nearest '
2337
- 'lower power of 2)', ge=2, le=max_step_size,
2338
- default=4)
2339
- step_size = 1 << int(np.log2(step_size))
2340
- while not accept and step_size:
2289
+ # Plot results
2290
+ if self._save_figs:
2291
+ fig.savefig(
2292
+ os_path.join(
2293
+ self._outputdir,
2294
+ f'recon_row_{row}_default_centers.png'))
2295
+ plt.close()
2296
+
2297
+ # Create reconstructions for a specified search range
2298
+ if self._interactive:
2299
+ if (center_search_range is None
2300
+ and input_yesno('\nDo you want to reconstruct images '
2301
+ 'for a range of rotation centers', 'n')):
2302
+ center_search_range = input_num_list(
2303
+ 'Enter up to 3 numbers (start, end, step), '
2304
+ '(range, step), or range', remove_duplicates=False,
2305
+ sort=False)
2306
+ if center_search_range is not None:
2307
+ if len(center_search_range) != 3:
2308
+ search_range = center_search_range[0]
2309
+ if len(center_search_range) == 1:
2310
+ step = search_range
2311
+ else:
2312
+ step = center_search_range[1]
2341
2313
  if selected_center_offset == 'all bad':
2342
- selected_center_offset = round(center_offset_default)
2343
- preselected_offsets = (
2344
- selected_center_offset-step_size,
2345
- selected_center_offset+step_size)
2314
+ center_search_range = [
2315
+ - search_range/2, search_range/2, step]
2346
2316
  else:
2347
- selected_center_offset = round(center_offset_default)
2348
- preselected_offsets = (
2349
- selected_center_offset-step_size,
2350
- selected_center_offset,
2351
- selected_center_offset+step_size)
2317
+ center_search_range = [
2318
+ selected_center_offset - search_range/2,
2319
+ selected_center_offset + search_range/2,
2320
+ step]
2321
+ center_search_range[1] += 1 # Make upper bound inclusive
2322
+ search_center_offsets = list(np.arange(*center_search_range))
2323
+ search_recon_planes = self._reconstruct_planes(
2324
+ sinogram, search_center_offsets, thetas, num_core=num_core,
2325
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width)
2326
+ for i, center in enumerate(search_center_offsets):
2327
+ title = f'Reconstruction for row {row}, center offset: ' \
2328
+ f'{center:.2f}'
2329
+ name = f'recon_row_{row}_center_{center:.2f}.png'
2330
+ if self._interactive:
2331
+ save_only = False
2332
+ block = True
2333
+ else:
2334
+ save_only = True
2335
+ block = False
2336
+ quick_imshow(
2337
+ search_recon_planes[i], title=title, row_label='y',
2338
+ column_label='x', path=self._outputdir, name=name,
2339
+ save_only=save_only, save_fig=True, block=block)
2340
+ center_offsets.append(center)
2341
+ recon_planes.append(search_recon_planes[i])
2342
+
2343
+ # Perform an interactive center finding search
2344
+ calibrate_interactively = False
2345
+ if self._interactive:
2346
+ if selected_center_offset == 'all bad':
2347
+ calibrate_interactively = input_yesno(
2348
+ '\nDo you want to perform an interactive search to '
2349
+ 'calibrate the rotation center (y/n)?', 'n')
2350
+ else:
2351
+ calibrate_interactively = input_yesno(
2352
+ '\nDo you want to perform an interactive search to '
2353
+ 'calibrate the rotation center around the selected value '
2354
+ f'of {selected_center_offset} (y/n)?', 'n')
2355
+ if calibrate_interactively:
2356
+ include_all_bad = True
2357
+ low = None
2358
+ upp = None
2359
+ if selected_center_offset == 'all bad':
2360
+ selected_center_offset = None
2361
+ selected_center_offset = input_num(
2362
+ '\nEnter the initial center offset in the center calibration '
2363
+ 'search', ge=-center_offset_range, le=center_offset_range,
2364
+ default=selected_center_offset)
2365
+ max_step_size = min(
2366
+ center_offset_range+selected_center_offset,
2367
+ center_offset_range-selected_center_offset-1)
2368
+ max_step_size = 1 << int(np.log2(max_step_size))-1
2369
+ step_size = input_int(
2370
+ '\nEnter the intial step size in the center calibration '
2371
+ 'search (will be truncated to the nearest lower power of 2)',
2372
+ ge=2, le=max_step_size, default=4)
2373
+ step_size = 1 << int(np.log2(step_size))
2374
+ selected_center_offset_prev = round(selected_center_offset)
2375
+ while step_size:
2376
+ preselected_offsets = (
2377
+ selected_center_offset_prev-step_size,
2378
+ selected_center_offset_prev,
2379
+ selected_center_offset_prev+step_size)
2352
2380
  indices = []
2353
2381
  for i, preselected_offset in enumerate(preselected_offsets):
2354
2382
  if preselected_offset in center_offsets:
2355
2383
  indices.append(
2356
2384
  center_offsets.index(preselected_offset))
2357
2385
  else:
2358
- recon_plane = self._reconstruct_one_plane(
2359
- sinogram_t, preselected_offset, thetas,
2360
- eff_pixel_size, cross_sectional_dim, False,
2361
- num_core, gaussian_sigma, ring_width)
2362
2386
  indices.append(len(center_offsets))
2363
2387
  center_offsets.append(preselected_offset)
2364
- recon_edges.append(
2365
- self._get_edges_one_plane(recon_plane))
2388
+ recon_planes.append(self._reconstruct_planes(
2389
+ sinogram, preselected_offset, thetas,
2390
+ num_core=num_core, gaussian_sigma=gaussian_sigma,
2391
+ ring_width=ring_width))
2366
2392
  fig, accept, selected_center_offset = \
2367
2393
  self._select_center_offset(
2368
- [recon_edges[i] for i in indices],
2369
- row, preselected_offsets,
2394
+ [recon_planes[i] for i in indices],
2395
+ row, preselected_offsets, default_offset_index=1,
2370
2396
  include_all_bad=include_all_bad)
2371
- if selected_center_offset == 'all bad':
2372
- step_size *=2
2373
- else:
2374
- include_all_bad = False
2375
- index = preselected_offsets.index(selected_center_offset)
2376
- if index != 1 and prev_index in (None, index) and up:
2377
- step_size *=2
2378
- else:
2379
- step_size = int(step_size/2)
2380
- up = False
2381
- prev_index = index
2382
- if step_size > max_step_size:
2383
- self._logger.warning('Exceeding maximum step size'
2384
- f'of {max_step_size}')
2385
- step_size = max_step_size
2386
2397
  # Plot results
2387
2398
  if self._save_figs:
2388
2399
  fig.savefig(
2389
2400
  os_path.join(
2390
- self._output_folder,
2391
- f'edges_center_{row}_{min(preselected_offsets)}_'\
2401
+ self._outputdir,
2402
+ f'recon_row_{row}_center_range_'
2403
+ f'{min(preselected_offsets)}_'\
2392
2404
  f'{max(preselected_offsets)}.png'))
2393
2405
  plt.close()
2394
- del sinogram_t
2395
- del recon_plane
2406
+ if accept and input_yesno(
2407
+ f'Accept center offset {selected_center_offset} '
2408
+ f'for row {row}? (y/n)', 'y'):
2409
+ break
2410
+ if selected_center_offset == 'all bad':
2411
+ step_size *=2
2412
+ else:
2413
+ if selected_center_offset == preselected_offsets[0]:
2414
+ upp = preselected_offsets[1]
2415
+ elif selected_center_offset == preselected_offsets[1]:
2416
+ low = preselected_offsets[0]
2417
+ upp = preselected_offsets[2]
2418
+ else:
2419
+ low = preselected_offsets[1]
2420
+ if None in (low, upp):
2421
+ step_size *= 2
2422
+ else:
2423
+ step_size = step_size//2
2424
+ include_all_bad = False
2425
+ selected_center_offset_prev = round(selected_center_offset)
2426
+ if step_size > max_step_size:
2427
+ self._logger.warning(
2428
+ 'Exceeding maximum step size of {max_step_size}')
2429
+ step_size = max_step_size
2396
2430
 
2397
- # Select center location
2398
- if self._interactive:
2399
- center_offset = selected_center_offset
2400
- else:
2401
- center_offset = center_offset_vo
2431
+ # Collect info for the currently selected center
2432
+ recon_planes = [recon_planes[
2433
+ center_offsets.index(selected_center_offset)]]
2434
+ center_offsets = [selected_center_offset]
2435
+ fig_titles = [f'Reconstruction for center offset = '
2436
+ f'{selected_center_offset:.2f}']
2437
+
2438
+ # Try Nghia Vo's method with the selected center
2439
+ step_size = min(step_size, 10)
2440
+ center_offset_min = selected_center_offset-step_size
2441
+ center_offset_max = selected_center_offset+step_size
2442
+ if num_core > NUM_CORE_TOMOPY_LIMIT:
2443
+ self._logger.debug(
2444
+ f'Running find_center_vo on {NUM_CORE_TOMOPY_LIMIT} '
2445
+ 'cores ...')
2446
+ tomo_center = find_center_vo(
2447
+ sinogram, ncore=NUM_CORE_TOMOPY_LIMIT,
2448
+ smin=center_offset_min, smax=center_offset_max)
2449
+ else:
2450
+ tomo_center = find_center_vo(
2451
+ sinogram, ncore=num_core, smin=center_offset_min,
2452
+ smax=center_offset_max)
2453
+ center_offset_vo = float(tomo_center-center_offset_range)
2454
+ self._logger.info(
2455
+ f'Center at row {row} using Nghia Vo\'s method = '
2456
+ f'{center_offset_vo:.2f}')
2457
+
2458
+ # Reconstruct the plane for the Nghia Vo's center
2459
+ center_offsets.append(center_offset_vo)
2460
+ fig_titles.append(f'Vo\'s method: center offset = '
2461
+ f'{center_offset_vo:.2f}')
2462
+ recon_planes.append(self._reconstruct_planes(
2463
+ sinogram, center_offset_vo, thetas, num_core=num_core,
2464
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2465
+
2466
+ # Select the best center
2467
+ fig, accept, selected_center_offset = \
2468
+ self._select_center_offset(
2469
+ recon_planes, row, center_offsets, default_offset_index=0,
2470
+ fig_titles=fig_titles, search_button=False)
2402
2471
 
2403
- return float(center_offset)
2472
+ # Plot results
2473
+ if self._save_figs:
2474
+ fig.savefig(
2475
+ os_path.join(
2476
+ self._outputdir,
2477
+ f'recon_row_{row}_center_'
2478
+ f'{selected_center_offset:.2f}.png'))
2479
+ plt.close()
2480
+
2481
+ del recon_planes
2482
+
2483
+ del sinogram
2404
2484
 
2405
- def _reconstruct_one_plane(
2406
- self, tomo_plane_t, center_offset, thetas, eff_pixel_size,
2407
- cross_sectional_dim, plot_sinogram=False, num_core=1,
2485
+ # Return the center location
2486
+ if self._interactive:
2487
+ if selected_center_offset == 'all bad':
2488
+ print('\nUnable to successfully calibrate center axis')
2489
+ selected_center_offset = input_num(
2490
+ 'Enter the center offset for row {row}',
2491
+ ge=-center_offset_range, le=center_offset_range)
2492
+ return float(selected_center_offset)
2493
+ return float(center_offset_vo)
2494
+
2495
+ def _reconstruct_planes(
2496
+ self, tomo_planes, center_offset, thetas, num_core=1,
2408
2497
  gaussian_sigma=None, ring_width=None):
2409
- """Invert the sinogram for a single tomography plane."""
2498
+ """Invert the sinogram for a single or multiple tomography
2499
+ planes using tomopy's recon routine."""
2410
2500
  # Third party modules
2411
2501
  from scipy.ndimage import gaussian_filter
2412
- from skimage.transform import iradon
2413
- from tomopy import misc
2414
-
2415
- # tomo_plane_t index order: column,theta
2416
- two_offset = 2 * int(np.round(center_offset))
2417
- two_offset_abs = np.abs(two_offset)
2418
- # Add 10% slack to max_rad to avoid edge effects
2419
- max_rad = int(0.55 * (cross_sectional_dim/eff_pixel_size))
2420
- if max_rad > 0.5*tomo_plane_t.shape[0]:
2421
- max_rad = 0.5*tomo_plane_t.shape[0]
2422
- dist_from_edge = max(1, int(np.floor(
2423
- (tomo_plane_t.shape[0] - two_offset_abs) / 2.) - max_rad))
2424
- if two_offset >= 0:
2425
- self._logger.debug(
2426
- f'sinogram range = [{two_offset+dist_from_edge}, '
2427
- f'{-dist_from_edge}]')
2428
- sinogram = tomo_plane_t[
2429
- two_offset+dist_from_edge:-dist_from_edge,:]
2430
- else:
2431
- self._logger.debug(
2432
- f'sinogram range = [{dist_from_edge}, '
2433
- f'{two_offset-dist_from_edge}]')
2434
- sinogram = tomo_plane_t[dist_from_edge:two_offset-dist_from_edge,:]
2435
- if plot_sinogram:
2436
- quick_imshow(
2437
- sinogram.T,
2438
- title=f'Sinogram for a center offset of {center_offset:.2f}',
2439
- name=f'sinogram_center_offset{center_offset:.2f}',
2440
- path=self._output_folder, save_fig=self._save_figs,
2441
- save_only=self._save_only, block=self._block, aspect='auto')
2502
+ from tomopy import (
2503
+ misc,
2504
+ recon,
2505
+ )
2442
2506
 
2443
- # Inverting sinogram
2444
- t0 = time()
2445
- recon_sinogram = iradon(sinogram, theta=thetas, circle=True)
2446
- self._logger.info(f'Inverting sinogram took {time()-t0:.2f} seconds')
2447
- del sinogram
2507
+ # Reconstruct the planes
2508
+ # tomo_planes axis data order: (row,)theta,column
2509
+ # thetas in radians
2510
+ if isinstance(center_offset, (int, float)):
2511
+ tomo_planes = np.expand_dims(tomo_planes, 0)
2512
+ center_offset = center_offset + tomo_planes.shape[2]/2
2513
+ elif is_num_series(center_offset):
2514
+ tomo_planes = np.array([tomo_planes]*len(center_offset))
2515
+ center_offset = np.asarray(center_offset) + tomo_planes.shape[2]/2
2516
+ else:
2517
+ raise ValueError(
2518
+ f'Invalid parameter center_offset ({center_offset})')
2519
+ recon_planes = recon(
2520
+ tomo_planes, thetas, center=center_offset, sinogram_order=True,
2521
+ algorithm='gridrec', ncore=num_core)
2448
2522
 
2449
2523
  # Performing Gaussian filtering and removing ring artifacts
2450
2524
  if gaussian_sigma is not None and gaussian_sigma:
2451
- recon_sinogram = gaussian_filter(
2452
- recon_sinogram, gaussian_sigma, mode='nearest')
2453
- recon_clean = np.expand_dims(recon_sinogram, axis=0)
2454
- del recon_sinogram
2525
+ recon_planes = gaussian_filter(
2526
+ recon_planes, gaussian_sigma, mode='nearest')
2455
2527
  if ring_width is not None and ring_width:
2456
- recon_clean = misc.corr.remove_ring(
2457
- recon_clean, rwidth=ring_width, ncore=num_core)
2458
-
2459
- return recon_clean
2460
-
2461
- def _get_edges_one_plane(self, recon_plane):
2462
- """
2463
- Create an "edges plot" image for a single reconstructed
2464
- tomography data plane.
2465
- """
2466
- # Third party modules
2467
- from skimage.restoration import denoise_tv_chambolle
2468
-
2469
- vis_parameters = None # self._config.get('vis_parameters')
2470
- if vis_parameters is None:
2471
- weight = 0.1
2472
- else:
2473
- weight = vis_parameters.get('denoise_weight', 0.1)
2474
- if not is_num(weight, ge=0.):
2475
- self._logger.warning(
2476
- f'Invalid weight ({weight}) in _get_edges_one_plane, '
2477
- 'set to a default of 0.1')
2478
- weight = 0.1
2479
- return denoise_tv_chambolle(recon_plane, weight=weight)[0]
2528
+ recon_planes = misc.corr.remove_ring(
2529
+ recon_planes, rwidth=ring_width, ncore=num_core)
2530
+
2531
+ # Apply a circular mask
2532
+ recon_planes = misc.corr.circ_mask(recon_planes, axis=0)
2533
+
2534
+ return np.squeeze(recon_planes)
2535
+
2536
+ # def _get_edges_one_plane(self, recon_plane):
2537
+ # """
2538
+ # Create an "edges plot" image for a single reconstructed
2539
+ # tomography data plane.
2540
+ # """
2541
+ # # Third party modules
2542
+ # from skimage.restoration import denoise_tv_chambolle
2543
+ #
2544
+ # vis_parameters = None # RV self._config.get('vis_parameters')
2545
+ # if vis_parameters is None:
2546
+ # weight = 0.1
2547
+ # else:
2548
+ # weight = vis_parameters.get('denoise_weight', 0.1)
2549
+ # if not is_num(weight, ge=0.):
2550
+ # self._logger.warning(
2551
+ # f'Invalid weight ({weight}) in _get_edges_one_plane, '
2552
+ # 'set to a default of 0.1')
2553
+ # weight = 0.1
2554
+ # return denoise_tv_chambolle(recon_plane, weight=weight)
2480
2555
 
2481
2556
  def _select_center_offset(
2482
- self, recon_edges, row, preselected_offsets, vo=False,
2557
+ self, recon_planes, row, preselected_offsets,
2558
+ default_offset_index=0, fig_titles=None, search_button=True,
2483
2559
  include_all_bad=False):
2484
- """Select a center offset value from an "edges plot" image
2560
+ """Select a center offset value from reconstructed images
2485
2561
  for a single reconstructed tomography data plane."""
2486
2562
  # Third party modules
2487
2563
  import matplotlib.pyplot as plt
2488
2564
  from matplotlib.widgets import RadioButtons, Button
2489
- from matplotlib.widgets import Button
2490
2565
 
2491
2566
  def select_offset(offset):
2492
2567
  """Callback function for the "Select offset" input."""
2493
- if offset in ('both bad', 'all bad'):
2494
- selected_offset.append(((False, 'all bad')))
2495
- else:
2496
- selected_offset.append(
2497
- ((False,
2498
- preselected_offsets[preselected_offsets.index(
2499
- float(radio_btn.value_selected))])))
2500
- plt.close()
2568
+ pass
2501
2569
 
2502
- def reject(event):
2503
- """Callback function for the "Reject" button."""
2504
- selected_offset.append((False, preselected_offsets[0]))
2570
+ def search(event):
2571
+ """Callback function for the "Search" button."""
2572
+ if num_plots == 1:
2573
+ selected_offset.append(
2574
+ (False, preselected_offsets[default_offset_index]))
2575
+ else:
2576
+ offset = radio_btn.value_selected
2577
+ if offset in ('both bad', 'all bad'):
2578
+ selected_offset.append((False, 'all bad'))
2579
+ else:
2580
+ selected_offset.append((False, float(offset)))
2505
2581
  plt.close()
2506
2582
 
2507
2583
  def accept(event):
2508
2584
  """Callback function for the "Accept" button."""
2509
2585
  if num_plots == 1:
2510
- selected_offset.append((True, preselected_offsets[0]))
2511
- else:
2512
2586
  selected_offset.append(
2513
- ((False,
2514
- preselected_offsets[preselected_offsets.index(
2515
- float(radio_btn.value_selected))])))
2587
+ (True, preselected_offsets[default_offset_index]))
2588
+ else:
2589
+ offset = radio_btn.value_selected
2590
+ if offset in ('both bad', 'all bad'):
2591
+ selected_offset.append((False, 'all bad'))
2592
+ else:
2593
+ selected_offset.append((True, float(offset)))
2516
2594
  plt.close()
2517
2595
 
2518
- if not isinstance(recon_edges, (tuple, list)):
2519
- recon_edges = [recon_edges]
2596
+ if not isinstance(recon_planes, (tuple, list)):
2597
+ recon_planes = [recon_planes]
2520
2598
  if not isinstance(preselected_offsets, (tuple, list)):
2521
2599
  preselected_offsets = [preselected_offsets]
2522
- assert len(recon_edges) == len(preselected_offsets)
2600
+ assert len(recon_planes) == len(preselected_offsets)
2601
+ if fig_titles is not None:
2602
+ assert len(fig_titles) == len(preselected_offsets)
2523
2603
 
2524
2604
  selected_offset = []
2525
2605
 
@@ -2531,68 +2611,58 @@ class Tomo:
2531
2611
  'horizontalalignment': 'center',
2532
2612
  'verticalalignment': 'bottom'}
2533
2613
 
2534
- num_plots = len(recon_edges)
2614
+ num_plots = len(recon_planes)
2535
2615
  if num_plots == 1:
2536
2616
  fig, axs = plt.subplots(figsize=(11, 8.5))
2537
2617
  axs = [axs]
2538
- vmax = np.max(recon_edges[0][:,:])
2618
+ vmax = np.max(recon_planes[0][:,:])
2539
2619
  else:
2540
2620
  fig, axs = plt.subplots(ncols=num_plots, figsize=(17, 8.5))
2541
2621
  axs = list(axs)
2542
- vmax = np.max(recon_edges[1][:,:])
2543
- for i, (ax, recon_edge, preselected_offset) in enumerate(zip(
2544
- axs, recon_edges, preselected_offsets)):
2545
- ax.imshow(recon_edge, vmin=-vmax, vmax=vmax, cmap='gray')
2546
- if num_plots == 1:
2547
- ax.set_title(
2548
- f'Reconstruction for row {row}, center offset: ' \
2549
- f'{preselected_offset:.2f}', fontsize='x-large')
2550
- else:
2551
- ax.set_title(
2552
- f'Center offset: {preselected_offset}',
2553
- fontsize='x-large')
2622
+ vmax = np.max(recon_planes[1][:,:])
2623
+ for i, (ax, recon_plane, preselected_offset) in enumerate(zip(
2624
+ axs, recon_planes, preselected_offsets)):
2625
+ ax.imshow(recon_plane, vmin=-vmax, vmax=vmax, cmap='gray')
2626
+ if fig_titles is None:
2627
+ if num_plots == 1:
2628
+ ax.set_title(
2629
+ f'Reconstruction for row {row}, center offset: ' \
2630
+ f'{preselected_offset:.2f}', fontsize='x-large')
2631
+ else:
2632
+ ax.set_title(
2633
+ f'Center offset: {preselected_offset}',
2634
+ fontsize='x-large')
2554
2635
  ax.set_xlabel('x', fontsize='x-large')
2555
2636
  if not i:
2556
2637
  ax.set_ylabel('y', fontsize='x-large')
2638
+ if fig_titles is not None:
2639
+ for (ax, fig_title) in zip(axs, fig_titles):
2640
+ ax.set_title(fig_title, fontsize='x-large')
2557
2641
 
2642
+ fig_title = plt.figtext(
2643
+ *title_pos, f'Reconstruction for row {row}', **title_props)
2558
2644
  if num_plots == 1:
2559
- if vo:
2560
- fig_title = plt.figtext(
2561
- *title_pos,
2562
- f'Reconstruction for row {row} (Nghia Vo\'s center '
2563
- f'offset: {preselected_offsets[0]})',
2564
- **title_props)
2565
- else:
2566
- fig_title = plt.figtext(
2567
- *title_pos,
2568
- f'Reconstruction for row {row} (previous center offset: '
2569
- f'{preselected_offsets[0]})',
2570
- **title_props)
2571
2645
  fig_subtitle = plt.figtext(
2572
2646
  *subtitle_pos,
2573
- 'Press "Accept" to accept this value or "Reject" to start a '
2574
- 'center calibration search',
2647
+ 'Press "Accept" to accept this value or "Reject" if not',
2575
2648
  **subtitle_props)
2576
2649
  else:
2577
- if vo:
2578
- fig_title = plt.figtext(
2579
- *title_pos,
2580
- f'Reconstruction for row {row} (previous center offset: '
2581
- f'{preselected_offsets[0]}, Nghia Vo\'s center '
2582
- f'offset: {preselected_offsets[1]})',
2583
- **title_props)
2650
+ if search_button:
2651
+ fig_subtitle = plt.figtext(
2652
+ *subtitle_pos,
2653
+ 'Select the best offset and press "Accept" to accept or '
2654
+ '"Search" to continue the search',
2655
+ **subtitle_props)
2584
2656
  else:
2585
- fig_title = plt.figtext(
2586
- *title_pos, f'Reconstruction for row {row}', **title_props)
2587
- fig_subtitle = plt.figtext(
2588
- *subtitle_pos,
2589
- 'Select the best offset or press "Accept" to accept the '
2590
- f'default value of {preselected_offsets[1]}',
2591
- **subtitle_props)
2657
+ fig_subtitle = plt.figtext(
2658
+ *subtitle_pos,
2659
+ 'Select the best offset and press "Accept" to accept',
2660
+ **subtitle_props)
2592
2661
 
2593
2662
  if not self._interactive:
2594
2663
 
2595
- selected_offset.append((True, preselected_offsets[0]))
2664
+ selected_offset.append(
2665
+ (True, preselected_offsets[default_offset_index]))
2596
2666
 
2597
2667
  else:
2598
2668
 
@@ -2605,7 +2675,6 @@ class Tomo:
2605
2675
  plt.axes([0.15, 0.05, 0.15, 0.075]), 'Reject')
2606
2676
  reject_cid = reject_btn.on_clicked(reject)
2607
2677
 
2608
-
2609
2678
  else:
2610
2679
 
2611
2680
  # Setup RadioButtons
@@ -2621,9 +2690,15 @@ class Tomo:
2621
2690
  labels = preselected_offsets
2622
2691
  radio_btn = RadioButtons(
2623
2692
  plt.axes([0.175, 0.05, 0.1, 0.1]),
2624
- labels = labels, active=1)
2693
+ labels = labels, active=default_offset_index)
2625
2694
  radio_cid = radio_btn.on_clicked(select_offset)
2626
2695
 
2696
+ # Setup "Search" button
2697
+ if search_button:
2698
+ search_btn = Button(
2699
+ plt.axes([0.4125, 0.05, 0.15, 0.075]), 'Search')
2700
+ search_cid = search_btn.on_clicked(search)
2701
+
2627
2702
  # Setup "Accept" button
2628
2703
  accept_btn = Button(
2629
2704
  plt.axes([0.7, 0.05, 0.15, 0.075]), 'Accept')
@@ -2639,6 +2714,9 @@ class Tomo:
2639
2714
  else:
2640
2715
  radio_btn.disconnect(radio_cid)
2641
2716
  radio_btn.ax.remove()
2717
+ if search_button:
2718
+ search_btn.disconnect(search_cid)
2719
+ search_btn.ax.remove()
2642
2720
  accept_btn.disconnect(accept_cid)
2643
2721
  accept_btn.ax.remove()
2644
2722
 
@@ -2646,18 +2724,20 @@ class Tomo:
2646
2724
  fig_title.remove()
2647
2725
  else:
2648
2726
  fig_title.set_in_layout(True)
2649
- select_text.remove()
2727
+ if self._interactive:
2728
+ select_text.remove()
2650
2729
  fig_subtitle.remove()
2651
2730
  fig.tight_layout(rect=(0, 0, 1, 0.95))
2652
- if not selected_offset and num_plots == 1:
2653
- selected_offset.append((True, preselected_offsets[0]))
2731
+ if not selected_offset:# and num_plots == 1:
2732
+ selected_offset.append(
2733
+ (True, preselected_offsets[default_offset_index]))
2654
2734
 
2655
2735
  return fig, *selected_offset[0]
2656
2736
 
2657
2737
  def _reconstruct_one_tomo_stack(
2658
2738
  self, tomo_stack, thetas, center_offsets=None, num_core=1,
2659
- algorithm='gridrec', secondary_iters=0, remove_stripe_sigma=None,
2660
- ring_width=None):
2739
+ algorithm='gridrec', secondary_iters=0, gaussian_sigma=None,
2740
+ remove_stripe_sigma=None, ring_width=None):
2661
2741
  """Reconstruct a single tomography stack."""
2662
2742
  # Third party modules
2663
2743
  from tomopy import (
@@ -2667,16 +2747,10 @@ class Tomo:
2667
2747
  recon,
2668
2748
  )
2669
2749
 
2670
- # tomo_stack order: row,theta,column
2671
- # input thetas must be in degrees
2750
+ # tomo_stack axis data order: row,theta,column
2751
+ # thetas in radians
2672
2752
  # centers_offset: tomography axis shift in pixels relative
2673
- # to column center
2674
- # RV should we remove stripes?
2675
- # https://tomopy.readthedocs.io/en/latest/api/tomopy.prep.stripe.html
2676
- # RV should we remove rings?
2677
- # https://tomopy.readthedocs.io/en/latest/api/tomopy.misc.corr.html
2678
- # RV add an option to do (extra) secondary iterations later or
2679
- # to do some sort of convergence test?
2753
+ # to column center
2680
2754
  if center_offsets is None:
2681
2755
  centers = np.zeros((tomo_stack.shape[0]))
2682
2756
  elif len(center_offsets) == 2:
@@ -2690,24 +2764,9 @@ class Tomo:
2690
2764
  centers = center_offsets
2691
2765
  centers += tomo_stack.shape[2]/2
2692
2766
 
2693
- # tomo_recon_stack = []
2694
- # eff_pixel_size = 0.05
2695
- # cross_sectional_dim = 20.0
2696
- # gaussian_sigma = 0.05
2697
- # ring_width = 1
2698
- # for i in range(tomo_stack.shape[0]):
2699
- # sinogram_t = tomo_stack[i,:,:].T
2700
- # recon_plane = self._reconstruct_one_plane(
2701
- # sinogram_t, centers[i], thetas, eff_pixel_size,
2702
- # cross_sectional_dim, False, num_core, gaussian_sigma,
2703
- # ring_width)
2704
- # tomo_recon_stack.append(recon_plane[0,:,:])
2705
- # tomo_recon_stack = np.asarray(tomo_recon_stack)
2706
- # return tomo_recon_stack
2707
-
2708
2767
  # Remove horizontal stripe
2709
2768
  # RV prep.stripe.remove_stripe_fw seems flawed for hollow brick
2710
- # accross multiple stacks
2769
+ # accross multiple stacks
2711
2770
  if remove_stripe_sigma is not None and remove_stripe_sigma:
2712
2771
  if num_core > NUM_CORE_TOMOPY_LIMIT:
2713
2772
  tomo_stack = prep.stripe.remove_stripe_fw(
@@ -2721,7 +2780,7 @@ class Tomo:
2721
2780
  self._logger.debug('Performing initial image reconstruction')
2722
2781
  t0 = time()
2723
2782
  tomo_recon_stack = recon(
2724
- tomo_stack, np.radians(thetas), centers, sinogram_order=True,
2783
+ tomo_stack, thetas, centers, sinogram_order=True,
2725
2784
  algorithm=algorithm, ncore=num_core)
2726
2785
  self._logger.info(
2727
2786
  f'Performing initial image reconstruction took {time()-t0:.2f} '
@@ -2759,9 +2818,9 @@ class Tomo:
2759
2818
  }
2760
2819
  t0 = time()
2761
2820
  tomo_recon_stack = recon(
2762
- tomo_stack, np.radians(thetas), centers,
2763
- init_recon=tomo_recon_stack, options=options,
2764
- sinogram_order=True, algorithm=astra, ncore=num_core)
2821
+ tomo_stack, thetas, centers, init_recon=tomo_recon_stack,
2822
+ options=options, sinogram_order=True, algorithm=astra,
2823
+ ncore=num_core)
2765
2824
  self._logger.info(
2766
2825
  f'Performing secondary iterations took {time()-t0:.2f} '
2767
2826
  'seconds')
@@ -2772,6 +2831,11 @@ class Tomo:
2772
2831
  tomo_recon_stack, rwidth=ring_width, out=tomo_recon_stack,
2773
2832
  ncore=num_core)
2774
2833
 
2834
+ # Performing Gaussian filtering
2835
+ if gaussian_sigma is not None and gaussian_sigma:
2836
+ tomo_recon_stack = misc.corr.gaussian_filter(
2837
+ tomo_recon_stack, sigma=gaussian_sigma, ncore=num_core)
2838
+
2775
2839
  return tomo_recon_stack
2776
2840
 
2777
2841
  def _resize_reconstructed_data(
@@ -2797,16 +2861,16 @@ class Tomo:
2797
2861
  # Selecting x an y bounds (in z-plane)
2798
2862
  if x_bounds is None:
2799
2863
  if not self._interactive:
2800
- self._logger.warning('x_bounds unspecified, reconstruct '
2801
- 'data for full x-range')
2864
+ self._logger.warning('x_bounds unspecified, use data for '
2865
+ 'full x-range')
2802
2866
  x_bounds = (0, tomo_recon_stacks[0].shape[2])
2803
2867
  elif not is_int_pair(
2804
2868
  x_bounds, ge=0, le=tomo_recon_stacks[0].shape[2]):
2805
2869
  raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
2806
2870
  if y_bounds is None:
2807
2871
  if not self._interactive:
2808
- self._logger.warning('y_bounds unspecified, reconstruct '
2809
- 'data for full y-range')
2872
+ self._logger.warning('y_bounds unspecified, use data for '
2873
+ 'full y-range')
2810
2874
  y_bounds = (0, tomo_recon_stacks[0].shape[1])
2811
2875
  elif not is_int_pair(
2812
2876
  y_bounds, ge=0, le=tomo_recon_stacks[0].shape[1]):
@@ -2828,11 +2892,20 @@ class Tomo:
2828
2892
  tomosum = 0
2829
2893
  for i in range(num_tomo_stacks):
2830
2894
  tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=0)
2831
- fig, roi = select_roi_2d(
2895
+ if self._save_figs:
2896
+ if combine_data:
2897
+ filename = os_path.join(
2898
+ self._outputdir, 'combined_data_xy_roi.png')
2899
+ else:
2900
+ filename = os_path.join(
2901
+ self._outputdir, 'reconstructed_data_xy_roi.png')
2902
+ else:
2903
+ filename = None
2904
+ roi = select_roi_2d(
2832
2905
  tomosum, preselected_roi=preselected_roi,
2833
2906
  title_a='Reconstructed data summed over z',
2834
2907
  row_label='y', column_label='x',
2835
- interactive=self._interactive)
2908
+ interactive=self._interactive, filename=filename)
2836
2909
  if roi is None:
2837
2910
  x_bounds = (0, tomo_recon_stacks[0].shape[2])
2838
2911
  y_bounds = (0, tomo_recon_stacks[0].shape[1])
@@ -2841,12 +2914,6 @@ class Tomo:
2841
2914
  y_bounds = (int(roi[2]), int(roi[3]))
2842
2915
  self._logger.debug(f'x_bounds = {x_bounds}')
2843
2916
  self._logger.debug(f'y_bounds = {y_bounds}')
2844
- # Plot results
2845
- if self._save_figs:
2846
- fig.savefig(
2847
- os_path.join(
2848
- self._output_folder, 'reconstructed_data_xy_roi.png'))
2849
- plt.close()
2850
2917
 
2851
2918
  # Selecting z bounds (in xy-plane)
2852
2919
  # (only valid for a single image stack or when combining a stack)
@@ -2868,17 +2935,20 @@ class Tomo:
2868
2935
  tomosum = 0
2869
2936
  for i in range(num_tomo_stacks):
2870
2937
  tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=(1,2))
2871
- fig, z_bounds = select_roi_1d(
2938
+ if self._save_figs:
2939
+ if combine_data:
2940
+ filename = os_path.join(
2941
+ self._outputdir, 'combined_data_z_roi.png')
2942
+ else:
2943
+ filename = os_path.join(
2944
+ self._outputdir, 'reconstructed_data_z_roi.png')
2945
+ else:
2946
+ filename = None
2947
+ z_bounds = select_roi_1d(
2872
2948
  tomosum, preselected_roi=z_bounds,
2873
2949
  xlabel='z', ylabel='Reconstructed data summed over x and y',
2874
- interactive=self._interactive)
2950
+ interactive=self._interactive, filename=filename)
2875
2951
  self._logger.debug(f'z_bounds = {z_bounds}')
2876
- # Plot results
2877
- if self._save_figs:
2878
- fig.savefig(
2879
- os_path.join(
2880
- self._output_folder, 'reconstructed_data_z_roi.png'))
2881
- plt.close()
2882
2952
 
2883
2953
  return x_bounds, y_bounds, z_bounds
2884
2954
 
@@ -2890,7 +2960,7 @@ class TomoSimFieldProcessor(Processor):
2890
2960
  tomography detector images.
2891
2961
  """
2892
2962
 
2893
- def process(self, data, **kwargs):
2963
+ def process(self, data):
2894
2964
  """
2895
2965
  Process the input configuration and return a
2896
2966
  `nexusformat.nexus.NXroot` object with the simulated
@@ -2920,6 +2990,9 @@ class TomoSimFieldProcessor(Processor):
2920
2990
  sample_size = config.sample_size
2921
2991
  if len(sample_size) == 1:
2922
2992
  sample_size = (sample_size[0], sample_size[0])
2993
+ if sample_type == 'hollow_pyramid' and len(sample_size) != 3:
2994
+ raise ValueError('Invalid combindation of sample_type '
2995
+ f'({sample_type}) and sample_size ({sample_size}')
2923
2996
  wall_thickness = config.wall_thickness
2924
2997
  mu = config.mu
2925
2998
  theta_step = config.theta_step
@@ -2944,30 +3017,37 @@ class TomoSimFieldProcessor(Processor):
2944
3017
  f'({detector_size[0]*pixel_size[0]})')
2945
3018
 
2946
3019
  # Get the rotation angles (start at a arbitrarily choose angle
2947
- # and add thetas for a full 360 degrees rotation series)
3020
+ # and add thetas for a full 360 degrees rotation series)
2948
3021
  if station in ('id1a3', 'id3a'):
2949
3022
  theta_start = 0.
2950
3023
  else:
2951
3024
  theta_start = -17
2952
- #RV theta_end = theta_start + 360.
3025
+ # RV theta_end = theta_start + 360.
2953
3026
  theta_end = theta_start + 180.
2954
3027
  thetas = list(
2955
3028
  np.arange(theta_start, theta_end+0.5*theta_step, theta_step))
2956
3029
 
2957
3030
  # Get the number of horizontal stacks bases on the diagonal
2958
- # of the square and for now don't allow more than one
2959
- num_tomo_stack = 1 + int((sample_size[1]*np.sqrt(2)-pixel_size[1])
2960
- / (detector_size[1]*pixel_size[1]))
3031
+ # of the square and for now don't allow more than one
3032
+ if (sample_size) == 3:
3033
+ num_tomo_stack = 1 + int(
3034
+ (max(sample_size[1:2])*np.sqrt(2)-pixel_size[1])
3035
+ / (detector_size[1]*pixel_size[1]))
3036
+ else:
3037
+ num_tomo_stack = 1 + int((sample_size[1]*np.sqrt(2)-pixel_size[1])
3038
+ / (detector_size[1]*pixel_size[1]))
2961
3039
  if num_tomo_stack > 1:
2962
3040
  raise ValueError('Sample is too wide for the detector')
2963
3041
 
2964
3042
  # Create the x-ray path length through a solid square
2965
- # crosssection for a set of rotation angles.
2966
- path_lengths_solid = self._create_pathlength_solid_square(
2967
- sample_size[1], thetas, pixel_size[1], detector_size[1])
3043
+ # crosssection for a set of rotation angles.
3044
+ path_lengths_solid = None
3045
+ if sample_type != 'hollow_pyramid':
3046
+ path_lengths_solid = self._create_pathlength_solid_square(
3047
+ sample_size[1], thetas, pixel_size[1], detector_size[1])
2968
3048
 
2969
3049
  # Create the x-ray path length through a hollow square
2970
- # crosssection for a set of rotation angles.
3050
+ # crosssection for a set of rotation angles.
2971
3051
  path_lengths_hollow = None
2972
3052
  if sample_type in ('square_pipe', 'hollow_cube', 'hollow_brick'):
2973
3053
  path_lengths_hollow = path_lengths_solid \
@@ -2990,7 +3070,12 @@ class TomoSimFieldProcessor(Processor):
2990
3070
  num_theta = len(thetas)
2991
3071
  vertical_shifts = []
2992
3072
  tomo_fields_stack = []
2993
- img_dim = (len(img_row_coords), path_lengths_solid.shape[1])
3073
+ len_img_y = (detector_size[1]+1)//2
3074
+ if len_img_y%2:
3075
+ len_img_y = 2*len_img_y - 1
3076
+ else:
3077
+ len_img_y = 2*len_img_y
3078
+ img_dim = (len(img_row_coords), len_img_y)
2994
3079
  intensities_solid = None
2995
3080
  intensities_hollow = None
2996
3081
  for n in range(num_tomo_stack):
@@ -3007,6 +3092,37 @@ class TomoSimFieldProcessor(Processor):
3007
3092
  beam_intensity * np.exp(-mu*path_lengths_hollow)
3008
3093
  for n in range(num_theta):
3009
3094
  tomo_field[n,:,:] = intensities_hollow[n]
3095
+ elif sample_type == 'hollow_pyramid':
3096
+ outer_indices = \
3097
+ np.where(abs(img_row_coords) <= sample_size[0]/2)[0]
3098
+ inner_indices = np.where(
3099
+ abs(img_row_coords) < sample_size[0]/2 - wall_thickness)[0]
3100
+ wall_indices = list(set(outer_indices)-set(inner_indices))
3101
+ ratio = abs(sample_size[1]-sample_size[2])/sample_size[0]
3102
+ baselength = max(sample_size[1:2])
3103
+ for i in wall_indices:
3104
+ path_lengths_solid = self._create_pathlength_solid_square(
3105
+ baselength - ratio*(
3106
+ img_row_coords[i] + 0.5*sample_size[0]),
3107
+ thetas, pixel_size[1], detector_size[1])
3108
+ intensities_solid = \
3109
+ beam_intensity * np.exp(-mu*path_lengths_solid)
3110
+ for n in range(num_theta):
3111
+ tomo_field[n,i] = intensities_solid[n]
3112
+ for i in inner_indices:
3113
+ path_lengths_hollow = (
3114
+ self._create_pathlength_solid_square(
3115
+ baselength - ratio*(
3116
+ img_row_coords[i] + 0.5*sample_size[0]),
3117
+ thetas, pixel_size[1], detector_size[1])
3118
+ - self._create_pathlength_solid_square(
3119
+ baselength - 2*wall_thickness - ratio*(
3120
+ img_row_coords[i] + 0.5*sample_size[0]),
3121
+ thetas, pixel_size[1], detector_size[1]))
3122
+ intensities_hollow = \
3123
+ beam_intensity * np.exp(-mu*path_lengths_hollow)
3124
+ for n in range(num_theta):
3125
+ tomo_field[n,i] = intensities_hollow[n]
3010
3126
  else:
3011
3127
  intensities_solid = \
3012
3128
  beam_intensity * np.exp(-mu*path_lengths_solid)
@@ -3088,10 +3204,6 @@ class TomoSimFieldProcessor(Processor):
3088
3204
  nxdetector.z_translation = vertical_shifts
3089
3205
  nxdetector.starting_image_index = starting_image_index
3090
3206
  nxdetector.starting_image_offset = starting_image_offset
3091
- # nxdetector.path_lengths_solid = path_lengths_solid
3092
- # nxdetector.path_lengths_hollow = path_lengths_hollow
3093
- # nxdetector.intensities_solid = intensities_solid
3094
- # nxdetector.intensities_hollow = intensities_hollow
3095
3207
 
3096
3208
  return nxroot
3097
3209
 
@@ -3103,7 +3215,7 @@ class TomoSimFieldProcessor(Processor):
3103
3215
  """
3104
3216
  # Get the column coordinates
3105
3217
  img_y_coords = pixel_size * (0.5 * (1 - detector_size%2)
3106
- + np.asarray(range(int(0.5 * (detector_size+1)))))
3218
+ + np.asarray(range((detector_size+1)//2)))
3107
3219
 
3108
3220
  # Get the path lenghts for position column coordinates
3109
3221
  lengths = np.zeros((len(thetas), len(img_y_coords)), dtype=np.float64)
@@ -3145,7 +3257,7 @@ class TomoDarkFieldProcessor(Processor):
3145
3257
  tomography data set created by TomoSimProcessor.
3146
3258
  """
3147
3259
 
3148
- def process(self, data, num_image=5, **kwargs):
3260
+ def process(self, data, num_image=5):
3149
3261
  """
3150
3262
  Process the input configuration and return a
3151
3263
  `nexusformat.nexus.NXroot` object with the simulated
@@ -3218,7 +3330,7 @@ class TomoBrightFieldProcessor(Processor):
3218
3330
  tomography data set created by TomoSimProcessor.
3219
3331
  """
3220
3332
 
3221
- def process(self, data, num_image=5, **kwargs):
3333
+ def process(self, data, num_image=5):
3222
3334
  """
3223
3335
  Process the input configuration and return a
3224
3336
  `nexusformat.nexus.NXroot` object with the simulated
@@ -3235,7 +3347,6 @@ class TomoBrightFieldProcessor(Processor):
3235
3347
  """
3236
3348
  # Third party modules
3237
3349
  from nexusformat.nexus import (
3238
- NeXusError,
3239
3350
  NXroot,
3240
3351
  NXentry,
3241
3352
  NXinstrument,
@@ -3272,7 +3383,7 @@ class TomoBrightFieldProcessor(Processor):
3272
3383
  bright_field = np.concatenate((dummy_fields, bright_field))
3273
3384
  num_image += num_dummy_start
3274
3385
  # Add 20% to slit size to make the bright beam slightly taller
3275
- # than the vertical displacements between stacks
3386
+ # than the vertical displacements between stacks
3276
3387
  slit_size = 1.2*source.slit_size
3277
3388
  if slit_size < float(detector.row_pixel_size*detector_size[0]):
3278
3389
  img_row_coords = float(detector.row_pixel_size) \
@@ -3307,7 +3418,7 @@ class TomoSpecProcessor(Processor):
3307
3418
  simulated tomography data set created by TomoSimProcessor.
3308
3419
  """
3309
3420
 
3310
- def process(self, data, scan_numbers=[1], **kwargs):
3421
+ def process(self, data, scan_numbers=[1]):
3311
3422
  """
3312
3423
  Process the input configuration and return a list of strings
3313
3424
  representing a plain text SPEC file.
@@ -3325,17 +3436,14 @@ class TomoSpecProcessor(Processor):
3325
3436
  from json import dumps
3326
3437
  from datetime import datetime
3327
3438
 
3328
- # Third party modules
3329
3439
  from nexusformat.nexus import (
3330
- NeXusError,
3331
- NXcollection,
3332
3440
  NXentry,
3333
3441
  NXroot,
3334
3442
  NXsubentry,
3335
3443
  )
3336
3444
 
3337
3445
  # Get and validate the TomoSimField, TomoDarkField, or
3338
- # TomoBrightField configuration object in data
3446
+ # TomoBrightField configuration object in data
3339
3447
  configs = {}
3340
3448
  nxroot = get_nxroot(data, 'tomo.models.TomoDarkField')
3341
3449
  if nxroot is not None:
@@ -3364,7 +3472,7 @@ class TomoSpecProcessor(Processor):
3364
3472
  raise ValueError('Inconsistent sample_type among scans')
3365
3473
  detector = nxroot.entry.instrument.detector
3366
3474
  if 'z_translation' in detector:
3367
- num_stack = np.asarray(detector.z_translation).size
3475
+ num_stack = detector.z_translation.size
3368
3476
  else:
3369
3477
  num_stack = 1
3370
3478
  data_shape = detector.data.shape
@@ -3398,9 +3506,9 @@ class TomoSpecProcessor(Processor):
3398
3506
  if station in ('id1a3', 'id3a'):
3399
3507
  spec_file.append('#O0 ramsx ramsz')
3400
3508
  else:
3401
- #RV Fix main code to use independent dim info
3509
+ # RV Fix main code to use independent dim info
3402
3510
  spec_file.append('#O0 GI_samx GI_samz GI_samphi')
3403
- spec_file.append('#o0 samx samz samphi') #RV do I need this line?
3511
+ spec_file.append('#o0 samx samz samphi') # RV do I need this line?
3404
3512
  spec_file.append('')
3405
3513
 
3406
3514
  # Create the SPEC file scan info (and image and parfile data for SMB)
@@ -3412,10 +3520,10 @@ class TomoSpecProcessor(Processor):
3412
3520
  for schema, nxroot in configs.items():
3413
3521
  detector = nxroot.entry.instrument.detector
3414
3522
  if 'z_translation' in detector:
3415
- z_translations = list(np.asarray(detector.z_translation))
3523
+ z_translations = list(detector.z_translation.nxdata)
3416
3524
  else:
3417
3525
  z_translations = [0.]
3418
- thetas = np.asarray(detector.thetas)
3526
+ thetas = detector.thetas
3419
3527
  num_theta = thetas.size
3420
3528
  if schema == 'tomo.models.TomoDarkField':
3421
3529
  if station in ('id1a3', 'id3a'):
@@ -3454,9 +3562,11 @@ class TomoSpecProcessor(Processor):
3454
3562
  spec_file.append('#N 1')
3455
3563
  spec_file.append('#L ome')
3456
3564
  if scan_type == 'ts1':
3457
- image_sets.append(np.asarray(detector.data)[n])
3565
+ #image_sets.append(detector.data.nxdata[n])
3566
+ image_sets.append(detector.data[n])
3458
3567
  else:
3459
- image_sets.append(np.asarray(detector.data))
3568
+ #image_sets.append(detector.data.nxdata)
3569
+ image_sets.append(detector.data)
3460
3570
  par_file.append(
3461
3571
  f'{datetime.now().strftime("%Y%m%d")} '
3462
3572
  f'{datetime.now().strftime("%H%M%S")} '
@@ -3546,7 +3656,7 @@ class TomoSpecProcessor(Processor):
3546
3656
 
3547
3657
  nxroot = NXroot()
3548
3658
  nxroot[sample_type] = nxentry
3549
- nxroot.attrs['default'] = sample_type
3659
+ nxroot[sample_type].set_default()
3550
3660
 
3551
3661
  return nxroot
3552
3662