ChessAnalysisPipeline 0.0.11__py3-none-any.whl → 0.0.13__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
@@ -19,15 +19,13 @@ import numpy as np
19
19
  # Local modules
20
20
  from CHAP.utils.general import (
21
21
  is_num,
22
+ is_int_pair,
22
23
  input_int,
23
24
  input_num,
24
25
  input_yesno,
25
- select_image_bounds,
26
- select_one_image_bound,
27
- draw_mask_1d,
28
- clear_plot,
29
- clear_imshow,
30
- quick_plot,
26
+ select_image_indices,
27
+ select_roi_1d,
28
+ select_roi_2d,
31
29
  quick_imshow,
32
30
  )
33
31
  from CHAP.utils.fit import Fit
@@ -39,20 +37,22 @@ NUM_CORE_TOMOPY_LIMIT = 24
39
37
 
40
38
  def get_nxroot(data, schema=None, remove=True):
41
39
  """Look through `data` for an item whose value for the `'schema'`
42
- key matches `schema` and whose value for the `'data'` key matches
43
- an nexusformat.nexus.NXobject object and return this object.
40
+ key matches `schema` (if supplied) and whose value for the `'data'`
41
+ key matches a nexusformat.nexus.NXobject object and return this
42
+ object.
44
43
 
45
- :param data: Input list of `PipelineData` objects
44
+ :param data: Input list of `PipelineData` objects.
46
45
  :type data: list[PipelineData]
47
- :param schema: name associated with the nexusformat.nexus.NXobject
48
- object to match in `data`
46
+ :param schema: Name associated with the nexusformat.nexus.NXobject
47
+ object to match in `data`.
49
48
  :type schema: str, optional
50
- :param remove: if there is a matching entry in `data`, remove
51
- it from the list, defaults to `True`.
49
+ :param remove: Removes the matching entry in `data` when found,
50
+ defaults to `True`.
52
51
  :type remove: bool, optional
53
- :raises ValueError: if there's no match for `schema` in `data`
54
- :return: object matching with `schema`
55
- :rtype: nexusformat.nexus.NXroot
52
+ :raises ValueError: Found an invalid matching object or multiple
53
+ matching objects.
54
+ :return: Object matching with `schema` or None when not found.
55
+ :rtype: None, nexusformat.nexus.NXroot
56
56
  """
57
57
  # System modules
58
58
  from copy import deepcopy
@@ -94,9 +94,11 @@ class TomoCHESSMapConverter(Processor):
94
94
  nexusformat.nexus.NXtomo style format.
95
95
 
96
96
  :param data: Input map and configuration for tomographic image
97
- reduction.
97
+ reduction/reconstruction.
98
98
  :type data: list[PipelineData]
99
- :return: NXtomo style tomography input configuration.
99
+ :raises ValueError: Invalid input or configuration parameter.
100
+ :return: nexusformat.nexus.NXtomo style tomography input
101
+ configuration.
100
102
  :rtype: nexusformat.nexus.NXroot
101
103
  """
102
104
  # System modules
@@ -116,6 +118,7 @@ class TomoCHESSMapConverter(Processor):
116
118
 
117
119
  # Local modules
118
120
  from CHAP.common.models.map import MapConfig
121
+ from CHAP.utils.general import index_nearest
119
122
 
120
123
  darkfield = get_nxroot(data, 'darkfield')
121
124
  brightfield = get_nxroot(data, 'brightfield')
@@ -237,6 +240,10 @@ class TomoCHESSMapConverter(Processor):
237
240
  pixel_size[1]/detector_config.lens_magnification
238
241
  nxdetector.row_pixel_size.units = 'mm'
239
242
  nxdetector.column_pixel_size.units = 'mm'
243
+ nxdetector.rows = detector_config.rows
244
+ nxdetector.columns = detector_config.columns
245
+ nxdetector.rows.units = 'pixels'
246
+ nxdetector.columns.units = 'pixels'
240
247
 
241
248
  # Add an NXsample to NXentry
242
249
  # (do not fill in data fields yet)
@@ -376,7 +383,6 @@ class TomoCHESSMapConverter(Processor):
376
383
  thetas = np.asarray(tomofields.data.rotation_angles)
377
384
  #RV num_image = len(tomofields.data.rotation_angles)
378
385
  assert len(thetas) > 2
379
- from CHAP.utils.general import index_nearest
380
386
  delta_theta = thetas[1]-thetas[0]
381
387
  if thetas[-1]-thetas[0] > 180-delta_theta:
382
388
  image_end = index_nearest(thetas, thetas[0]+180)
@@ -434,8 +440,8 @@ class TomoDataProcessor(Processor):
434
440
 
435
441
  def process(
436
442
  self, data, interactive=False, reduce_data=False,
437
- find_center=False, reconstruct_data=False, combine_data=False,
438
- output_folder='.', save_figs='no', **kwargs):
443
+ find_center=False, calibrate_center=False, reconstruct_data=False,
444
+ combine_data=False, output_folder='.', save_figs='no', **kwargs):
439
445
  """
440
446
  Process the input map or configuration with the step specific
441
447
  instructions and return either a dictionary or a
@@ -450,9 +456,12 @@ class TomoDataProcessor(Processor):
450
456
  :param reduce_data: Generate reduced tomography images,
451
457
  defaults to False.
452
458
  :type reduce_data: bool, optional
453
- :param find_center: Find the calibrated center axis info,
459
+ :param find_center: Generate calibrated center axis info,
454
460
  defaults to False.
455
461
  :type find_center: bool, optional
462
+ :param calibrate_center: Calibrate the rotation axis,
463
+ defaults to False.
464
+ :type calibrate_center: bool, optional
456
465
  :param reconstruct_data: Reconstruct the tomography data,
457
466
  defaults to False.
458
467
  :type reconstruct_data: bool, optional
@@ -464,6 +473,9 @@ class TomoDataProcessor(Processor):
464
473
  :param save_figs: Safe figures to file ('yes' or 'only') and/or
465
474
  display figures ('yes' or 'no'), defaults to 'no'.
466
475
  :type save_figs: Literal['yes', 'no', 'only'], optional
476
+ :raises ValueError: Invalid input or configuration parameter.
477
+ :raises RuntimeError: Missing map configuration to generate
478
+ reduced tomography images.
467
479
  :return: Processed (meta)data of the last step.
468
480
  :rtype: Union[dict, nexusformat.nexus.NXroot]
469
481
  """
@@ -484,6 +496,9 @@ class TomoDataProcessor(Processor):
484
496
  raise ValueError(f'Invalid parameter reduce_data ({reduce_data})')
485
497
  if not isinstance(find_center, bool):
486
498
  raise ValueError(f'Invalid parameter find_center ({find_center})')
499
+ if not isinstance(calibrate_center, bool):
500
+ raise ValueError(
501
+ f'Invalid parameter calibrate_center ({calibrate_center})')
487
502
  if not isinstance(reconstruct_data, bool):
488
503
  raise ValueError(
489
504
  f'Invalid parameter reconstruct_data ({reconstruct_data})')
@@ -519,14 +534,36 @@ class TomoDataProcessor(Processor):
519
534
 
520
535
  nxsetconfig(memory=100000)
521
536
 
537
+ # Calibrate the rotation axis
538
+ if calibrate_center:
539
+ if (reduce_data or find_center
540
+ or reconstruct_data or reconstruct_data_config is not None
541
+ or combine_data or combine_data_config is not None):
542
+ self._logger.warning('Ignoring any step specific instructions '
543
+ 'during center calibration')
544
+ if nxroot is None:
545
+ raise RuntimeError('Map info required to calibrate the '
546
+ 'rotation axis')
547
+ if find_center_config is None:
548
+ find_center_config = TomoFindCenterConfig()
549
+ calibrate_center_rows = True
550
+ else:
551
+ calibrate_center_rows = find_center_config.center_rows
552
+ if calibrate_center_rows == None:
553
+ calibrate_center_rows = True
554
+ nxroot, calibrate_center_rows = tomo.reduce_data(
555
+ nxroot, reduce_data_config, calibrate_center_rows)
556
+ return tomo.find_centers(
557
+ nxroot, find_center_config, calibrate_center_rows)
558
+
522
559
  # Reduce tomography images
523
560
  if reduce_data or reduce_data_config is not None:
524
561
  if nxroot is None:
525
562
  raise RuntimeError('Map info required to reduce the '
526
563
  'tomography images')
527
- nxroot = tomo.gen_reduced_data(nxroot, reduce_data_config)
564
+ nxroot, _ = tomo.reduce_data(nxroot, reduce_data_config)
528
565
 
529
- # Find rotation axis centers for the tomography stacks
566
+ # Find calibrated center axis info for the tomography stacks
530
567
  center_config = None
531
568
  if find_center or find_center_config is not None:
532
569
  run_find_centers = False
@@ -534,22 +571,17 @@ class TomoDataProcessor(Processor):
534
571
  find_center_config = TomoFindCenterConfig()
535
572
  run_find_centers = True
536
573
  else:
537
- if (None in (find_center_config.lower_row,
538
- find_center_config.upper_row)
539
- or find_center_config.lower_center_offset is None
540
- or find_center_config.upper_center_offset is None):
574
+ if (find_center_config.center_rows is None
575
+ or find_center_config.center_offsets is None):
541
576
  run_find_centers = True
542
577
  if run_find_centers:
543
578
  center_config = tomo.find_centers(nxroot, find_center_config)
544
579
  else:
545
580
  # RV make a convert to dict in basemodel?
546
581
  center_config = {
547
- 'lower_row': find_center_config.lower_row,
548
- 'lower_center_offset':
549
- find_center_config.lower_center_offset,
550
- 'upper_row': find_center_config.upper_row,
551
- 'upper_center_offset':
552
- find_center_config.upper_center_offset,
582
+ 'center_rows': find_center_config.center_rows,
583
+ 'center_offsets':
584
+ find_center_config.center_offsets,
553
585
  'center_stack_index':
554
586
  find_center_config.center_stack_index,
555
587
  }
@@ -579,13 +611,13 @@ def nxcopy(nxobject, exclude_nxpaths=None, nxpath_prefix=''):
579
611
  Function that returns a copy of a nexus object, optionally exluding
580
612
  certain child items.
581
613
 
582
- :param nxobject: the original nexus object to return a "copy" of
614
+ :param nxobject: The input nexus object to "copy".
583
615
  :type nxobject: nexusformat.nexus.NXobject
584
- :param exlude_nxpaths: a list of paths to child nexus objects that
585
- should be excluded from the returned "copy", defaults to `[]`
616
+ :param exlude_nxpaths: A list of paths to child nexus objects that
617
+ should be excluded from the returned "copy", defaults to `[]`.
586
618
  :type exclude_nxpaths: list[str], optional
587
619
  :param nxpath_prefix: For use in recursive calls from inside this
588
- function only!
620
+ function only.
589
621
  :type nxpath_prefix: str
590
622
  :return: Copy of the input `nxobject` with some children optionally
591
623
  exluded.
@@ -629,7 +661,7 @@ class SetNumexprThreads:
629
661
  Initialize SetNumexprThreads.
630
662
 
631
663
  :param num_core: Number of processors used by the num_expr
632
- package
664
+ package.
633
665
  :type num_core: int
634
666
  """
635
667
  # System modules
@@ -663,23 +695,21 @@ class Tomo:
663
695
 
664
696
  def __init__(
665
697
  self, interactive=False, num_core=-1, output_folder='.',
666
- save_figs='no', test_mode=False):
698
+ save_figs='no'):
667
699
  """
668
700
  Initialize Tomo.
669
701
 
670
702
  :param interactive: Allows for user interactions,
671
703
  defaults to False.
672
704
  :type interactive: bool, optional
673
- :param num_core: Number of processors
705
+ :param num_core: Number of processors.
674
706
  :type num_core: int
675
707
  :param output_folder: Output folder name, defaults to '.'.
676
708
  :type output_folder:: str, optional
677
709
  :param save_figs: Safe figures to file ('yes' or 'only') and/or
678
710
  display figures ('yes' or 'no'), defaults to 'no'.
679
711
  :type save_figs: Literal['yes', 'no', 'only'], optional
680
- :param test_mode: Run in test mode (non-interactively), defaults
681
- to False
682
- :type test_mode: bool, optional
712
+ :raises ValueError: Invalid input parameter.
683
713
  """
684
714
  # System modules
685
715
  from logging import getLogger
@@ -696,17 +726,7 @@ class Tomo:
696
726
  self._output_folder = os_path.abspath(output_folder)
697
727
  if not os_path.isdir(self._output_folder):
698
728
  mkdir(self._output_folder)
699
- if self._interactive:
700
- self._test_mode = False
701
- else:
702
- if not isinstance(test_mode, bool):
703
- raise ValueError(f'Invalid parameter test_mode ({test_mode})')
704
- self._test_mode = test_mode
705
729
  self._test_config = {}
706
- if self._test_mode:
707
- if save_figs != 'only':
708
- self._logger.warning('Ignoring save_figs in test mode')
709
- save_figs = 'only'
710
730
  if save_figs == 'only':
711
731
  self._save_only = True
712
732
  self._save_figs = True
@@ -732,16 +752,18 @@ class Tomo:
732
752
  f'of available processors and reduced to {cpu_count()}')
733
753
  self._num_core = cpu_count()
734
754
 
735
- def gen_reduced_data(self, nxroot, tool_config=None):
755
+ def reduce_data(
756
+ self, nxroot, tool_config=None, calibrate_center_rows=False):
736
757
  """
737
- Generate the reduced tomography images.
758
+ Reduced the tomography images.
738
759
 
739
760
  :param nxroot: Data object containing the raw data info and
740
- metadata required for a tomography data reduction
761
+ metadata required for a tomography data reduction.
741
762
  :type nxroot: nexusformat.nexus.NXroot
742
- :param tool_config: Tool configuration
763
+ :param tool_config: Tool configuration.
743
764
  :type tool_config: CHAP.tomo.models.TomoReduceConfig, optional
744
- :return: Reduced tomography data
765
+ :raises ValueError: Invalid input or configuration parameter.
766
+ :return: Reduced tomography data.
745
767
  :rtype: nexusformat.nexus.NXroot
746
768
  """
747
769
  # Third party modules
@@ -763,25 +785,36 @@ class Tomo:
763
785
  img_row_bounds = None
764
786
  else:
765
787
  delta_theta = tool_config.delta_theta
766
- img_row_bounds = tool_config.img_row_bounds
788
+ img_row_bounds = tuple(tool_config.img_row_bounds)
789
+ if img_row_bounds is not None:
790
+ if (nxentry.instrument.source.attrs['station']
791
+ in ('id1a3', 'id3a')):
792
+ self._logger.warning('Ignoring parameter img_row_bounds '
793
+ 'for id1a3 and id3a')
794
+ img_row_bounds = None
795
+ elif calibrate_center_rows:
796
+ self._logger.warning('Ignoring parameter img_row_bounds '
797
+ 'during rotation axis calibration')
798
+ img_row_bounds = None
767
799
 
768
800
  image_key = nxentry.instrument.detector.get('image_key', None)
769
801
  if image_key is None or 'data' not in nxentry.instrument.detector:
770
802
  raise ValueError(f'Unable to find image_key or data in '
771
803
  'instrument.detector '
772
804
  f'({nxentry.instrument.detector.tree})')
805
+ image_key = np.asarray(image_key)
773
806
 
774
807
  # Create an NXprocess to store data reduction (meta)data
775
808
  reduced_data = NXprocess()
776
809
 
777
810
  # Generate dark field
778
- reduced_data = self._gen_dark(nxentry, reduced_data)
811
+ reduced_data = self._gen_dark(nxentry, reduced_data, image_key)
779
812
 
780
813
  # Generate bright field
781
- reduced_data = self._gen_bright(nxentry, reduced_data)
814
+ reduced_data = self._gen_bright(nxentry, reduced_data, image_key)
782
815
 
783
816
  # Get rotation angles for image stacks
784
- thetas = self._gen_thetas(nxentry)
817
+ thetas = self._gen_thetas(nxentry, image_key)
785
818
 
786
819
  # Get the image stack mask to remove bad images from stack
787
820
  image_mask = None
@@ -790,7 +823,7 @@ class Tomo:
790
823
  if delta_theta is not None:
791
824
  delta_theta = None
792
825
  self._logger.warning(
793
- 'Ignore delta_theta when an image mask is used')
826
+ 'Ignoring delta_theta when an image mask is used')
794
827
  np.random.seed(0)
795
828
  image_mask = np.where(np.random.rand(
796
829
  len(thetas)) < drop_fraction/100, 0, 1).astype(bool)
@@ -812,19 +845,32 @@ class Tomo:
812
845
  self._logger.debug(f'image_mask = {image_mask}')
813
846
  reduced_data.image_mask = image_mask
814
847
  thetas = thetas[image_mask]
815
- self._logger.debug(f'thetas = {thetas}')
816
- reduced_data.rotation_angle = thetas
817
- reduced_data.rotation_angle.units = 'degrees'
818
848
 
819
- # Set vertical detector bounds for image stack
849
+ # Set vertical detector bounds for image stack or rotation
850
+ # axis calibration rows
820
851
  img_row_bounds = self._set_detector_bounds(
821
- nxentry, reduced_data, thetas[0], img_row_bounds=img_row_bounds)
852
+ nxentry, reduced_data, image_key, thetas[0],
853
+ img_row_bounds, calibrate_center_rows)
822
854
  self._logger.info(f'img_row_bounds = {img_row_bounds}')
855
+ if calibrate_center_rows:
856
+ calibrate_center_rows = tuple(sorted(img_row_bounds))
857
+ img_row_bounds = None
858
+ if img_row_bounds is None:
859
+ tbf_shape = np.asarray(reduced_data.data.bright_field).shape
860
+ img_row_bounds = (0, tbf_shape[0])
823
861
  reduced_data.img_row_bounds = img_row_bounds
824
862
  reduced_data.img_row_bounds.units = 'pixels'
863
+ reduced_data.img_row_bounds.attrs['long_name'] = \
864
+ 'image row boundaries in detector frame of reference'
865
+
866
+ # Store rotation angles for image stacks
867
+ self._logger.debug(f'thetas = {thetas}')
868
+ reduced_data.rotation_angle = thetas
869
+ reduced_data.rotation_angle.units = 'degrees'
825
870
 
826
871
  # Generate reduced tomography fields
827
- reduced_data = self._gen_tomo(nxentry, reduced_data)
872
+ reduced_data = self._gen_tomo(
873
+ nxentry, reduced_data, image_key, calibrate_center_rows)
828
874
 
829
875
  # Create a copy of the input Nexus object and remove raw and
830
876
  # any existing reduced data
@@ -857,18 +903,20 @@ class Tomo:
857
903
  nxentry.reduced_data.rotation_angle, name='rotation_angle')
858
904
  nxentry.data.attrs['signal'] = 'reduced_data'
859
905
 
860
- return nxroot
906
+ return nxroot, calibrate_center_rows
861
907
 
862
- def find_centers(self, nxroot, tool_config):
908
+ def find_centers(self, nxroot, tool_config, calibrate_center_rows=False):
863
909
  """
864
910
  Find the calibrated center axis info
865
911
 
866
912
  :param nxroot: Data object containing the reduced data and
867
- metadata required to find the calibrated center axis info
913
+ metadata required to find the calibrated center axis info.
868
914
  :type data: nexusformat.nexus.NXroot
869
- :param tool_config: Tool configuration
915
+ :param tool_config: Tool configuration.
870
916
  :type tool_config: CHAP.tomo.models.TomoFindCenterConfig
871
- :return: Calibrated center axis info
917
+ :raises ValueError: Invalid or missing input or configuration
918
+ parameter.
919
+ :return: Calibrated center axis info.
872
920
  :rtype: dict
873
921
  """
874
922
  # Third party modules
@@ -884,39 +932,24 @@ class Tomo:
884
932
  nxentry = nxroot[nxroot.attrs['default']]
885
933
  else:
886
934
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
887
- center_rows = (tool_config.lower_row, tool_config.upper_row)
888
- center_stack_index = tool_config.center_stack_index
889
- if not self._interactive and center_rows == (None, None):
890
- self._logger.warning(
891
- 'center_rows unspecified, find centers at reduced data bounds')
892
- if (center_stack_index is not None
893
- and (not isinstance(center_stack_index, int)
894
- or center_stack_index < 0)):
895
- raise ValueError(
896
- 'Invalid parameter center_stack_index '
897
- f'({center_stack_index})')
898
935
 
899
936
  # Check if reduced data is available
900
937
  if ('reduced_data' not in nxentry
901
938
  or 'reduced_data' not in nxentry.data):
902
- raise KeyError(f'Unable to find valid reduced data in {nxentry}.')
939
+ raise ValueError(f'Unable to find valid reduced data in {nxentry}.')
903
940
 
904
- # Select the image stack to calibrate the center axis
941
+ # Select the image stack to find the calibrated center axis
905
942
  # reduced data axes order: stack,theta,row,column
906
943
  # Note: Nexus can't follow a link if the data it points to is
907
944
  # too big get the data from the actual place, not from
908
945
  # nxentry.data
909
946
  num_tomo_stacks = nxentry.reduced_data.data.tomo_fields.shape[0]
910
- img_shape = nxentry.reduced_data.data.bright_field.shape
911
- num_row = int(nxentry.reduced_data.img_row_bounds[1]
912
- - nxentry.reduced_data.img_row_bounds[0])
913
947
  if num_tomo_stacks == 1:
914
948
  center_stack_index = 0
915
- default = 'n'
916
949
  else:
917
- if self._test_mode:
918
- # Convert input value to offset 0
919
- center_stack_index = self._test_config['center_stack_index']
950
+ center_stack_index = tool_config.center_stack_index
951
+ if calibrate_center_rows:
952
+ center_stack_index = int(num_tomo_stacks/2)
920
953
  elif self._interactive:
921
954
  if center_stack_index is None:
922
955
  center_stack_index = input_int(
@@ -928,119 +961,121 @@ class Tomo:
928
961
  center_stack_index = int(num_tomo_stacks/2)
929
962
  self._logger.warning(
930
963
  'center_stack_index unspecified, use stack '
931
- f'{center_stack_index} to find centers')
932
- default = 'y'
964
+ f'{center_stack_index} to find center axis info')
933
965
 
934
966
  # Get thetas (in degrees)
935
967
  thetas = np.asarray(nxentry.reduced_data.rotation_angle)
936
968
 
969
+ # Select center rows
970
+ if calibrate_center_rows:
971
+ center_rows = calibrate_center_rows
972
+ offset_center_rows = (0, 1)
973
+ else:
974
+ # Third party modules
975
+ import matplotlib.pyplot as plt
976
+
977
+ # Get full bright field
978
+ tbf = np.asarray(nxentry.reduced_data.data.bright_field)
979
+ tbf_shape = tbf.shape
980
+
981
+ # Get image bounds
982
+ img_row_bounds = nxentry.reduced_data.get(
983
+ 'img_row_bounds', (0, tbf_shape[0]))
984
+ img_row_bounds = (int(img_row_bounds[0]), int(img_row_bounds[1]))
985
+ img_column_bounds = nxentry.reduced_data.get(
986
+ 'img_column_bounds', (0, tbf_shape[1]))
987
+ img_column_bounds = (
988
+ int(img_column_bounds[0]), int(img_column_bounds[1]))
989
+
990
+ center_rows = tool_config.center_rows
991
+ if center_rows is None:
992
+ if num_tomo_stacks == 1:
993
+ # Add a small margin to avoid edge effects
994
+ offset = min(
995
+ 5, int(0.1*(img_row_bounds[1] - img_row_bounds[0])))
996
+ center_rows = (
997
+ img_row_bounds[0]+offset, img_row_bounds[1]-1-offset)
998
+ else:
999
+ if not self._interactive:
1000
+ self._logger.warning('center_rows unspecified, find '
1001
+ 'centers at reduced data bounds')
1002
+ center_rows = (img_row_bounds[0], img_row_bounds[1]-1)
1003
+ fig, center_rows = select_image_indices(
1004
+ nxentry.reduced_data.data.tomo_fields[
1005
+ center_stack_index,0,:,:],
1006
+ 0,
1007
+ b=tbf[img_row_bounds[0]:img_row_bounds[1],
1008
+ img_column_bounds[0]:img_column_bounds[1]],
1009
+ preselected_indices=center_rows,
1010
+ axis_index_offset=img_row_bounds[0],
1011
+ title='Select two detector image row indices to find center '
1012
+ f'axis (in range [{img_row_bounds[0]}, '
1013
+ f'{img_row_bounds[1]-1}])',
1014
+ title_a=r'Tomography image at $\theta$ = '
1015
+ f'{round(thetas[0], 2)+0}',
1016
+ title_b='Bright field', interactive=self._interactive)
1017
+ if center_rows[1] == img_row_bounds[1]:
1018
+ center_rows = (center_rows[0], center_rows[1]-1)
1019
+ offset_center_rows = (
1020
+ center_rows[0] - img_row_bounds[0],
1021
+ center_rows[1] - img_row_bounds[0])
1022
+ # Plot results
1023
+ if self._save_figs:
1024
+ fig.savefig(
1025
+ os_path.join(
1026
+ self._output_folder, 'center_finding_rows.png'))
1027
+ plt.close()
1028
+
937
1029
  # Get effective pixel_size
938
1030
  if 'zoom_perc' in nxentry.reduced_data:
939
- eff_pixel_size = \
1031
+ eff_pixel_size = float(
940
1032
  100. * (nxentry.instrument.detector.row_pixel_size
941
- / nxentry.reduced_data.attrs['zoom_perc'])
1033
+ / nxentry.reduced_data.attrs['zoom_perc']))
942
1034
  else:
943
- eff_pixel_size = nxentry.instrument.detector.row_pixel_size
1035
+ eff_pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1036
+ self._logger.debug(f'eff_pixel_size = {eff_pixel_size}')
944
1037
 
945
1038
  # Get cross sectional diameter
946
- cross_sectional_dim = img_shape[1]*eff_pixel_size
1039
+ cross_sectional_dim = \
1040
+ eff_pixel_size * nxentry.reduced_data.data.bright_field.shape[1]
947
1041
  self._logger.debug(f'cross_sectional_dim = {cross_sectional_dim}')
948
1042
 
949
- # Determine center offset at sample row boundaries
950
- self._logger.info('Determine center offset at sample row boundaries')
951
-
952
- # Lower row center
953
- if self._test_mode:
954
- lower_row = self._test_config['lower_row']
955
- elif self._interactive:
956
- if center_rows is not None and center_rows[0] is not None:
957
- lower_row = center_rows[0]
958
- if not 0 <= lower_row < num_row-1:
959
- raise ValueError(
960
- f'Invalid parameter center_rows ({center_rows})')
961
- else:
962
- lower_row = select_one_image_bound(
963
- nxentry.reduced_data.data.tomo_fields[
964
- center_stack_index,0,:,:],
965
- 0, bound=0,
966
- title=f'theta={round(thetas[0], 2)+0}',
967
- bound_name='row index to find lower center',
968
- default=default, raise_error=True)
969
- else:
970
- if center_rows is None or center_rows[0] is None:
971
- lower_row = 0
972
- else:
973
- lower_row = center_rows[0]
974
- if not 0 <= lower_row < num_row-1:
975
- raise ValueError(
976
- f'Invalid parameter center_rows ({center_rows})')
977
- t0 = time()
978
- lower_center_offset = self._find_center_one_plane(
979
- nxentry.reduced_data.data.tomo_fields[
980
- center_stack_index,:,lower_row,:],
981
- lower_row, thetas, eff_pixel_size, cross_sectional_dim,
982
- path=self._output_folder, num_core=self._num_core,
983
- search_range=tool_config.search_range,
984
- search_step=tool_config.search_step,
985
- gaussian_sigma=tool_config.gaussian_sigma,
986
- ring_width=tool_config.ring_width)
987
- self._logger.info(f'Finding center took {time()-t0:.2f} seconds')
988
- self._logger.debug(f'lower_row = {lower_row:.2f}')
989
- self._logger.debug(f'lower_center_offset = {lower_center_offset:.2f}')
990
-
991
- # Upper row center
992
- if self._test_mode:
993
- upper_row = self._test_config['upper_row']
994
- elif self._interactive:
995
- if center_rows is not None and center_rows[1] is not None:
996
- upper_row = center_rows[1]
997
- if not lower_row < upper_row < num_row:
998
- raise ValueError(
999
- f'Invalid parameter center_rows ({center_rows})')
1000
- else:
1001
- upper_row = select_one_image_bound(
1043
+ # Find the center offsets at each of the center rows
1044
+ prev_center_offset = None
1045
+ center_offsets = []
1046
+ for row, offset_row in zip(center_rows, offset_center_rows):
1047
+ t0 = time()
1048
+ center_offsets.append(
1049
+ self._find_center_one_plane(
1002
1050
  nxentry.reduced_data.data.tomo_fields[
1003
- center_stack_index,0,:,:],
1004
- 0, bound=num_row-1,
1005
- title=f'theta = {round(thetas[0], 2)+0}',
1006
- bound_name='row index to find upper center',
1007
- default=default, raise_error=True)
1008
- else:
1009
- if center_rows is None or center_rows[1] is None:
1010
- upper_row = num_row-1
1011
- else:
1012
- upper_row = center_rows[1]
1013
- if not lower_row < upper_row < num_row:
1014
- raise ValueError(
1015
- f'Invalid parameter center_rows ({center_rows})')
1016
- t0 = time()
1017
- upper_center_offset = self._find_center_one_plane(
1018
- nxentry.reduced_data.data.tomo_fields[
1019
- center_stack_index,:,upper_row,:],
1020
- upper_row, thetas, eff_pixel_size, cross_sectional_dim,
1021
- path=self._output_folder, num_core=self._num_core,
1022
- search_range=tool_config.search_range,
1023
- search_step=tool_config.search_step,
1024
- gaussian_sigma=tool_config.gaussian_sigma,
1025
- ring_width=tool_config.ring_width)
1026
- self._logger.info(f'Finding center took {time()-t0:.2f} seconds')
1027
- self._logger.debug(f'upper_row = {upper_row:.2f}')
1028
- self._logger.debug(f'upper_center_offset = {upper_center_offset:.2f}')
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,
1054
+ center_offset_min=tool_config.center_offset_min,
1055
+ center_offset_max=tool_config.center_offset_max,
1056
+ gaussian_sigma=tool_config.gaussian_sigma,
1057
+ ring_width=tool_config.ring_width,
1058
+ prev_center_offset=prev_center_offset))
1059
+ self._logger.info(
1060
+ f'Finding center row {row} took {time()-t0:.2f} seconds')
1061
+ self._logger.debug(f'center_row = {row:.2f}')
1062
+ self._logger.debug(f'center_offset = {center_offsets[-1]:.2f}')
1063
+ prev_center_offset = center_offsets[-1]
1029
1064
 
1030
1065
  center_config = {
1031
- 'lower_row': lower_row,
1032
- 'lower_center_offset': lower_center_offset,
1033
- 'upper_row': upper_row,
1034
- 'upper_center_offset': upper_center_offset,
1066
+ 'center_rows': list(center_rows),
1067
+ 'center_offsets': center_offsets,
1035
1068
  }
1036
1069
  if num_tomo_stacks > 1:
1037
1070
  center_config['center_stack_index'] = center_stack_index
1038
-
1039
- # Save test data to file
1040
- if self._test_mode:
1041
- with open(f'{self._output_folder}/center_config.yaml', 'w',
1042
- encoding='utf8') as f:
1043
- safe_dump(center_config, f)
1071
+ if tool_config.center_offset_min is not None:
1072
+ center_config['center_offset_min'] = tool_config.center_offset_min
1073
+ if tool_config.center_offset_max is not None:
1074
+ center_config['center_offset_max'] = tool_config.center_offset_max
1075
+ if tool_config.gaussian_sigma is not None:
1076
+ center_config['gaussian_sigma'] = tool_config.gaussian_sigma
1077
+ if tool_config.ring_width is not None:
1078
+ center_config['ring_width'] = tool_config.ring_width
1044
1079
 
1045
1080
  return center_config
1046
1081
 
@@ -1048,13 +1083,16 @@ class Tomo:
1048
1083
  """
1049
1084
  Reconstruct the tomography data.
1050
1085
 
1051
- :param nxroot: Reduced data
1086
+ :param nxroot: Data object containing the reduced data and
1087
+ metadata required for a tomography data reconstruction.
1052
1088
  :type data: nexusformat.nexus.NXroot
1053
- :param center_info: Calibrated center axis info
1089
+ :param center_info: Calibrated center axis info.
1054
1090
  :type center_info: dict
1055
- :param tool_config: Tool configuration
1091
+ :param tool_config: Tool configuration.
1056
1092
  :type tool_config: CHAP.tomo.models.TomoReconstructConfig
1057
- :return: Reconstructed tomography data
1093
+ :raises ValueError: Invalid or missing input or configuration
1094
+ parameter.
1095
+ :return: Reconstructed tomography data.
1058
1096
  :rtype: nexusformat.nexus.NXroot
1059
1097
  """
1060
1098
  # Third party modules
@@ -1066,9 +1104,6 @@ class Tomo:
1066
1104
  NXroot,
1067
1105
  )
1068
1106
 
1069
- # Local modules
1070
- from CHAP.utils.general import is_int_pair
1071
-
1072
1107
  self._logger.info('Reconstruct the tomography data')
1073
1108
 
1074
1109
  if isinstance(nxroot, NXroot):
@@ -1081,30 +1116,27 @@ class Tomo:
1081
1116
  # Check if reduced data is available
1082
1117
  if ('reduced_data' not in nxentry
1083
1118
  or 'reduced_data' not in nxentry.data):
1084
- raise KeyError(f'Unable to find valid reduced data in {nxentry}.')
1119
+ raise ValueError(f'Unable to find valid reduced data in {nxentry}.')
1085
1120
 
1086
1121
  # Create an NXprocess to store image reconstruction (meta)data
1087
1122
  nxprocess = NXprocess()
1088
1123
 
1089
- # Get rotation axis rows and centers
1090
- lower_row = center_info.get('lower_row')
1091
- lower_center_offset = center_info.get('lower_center_offset')
1092
- upper_row = center_info.get('upper_row')
1093
- upper_center_offset = center_info.get('upper_center_offset')
1094
- if (lower_row is None or lower_center_offset is None
1095
- or upper_row is None or upper_center_offset is None):
1124
+ # Get calibrated center axis rows and centers
1125
+ center_rows = center_info.get('center_rows')
1126
+ center_offsets = center_info.get('center_offsets')
1127
+ if center_rows is None or center_offsets is None:
1096
1128
  raise KeyError(
1097
1129
  'Unable to find valid calibrated center axis info in '
1098
1130
  f'{center_info}.')
1099
- center_slope = (upper_center_offset-lower_center_offset) \
1100
- / (upper_row-lower_row)
1131
+ center_slope = (center_offsets[1]-center_offsets[0]) \
1132
+ / (center_rows[1]-center_rows[0])
1101
1133
 
1102
1134
  # Get thetas (in degrees)
1103
1135
  thetas = np.asarray(nxentry.reduced_data.rotation_angle)
1104
1136
 
1105
1137
  # Reconstruct tomography data
1106
1138
  # reduced data axes order: stack,theta,row,column
1107
- # reconstructed data order in each stack: row/z,x,y
1139
+ # reconstructed data: row/-z,y,x
1108
1140
  # Note: Nexus can't follow a link if the data it points to is
1109
1141
  # too big get the data from the actual place, not from
1110
1142
  # nxentry.data
@@ -1115,6 +1147,9 @@ class Tomo:
1115
1147
  tomo_stacks = np.asarray(nxentry.reduced_data.data.tomo_fields)
1116
1148
  num_tomo_stacks = tomo_stacks.shape[0]
1117
1149
  tomo_recon_stacks = num_tomo_stacks*[np.array([])]
1150
+ img_row_bounds = tuple(nxentry.reduced_data.get(
1151
+ 'img_row_bounds', (0, tomo_stacks.shape[2])))
1152
+ center_rows -= img_row_bounds[0]
1118
1153
  for i in range(num_tomo_stacks):
1119
1154
  # Convert reduced data stack from theta,row,column to
1120
1155
  # row,theta,column
@@ -1130,11 +1165,11 @@ class Tomo:
1130
1165
  'reconstruction')
1131
1166
  tomo_stack = np.swapaxes(tomo_stack, 0, 1)
1132
1167
  assert len(thetas) == tomo_stack.shape[1]
1133
- assert 0 <= lower_row < upper_row < tomo_stack.shape[0]
1168
+ assert 0 <= center_rows[0] < center_rows[1] < tomo_stack.shape[0]
1134
1169
  center_offsets = [
1135
- lower_center_offset-lower_row*center_slope,
1136
- upper_center_offset + center_slope * (
1137
- tomo_stack.shape[0]-1-upper_row),
1170
+ center_offsets[0]-center_rows[0]*center_slope,
1171
+ center_offsets[1] + center_slope * (
1172
+ tomo_stack.shape[0]-1-center_rows[1]),
1138
1173
  ]
1139
1174
  t0 = time()
1140
1175
  tomo_recon_stack = self._reconstruct_one_tomo_stack(
@@ -1150,124 +1185,181 @@ class Tomo:
1150
1185
  tomo_recon_stacks[i] = tomo_recon_stack
1151
1186
 
1152
1187
  # Resize the reconstructed tomography data
1153
- # reconstructed data order in each stack: row/z,x,y
1154
- if self._test_mode:
1155
- x_bounds = tuple(self._test_config.get('x_bounds'))
1156
- y_bounds = tuple(self._test_config.get('y_bounds'))
1157
- z_bounds = None
1158
- elif self._interactive:
1159
- x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1160
- tomo_recon_stacks, x_bounds=tool_config.x_bounds,
1161
- y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
1162
- else:
1163
- x_bounds = tool_config.x_bounds
1164
- if x_bounds is None:
1165
- self._logger.warning(
1166
- 'x_bounds unspecified, reconstruct data for full x-range')
1167
- elif not is_int_pair(x_bounds, ge=0,
1168
- lt=tomo_recon_stacks[0].shape[1]):
1169
- raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
1170
- y_bounds = tool_config.y_bounds
1171
- if y_bounds is None:
1172
- self._logger.warning(
1173
- 'y_bounds unspecified, reconstruct data for full y-range')
1174
- elif not is_int_pair(
1175
- y_bounds, ge=0, lt=tomo_recon_stacks[0].shape[2]):
1176
- raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
1177
- z_bounds = None
1188
+ # reconstructed data order in each stack: row/-z,y,x
1189
+ tomo_recon_shape = tomo_recon_stacks[0].shape
1190
+ x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1191
+ tomo_recon_stacks, x_bounds=tool_config.x_bounds,
1192
+ y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
1178
1193
  if x_bounds is None:
1179
- x_range = (0, tomo_recon_stacks[0].shape[1])
1194
+ x_range = (0, tomo_recon_shape[2])
1180
1195
  x_slice = int(x_range[1]/2)
1181
1196
  else:
1182
1197
  x_range = (min(x_bounds), max(x_bounds))
1183
1198
  x_slice = int((x_bounds[0]+x_bounds[1]) / 2)
1184
1199
  if y_bounds is None:
1185
- y_range = (0, tomo_recon_stacks[0].shape[2])
1200
+ y_range = (0, tomo_recon_shape[1])
1186
1201
  y_slice = int(y_range[1] / 2)
1187
1202
  else:
1188
1203
  y_range = (min(y_bounds), max(y_bounds))
1189
1204
  y_slice = int((y_bounds[0]+y_bounds[1]) / 2)
1190
1205
  if z_bounds is None:
1191
- z_range = (0, tomo_recon_stacks[0].shape[0])
1206
+ z_range = (0, tomo_recon_shape[0])
1192
1207
  z_slice = int(z_range[1] / 2)
1193
1208
  else:
1194
1209
  z_range = (min(z_bounds), max(z_bounds))
1195
1210
  z_slice = int((z_bounds[0]+z_bounds[1]) / 2)
1211
+ z_dim_org = tomo_recon_shape[0]
1196
1212
  for i, stack in enumerate(tomo_recon_stacks):
1197
1213
  tomo_recon_stacks[i] = stack[
1198
- z_range[0]:z_range[1],x_range[0]:x_range[1],y_range[0]:y_range[1]]
1214
+ z_range[0]:z_range[1],y_range[0]:y_range[1],
1215
+ x_range[0]:x_range[1]]
1199
1216
  tomo_recon_stacks = np.asarray(tomo_recon_stacks)
1200
1217
 
1201
- # Plot a few reconstructed image slices
1202
- if self._save_figs:
1203
- for i in range(tomo_recon_stacks.shape[0]):
1204
- if num_tomo_stacks == 1:
1205
- basetitle = 'recon'
1206
- else:
1207
- basetitle = f'recon stack {i}'
1208
- title = f'{basetitle} {res_title} xslice{x_slice}'
1209
- row_pixel_size = nxentry.instrument.detector.row_pixel_size
1210
- column_pixel_size = \
1211
- nxentry.instrument.detector.column_pixel_size
1218
+ row_pixel_size = float(
1219
+ nxentry.instrument.detector.row_pixel_size)
1220
+ column_pixel_size = float(
1221
+ nxentry.instrument.detector.column_pixel_size)
1222
+ if num_tomo_stacks == 1:
1223
+ # 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
1230
+ tomo_recon_stack = np.flip(tomo_recon_stacks[0], 0)
1231
+ z_range = (z_dim_org-z_range[1], z_dim_org-z_range[0])
1232
+
1233
+ # Get coordinate axes
1234
+ x = column_pixel_size * (
1235
+ np.linspace(
1236
+ x_range[0], x_range[1], x_range[1]-x_range[0], False)
1237
+ - 0.5*nxentry.instrument.detector.columns
1238
+ + 0.5)
1239
+ x = np.asarray(x + nxentry.reduced_data.x_translation[0])
1240
+ y = np.asarray(
1241
+ column_pixel_size * (
1242
+ np.linspace(
1243
+ y_range[0], y_range[1], y_range[1]-y_range[0], False)
1244
+ - 0.5*nxentry.instrument.detector.columns
1245
+ + 0.5))
1246
+ z = row_pixel_size*(
1247
+ np.linspace(
1248
+ z_range[0], z_range[1], z_range[1]-z_range[0], False)
1249
+ + nxentry.instrument.detector.rows
1250
+ - int(nxentry.reduced_data.img_row_bounds[1])
1251
+ + 0.5)
1252
+ z = np.asarray(z + nxentry.reduced_data.z_translation[0])
1253
+
1254
+ # Plot a few reconstructed image slices
1255
+ if self._save_figs:
1256
+ x_index = x_slice-x_range[0]
1212
1257
  extent = (
1213
- 0,
1214
- float(column_pixel_size*tomo_recon_stacks.shape[3]),
1215
- float(row_pixel_size*tomo_recon_stacks.shape[1]),
1216
- 0)
1258
+ y[0],
1259
+ y[-1],
1260
+ z[0],
1261
+ z[-1])
1217
1262
  quick_imshow(
1218
- tomo_recon_stacks[i,:,x_slice-x_range[0],:],
1219
- title=title, path=self._output_folder, save_fig=True,
1220
- save_only=True, extent=extent)
1221
- title = f'{basetitle} {res_title} yslice{y_slice}'
1263
+ tomo_recon_stack[:,:,x_index],
1264
+ title=f'recon {res_title} x={x[x_index]:.4f}',
1265
+ origin='lower', extent=extent, path=self._output_folder,
1266
+ save_fig=True, save_only=True)
1267
+ y_index = y_slice-y_range[0]
1222
1268
  extent = (
1223
- 0,
1224
- float(column_pixel_size*tomo_recon_stacks.shape[2]),
1225
- float(row_pixel_size*tomo_recon_stacks.shape[1]),
1226
- 0)
1269
+ x[0],
1270
+ x[-1],
1271
+ z[0],
1272
+ z[-1])
1227
1273
  quick_imshow(
1228
- tomo_recon_stacks[i,:,:,y_slice-y_range[0]],
1229
- title=title, path=self._output_folder, save_fig=True,
1230
- save_only=True, extent=extent)
1231
- title = f'{basetitle} {res_title} zslice{z_slice}'
1274
+ tomo_recon_stack[:,y_index,:],
1275
+ title=f'recon {res_title} y={y[y_index]:.4f}',
1276
+ origin='lower', extent=extent, path=self._output_folder,
1277
+ save_fig=True, save_only=True)
1278
+ z_index = z_slice-z_range[0]
1232
1279
  extent = (
1233
- 0,
1234
- float(column_pixel_size*tomo_recon_stacks.shape[3]),
1235
- float(column_pixel_size*tomo_recon_stacks.shape[2]),
1236
- 0)
1280
+ x[0],
1281
+ x[-1],
1282
+ y[0],
1283
+ y[-1])
1237
1284
  quick_imshow(
1238
- tomo_recon_stacks[i,z_slice-z_range[0],:,:],
1239
- title=title, path=self._output_folder, save_fig=True,
1240
- save_only=True, extent=extent)
1241
-
1242
- # Save test data to file
1243
- # reconstructed data order in each stack: row/z,x,y
1244
- if self._test_mode:
1245
- for i in range(tomo_recon_stacks.shape[0]):
1246
- np.savetxt(
1247
- f'{self._output_folder}/recon_stack_{i}.txt',
1248
- tomo_recon_stacks[i,z_slice,:,:],
1249
- fmt='%.6e')
1285
+ tomo_recon_stack[z_index,:,:],
1286
+ title=f'recon {res_title} z={z[z_index]:.4f}',
1287
+ origin='lower', extent=extent, path=self._output_folder,
1288
+ save_fig=True, save_only=True)
1289
+ else:
1290
+ # Plot a few reconstructed image slices
1291
+ if self._save_figs:
1292
+ for i in range(tomo_recon_stacks.shape[0]):
1293
+ basetitle = f'recon stack {i}'
1294
+ title = f'{basetitle} {res_title} xslice{x_slice}'
1295
+ quick_imshow(
1296
+ tomo_recon_stacks[i,:,:,x_slice-x_range[0]],
1297
+ title=title, path=self._output_folder,
1298
+ save_fig=True, save_only=True)
1299
+ title = f'{basetitle} {res_title} yslice{y_slice}'
1300
+ quick_imshow(
1301
+ tomo_recon_stacks[i,:,y_slice-y_range[0],:],
1302
+ title=title, path=self._output_folder,
1303
+ save_fig=True, save_only=True)
1304
+ title = f'{basetitle} {res_title} zslice{z_slice}'
1305
+ quick_imshow(
1306
+ tomo_recon_stacks[i,z_slice-z_range[0],:,:],
1307
+ title=title, path=self._output_folder,
1308
+ save_fig=True, save_only=True)
1250
1309
 
1251
1310
  # Add image reconstruction to reconstructed data NXprocess
1252
- # reconstructed data order in each stack: row/z,x,y
1311
+ # reconstructed data order:
1312
+ # - for one stack: z,y,x
1313
+ # - for multiple stacks: row/-z,y,x
1253
1314
  nxprocess.data = NXdata()
1254
1315
  nxprocess.attrs['default'] = 'data'
1255
1316
  for k, v in center_info.items():
1256
1317
  nxprocess[k] = v
1318
+ if k == 'center_rows' or k == 'center_offsets':
1319
+ nxprocess[k].units = 'pixels'
1320
+ if k == 'center_rows':
1321
+ nxprocess[k].attrs['long_name'] = \
1322
+ 'center row indices in detector frame of reference'
1257
1323
  if x_bounds is not None:
1258
1324
  nxprocess.x_bounds = x_bounds
1325
+ nxprocess.x_bounds.units = 'pixels'
1326
+ nxprocess.x_bounds.attrs['long_name'] = \
1327
+ 'x range indices in reduced data frame of reference'
1259
1328
  if y_bounds is not None:
1260
1329
  nxprocess.y_bounds = y_bounds
1330
+ nxprocess.y_bounds.units = 'pixels'
1331
+ nxprocess.y_bounds.attrs['long_name'] = \
1332
+ 'y range indices in reduced data frame of reference'
1261
1333
  if z_bounds is not None:
1262
1334
  nxprocess.z_bounds = z_bounds
1263
- nxprocess.data.reconstructed_data = tomo_recon_stacks
1335
+ nxprocess.z_bounds.units = 'pixels'
1336
+ nxprocess.z_bounds.attrs['long_name'] = \
1337
+ 'z range indices in reduced data frame of reference'
1264
1338
  nxprocess.data.attrs['signal'] = 'reconstructed_data'
1339
+ 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
1354
+ else:
1355
+ nxprocess.data.reconstructed_data = tomo_recon_stacks
1265
1356
 
1266
1357
  # Create a copy of the input Nexus object and remove reduced
1267
1358
  # data
1268
1359
  exclude_items = [
1269
1360
  f'{nxentry.nxname}/reduced_data/data',
1270
1361
  f'{nxentry.nxname}/data/reduced_data',
1362
+ f'{nxentry.nxname}/data/rotation_angle',
1271
1363
  ]
1272
1364
  nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1273
1365
 
@@ -1280,17 +1372,28 @@ class Tomo:
1280
1372
  nxentry_copy.data.makelink(
1281
1373
  nxprocess.data.reconstructed_data, name='reconstructed_data')
1282
1374
  nxentry_copy.data.attrs['signal'] = 'reconstructed_data'
1375
+ 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')
1283
1383
 
1284
1384
  return nxroot_copy
1285
1385
 
1286
1386
  def combine_data(self, nxroot, tool_config):
1287
1387
  """Combine the reconstructed tomography stacks.
1288
1388
 
1289
- :param nxroot: A stack of reconstructed tomography datasets
1389
+ :param nxroot: Data object containing the reconstructed data
1390
+ and metadata required to combine the tomography stacks.
1290
1391
  :type data: nexusformat.nexus.NXroot
1291
- :param tool_config: Tool configuration
1392
+ :param tool_config: Tool configuration.
1292
1393
  :type tool_config: CHAP.tomo.models.TomoCombineConfig
1293
- :return: Combined reconstructed tomography data
1394
+ :raises ValueError: Invalid or missing input or configuration
1395
+ parameter.
1396
+ :return: Combined reconstructed tomography data.
1294
1397
  :rtype: nexusformat.nexus.NXroot
1295
1398
  """
1296
1399
  # Third party modules
@@ -1301,9 +1404,6 @@ class Tomo:
1301
1404
  NXroot,
1302
1405
  )
1303
1406
 
1304
- # Local modules
1305
- from CHAP.utils.general import is_int_pair
1306
-
1307
1407
  self._logger.info('Combine the reconstructed tomography stacks')
1308
1408
 
1309
1409
  if isinstance(nxroot, NXroot):
@@ -1321,137 +1421,187 @@ class Tomo:
1321
1421
  # (meta)data
1322
1422
  nxprocess = NXprocess()
1323
1423
 
1324
- # Get the reconstructed data
1325
- # reconstructed data order: stack,row(z),x,y
1326
- # Note: Nexus can't follow a link if the data it points to is
1327
- # too big. So get the data from the actual place, not from
1328
- # nxentry.data
1329
- num_tomo_stacks = \
1330
- nxentry.reconstructed_data.data.reconstructed_data.shape[0]
1424
+ if nxentry.reconstructed_data.data.reconstructed_data.ndim == 3:
1425
+ num_tomo_stacks = 1
1426
+ else:
1427
+ num_tomo_stacks = \
1428
+ nxentry.reconstructed_data.data.reconstructed_data.shape[0]
1331
1429
  if num_tomo_stacks == 1:
1332
1430
  self._logger.info('Only one stack available: leaving combine_data')
1333
- return None
1431
+ return nxroot
1334
1432
 
1335
- # Combine the reconstructed stacks
1433
+ # Get and combine the reconstructed stacks
1434
+ # reconstructed data order: stack,row/-z,y,x
1435
+ # 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
1336
1438
  # (load one stack at a time to reduce risk of hitting Nexus
1337
1439
  # data access limit)
1338
1440
  t0 = time()
1339
1441
  tomo_recon_combined = \
1340
1442
  nxentry.reconstructed_data.data.reconstructed_data[0,:,:,:]
1341
- if num_tomo_stacks > 2:
1342
- tomo_recon_combined = np.concatenate(
1343
- [tomo_recon_combined]
1344
- + [nxentry.reconstructed_data.data.reconstructed_data[i,:,:,:]
1345
- for i in range(1, num_tomo_stacks-1)])
1346
- if num_tomo_stacks > 1:
1347
- tomo_recon_combined = np.concatenate(
1348
- [tomo_recon_combined]
1349
- + [nxentry.reconstructed_data.data.reconstructed_data[
1350
- num_tomo_stacks-1,:,:,:]])
1443
+ 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)])
1351
1447
  self._logger.info(
1352
1448
  f'Combining the reconstructed stacks took {time()-t0:.2f} seconds')
1449
+ tomo_shape = tomo_recon_combined.shape
1353
1450
 
1354
1451
  # Resize the combined tomography data stacks
1355
- # combined data order: row/z,x,y
1356
- if self._test_mode:
1357
- x_bounds = None
1358
- y_bounds = None
1359
- z_bounds = tuple(self._test_config.get('z_bounds'))
1360
- elif self._interactive:
1452
+ # combined data order: row/-z,y,x
1453
+ if self._interactive:
1361
1454
  x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1362
- tomo_recon_combined, z_only=True)
1455
+ tomo_recon_combined, combine_data=True)
1363
1456
  else:
1364
1457
  x_bounds = tool_config.x_bounds
1365
1458
  if x_bounds is None:
1366
1459
  self._logger.warning(
1367
1460
  'x_bounds unspecified, reconstruct data for full x-range')
1368
1461
  elif not is_int_pair(
1369
- x_bounds, ge=0, lt=tomo_recon_combined.shape[1]):
1462
+ x_bounds, ge=0, le=tomo_shape[2]):
1370
1463
  raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
1371
1464
  y_bounds = tool_config.y_bounds
1372
1465
  if y_bounds is None:
1373
1466
  self._logger.warning(
1374
1467
  'y_bounds unspecified, reconstruct data for full y-range')
1375
1468
  elif not is_int_pair(
1376
- y_bounds, ge=0, lt=tomo_recon_combined.shape[2]):
1469
+ y_bounds, ge=0, le=tomo_shape[1]):
1377
1470
  raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
1378
- z_bounds = None
1471
+ z_bounds = tool_config.z_bounds
1472
+ if z_bounds is None:
1473
+ self._logger.warning(
1474
+ 'z_bounds unspecified, reconstruct data for full z-range')
1475
+ elif not is_int_pair(
1476
+ z_bounds, ge=0, le=tomo_shape[0]):
1477
+ raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
1379
1478
  if x_bounds is None:
1380
- x_range = (0, tomo_recon_combined.shape[1])
1479
+ x_range = (0, tomo_shape[2])
1381
1480
  x_slice = int(x_range[1]/2)
1382
1481
  else:
1383
- x_range = x_bounds
1482
+ x_range = (min(x_bounds), max(x_bounds))
1384
1483
  x_slice = int((x_bounds[0]+x_bounds[1]) / 2)
1385
1484
  if y_bounds is None:
1386
- y_range = (0, tomo_recon_combined.shape[2])
1485
+ y_range = (0, tomo_shape[1])
1387
1486
  y_slice = int(y_range[1]/2)
1388
1487
  else:
1389
- y_range = y_bounds
1488
+ y_range = (min(y_bounds), max(y_bounds))
1390
1489
  y_slice = int((y_bounds[0]+y_bounds[1]) / 2)
1391
1490
  if z_bounds is None:
1392
- z_range = (0, tomo_recon_combined.shape[0])
1491
+ z_range = (0, tomo_shape[0])
1393
1492
  z_slice = int(z_range[1]/2)
1394
1493
  else:
1395
- z_range = z_bounds
1494
+ z_range = (min(z_bounds), max(z_bounds))
1396
1495
  z_slice = int((z_bounds[0]+z_bounds[1]) / 2)
1496
+ z_dim_org = tomo_shape[0]
1497
+ tomo_recon_combined = tomo_recon_combined[
1498
+ z_range[0]:z_range[1],y_range[0]:y_range[1],x_range[0]:x_range[1]]
1499
+
1500
+ # 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
1505
+ tomo_recon_combined = np.flip(tomo_recon_combined, 0)
1506
+ tomo_shape = tomo_recon_combined.shape
1507
+ z_range = (z_dim_org-z_range[1], z_dim_org-z_range[0])
1508
+
1509
+ # 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)
1514
+ x = column_pixel_size * (
1515
+ 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)
1518
+ if nxentry.reconstructed_data.get('x_bounds', None) is not None:
1519
+ x += column_pixel_size*nxentry.reconstructed_data.x_bounds[0]
1520
+ x = np.asarray(x + nxentry.reduced_data.x_translation[0])
1521
+ y = column_pixel_size * (
1522
+ 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)
1525
+ if nxentry.reconstructed_data.get('y_bounds', None) is not None:
1526
+ y += column_pixel_size*nxentry.reconstructed_data.y_bounds[0]
1527
+ y = np.asarray(y)
1528
+ z = row_pixel_size*(
1529
+ np.linspace(z_range[0], z_range[1], z_range[1]-z_range[0], False)
1530
+ - int(nxentry.reduced_data.img_row_bounds[0])
1531
+ + 0.5*(nxentry.instrument.detector.rows)
1532
+ -0.5)
1533
+ z = np.asarray(z + nxentry.reduced_data.z_translation[0])
1397
1534
 
1398
1535
  # Plot a few combined image slices
1399
1536
  if self._save_figs:
1400
- row_pixel_size = nxentry.instrument.detector.row_pixel_size
1401
- column_pixel_size = nxentry.instrument.detector.column_pixel_size
1402
1537
  extent = (
1403
- 0,
1404
- float(column_pixel_size*tomo_recon_combined.shape[2]),
1405
- float(row_pixel_size*tomo_recon_combined.shape[0]),
1406
- 0)
1538
+ y[0],
1539
+ y[-1],
1540
+ z[0],
1541
+ z[-1])
1542
+ x_slice = int(tomo_shape[2]/2)
1407
1543
  quick_imshow(
1408
- tomo_recon_combined[
1409
- z_range[0]:z_range[1],x_slice,y_range[0]:y_range[1]],
1410
- title=f'recon combined xslice{x_slice}', extent=extent,
1411
- path=self._output_folder, save_fig=True, save_only=True)
1544
+ tomo_recon_combined[:,:,x_slice],
1545
+ title=f'recon combined x={x[x_slice]:.4f}', origin='lower',
1546
+ extent=extent, path=self._output_folder, save_fig=True,
1547
+ save_only=True)
1412
1548
  extent = (
1413
- 0,
1414
- float(column_pixel_size*tomo_recon_combined.shape[1]),
1415
- float(row_pixel_size*tomo_recon_combined.shape[0]),
1416
- 0)
1549
+ x[0],
1550
+ x[-1],
1551
+ z[0],
1552
+ z[-1])
1553
+ y_slice = int(tomo_shape[1]/2)
1417
1554
  quick_imshow(
1418
- tomo_recon_combined[
1419
- z_range[0]:z_range[1],x_range[0]:x_range[1],y_slice],
1420
- title=f'recon combined yslice{y_slice}', extent=extent,
1421
- path=self._output_folder, save_fig=True, save_only=True)
1555
+ tomo_recon_combined[:,y_slice,:],
1556
+ title=f'recon combined y={y[y_slice]:.4f}', origin='lower',
1557
+ extent=extent, path=self._output_folder, save_fig=True,
1558
+ save_only=True)
1422
1559
  extent = (
1423
- 0,
1424
- float(column_pixel_size*tomo_recon_combined.shape[2]),
1425
- float(column_pixel_size*tomo_recon_combined.shape[1]),
1426
- 0)
1560
+ x[0],
1561
+ x[-1],
1562
+ y[0],
1563
+ y[-1])
1564
+ z_slice = int(tomo_shape[0]/2)
1427
1565
  quick_imshow(
1428
- tomo_recon_combined[
1429
- z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]],
1430
- title=f'recon combined zslice{z_slice}', extent=extent,
1431
- path=self._output_folder, save_fig=True, save_only=True)
1432
-
1433
- # Save test data to file
1434
- # combined data order: row/z,x,y
1435
- if self._test_mode:
1436
- np.savetxt(
1437
- f'{self._output_folder}/recon_combined.txt',
1438
- tomo_recon_combined[
1439
- z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]],
1440
- fmt='%.6e')
1566
+ tomo_recon_combined[z_slice,:,:],
1567
+ title=f'recon combined z={z[z_slice]:.4f}', origin='lower',
1568
+ extent=extent, path=self._output_folder, save_fig=True,
1569
+ save_only=True)
1441
1570
 
1442
1571
  # Add image reconstruction to reconstructed data NXprocess
1443
- # combined data order: row/z,x,y
1572
+ # combined data order: z,y,x
1444
1573
  nxprocess.data = NXdata()
1445
1574
  nxprocess.attrs['default'] = 'data'
1446
- if x_bounds is not None:
1575
+ if x_bounds is not None and x_bounds != (0, tomo_shape[2]):
1447
1576
  nxprocess.x_bounds = x_bounds
1448
- if y_bounds is not None:
1577
+ nxprocess.x_bounds.units = 'pixels'
1578
+ nxprocess.x_bounds.attrs['long_name'] = \
1579
+ 'x range indices in reconstructed data frame of reference'
1580
+ if y_bounds is not None and y_bounds != (0, tomo_shape[1]):
1449
1581
  nxprocess.y_bounds = y_bounds
1450
- if z_bounds is not None:
1582
+ nxprocess.y_bounds.units = 'pixels'
1583
+ nxprocess.y_bounds.attrs['long_name'] = \
1584
+ 'y range indices in reconstructed data frame of reference'
1585
+ if z_bounds is not None and z_bounds != (0, tomo_shape[0]):
1451
1586
  nxprocess.z_bounds = z_bounds
1452
- nxprocess.data.combined_data = tomo_recon_combined[
1453
- z_range[0]:z_range[1],x_range[0]:x_range[1],y_range[0]:y_range[1]]
1587
+ nxprocess.z_bounds.units = 'pixels'
1588
+ nxprocess.z_bounds.attrs['long_name'] = \
1589
+ 'z range indices in reconstructed data frame of reference'
1590
+ nxprocess.data.combined_data = tomo_recon_combined
1454
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
1455
1605
 
1456
1606
  # Create a copy of the input Nexus object and remove
1457
1607
  # reconstructed data
@@ -1470,16 +1620,22 @@ class Tomo:
1470
1620
  nxentry_copy.data.makelink(
1471
1621
  nxprocess.data.combined_data, name='combined_data')
1472
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')
1473
1630
 
1474
1631
  return nxroot_copy
1475
1632
 
1476
- def _gen_dark(self, nxentry, reduced_data):
1633
+ def _gen_dark(self, nxentry, reduced_data, image_key):
1477
1634
  """Generate dark field."""
1478
1635
  # Third party modules
1479
1636
  from nexusformat.nexus import NXdata
1480
1637
 
1481
1638
  # Get the dark field images
1482
- image_key = nxentry.instrument.detector.get('image_key', None)
1483
1639
  field_indices = [
1484
1640
  index for index, key in enumerate(image_key) if key == 2]
1485
1641
  if field_indices:
@@ -1518,16 +1674,9 @@ class Tomo:
1518
1674
 
1519
1675
  # Plot dark field
1520
1676
  if self._save_figs:
1521
- extent = (
1522
- 0,
1523
- float(nxentry.instrument.detector.column_pixel_size
1524
- * tdf.shape[1]),
1525
- float(nxentry.instrument.detector.row_pixel_size
1526
- * tdf.shape[0]),
1527
- 0)
1528
1677
  quick_imshow(
1529
- tdf, title='dark field', path=self._output_folder,
1530
- extent=extent, save_fig=True, save_only=True)
1678
+ tdf, title='Dark field', name='dark_field',
1679
+ path=self._output_folder, save_fig=True, save_only=True)
1531
1680
 
1532
1681
  # Add dark field to reduced data NXprocess
1533
1682
  reduced_data.data = NXdata()
@@ -1535,13 +1684,12 @@ class Tomo:
1535
1684
 
1536
1685
  return reduced_data
1537
1686
 
1538
- def _gen_bright(self, nxentry, reduced_data):
1687
+ def _gen_bright(self, nxentry, reduced_data, image_key):
1539
1688
  """Generate bright field."""
1540
1689
  # Third party modules
1541
1690
  from nexusformat.nexus import NXdata
1542
1691
 
1543
1692
  # Get the bright field images
1544
- image_key = nxentry.instrument.detector.get('image_key', None)
1545
1693
  field_indices = [
1546
1694
  index for index, key in enumerate(image_key) if key == 1]
1547
1695
  if field_indices:
@@ -1571,28 +1719,15 @@ class Tomo:
1571
1719
  else:
1572
1720
  raise RuntimeError(f'Invalid tbf_stack shape ({tbf_stack.shape})')
1573
1721
 
1574
- # Subtract dark field
1575
- if 'data' in reduced_data and 'dark_field' in reduced_data.data:
1576
- tbf -= reduced_data.data.dark_field
1577
- else:
1578
- self._logger.warning('Dark field unavailable')
1579
-
1580
1722
  # Set any non-positive values to one
1581
1723
  # (avoid negative bright field values for spikes in dark field)
1582
1724
  tbf[tbf < 1] = 1
1583
1725
 
1584
1726
  # Plot bright field
1585
1727
  if self._save_figs:
1586
- extent = (
1587
- 0,
1588
- float(nxentry.instrument.detector.column_pixel_size
1589
- * tbf.shape[1]),
1590
- float(nxentry.instrument.detector.row_pixel_size
1591
- * tbf.shape[0]),
1592
- 0)
1593
1728
  quick_imshow(
1594
- tbf, title='bright field', path=self._output_folder,
1595
- extent=extent, save_fig=True, save_only=True)
1729
+ tbf, title='Bright field', name='bright_field',
1730
+ path=self._output_folder, save_fig=True, save_only=True)
1596
1731
 
1597
1732
  # Add bright field to reduced data NXprocess
1598
1733
  if 'data' not in reduced_data:
@@ -1601,32 +1736,32 @@ class Tomo:
1601
1736
 
1602
1737
  return reduced_data
1603
1738
 
1604
- def _set_detector_bounds(self, nxentry, reduced_data, theta,
1605
- img_row_bounds=None):
1739
+ def _set_detector_bounds(
1740
+ self, nxentry, reduced_data, image_key, theta, img_row_bounds,
1741
+ calibrate_center_rows):
1606
1742
  """
1607
1743
  Set vertical detector bounds for each image stack.Right now the
1608
1744
  range is the same for each set in the image stack.
1609
1745
  """
1746
+ # Third party modules
1747
+ import matplotlib.pyplot as plt
1748
+
1610
1749
  # Local modules
1611
1750
  from CHAP.utils.general import is_index_range
1612
1751
 
1613
- if self._test_mode:
1614
- return tuple(self._test_config['img_row_bounds'])
1615
-
1616
1752
  # Get the first tomography image and the reference heights
1617
1753
  image_mask = reduced_data.get('image_mask')
1618
1754
  if image_mask is None:
1619
1755
  first_image_index = 0
1620
1756
  else:
1621
- raise RuntimeError('image_mask not tested yet')
1622
1757
  image_mask = np.asarray(image_mask)
1623
1758
  first_image_index = int(np.argmax(image_mask))
1624
- image_key = nxentry.instrument.detector.get('image_key', None)
1625
1759
  field_indices_all = [
1626
1760
  index for index, key in enumerate(image_key) if key == 0]
1627
1761
  if not field_indices_all:
1628
1762
  raise ValueError('Tomography field(s) unavailable')
1629
- z_translation_all = nxentry.sample.z_translation[field_indices_all]
1763
+ z_translation_all = np.asarray(
1764
+ nxentry.sample.z_translation)[field_indices_all]
1630
1765
  z_translation_levels = sorted(list(set(z_translation_all)))
1631
1766
  num_tomo_stacks = len(z_translation_levels)
1632
1767
  center_stack_index = int(num_tomo_stacks/2)
@@ -1642,173 +1777,129 @@ class Tomo:
1642
1777
  raise RuntimeError('Unable to load the tomography images '
1643
1778
  f'for stack {i}')
1644
1779
 
1645
- # Select image bounds
1646
- title = f'tomography image at theta={round(theta, 2)+0}'
1647
- if img_row_bounds is not None:
1648
- if is_index_range(img_row_bounds, ge=0, le=first_image.shape[0]):
1649
- return img_row_bounds
1650
- if self._interactive:
1651
- self._logger.warning(
1652
- f'Invalid parameter img_row_bounds ({img_row_bounds}), '
1653
- 'ignoring img_row_bounds')
1654
- img_row_bounds = None
1655
- else:
1656
- raise ValueError(
1657
- f'Invalid parameter img_row_bounds ({img_row_bounds})')
1658
- if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1659
- pixel_size = nxentry.instrument.detector.row_pixel_size
1660
- # Try to get a fit from the bright field
1661
- tbf = np.asarray(reduced_data.data.bright_field)
1662
- tbf_shape = tbf.shape
1663
- row_sum = np.sum(tbf, 1)
1664
- row_sum_min = row_sum.min()
1665
- row_sum_max = row_sum.max()
1666
- fit = Fit.fit_data(
1667
- row_sum, 'rectangle', x=np.array(range(len(row_sum))),
1668
- form='atan', guess=True)
1669
- parameters = fit.best_values
1670
- row_low_fit = parameters.get('center1', None)
1671
- row_upp_fit = parameters.get('center2', None)
1672
- sig_low = parameters.get('sigma1', None)
1673
- sig_upp = parameters.get('sigma2', None)
1674
- have_fit = (fit.success and row_low_fit is not None
1675
- and row_upp_fit is not None and sig_low is not None
1676
- and sig_upp is not None
1677
- and 0 <= row_low_fit < row_upp_fit <= row_sum.size
1678
- and (sig_low+sig_upp) / (row_upp_fit-row_low_fit) < 0.1)
1679
- if have_fit:
1680
- # Set a 5% margin on each side
1681
- margin = 0.05 * (row_upp_fit-row_low_fit)
1682
- row_low_fit = max(0, row_low_fit-margin)
1683
- row_upp_fit = min(tbf_shape[0], row_upp_fit+margin)
1684
- if num_tomo_stacks == 1:
1685
- if have_fit:
1686
- # Set the default range to enclose the full fitted
1687
- # window
1688
- row_low = int(row_low_fit)
1689
- row_upp = int(row_upp_fit)
1780
+ # Set initial image bounds or rotation calibration rows
1781
+ tbf = np.asarray(reduced_data.data.bright_field)
1782
+ if (not isinstance(calibrate_center_rows, bool)
1783
+ and is_int_pair(calibrate_center_rows)):
1784
+ img_row_bounds = calibrate_center_rows
1785
+ else:
1786
+ if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1787
+ pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1788
+ # Try to get a fit from the bright field
1789
+ 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
1794
+ row_low_fit = parameters.get('center1', None)
1795
+ row_upp_fit = parameters.get('center2', None)
1796
+ sig_low = parameters.get('sigma1', None)
1797
+ sig_upp = parameters.get('sigma2', None)
1798
+ have_fit = (fit.success and row_low_fit is not None
1799
+ and row_upp_fit is not None and sig_low is not None
1800
+ and sig_upp is not None
1801
+ and 0 <= row_low_fit < row_upp_fit <= row_sum.size
1802
+ and (sig_low+sig_upp) / (row_upp_fit-row_low_fit) < 0.1)
1803
+ if num_tomo_stacks == 1:
1804
+ if have_fit:
1805
+ # Add a pixel margin for roundoff effects in fit
1806
+ row_low_fit += 1
1807
+ row_upp_fit -= 1
1808
+ delta_z = (row_upp_fit-row_low_fit) * pixel_size
1809
+ else:
1810
+ # Set a default range of 1 mm
1811
+ # RV can we get this from the slits?
1812
+ delta_z = 1.0
1690
1813
  else:
1691
- # Center a default range of 1 mm
1692
- # RV can we get this from the slits?
1693
- num_row_min = int((1. + 0.5*pixel_size) / pixel_size)
1694
- row_low = int((tbf_shape[0]-num_row_min) / 2)
1695
- row_upp = row_low+num_row_min
1696
- else:
1697
- # Get the default range from the reference heights
1698
- delta_z = z_translation_levels[1]-z_translation_levels[0]
1699
- for i in range(2, num_tomo_stacks):
1700
- delta_z = min(
1701
- delta_z,
1702
- z_translation_levels[i]-z_translation_levels[i-1])
1814
+ # Get the default range from the reference heights
1815
+ delta_z = z_translation_levels[1]-z_translation_levels[0]
1816
+ for i in range(2, num_tomo_stacks):
1817
+ delta_z = min(
1818
+ delta_z,
1819
+ z_translation_levels[i]-z_translation_levels[i-1])
1703
1820
  self._logger.debug(f'delta_z = {delta_z}')
1704
1821
  num_row_min = int((delta_z + 0.5*pixel_size) / pixel_size)
1705
- self._logger.debug(f'num_row_min = {num_row_min}')
1706
- if num_row_min > tbf_shape[0]:
1822
+ if num_row_min > tbf.shape[0]:
1707
1823
  self._logger.warning(
1708
1824
  'Image bounds and pixel size prevent seamless '
1709
1825
  'stacking')
1710
- if have_fit:
1711
- # Center the default range relative to the fitted
1712
- # window
1713
- row_low = int((row_low_fit+row_upp_fit-num_row_min) / 2)
1714
- row_upp = row_low+num_row_min
1826
+ row_low = 0
1827
+ row_upp = tbf.shape[0]
1715
1828
  else:
1716
- # Center the default range
1717
- row_low = int((tbf_shape[0]-num_row_min) / 2)
1718
- row_upp = row_low+num_row_min
1719
- if not self._interactive:
1829
+ self._logger.debug(f'num_row_min = {num_row_min}')
1830
+ if have_fit:
1831
+ # Center the default range relative to the fitted
1832
+ # window
1833
+ row_low = int((row_low_fit+row_upp_fit-num_row_min) / 2)
1834
+ row_upp = row_low+num_row_min
1835
+ else:
1836
+ # Center the default range
1837
+ row_low = int((tbf.shape[0]-num_row_min) / 2)
1838
+ row_upp = row_low+num_row_min
1720
1839
  img_row_bounds = (row_low, row_upp)
1840
+ if calibrate_center_rows:
1841
+ # Add a small margin to avoid edge effects
1842
+ offset = int(min(5, 0.1*(row_upp-row_low)))
1843
+ img_row_bounds = (row_low+offset, row_upp-1-offset)
1721
1844
  else:
1722
- tmp = np.copy(tbf)
1723
- tmp_max = tmp.max()
1724
- tmp[row_low,:] = tmp_max
1725
- tmp[row_upp-1,:] = tmp_max
1726
- quick_imshow(tmp, title='bright field')
1727
- tmp = np.copy(first_image)
1728
- tmp_max = tmp.max()
1729
- tmp[row_low,:] = tmp_max
1730
- tmp[row_upp-1,:] = tmp_max
1731
- quick_imshow(tmp, title=title)
1732
- del tmp
1733
- quick_plot(
1734
- (range(row_sum.size), row_sum),
1735
- ([row_low, row_low], [row_sum_min, row_sum_max], 'r-'),
1736
- ([row_upp, row_upp], [row_sum_min, row_sum_max], 'r-'),
1737
- title='sum over theta and y')
1738
- print(f'lower bound = {row_low} (inclusive)')
1739
- print(f'upper bound = {row_upp} (exclusive)]')
1740
- accept = input_yesno('Accept these bounds (y/n)?', 'y')
1741
- clear_imshow('bright field')
1742
- clear_imshow(title)
1743
- clear_plot('sum over theta and y')
1744
- if accept:
1745
- img_row_bounds = (row_low, row_upp)
1746
- else:
1747
- while True:
1748
- _, img_row_bounds = draw_mask_1d(
1749
- row_sum, title='select x data range',
1750
- ylabel='sum over theta and y')
1751
- if len(img_row_bounds) == 1:
1752
- break
1753
- print('Choose a single connected data range')
1754
- img_row_bounds = tuple(img_row_bounds[0])
1755
- if (num_tomo_stacks > 1
1756
- and (img_row_bounds[1]-img_row_bounds[0]+1)
1757
- < int((delta_z - 0.5*pixel_size) / pixel_size)):
1758
- self._logger.warning(
1759
- 'Image bounds and pixel size prevent seamless stacking')
1845
+ if num_tomo_stacks > 1:
1846
+ raise NotImplementedError(
1847
+ 'Selecting image bounds or calibrating rotation axis '
1848
+ 'for multiple stacks on FMB')
1849
+ # For FMB: use the first tomography image to select range
1850
+ # RV revisit if they do tomography with multiple stacks
1851
+ if img_row_bounds is None and not self._interactive:
1852
+ if calibrate_center_rows:
1853
+ self._logger.warning(
1854
+ 'calibrate_center_rows unspecified, find rotation '
1855
+ 'axis at detector bounds (with a small margin)')
1856
+ # Add a small margin to avoid edge effects
1857
+ offset = min(5, 0.1*first_image.shape[0])
1858
+ img_row_bounds = (
1859
+ offset, first_image.shape[0]-1-offset)
1860
+ else:
1861
+ self._logger.warning(
1862
+ 'img_row_bounds unspecified, reduce data for '
1863
+ 'entire detector range')
1864
+ img_row_bounds = (0, first_image.shape[0])
1865
+ if calibrate_center_rows:
1866
+ title='Select two detector image row indices to '\
1867
+ 'calibrate rotation axis (in range '\
1868
+ f'[0, {first_image.shape[0]-1}])'
1760
1869
  else:
1761
- if num_tomo_stacks > 1:
1762
- raise NotImplementedError(
1763
- 'Selecting image bounds for multiple stacks on FMB')
1764
- # For FMB: use the first tomography image to select range
1765
- # RV revisit if they do tomography with multiple stacks
1766
- row_sum = np.sum(first_image, 1)
1767
- row_sum_min = row_sum.min()
1768
- row_sum_max = row_sum.max()
1769
- if self._interactive:
1770
- print(
1771
- 'Select vertical data reduction range from first '
1772
- 'tomography image')
1773
- img_row_bounds = select_image_bounds(
1774
- first_image, 0, title=title)
1775
- if img_row_bounds is None:
1776
- raise RuntimeError('Unable to select image bounds')
1777
- else:
1778
- if img_row_bounds is None:
1779
- self._logger.warning(
1780
- 'img_row_bounds unspecified, reduce data for entire '
1781
- 'detector range')
1782
- img_row_bounds = (0, first_image.shape[0])
1870
+ title='Select detector image row bounds for data '\
1871
+ f'reduction (in range [0, {first_image.shape[0]}])'
1872
+ fig, img_row_bounds = select_image_indices(
1873
+ first_image, 0, b=tbf, preselected_indices=img_row_bounds,
1874
+ title=title,
1875
+ title_a=r'Tomography image at $\theta$ = 'f'{round(theta, 2)+0}',
1876
+ title_b='Bright field',
1877
+ interactive=self._interactive)
1878
+ if not calibrate_center_rows and (num_tomo_stacks > 1
1879
+ and (img_row_bounds[1]-img_row_bounds[0]+1)
1880
+ < int((delta_z - 0.5*pixel_size) / pixel_size)):
1881
+ self._logger.warning(
1882
+ 'Image bounds and pixel size prevent seamless stacking')
1783
1883
 
1784
1884
  # Plot results
1785
1885
  if self._save_figs:
1786
- row_low = img_row_bounds[0]
1787
- row_upp = img_row_bounds[1]
1788
- tmp = np.copy(first_image)
1789
- tmp_max = tmp.max()
1790
- tmp[row_low,:] = tmp_max
1791
- tmp[row_upp-1,:] = tmp_max
1792
- quick_imshow(
1793
- tmp, title=title, path=self._output_folder, save_fig=True,
1794
- save_only=True)
1795
- quick_plot(
1796
- (range(row_sum.size), row_sum),
1797
- ([row_low, row_low], [row_sum_min, row_sum_max], 'r-'),
1798
- ([row_upp, row_upp], [row_sum_min, row_sum_max], 'r-'),
1799
- title='sum over theta and y', path=self._output_folder,
1800
- save_fig=True, save_only=True)
1801
- del tmp
1886
+ if calibrate_center_rows:
1887
+ fig.savefig(os_path.join(
1888
+ self._output_folder, 'rotation_calibration_rows.png'))
1889
+ else:
1890
+ fig.savefig(os_path.join(
1891
+ self._output_folder, 'detector_image_bounds.png'))
1892
+ plt.close()
1802
1893
 
1803
1894
  return img_row_bounds
1804
1895
 
1805
- def _gen_thetas(self, nxentry):
1896
+ def _gen_thetas(self, nxentry, image_key):
1806
1897
  """Get the rotation angles for the image stacks."""
1807
1898
  # Get the rotation angles
1808
- image_key = nxentry.instrument.detector.get('image_key', None)
1809
1899
  field_indices_all = [
1810
1900
  index for index, key in enumerate(image_key) if key == 0]
1811
- z_translation_all = nxentry.sample.z_translation[field_indices_all]
1901
+ z_translation_all = np.asarray(
1902
+ nxentry.sample.z_translation)[field_indices_all]
1812
1903
  z_translation_levels = sorted(list(set(z_translation_all)))
1813
1904
  thetas = None
1814
1905
  for i, z_translation in enumerate(z_translation_levels):
@@ -1816,18 +1907,19 @@ class Tomo:
1816
1907
  field_indices_all[index]
1817
1908
  for index, z in enumerate(z_translation_all)
1818
1909
  if z == z_translation]
1819
- sequence_numbers = \
1820
- nxentry.instrument.detector.sequence_number[field_indices]
1910
+ sequence_numbers = np.asarray(
1911
+ nxentry.instrument.detector.sequence_number)[field_indices]
1821
1912
  assert (list(sequence_numbers)
1822
1913
  == list(range((len(sequence_numbers)))))
1823
1914
  if thetas is None:
1824
1915
  thetas = np.asarray(
1825
- nxentry.sample.rotation_angle[
1826
- field_indices])[sequence_numbers]
1916
+ nxentry.sample.rotation_angle)[
1917
+ field_indices][sequence_numbers]
1827
1918
  else:
1828
1919
  assert all(
1829
- thetas[i] == nxentry.sample.rotation_angle[
1830
- field_indices[index]]
1920
+ thetas[i] == np.asarray(
1921
+ nxentry.sample.rotation_angle)[
1922
+ field_indices[index]]
1831
1923
  for i, index in enumerate(sequence_numbers))
1832
1924
 
1833
1925
  return thetas
@@ -1840,9 +1932,6 @@ class Tomo:
1840
1932
  # Local modules
1841
1933
  from CHAP.utils.general import index_nearest
1842
1934
 
1843
- if self._test_mode:
1844
- return tuple(self._test_config['delta_theta'])
1845
-
1846
1935
  # if input_yesno(
1847
1936
  # '\nDo you want to zoom in to reduce memory '
1848
1937
  # 'requirement (y/n)?', 'n'):
@@ -1860,13 +1949,13 @@ class Tomo:
1860
1949
  if self._interactive:
1861
1950
  if delta_theta is None:
1862
1951
  delta_theta = thetas[1]-thetas[0]
1863
- print(f'Available theta range: [{thetas[0]}, {thetas[-1]}]')
1864
- print(f'Current theta interval: {delta_theta}')
1952
+ print(f'Available \u03b8 range: [{thetas[0]}, {thetas[-1]}]')
1953
+ print(f'Current \u03b8 interval: {delta_theta}')
1865
1954
  if input_yesno(
1866
- 'Do you want to change the theta interval to reduce the '
1955
+ 'Do you want to change the \u03b8 interval to reduce the '
1867
1956
  'memory requirement (y/n)?', 'n'):
1868
1957
  delta_theta = input_num(
1869
- ' Enter the desired theta interval',
1958
+ ' Enter the desired \u03b8 interval',
1870
1959
  ge=thetas[1]-thetas[0], lt=(thetas[-1]-thetas[0])/2)
1871
1960
  if delta_theta is not None:
1872
1961
  delta_theta = index_nearest(thetas, thetas[0]+delta_theta)
@@ -1875,38 +1964,58 @@ class Tomo:
1875
1964
 
1876
1965
  return zoom_perc, delta_theta
1877
1966
 
1878
- def _gen_tomo(self, nxentry, reduced_data):
1967
+ def _gen_tomo(
1968
+ self, nxentry, reduced_data, image_key, calibrate_center_rows):
1879
1969
  """Generate tomography fields."""
1880
1970
  # Third party modules
1881
1971
  from numexpr import evaluate
1882
1972
  from scipy.ndimage import zoom
1883
1973
 
1884
- # Get full bright field
1974
+ # Get dark field
1975
+ if 'dark_field' in reduced_data.data:
1976
+ tdf = np.asarray(reduced_data.data.dark_field)
1977
+ else:
1978
+ self._logger.warning('Dark field unavailable')
1979
+ tdf = None
1980
+
1981
+ # Get bright field
1885
1982
  tbf = np.asarray(reduced_data.data.bright_field)
1886
1983
  tbf_shape = tbf.shape
1887
1984
 
1985
+ # Subtract dark field
1986
+ if tdf is not None:
1987
+ try:
1988
+ with SetNumexprThreads(self._num_core):
1989
+ evaluate('tbf-tdf', out=tbf)
1990
+ except TypeError as e:
1991
+ sys_exit(
1992
+ f'\nA {type(e).__name__} occured while subtracting '
1993
+ 'the dark field with num_expr.evaluate()'
1994
+ '\nTry reducing the detector range'
1995
+ f'\n(currently img_row_bounds = {img_row_bounds}, and '
1996
+ f'img_column_bounds = {img_column_bounds})\n')
1997
+
1888
1998
  # Get image bounds
1889
- img_row_bounds = tuple(
1890
- reduced_data.get('img_row_bounds', (0, tbf_shape[0])))
1999
+ img_row_bounds = tuple(reduced_data.get('img_row_bounds'))
1891
2000
  img_column_bounds = tuple(
1892
2001
  reduced_data.get('img_column_bounds', (0, tbf_shape[1])))
1893
2002
 
1894
- # Get resized dark field
1895
- if 'dark_field' in reduced_data.data:
1896
- tdf = np.asarray(
1897
- reduced_data.data.dark_field)[
2003
+ # Check if this run is a rotation axis calibration
2004
+ # and resize dark and bright fields accordingly
2005
+ if calibrate_center_rows:
2006
+ if tdf is not None:
2007
+ tdf = tdf[calibrate_center_rows,:]
2008
+ tbf = tbf[calibrate_center_rows,:]
2009
+ else:
2010
+ if (img_row_bounds != (0, tbf.shape[0])
2011
+ or img_column_bounds != (0, tbf.shape[1])):
2012
+ if tdf is not None:
2013
+ tdf = tdf[
2014
+ img_row_bounds[0]:img_row_bounds[1],
2015
+ img_column_bounds[0]:img_column_bounds[1]]
2016
+ tbf = tbf[
1898
2017
  img_row_bounds[0]:img_row_bounds[1],
1899
2018
  img_column_bounds[0]:img_column_bounds[1]]
1900
- else:
1901
- self._logger.warning('Dark field unavailable')
1902
- tdf = None
1903
-
1904
- # Resize bright field
1905
- if (img_row_bounds != (0, tbf.shape[0])
1906
- or img_column_bounds != (0, tbf.shape[1])):
1907
- tbf = tbf[
1908
- img_row_bounds[0]:img_row_bounds[1],
1909
- img_column_bounds[0]:img_column_bounds[1]]
1910
2019
 
1911
2020
  # Get thetas (in degrees)
1912
2021
  thetas = np.asarray(reduced_data.rotation_angle)
@@ -1919,35 +2028,38 @@ class Tomo:
1919
2028
  image_mask = np.asarray(image_mask)
1920
2029
 
1921
2030
  # Get the tomography images
1922
- image_key = nxentry.instrument.detector.get('image_key', None)
1923
2031
  field_indices_all = [
1924
2032
  index for index, key in enumerate(image_key) if key == 0]
1925
2033
  if not field_indices_all:
1926
2034
  raise ValueError('Tomography field(s) unavailable')
1927
- z_translation_all = nxentry.sample.z_translation[field_indices_all]
2035
+ z_translation_all = np.asarray(
2036
+ nxentry.sample.z_translation)[field_indices_all]
1928
2037
  z_translation_levels = sorted(list(set(z_translation_all)))
1929
2038
  num_tomo_stacks = len(z_translation_levels)
2039
+ if calibrate_center_rows:
2040
+ center_stack_index = int(num_tomo_stacks/2)
1930
2041
  tomo_stacks = num_tomo_stacks*[np.array([])]
1931
2042
  horizontal_shifts = []
1932
2043
  vertical_shifts = []
1933
- tomo_stacks = []
1934
2044
  for i, z_translation in enumerate(z_translation_levels):
2045
+ if calibrate_center_rows and i != center_stack_index:
2046
+ continue
1935
2047
  try:
1936
2048
  field_indices = [
1937
2049
  field_indices_all[index]
1938
2050
  for index, z in enumerate(z_translation_all)
1939
2051
  if z == z_translation]
1940
2052
  field_indices_masked = np.asarray(field_indices)[image_mask]
1941
- horizontal_shift = list(set(
1942
- nxentry.sample.x_translation[field_indices_masked]))
2053
+ horizontal_shift = list(set(np.asarray(
2054
+ nxentry.sample.x_translation)[field_indices_masked]))
1943
2055
  assert len(horizontal_shift) == 1
1944
2056
  horizontal_shifts += horizontal_shift
1945
- vertical_shift = list(set(
1946
- nxentry.sample.z_translation[field_indices_masked]))
2057
+ vertical_shift = list(set(np.asarray(
2058
+ nxentry.sample.z_translation)[field_indices_masked]))
1947
2059
  assert len(vertical_shift) == 1
1948
2060
  vertical_shifts += vertical_shift
1949
- sequence_numbers = \
1950
- nxentry.instrument.detector.sequence_number[
2061
+ sequence_numbers = np.asarray(
2062
+ nxentry.instrument.detector.sequence_number)[
1951
2063
  field_indices]
1952
2064
  assert (list(sequence_numbers)
1953
2065
  == list(range((len(sequence_numbers)))))
@@ -1956,27 +2068,35 @@ class Tomo:
1956
2068
  except:
1957
2069
  raise RuntimeError('Unable to load the tomography images '
1958
2070
  f'for stack {i}')
1959
- tomo_stacks.append(tomo_stack)
1960
- if not i:
1961
- tomo_stack_shape = tomo_stack.shape
1962
- else:
1963
- assert tomo_stack_shape == tomo_stack.shape
2071
+ tomo_stacks[i] = tomo_stack
2072
+ if not calibrate_center_rows:
2073
+ if not i:
2074
+ tomo_stack_shape = tomo_stack.shape
2075
+ else:
2076
+ assert tomo_stack_shape == tomo_stack.shape
1964
2077
 
1965
- row_pixel_size = nxentry.instrument.detector.row_pixel_size
1966
- column_pixel_size = nxentry.instrument.detector.column_pixel_size
1967
- reduced_tomo_stacks = []
2078
+ row_pixel_size = float(nxentry.instrument.detector.row_pixel_size)
2079
+ column_pixel_size = float(
2080
+ nxentry.instrument.detector.column_pixel_size)
2081
+ reduced_tomo_stacks = num_tomo_stacks*[np.array([])]
2082
+ tomo_stack_shape = None
1968
2083
  for i, tomo_stack in enumerate(tomo_stacks):
2084
+ if not tomo_stack.size:
2085
+ continue
1969
2086
  # Resize the tomography images
1970
2087
  # Right now the range is the same for each set in the stack
1971
- assert len(thetas) == tomo_stack.shape[0]
1972
- if (img_row_bounds == (0, tbf.shape[0])
1973
- and img_column_bounds == (0, tbf.shape[1])):
1974
- tomo_stack = tomo_stack.astype('float64', copy=False)
1975
- else:
1976
- tomo_stack = tomo_stack[
1977
- :,img_row_bounds[0]:img_row_bounds[1],
1978
- img_column_bounds[0]:img_column_bounds[1]].astype(
2088
+ if calibrate_center_rows:
2089
+ tomo_stack = tomo_stack[:,calibrate_center_rows,:].astype(
1979
2090
  'float64', copy=False)
2091
+ else:
2092
+ if (img_row_bounds != (0, tomo_stack.shape[1])
2093
+ or img_column_bounds != (0, tomo_stack.shape[2])):
2094
+ tomo_stack = tomo_stack[
2095
+ :,img_row_bounds[0]:img_row_bounds[1],
2096
+ img_column_bounds[0]:img_column_bounds[1]].astype(
2097
+ 'float64', copy=False)
2098
+ else:
2099
+ tomo_stack = tomo_stack.astype('float64', copy=False)
1980
2100
 
1981
2101
  # Subtract dark field
1982
2102
  if tdf is not None:
@@ -2017,23 +2137,18 @@ class Tomo:
2017
2137
 
2018
2138
  # Downsize tomography stack to smaller size
2019
2139
  tomo_stack = tomo_stack.astype('float32', copy=False)
2020
- if not self._test_mode and (self._save_figs or self._save_only):
2140
+ if self._save_figs or self._save_only:
2141
+ theta = round(thetas[0], 2)
2021
2142
  if len(tomo_stacks) == 1:
2022
- title = f'red fullres theta {round(thetas[0], 2)+0}'
2143
+ title = r'Reduced data, $\theta$ = 'f'{theta}'
2144
+ name = f'reduced_data_theta_{theta}'
2023
2145
  else:
2024
- title = f'red stack {i} fullres theta ' \
2025
- f'{round(thetas[0], 2)+0}'
2026
- extent = (
2027
- 0,
2028
- float(column_pixel_size*tomo_stack.shape[2]),
2029
- float(row_pixel_size*tomo_stack.shape[1]),
2030
- 0)
2146
+ title = f'Reduced data stack {i}, 'r'$\theta$ = 'f'{theta}'
2147
+ name = f'reduced_data_stack_{i}_theta_{theta}'
2031
2148
  quick_imshow(
2032
- tomo_stack[0,:,:], title=title, path=self._output_folder,
2033
- save_fig=self._save_figs, save_only=self._save_only,
2034
- extent=extent, block=self._block)
2035
- # if not self._block:
2036
- # clear_imshow(title)
2149
+ tomo_stack[0,:,:], title=title, name=name,
2150
+ path=self._output_folder, save_fig=self._save_figs,
2151
+ save_only=self._save_only, block=self._block)
2037
2152
  zoom_perc = 100
2038
2153
  if zoom_perc != 100:
2039
2154
  t0 = time()
@@ -2044,26 +2159,24 @@ class Tomo:
2044
2159
  tomo_zoom_list.append(tomo_zoom)
2045
2160
  tomo_stack = np.stack(tomo_zoom_list)
2046
2161
  self._logger.info(f'Zooming in took {time()-t0:.2f} seconds')
2162
+ title = f'red stack {zoom_perc}p theta ' \
2163
+ f'{round(thetas[0], 2)+0}'
2164
+ quick_imshow(
2165
+ tomo_stack[0,:,:], title=title,
2166
+ path=self._output_folder, save_fig=self._save_figs,
2167
+ save_only=self._save_only, block=self._block)
2047
2168
  del tomo_zoom_list
2048
- if not self._test_mode:
2049
- title = f'red stack {zoom_perc}p theta ' \
2050
- f'{round(thetas[0], 2)+0}'
2051
- quick_imshow(
2052
- tomo_stack[0,:,:], title=title,
2053
- path=self._output_folder, save_fig=self._save_figs,
2054
- save_only=self._save_only, block=self._block)
2055
- # if not self._block:
2056
- # clear_imshow(title)
2057
-
2058
- # Save test data to file
2059
- if self._test_mode:
2060
- row_index = int(tomo_stack.shape[1]/2)
2061
- np.savetxt(
2062
- f'{self._output_folder}/red_stack_{i}.txt',
2063
- tomo_stack[:,row_index,:], fmt='%.6e')
2064
2169
 
2065
2170
  # Combine resized stacks
2066
- reduced_tomo_stacks.append(tomo_stack)
2171
+ reduced_tomo_stacks[i] = tomo_stack
2172
+ if tomo_stack_shape is None:
2173
+ tomo_stack_shape = tomo_stack.shape
2174
+ else:
2175
+ assert tomo_stack_shape == tomo_stack.shape
2176
+
2177
+ for i, stack in enumerate(reduced_tomo_stacks):
2178
+ if not stack.size:
2179
+ reduced_tomo_stacks[i] = np.zeros(tomo_stack_shape)
2067
2180
 
2068
2181
  # Add tomo field info to reduced data NXprocess
2069
2182
  reduced_data.x_translation = np.asarray(horizontal_shifts)
@@ -2081,183 +2194,217 @@ class Tomo:
2081
2194
 
2082
2195
  def _find_center_one_plane(
2083
2196
  self, sinogram, row, thetas, eff_pixel_size, cross_sectional_dim,
2084
- path=None, num_core=1, search_range=None, search_step=None,
2085
- gaussian_sigma=None, ring_width=None):
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):
2086
2199
  """Find center for a single tomography plane."""
2087
2200
  # Third party modules
2201
+ import matplotlib.pyplot as plt
2088
2202
  from tomopy import find_center_vo
2089
2203
 
2204
+ # sinogram index order: theta,column
2205
+ sinogram = np.asarray(sinogram)
2090
2206
  if not gaussian_sigma:
2091
2207
  gaussian_sigma = None
2092
2208
  if not ring_width:
2093
2209
  ring_width = None
2094
- # Try automatic center finding routines for initial value
2095
- # sinogram index order: theta,column
2096
- # need column,theta for iradon, so take transpose
2097
- sinogram = np.asarray(sinogram)
2098
- sinogram_t = sinogram.T
2099
- center = sinogram.shape[1]/2
2100
-
2101
- # quick_imshow(
2102
- # sinogram_t, f'sinogram row{row}',
2103
- # aspect='auto', path=self._output_folder,
2104
- # save_fig=self._save_figs, save_only=self._save_only,
2105
- # block=self._block)
2106
2210
 
2107
- # Try using Nghia Vos method
2211
+ # Try Nghia Vo's method to get the default center offset
2108
2212
  t0 = time()
2213
+ if center_offset_min is None:
2214
+ center_offset_min = -50
2215
+ if center_offset_max is None:
2216
+ center_offset_max = 50
2109
2217
  if num_core > NUM_CORE_TOMOPY_LIMIT:
2110
2218
  self._logger.debug(
2111
- f'Running find_center_vo on {NUM_CORE_TOMOPY_LIMIT} cores ...')
2219
+ f'Running find_center_vo on {NUM_CORE_TOMOPY_LIMIT} '
2220
+ 'cores ...')
2112
2221
  tomo_center = find_center_vo(
2113
- sinogram, ncore=NUM_CORE_TOMOPY_LIMIT)
2222
+ sinogram, ncore=NUM_CORE_TOMOPY_LIMIT, smin=center_offset_min,
2223
+ smax=center_offset_max)
2114
2224
  else:
2115
- tomo_center = find_center_vo(sinogram, ncore=num_core)
2225
+ tomo_center = find_center_vo(
2226
+ sinogram, ncore=num_core, smin=center_offset_min,
2227
+ smax=center_offset_max)
2116
2228
  self._logger.info(
2117
- f'Finding center using Nghia Vos method took {time()-t0:.2f} '
2229
+ f'Finding center using Nghia Vo\'s method took {time()-t0:.2f} '
2118
2230
  'seconds')
2119
- center_offset_vo = float(tomo_center-center)
2231
+ center_offset_range = sinogram.shape[1]/2
2232
+ center_offset_vo = float(tomo_center-center_offset_range)
2120
2233
  self._logger.info(
2121
- f'Center at row {row} using Nghia Vos method = '
2234
+ f'Center at row {row} using Nghia Vo\'s method = '
2122
2235
  f'{center_offset_vo:.2f}')
2123
2236
 
2124
- if self._save_figs:
2237
+ center_offset_default = None
2238
+ if self._interactive or self._save_figs:
2239
+
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
2125
2244
  t0 = time()
2126
2245
  recon_plane = self._reconstruct_one_plane(
2127
- sinogram_t, tomo_center, thetas, eff_pixel_size,
2246
+ sinogram_t, center_offset_vo, thetas, eff_pixel_size,
2128
2247
  cross_sectional_dim, False, num_core, gaussian_sigma,
2129
2248
  ring_width)
2130
2249
  self._logger.info(
2131
- f'Reconstructing row {row} took {time()-t0:.2f} seconds')
2132
- title = f'edges row{row} center offset{center_offset_vo:.2f} Vo'
2133
- self._plot_edges_one_plane(recon_plane, title, path=path)
2134
-
2135
- # Try using phase correlation method
2136
- # if input_yesno('
2137
- # Try finding center using phase correlation (y/n)?',
2138
- # 'n'):
2139
- # t0 = time()
2140
- # tomo_center = find_center_pc(
2141
- # sinogram, sinogram, tol=0.1, rotc_guess=tomo_center)
2142
- # error = 1.
2143
- # while error > tol:
2144
- # prev = tomo_center
2145
- # tomo_center = find_center_pc(
2146
- # sinogram, sinogram, tol=tol, rotc_guess=tomo_center)
2147
- # error = np.abs(tomo_center-prev)
2148
- # self._logger.info(
2149
- # 'Finding center using the phase correlation method '
2150
- # f'took {time()-t0:.2f} seconds')
2151
- # center_offset = tomo_center-center
2152
- # print(
2153
- # f'Center at row {row} using phase correlation = '
2154
- # f'{center_offset:.2f}')
2155
- # t0 = time()
2156
- # recon_plane = self._reconstruct_one_plane(
2157
- # sinogram_t, tomo_center, thetas, eff_pixel_size,
2158
- # cross_sectional_dim, False, num_core, gaussian_sigma, ring_width)
2159
- # self._logger.info(
2160
- # f'Reconstructing row {row} took {time()-t0:.2f} seconds')
2161
- #
2162
- # title = \
2163
- # f'edges row{row} center_offset{center_offset:.2f} PC'
2164
- # self._plot_edges_one_plane(recon_plane, title, path=path)
2165
-
2166
- # Perform center finding search
2250
+ f'Reconstructing row {row} with center at '
2251
+ f'{center_offset_vo} took {time()-t0:.2f} seconds')
2252
+
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
+
2167
2267
  if self._interactive:
2168
- print(
2169
- f'Center at row {row} using Nghia Vo’s method = '
2170
- f'{center_offset_vo:.2f}')
2171
- accept_vo = input_yesno(
2172
- '\nAccept this center location (y) or perform a search (n)?',
2173
- 'y')
2174
- elif search_range is not None or search_step is not None:
2175
- accept_vo = False
2176
- else:
2177
- accept_vo = True
2178
- while not accept_vo:
2179
- if search_range is None:
2180
- if search_step is None:
2181
- center_offset_low = max(-center, center_offset_vo-10)
2182
- center_offset_upp = min(center, center_offset_vo+10)
2183
- else:
2184
- center_offset_low = max(
2185
- -center, center_offset_vo-search_step)
2186
- center_offset_upp = min(
2187
- center, center_offset_vo+search_step)
2188
- else:
2189
- center_offset_low = max(-center, center_offset_vo-search_range)
2190
- center_offset_upp = min(center, center_offset_vo+search_range)
2191
- if search_step is None:
2192
- center_offset_step = center_offset_upp-center_offset_vo
2193
- else:
2194
- center_offset_step = min(
2195
- search_step, center_offset_upp-center_offset_vo)
2196
- if self._interactive:
2197
- center_offset_low = input_num(
2198
- '\nEnter lower bound for center offset', ge=-int(center),
2199
- le=int(center), default=center_offset_low)
2200
- center_offset_upp = input_num(
2201
- 'Enter upper bound for center offset',
2202
- ge=center_offset_low, le=int(center),
2203
- default=center_offset_upp)
2204
- if search_step is None:
2205
- center_offset_step = 1
2206
- else:
2207
- center_offset_step = min(search_range, search_step)
2208
- if center_offset_upp == center_offset_low:
2209
- center_offset_step = 1
2210
- else:
2211
- center_offset_step = input_num(
2212
- 'Enter step size for center offset search',
2213
- ge=1, le=center_offset_upp-center_offset_low,
2214
- default=center_offset_step)
2215
- num_center_offset = 1 + int(
2216
- (center_offset_upp-center_offset_low) / center_offset_step)
2217
- center_offsets = np.linspace(
2218
- center_offset_low, center_offset_upp, num_center_offset)
2219
- if self._interactive:
2220
- save_figs = self._save_figs
2221
- save_only = False
2222
- else:
2223
- save_figs = True
2224
- save_only = True
2225
- for center_offset in center_offsets:
2226
- if (not self._interactive and center_offset == center_offset_vo
2227
- and self._save_figs):
2228
- continue
2268
+
2269
+ if center_offset_default is None:
2270
+ # Reconstruct the plane at the previous center offset
2229
2271
  t0 = time()
2230
2272
  recon_plane = self._reconstruct_one_plane(
2231
- sinogram_t, center_offset+center, thetas, eff_pixel_size,
2273
+ sinogram_t, prev_center_offset, thetas, eff_pixel_size,
2232
2274
  cross_sectional_dim, False, num_core, gaussian_sigma,
2233
2275
  ring_width)
2234
2276
  self._logger.info(
2235
- f'Reconstructing center_offset {center_offset} took '
2236
- 'f{time()-t0:.2f} seconds')
2237
- title = f'edges row{row} center_offset{center_offset:.2f}'
2238
- self._plot_edges_one_plane(
2239
- recon_plane, title, path=path, save_figs=save_figs,
2240
- save_only=save_only)
2241
- if (not self._interactive
2242
- or input_yesno('\nEnd the search (y/n)?', 'y')):
2243
- break
2277
+ f'Reconstructing row {row} with center at '
2278
+ f'{prev_center_offset} took {time()-t0:.2f} seconds')
2279
+
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()
2294
+
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
2321
+
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:
2341
+ 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)
2346
+ 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)
2352
+ indices = []
2353
+ for i, preselected_offset in enumerate(preselected_offsets):
2354
+ if preselected_offset in center_offsets:
2355
+ indices.append(
2356
+ center_offsets.index(preselected_offset))
2357
+ 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
+ indices.append(len(center_offsets))
2363
+ center_offsets.append(preselected_offset)
2364
+ recon_edges.append(
2365
+ self._get_edges_one_plane(recon_plane))
2366
+ fig, accept, selected_center_offset = \
2367
+ self._select_center_offset(
2368
+ [recon_edges[i] for i in indices],
2369
+ row, preselected_offsets,
2370
+ 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
+ # Plot results
2387
+ if self._save_figs:
2388
+ fig.savefig(
2389
+ os_path.join(
2390
+ self._output_folder,
2391
+ f'edges_center_{row}_{min(preselected_offsets)}_'\
2392
+ f'{max(preselected_offsets)}.png'))
2393
+ plt.close()
2394
+ del sinogram_t
2395
+ del recon_plane
2244
2396
 
2245
2397
  # Select center location
2246
- if not accept_vo and self._interactive:
2247
- center_offset = input_num(
2248
- ' Enter chosen center offset', ge=-center, le=center,
2249
- default=center_offset_vo)
2398
+ if self._interactive:
2399
+ center_offset = selected_center_offset
2250
2400
  else:
2251
2401
  center_offset = center_offset_vo
2252
2402
 
2253
- del sinogram_t
2254
- del recon_plane
2255
-
2256
2403
  return float(center_offset)
2257
2404
 
2258
2405
  def _reconstruct_one_plane(
2259
- self, tomo_plane_t, center, thetas, eff_pixel_size,
2260
- cross_sectional_dim, plot_sinogram=True, num_core=1,
2406
+ self, tomo_plane_t, center_offset, thetas, eff_pixel_size,
2407
+ cross_sectional_dim, plot_sinogram=False, num_core=1,
2261
2408
  gaussian_sigma=None, ring_width=None):
2262
2409
  """Invert the sinogram for a single tomography plane."""
2263
2410
  # Third party modules
@@ -2266,8 +2413,6 @@ class Tomo:
2266
2413
  from tomopy import misc
2267
2414
 
2268
2415
  # tomo_plane_t index order: column,theta
2269
- assert 0 <= center < tomo_plane_t.shape[0]
2270
- center_offset = center-tomo_plane_t.shape[0]/2
2271
2416
  two_offset = 2 * int(np.round(center_offset))
2272
2417
  two_offset_abs = np.abs(two_offset)
2273
2418
  # Add 10% slack to max_rad to avoid edge effects
@@ -2289,10 +2434,11 @@ class Tomo:
2289
2434
  sinogram = tomo_plane_t[dist_from_edge:two_offset-dist_from_edge,:]
2290
2435
  if plot_sinogram:
2291
2436
  quick_imshow(
2292
- sinogram.T, f'sinogram center offset{center_offset:.2f}',
2293
- aspect='auto', path=self._output_folder,
2294
- save_fig=self._save_figs, save_only=self._save_only,
2295
- block=self._block)
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')
2296
2442
 
2297
2443
  # Inverting sinogram
2298
2444
  t0 = time()
@@ -2312,20 +2458,14 @@ class Tomo:
2312
2458
 
2313
2459
  return recon_clean
2314
2460
 
2315
- def _plot_edges_one_plane(
2316
- self, recon_plane, title, path=None, save_figs=None,
2317
- save_only=None):
2461
+ def _get_edges_one_plane(self, recon_plane):
2318
2462
  """
2319
- Create an "edges plot" for a singled reconstructed tomography
2320
- data plane.
2463
+ Create an "edges plot" image for a single reconstructed
2464
+ tomography data plane.
2321
2465
  """
2322
2466
  # Third party modules
2323
2467
  from skimage.restoration import denoise_tv_chambolle
2324
2468
 
2325
- if save_figs is None:
2326
- save_figs = self._save_figs
2327
- if save_only is None:
2328
- save_only = self._save_only
2329
2469
  vis_parameters = None # self._config.get('vis_parameters')
2330
2470
  if vis_parameters is None:
2331
2471
  weight = 0.1
@@ -2333,22 +2473,186 @@ class Tomo:
2333
2473
  weight = vis_parameters.get('denoise_weight', 0.1)
2334
2474
  if not is_num(weight, ge=0.):
2335
2475
  self._logger.warning(
2336
- f'Invalid weight ({weight}) in _plot_edges_one_plane, '
2476
+ f'Invalid weight ({weight}) in _get_edges_one_plane, '
2337
2477
  'set to a default of 0.1')
2338
2478
  weight = 0.1
2339
- edges = denoise_tv_chambolle(recon_plane, weight=weight)
2340
- vmax = np.max(edges[0,:,:])
2341
- vmin = -vmax
2342
- if path is None:
2343
- path = self._output_folder
2344
- quick_imshow(
2345
- edges[0,:,:], f'{title} coolwarm', path=path, cmap='coolwarm',
2346
- save_fig=save_figs, save_only=save_only, block=self._block)
2347
- quick_imshow(
2348
- edges[0,:,:], f'{title} gray', path=path, cmap='gray', vmin=vmin,
2349
- vmax=vmax, save_fig=save_figs, save_only=save_only,
2350
- block=self._block)
2351
- del edges
2479
+ return denoise_tv_chambolle(recon_plane, weight=weight)[0]
2480
+
2481
+ def _select_center_offset(
2482
+ self, recon_edges, row, preselected_offsets, vo=False,
2483
+ include_all_bad=False):
2484
+ """Select a center offset value from an "edges plot" image
2485
+ for a single reconstructed tomography data plane."""
2486
+ # Third party modules
2487
+ import matplotlib.pyplot as plt
2488
+ from matplotlib.widgets import RadioButtons, Button
2489
+ from matplotlib.widgets import Button
2490
+
2491
+ def select_offset(offset):
2492
+ """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()
2501
+
2502
+ def reject(event):
2503
+ """Callback function for the "Reject" button."""
2504
+ selected_offset.append((False, preselected_offsets[0]))
2505
+ plt.close()
2506
+
2507
+ def accept(event):
2508
+ """Callback function for the "Accept" button."""
2509
+ if num_plots == 1:
2510
+ selected_offset.append((True, preselected_offsets[0]))
2511
+ else:
2512
+ selected_offset.append(
2513
+ ((False,
2514
+ preselected_offsets[preselected_offsets.index(
2515
+ float(radio_btn.value_selected))])))
2516
+ plt.close()
2517
+
2518
+ if not isinstance(recon_edges, (tuple, list)):
2519
+ recon_edges = [recon_edges]
2520
+ if not isinstance(preselected_offsets, (tuple, list)):
2521
+ preselected_offsets = [preselected_offsets]
2522
+ assert len(recon_edges) == len(preselected_offsets)
2523
+
2524
+ selected_offset = []
2525
+
2526
+ title_pos = (0.5, 0.95)
2527
+ title_props = {'fontsize': 'xx-large', 'horizontalalignment': 'center',
2528
+ 'verticalalignment': 'bottom'}
2529
+ subtitle_pos = (0.5, 0.90)
2530
+ subtitle_props = {'fontsize': 'xx-large',
2531
+ 'horizontalalignment': 'center',
2532
+ 'verticalalignment': 'bottom'}
2533
+
2534
+ num_plots = len(recon_edges)
2535
+ if num_plots == 1:
2536
+ fig, axs = plt.subplots(figsize=(11, 8.5))
2537
+ axs = [axs]
2538
+ vmax = np.max(recon_edges[0][:,:])
2539
+ else:
2540
+ fig, axs = plt.subplots(ncols=num_plots, figsize=(17, 8.5))
2541
+ 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')
2554
+ ax.set_xlabel('x', fontsize='x-large')
2555
+ if not i:
2556
+ ax.set_ylabel('y', fontsize='x-large')
2557
+
2558
+ 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
+ fig_subtitle = plt.figtext(
2572
+ *subtitle_pos,
2573
+ 'Press "Accept" to accept this value or "Reject" to start a '
2574
+ 'center calibration search',
2575
+ **subtitle_props)
2576
+ 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)
2584
+ 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)
2592
+
2593
+ if not self._interactive:
2594
+
2595
+ selected_offset.append((True, preselected_offsets[0]))
2596
+
2597
+ else:
2598
+
2599
+ fig.subplots_adjust(bottom=0.25, top=0.85)
2600
+
2601
+ if num_plots == 1:
2602
+
2603
+ # Setup "Reject" button
2604
+ reject_btn = Button(
2605
+ plt.axes([0.15, 0.05, 0.15, 0.075]), 'Reject')
2606
+ reject_cid = reject_btn.on_clicked(reject)
2607
+
2608
+
2609
+ else:
2610
+
2611
+ # Setup RadioButtons
2612
+ select_text = plt.figtext(
2613
+ 0.225, 0.175, 'Select offset', fontsize='x-large',
2614
+ horizontalalignment='center', verticalalignment='center')
2615
+ if include_all_bad:
2616
+ if num_plots == 2:
2617
+ labels = (*preselected_offsets, 'both bad')
2618
+ else:
2619
+ labels = (*preselected_offsets, 'all bad')
2620
+ else:
2621
+ labels = preselected_offsets
2622
+ radio_btn = RadioButtons(
2623
+ plt.axes([0.175, 0.05, 0.1, 0.1]),
2624
+ labels = labels, active=1)
2625
+ radio_cid = radio_btn.on_clicked(select_offset)
2626
+
2627
+ # Setup "Accept" button
2628
+ accept_btn = Button(
2629
+ plt.axes([0.7, 0.05, 0.15, 0.075]), 'Accept')
2630
+ accept_cid = accept_btn.on_clicked(accept)
2631
+
2632
+ plt.show()
2633
+
2634
+ # Disconnect all widget callbacks when figure is closed
2635
+ # and remove the buttons before returning the figure
2636
+ if num_plots == 1:
2637
+ reject_btn.disconnect(reject_cid)
2638
+ reject_btn.ax.remove()
2639
+ else:
2640
+ radio_btn.disconnect(radio_cid)
2641
+ radio_btn.ax.remove()
2642
+ accept_btn.disconnect(accept_cid)
2643
+ accept_btn.ax.remove()
2644
+
2645
+ if num_plots == 1:
2646
+ fig_title.remove()
2647
+ else:
2648
+ fig_title.set_in_layout(True)
2649
+ select_text.remove()
2650
+ fig_subtitle.remove()
2651
+ 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]))
2654
+
2655
+ return fig, *selected_offset[0]
2352
2656
 
2353
2657
  def _reconstruct_one_tomo_stack(
2354
2658
  self, tomo_stack, thetas, center_offsets=None, num_core=1,
@@ -2405,7 +2709,6 @@ class Tomo:
2405
2709
  # RV prep.stripe.remove_stripe_fw seems flawed for hollow brick
2406
2710
  # accross multiple stacks
2407
2711
  if remove_stripe_sigma is not None and remove_stripe_sigma:
2408
- self._logger.warning('Ignoring remove_stripe_sigma')
2409
2712
  if num_core > NUM_CORE_TOMOPY_LIMIT:
2410
2713
  tomo_stack = prep.stripe.remove_stripe_fw(
2411
2714
  tomo_stack, sigma=remove_stripe_sigma,
@@ -2473,12 +2776,17 @@ class Tomo:
2473
2776
 
2474
2777
  def _resize_reconstructed_data(
2475
2778
  self, data, x_bounds=None, y_bounds=None, z_bounds=None,
2476
- z_only=False):
2779
+ combine_data=False):
2477
2780
  """Resize the reconstructed tomography data."""
2478
- # Data order: row(z),x,y or stack,row(z),x,y
2781
+ # Third party modules
2782
+ import matplotlib.pyplot as plt
2783
+
2784
+ # Data order: row/-z,y,x or stack,row/-z,y,x
2479
2785
  if isinstance(data, list):
2480
- for stack in data:
2786
+ for i, stack in enumerate(data):
2481
2787
  assert stack.ndim == 3
2788
+ if i:
2789
+ assert stack.shape[1:] == data[0].shape[1:]
2482
2790
  num_tomo_stacks = len(data)
2483
2791
  tomo_recon_stacks = data
2484
2792
  else:
@@ -2486,84 +2794,91 @@ class Tomo:
2486
2794
  num_tomo_stacks = 1
2487
2795
  tomo_recon_stacks = [data]
2488
2796
 
2489
- if not z_only and x_bounds is None:
2490
- # Selecting x bounds (in yz-plane)
2491
- tomosum = 0
2492
- for i in range(num_tomo_stacks):
2493
- tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=(0,2))
2494
- select_x_bounds = input_yesno(
2495
- '\nDo you want to change the image x-bounds (y/n)?', 'y')
2496
- if not select_x_bounds:
2497
- x_bounds = None
2498
- else:
2499
- accept = False
2500
- index_ranges = None
2501
- while not accept:
2502
- _, x_bounds = draw_mask_1d(
2503
- tomosum, current_index_ranges=index_ranges,
2504
- title='select x data range',
2505
- ylabel='sum yz')
2506
- while len(x_bounds) != 1:
2507
- print('Please select exactly one continuous range')
2508
- _, x_bounds = draw_mask_1d(
2509
- tomosum, title='select x data range',
2510
- ylabel='sum yz')
2511
- x_bounds = x_bounds[0]
2512
- accept = True
2513
- self._logger.debug(f'x_bounds = {x_bounds}')
2514
-
2515
- if not z_only and y_bounds is None:
2516
- # Selecting y bounds (in xz-plane)
2517
- tomosum = 0
2518
- for i in range(num_tomo_stacks):
2519
- tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=(0,1))
2520
- select_y_bounds = input_yesno(
2521
- '\nDo you want to change the image y-bounds (y/n)?', 'y')
2522
- if not select_y_bounds:
2523
- y_bounds = None
2524
- else:
2525
- accept = False
2526
- index_ranges = None
2527
- while not accept:
2528
- _, y_bounds = draw_mask_1d(
2529
- tomosum, current_index_ranges=index_ranges,
2530
- title='select x data range',
2531
- ylabel='sum xz')
2532
- while len(y_bounds) != 1:
2533
- print('Please select exactly one continuous range')
2534
- _, y_bounds = draw_mask_1d(
2535
- tomosum, title='select x data range',
2536
- ylabel='sum xz')
2537
- y_bounds = y_bounds[0]
2538
- accept = True
2539
- self._logger.debug(f'y_bounds = {y_bounds}')
2797
+ # Selecting x an y bounds (in z-plane)
2798
+ if x_bounds is None:
2799
+ if not self._interactive:
2800
+ self._logger.warning('x_bounds unspecified, reconstruct '
2801
+ 'data for full x-range')
2802
+ x_bounds = (0, tomo_recon_stacks[0].shape[2])
2803
+ elif not is_int_pair(
2804
+ x_bounds, ge=0, le=tomo_recon_stacks[0].shape[2]):
2805
+ raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
2806
+ if y_bounds is None:
2807
+ if not self._interactive:
2808
+ self._logger.warning('y_bounds unspecified, reconstruct '
2809
+ 'data for full y-range')
2810
+ y_bounds = (0, tomo_recon_stacks[0].shape[1])
2811
+ elif not is_int_pair(
2812
+ y_bounds, ge=0, le=tomo_recon_stacks[0].shape[1]):
2813
+ raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
2814
+ if x_bounds is None and y_bounds is None:
2815
+ preselected_roi = None
2816
+ elif x_bounds is None:
2817
+ preselected_roi = (
2818
+ 0, tomo_recon_stacks[0].shape[2],
2819
+ y_bounds[0], y_bounds[1])
2820
+ elif y_bounds is None:
2821
+ preselected_roi = (
2822
+ x_bounds[0], x_bounds[1],
2823
+ 0, tomo_recon_stacks[0].shape[1])
2824
+ else:
2825
+ preselected_roi = (
2826
+ x_bounds[0], x_bounds[1],
2827
+ y_bounds[0], y_bounds[1])
2828
+ tomosum = 0
2829
+ for i in range(num_tomo_stacks):
2830
+ tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=0)
2831
+ fig, roi = select_roi_2d(
2832
+ tomosum, preselected_roi=preselected_roi,
2833
+ title_a='Reconstructed data summed over z',
2834
+ row_label='y', column_label='x',
2835
+ interactive=self._interactive)
2836
+ if roi is None:
2837
+ x_bounds = (0, tomo_recon_stacks[0].shape[2])
2838
+ y_bounds = (0, tomo_recon_stacks[0].shape[1])
2839
+ else:
2840
+ x_bounds = (int(roi[0]), int(roi[1]))
2841
+ y_bounds = (int(roi[2]), int(roi[3]))
2842
+ self._logger.debug(f'x_bounds = {x_bounds}')
2843
+ 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()
2540
2850
 
2541
2851
  # Selecting z bounds (in xy-plane)
2542
- # (only valid for a single image stack)
2543
- if z_bounds is None and num_tomo_stacks != 1:
2852
+ # (only valid for a single image stack or when combining a stack)
2853
+ if num_tomo_stacks == 1 or combine_data:
2854
+ if z_bounds is None:
2855
+ if not self._interactive:
2856
+ if combine_data:
2857
+ self._logger.warning(
2858
+ 'z_bounds unspecified, combine reconstructed data '
2859
+ 'for full z-range')
2860
+ else:
2861
+ self._logger.warning(
2862
+ 'z_bounds unspecified, reconstruct data for '
2863
+ 'full z-range')
2864
+ z_bounds = (0, tomo_recon_stacks[0].shape[0])
2865
+ elif not is_int_pair(
2866
+ z_bounds, ge=0, le=tomo_recon_stacks[0].shape[0]):
2867
+ raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
2544
2868
  tomosum = 0
2545
2869
  for i in range(num_tomo_stacks):
2546
2870
  tomosum = tomosum + np.sum(tomo_recon_stacks[i], axis=(1,2))
2547
- select_z_bounds = input_yesno(
2548
- 'Do you want to change the image z-bounds (y/n)?', 'n')
2549
- if not select_z_bounds:
2550
- z_bounds = None
2551
- else:
2552
- accept = False
2553
- index_ranges = None
2554
- while not accept:
2555
- _, z_bounds = draw_mask_1d(
2556
- tomosum, current_index_ranges=index_ranges,
2557
- title='select x data range',
2558
- ylabel='sum xy')
2559
- while len(z_bounds) != 1:
2560
- print('Please select exactly one continuous range')
2561
- _, z_bounds = draw_mask_1d(
2562
- tomosum, title='select x data range',
2563
- ylabel='sum xy')
2564
- z_bounds = z_bounds[0]
2565
- accept = True
2871
+ fig, z_bounds = select_roi_1d(
2872
+ tomosum, preselected_roi=z_bounds,
2873
+ xlabel='z', ylabel='Reconstructed data summed over x and y',
2874
+ interactive=self._interactive)
2566
2875
  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()
2567
2882
 
2568
2883
  return x_bounds, y_bounds, z_bounds
2569
2884
 
@@ -2583,6 +2898,7 @@ class TomoSimFieldProcessor(Processor):
2583
2898
 
2584
2899
  :param data: Input configuration for the simulation.
2585
2900
  :type data: list[PipelineData]
2901
+ :raises ValueError: Invalid input or configuration parameter.
2586
2902
  :return: Simulated tomographic images.
2587
2903
  :rtype: nexusformat.nexus.NXroot
2588
2904
  """
@@ -2667,7 +2983,7 @@ class TomoSimFieldProcessor(Processor):
2667
2983
  # Get the column coordinates
2668
2984
  img_row_offset = -0.5 * (detector_size[0]*pixel_size[0]
2669
2985
  + slit_size * (num_tomo_stack-1))
2670
- img_row_coords = (img_row_offset
2986
+ img_row_coords = np.flip(img_row_offset
2671
2987
  + pixel_size[0] * (0.5 + np.asarray(range(int(detector_size[0])))))
2672
2988
 
2673
2989
  # Get the transmitted intensities
@@ -2678,7 +2994,8 @@ class TomoSimFieldProcessor(Processor):
2678
2994
  intensities_solid = None
2679
2995
  intensities_hollow = None
2680
2996
  for n in range(num_tomo_stack):
2681
- vertical_shifts.append(img_row_offset + n*slit_size)
2997
+ vertical_shifts.append(img_row_offset + n*slit_size
2998
+ + 0.5*detector_size[0]*pixel_size[0])
2682
2999
  tomo_field = beam_intensity * np.ones((num_theta, *img_dim))
2683
3000
  if sample_type == 'square_rod':
2684
3001
  intensities_solid = \
@@ -2838,6 +3155,8 @@ class TomoDarkFieldProcessor(Processor):
2838
3155
  :type data: list[PipelineData]
2839
3156
  :param num_image: Number of dark field images, defaults to 5.
2840
3157
  :type num_image: int, optional.
3158
+ :raises ValueError: Missing or invalid input or configuration
3159
+ parameter.
2841
3160
  :return: Simulated dark field images.
2842
3161
  :rtype: nexusformat.nexus.NXroot
2843
3162
  """
@@ -2879,7 +3198,7 @@ class TomoDarkFieldProcessor(Processor):
2879
3198
  nxdark.entry.sample = nxroot.entry.sample
2880
3199
  nxinstrument = NXinstrument()
2881
3200
  nxdark.entry.instrument = nxinstrument
2882
- nxinstrument.source = nxroot.entry.instrument.source
3201
+ nxinstrument.source = source
2883
3202
  nxdetector = NXdetector()
2884
3203
  nxinstrument.detector = nxdetector
2885
3204
  nxdetector.local_name = detector.local_name
@@ -2909,6 +3228,8 @@ class TomoBrightFieldProcessor(Processor):
2909
3228
  :type data: list[PipelineData]
2910
3229
  :param num_image: Number of bright field images, defaults to 5.
2911
3230
  :type num_image: int, optional.
3231
+ :raises ValueError: Missing or invalid input or configuration
3232
+ parameter.
2912
3233
  :return: Simulated bright field images.
2913
3234
  :rtype: nexusformat.nexus.NXroot
2914
3235
  """
@@ -2945,16 +3266,16 @@ class TomoBrightFieldProcessor(Processor):
2945
3266
  bright_field = int(background_intensity+beam_intensity) * np.ones(
2946
3267
  (num_image, detector_size[0], detector_size[1]), dtype=np.int64)
2947
3268
  if num_dummy_start:
2948
- dummy_fields = background_intensity * np.ones(
3269
+ dummy_fields = int(background_intensity) * np.ones(
2949
3270
  (num_dummy_start, detector_size[0], detector_size[1]),
2950
3271
  dtype=np.int64)
2951
3272
  bright_field = np.concatenate((dummy_fields, bright_field))
2952
3273
  num_image += num_dummy_start
2953
- # Add 10% to slit size to make the bright beam slightly taller
3274
+ # Add 20% to slit size to make the bright beam slightly taller
2954
3275
  # than the vertical displacements between stacks
2955
- slit_size = 1.10*source.slit_size
2956
- if slit_size < detector.row_pixel_size*detector_size[0]:
2957
- img_row_coords = detector.row_pixel_size \
3276
+ slit_size = 1.2*source.slit_size
3277
+ if slit_size < float(detector.row_pixel_size*detector_size[0]):
3278
+ img_row_coords = float(detector.row_pixel_size) \
2958
3279
  * (0.5 + np.asarray(range(int(detector_size[0])))
2959
3280
  - 0.5*detector_size[0])
2960
3281
  outer_indices = np.where(abs(img_row_coords) > slit_size/2)[0]
@@ -2966,7 +3287,7 @@ class TomoBrightFieldProcessor(Processor):
2966
3287
  nxbright.entry.sample = nxroot.entry.sample
2967
3288
  nxinstrument = NXinstrument()
2968
3289
  nxbright.entry.instrument = nxinstrument
2969
- nxinstrument.source = nxroot.entry.instrument.source
3290
+ nxinstrument.source = source
2970
3291
  nxdetector = NXdetector()
2971
3292
  nxinstrument.detector = nxdetector
2972
3293
  nxdetector.local_name = detector.local_name
@@ -2993,8 +3314,10 @@ class TomoSpecProcessor(Processor):
2993
3314
 
2994
3315
  :param data: Input configuration for the simulation.
2995
3316
  :type data: list[PipelineData]
2996
- :param scan_numbers: List of SPEC scan numbers, defaults to [1].
3317
+ :param scan_numbers: List of SPEC scan numbers,
3318
+ defaults to [1].
2997
3319
  :type scan_numbers: list[int]
3320
+ :raises ValueError: Invalid input or configuration parameter.
2998
3321
  :return: Simulated SPEC file.
2999
3322
  :rtype: list[str]
3000
3323
  """