ChessAnalysisPipeline 0.0.12__py3-none-any.whl → 0.0.14__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,17 @@ import numpy as np
19
19
  # Local modules
20
20
  from CHAP.utils.general import (
21
21
  is_num,
22
+ is_num_series,
22
23
  is_int_pair,
23
24
  input_int,
24
25
  input_num,
26
+ input_num_list,
25
27
  input_yesno,
26
28
  select_image_indices,
27
29
  select_roi_1d,
28
30
  select_roi_2d,
29
- quick_plot,
30
31
  quick_imshow,
32
+ nxcopy,
31
33
  )
32
34
  from CHAP.utils.fit import Fit
33
35
  from CHAP.processor import Processor
@@ -112,6 +114,7 @@ class TomoCHESSMapConverter(Processor):
112
114
  NXdetector,
113
115
  NXentry,
114
116
  NXinstrument,
117
+ NXlink,
115
118
  NXroot,
116
119
  NXsample,
117
120
  NXsource,
@@ -119,18 +122,26 @@ class TomoCHESSMapConverter(Processor):
119
122
 
120
123
  # Local modules
121
124
  from CHAP.common.models.map import MapConfig
125
+ from CHAP.utils.general import index_nearest
122
126
 
123
127
  darkfield = get_nxroot(data, 'darkfield')
124
128
  brightfield = get_nxroot(data, 'brightfield')
125
129
  tomofields = get_nxroot(data, 'tomofields')
126
130
  detector_config = self.get_config(data, 'tomo.models.Detector')
127
131
 
128
- if darkfield is not None and not isinstance(darkfield, NXentry):
129
- raise ValueError('Invalid parameter darkfield ({darkfield})')
132
+ if darkfield is not None:
133
+ if isinstance(darkfield, NXroot):
134
+ darkfield = darkfield[darkfield.default]
135
+ if not isinstance(darkfield, NXentry):
136
+ raise ValueError(f'Invalid parameter darkfield ({darkfield})')
137
+ if isinstance(brightfield, NXroot):
138
+ brightfield = brightfield[brightfield.default]
130
139
  if not isinstance(brightfield, NXentry):
131
- raise ValueError('Invalid parameter brightfield ({brightfield})')
140
+ raise ValueError(f'Invalid parameter brightfield ({brightfield})')
141
+ if isinstance(tomofields, NXroot):
142
+ tomofields = tomofields[tomofields.default]
132
143
  if not isinstance(tomofields, NXentry):
133
- raise ValueError('Invalid parameter tomofields {tomofields})')
144
+ raise ValueError(f'Invalid parameter tomofields {tomofields})')
134
145
 
135
146
  # Construct NXroot
136
147
  nxroot = NXroot()
@@ -148,7 +159,7 @@ class TomoCHESSMapConverter(Processor):
148
159
  '(available independent dimensions: '
149
160
  f'{independent_dimensions})')
150
161
  rotation_angles_index = \
151
- tomofields.data.attrs['rotation_angles_indices']
162
+ tomofields.data.axes.index('rotation_angles')
152
163
  rotation_angle_data_type = \
153
164
  tomofields.data.rotation_angles.attrs['data_type']
154
165
  if rotation_angle_data_type != 'scan_column':
@@ -157,7 +168,7 @@ class TomoCHESSMapConverter(Processor):
157
168
  matched_dimensions.pop(matched_dimensions.index('rotation_angles'))
158
169
  if 'x_translation' in independent_dimensions:
159
170
  x_translation_index = \
160
- tomofields.data.attrs['x_translation_indices']
171
+ tomofields.data.axes.index('x_translation')
161
172
  x_translation_data_type = \
162
173
  tomofields.data.x_translation.attrs['data_type']
163
174
  x_translation_name = \
@@ -170,7 +181,7 @@ class TomoCHESSMapConverter(Processor):
170
181
  x_translation_data_type = None
171
182
  if 'z_translation' in independent_dimensions:
172
183
  z_translation_index = \
173
- tomofields.data.attrs['z_translation_indices']
184
+ tomofields.data.axes.index('z_translation')
174
185
  z_translation_data_type = \
175
186
  tomofields.data.z_translation.attrs['data_type']
176
187
  z_translation_name = \
@@ -188,9 +199,11 @@ class TomoCHESSMapConverter(Processor):
188
199
  '"rotation_angles"}')
189
200
 
190
201
  # Construct base NXentry and add to NXroot
191
- nxentry = NXentry()
192
- nxroot[map_config.title] = nxentry
193
- nxroot.attrs['default'] = map_config.title
202
+ nxentry = NXentry(name=map_config.title)
203
+ nxroot[nxentry.nxname] = nxentry
204
+ nxentry.set_default()
205
+
206
+ # Add configuration fields
194
207
  nxentry.definition = 'NXtomo'
195
208
  nxentry.map_config = tomofields.map_config
196
209
 
@@ -214,12 +227,12 @@ class TomoCHESSMapConverter(Processor):
214
227
  # Add an NXdetector to the NXinstrument
215
228
  # (do not fill in data fields yet)
216
229
  detector_prefix = detector_config.prefix
217
- detectors = list(set(tomofields.data.entries)
218
- - set(independent_dimensions))
230
+ detectors = list(
231
+ set(tomofields.data.entries) - set(independent_dimensions))
219
232
  if detector_prefix not in detectors:
220
233
  raise ValueError(f'Data for detector {detector_prefix} is '
221
234
  f'unavailable (available detectors: {detectors})')
222
- tomo_stacks = np.asarray(tomofields.data[detector_prefix])
235
+ tomo_stacks = tomofields.data[detector_prefix]
223
236
  tomo_stack_shape = tomo_stacks.shape
224
237
  assert len(tomo_stack_shape) == 2+len(independent_dimensions)
225
238
  assert tomo_stack_shape[-2] == detector_config.rows
@@ -272,8 +285,8 @@ class TomoCHESSMapConverter(Processor):
272
285
  num_image = data_shape[0]
273
286
  image_keys += num_image*[2]
274
287
  sequence_numbers += list(range(num_image))
275
- image_stacks.append(np.asarray(
276
- nxcollection.data[detector_prefix]))
288
+ image_stacks.append(
289
+ nxcollection.data[detector_prefix])
277
290
  rotation_angles += num_image*[0.0]
278
291
  if (x_translation_data_type == 'spec_motor' or
279
292
  z_translation_data_type == 'spec_motor'):
@@ -312,8 +325,8 @@ class TomoCHESSMapConverter(Processor):
312
325
  num_image = data_shape[0]
313
326
  image_keys += num_image*[1]
314
327
  sequence_numbers += list(range(num_image))
315
- image_stacks.append(np.asarray(
316
- nxcollection.data[detector_prefix]))
328
+ image_stacks.append(
329
+ nxcollection.data[detector_prefix])
317
330
  rotation_angles += num_image*[0.0]
318
331
  if (x_translation_data_type == 'spec_motor' or
319
332
  z_translation_data_type == 'spec_motor'):
@@ -347,10 +360,11 @@ class TomoCHESSMapConverter(Processor):
347
360
  z_trans = [0.0]
348
361
  tomo_stacks = np.reshape(tomo_stacks, (1,1,*tomo_stacks.shape))
349
362
  else:
350
- if len(list(tomofields.data.z_translation)):
351
- z_trans = list(tomofields.data.z_translation)
352
- else:
353
- z_trans = [float(tomofields.data.z_translation)]
363
+ z_trans = tomofields.data.z_translation.nxdata
364
+ # if len(list(tomofields.data.z_translation)):
365
+ # z_trans = list(tomofields.data.z_translation)
366
+ # else:
367
+ # z_trans = [float(tomofields.data.z_translation)]
354
368
  if rotation_angles_index < z_translation_index:
355
369
  tomo_stacks = np.swapaxes(
356
370
  tomo_stacks, rotation_angles_index,
@@ -363,14 +377,16 @@ class TomoCHESSMapConverter(Processor):
363
377
  tomo_stacks, rotation_angles_index, x_translation_index)
364
378
  tomo_stacks = np.expand_dims(tomo_stacks, 0)
365
379
  else:
366
- if len(list(tomofields.data.x_translation)):
367
- x_trans = list(tomofields.data.x_translation)
368
- else:
369
- x_trans = [float(tomofields.data.x_translation)]
370
- if len(list(tomofields.data.z_translation)):
371
- z_trans = list(tomofields.data.z_translation)
372
- else:
373
- z_trans = [float(tomofields.data.z_translation)]
380
+ x_trans = tomofields.data.x_translation.nxdata
381
+ z_trans = tomofields.data.z_translation.nxdata
382
+ #if tomofields.data.x_translation.size > 1:
383
+ # x_trans = list(tomofields.data.x_translation)
384
+ #else:
385
+ # x_trans = [float(tomofields.data.x_translation)]
386
+ #if len(list(tomofields.data.z_translation)):
387
+ # z_trans = list(tomofields.data.z_translation)
388
+ #else:
389
+ # z_trans = [float(tomofields.data.z_translation)]
374
390
  if (rotation_angles_index
375
391
  < max(x_translation_index, z_translation_index)):
376
392
  tomo_stacks = np.swapaxes(
@@ -380,10 +396,8 @@ class TomoCHESSMapConverter(Processor):
380
396
  tomo_stacks = np.swapaxes(
381
397
  tomo_stacks, x_translation_index, z_translation_index)
382
398
  # Restrict to 180 degrees set of data for now to match old code
383
- thetas = np.asarray(tomofields.data.rotation_angles)
384
- #RV num_image = len(tomofields.data.rotation_angles)
399
+ thetas = tomofields.data.rotation_angles.nxdata
385
400
  assert len(thetas) > 2
386
- from CHAP.utils.general import index_nearest
387
401
  delta_theta = thetas[1]-thetas[0]
388
402
  if thetas[-1]-thetas[0] > 180-delta_theta:
389
403
  image_end = index_nearest(thetas, thetas[0]+180)
@@ -395,10 +409,8 @@ class TomoCHESSMapConverter(Processor):
395
409
  for j, x in enumerate(x_trans):
396
410
  image_keys += num_image*[0]
397
411
  sequence_numbers += list(range(num_image))
398
- image_stacks.append(np.asarray(
399
- tomo_stacks[i,j][:image_end,:,:]))
412
+ image_stacks.append(tomo_stacks[i,j,:image_end,:,:])
400
413
  rotation_angles += list(thetas)
401
- #RV rotation_angles += list(tomofields.data.rotation_angles)
402
414
  x_translations += num_image*[x]
403
415
  z_translations += num_image*[z]
404
416
 
@@ -416,18 +428,12 @@ class TomoCHESSMapConverter(Processor):
416
428
  nxsample.z_translation.units = 'mm'
417
429
 
418
430
  # Add an NXdata to NXentry
419
- nxdata = NXdata()
420
- nxentry.data = nxdata
421
- nxdata.makelink(nxentry.instrument.detector.data, name='data')
422
- nxdata.makelink(nxentry.instrument.detector.image_key)
423
- nxdata.makelink(nxentry.sample.rotation_angle)
424
- nxdata.makelink(nxentry.sample.x_translation)
425
- nxdata.makelink(nxentry.sample.z_translation)
426
- nxdata.attrs['signal'] = 'data'
427
- # nxdata.attrs['axes'] = ['field', 'row', 'column']
428
- # nxdata.attrs['field_indices'] = 0
429
- # nxdata.attrs['row_indices'] = 1
430
- # nxdata.attrs['column_indices'] = 2
431
+ nxentry.data = NXdata(NXlink(nxentry.instrument.detector.data))
432
+ nxentry.data.makelink(nxentry.instrument.detector.image_key)
433
+ nxentry.data.makelink(nxentry.sample.rotation_angle)
434
+ nxentry.data.makelink(nxentry.sample.x_translation)
435
+ nxentry.data.makelink(nxentry.sample.z_translation)
436
+ nxentry.data.set_default()
431
437
 
432
438
  return nxroot
433
439
 
@@ -440,9 +446,9 @@ class TomoDataProcessor(Processor):
440
446
  """
441
447
 
442
448
  def process(
443
- self, data, interactive=False, reduce_data=False,
444
- find_center=False, reconstruct_data=False, combine_data=False,
445
- output_folder='.', save_figs='no', **kwargs):
449
+ self, data, outputdir='.', interactive=False, reduce_data=False,
450
+ find_center=False, calibrate_center=False, reconstruct_data=False,
451
+ combine_data=False, save_figs='no'):
446
452
  """
447
453
  Process the input map or configuration with the step specific
448
454
  instructions and return either a dictionary or a
@@ -451,23 +457,26 @@ class TomoDataProcessor(Processor):
451
457
  :param data: Input configuration and specific step instructions
452
458
  for tomographic image reduction.
453
459
  :type data: list[PipelineData]
460
+ :param outputdir: Output folder name, defaults to '.'.
461
+ :type outputdir:: str, optional
454
462
  :param interactive: Allows for user interactions,
455
463
  defaults to False.
456
464
  :type interactive: bool, optional
457
465
  :param reduce_data: Generate reduced tomography images,
458
466
  defaults to False.
459
467
  :type reduce_data: bool, optional
460
- :param find_center: Find the calibrated center axis info,
468
+ :param find_center: Generate calibrated center axis info,
461
469
  defaults to False.
462
470
  :type find_center: bool, optional
471
+ :param calibrate_center: Calibrate the rotation axis,
472
+ defaults to False.
473
+ :type calibrate_center: bool, optional
463
474
  :param reconstruct_data: Reconstruct the tomography data,
464
475
  defaults to False.
465
476
  :type reconstruct_data: bool, optional
466
477
  :param combine_data: Combine the reconstructed tomography
467
478
  stacks, defaults to False.
468
479
  :type combine_data: bool, optional
469
- :param output_folder: Output folder name, defaults to '.'.
470
- :type output_folder:: str, optional
471
480
  :param save_figs: Safe figures to file ('yes' or 'only') and/or
472
481
  display figures ('yes' or 'no'), defaults to 'no'.
473
482
  :type save_figs: Literal['yes', 'no', 'only'], optional
@@ -478,10 +487,7 @@ class TomoDataProcessor(Processor):
478
487
  :rtype: Union[dict, nexusformat.nexus.NXroot]
479
488
  """
480
489
  # Local modules
481
- from nexusformat.nexus import (
482
- nxsetconfig,
483
- NXroot,
484
- )
490
+ from nexusformat.nexus import nxsetconfig
485
491
  from CHAP.pipeline import PipelineItem
486
492
  from CHAP.tomo.models import (
487
493
  TomoReduceConfig,
@@ -494,6 +500,9 @@ class TomoDataProcessor(Processor):
494
500
  raise ValueError(f'Invalid parameter reduce_data ({reduce_data})')
495
501
  if not isinstance(find_center, bool):
496
502
  raise ValueError(f'Invalid parameter find_center ({find_center})')
503
+ if not isinstance(calibrate_center, bool):
504
+ raise ValueError(
505
+ f'Invalid parameter calibrate_center ({calibrate_center})')
497
506
  if not isinstance(reconstruct_data, bool):
498
507
  raise ValueError(
499
508
  f'Invalid parameter reconstruct_data ({reconstruct_data})')
@@ -524,19 +533,41 @@ class TomoDataProcessor(Processor):
524
533
  nxroot = get_nxroot(data)
525
534
 
526
535
  tomo = Tomo(
527
- interactive=interactive, output_folder=output_folder,
528
- save_figs=save_figs)
536
+ logger=self.logger, interactive=interactive,
537
+ outputdir=outputdir, save_figs=save_figs)
529
538
 
530
539
  nxsetconfig(memory=100000)
531
540
 
541
+ # Calibrate the rotation axis
542
+ if calibrate_center:
543
+ if (reduce_data or find_center
544
+ or reconstruct_data or reconstruct_data_config is not None
545
+ or combine_data or combine_data_config is not None):
546
+ self.logger.warning('Ignoring any step specific instructions '
547
+ 'during center calibration')
548
+ if nxroot is None:
549
+ raise RuntimeError('Map info required to calibrate the '
550
+ 'rotation axis')
551
+ if find_center_config is None:
552
+ find_center_config = TomoFindCenterConfig()
553
+ calibrate_center_rows = True
554
+ else:
555
+ calibrate_center_rows = find_center_config.center_rows
556
+ if calibrate_center_rows == None:
557
+ calibrate_center_rows = True
558
+ nxroot, calibrate_center_rows = tomo.reduce_data(
559
+ nxroot, reduce_data_config, calibrate_center_rows)
560
+ return tomo.find_centers(
561
+ nxroot, find_center_config, calibrate_center_rows)
562
+
532
563
  # Reduce tomography images
533
564
  if reduce_data or reduce_data_config is not None:
534
565
  if nxroot is None:
535
566
  raise RuntimeError('Map info required to reduce the '
536
567
  'tomography images')
537
- nxroot = tomo.reduce_data(nxroot, reduce_data_config)
568
+ nxroot, _ = tomo.reduce_data(nxroot, reduce_data_config)
538
569
 
539
- # Find rotation axis centers for the tomography stacks
570
+ # Find calibrated center axis info for the tomography stacks
540
571
  center_config = None
541
572
  if find_center or find_center_config is not None:
542
573
  run_find_centers = False
@@ -561,7 +592,7 @@ class TomoDataProcessor(Processor):
561
592
 
562
593
  # Reconstruct tomography stacks
563
594
  # RV pass reconstruct_data_config and center_config directly to
564
- # tomo.reconstruct_data?
595
+ # tomo.reconstruct_data?
565
596
  if reconstruct_data or reconstruct_data_config is not None:
566
597
  if reconstruct_data_config is None:
567
598
  reconstruct_data_config = TomoReconstructConfig()
@@ -579,49 +610,6 @@ class TomoDataProcessor(Processor):
579
610
  return center_config
580
611
  return nxroot
581
612
 
582
- def nxcopy(nxobject, exclude_nxpaths=None, nxpath_prefix=''):
583
- """
584
- Function that returns a copy of a nexus object, optionally exluding
585
- certain child items.
586
-
587
- :param nxobject: The input nexus object to "copy".
588
- :type nxobject: nexusformat.nexus.NXobject
589
- :param exlude_nxpaths: A list of paths to child nexus objects that
590
- should be excluded from the returned "copy", defaults to `[]`.
591
- :type exclude_nxpaths: list[str], optional
592
- :param nxpath_prefix: For use in recursive calls from inside this
593
- function only.
594
- :type nxpath_prefix: str
595
- :return: Copy of the input `nxobject` with some children optionally
596
- exluded.
597
- :rtype: nexusformat.nexus.NXobject
598
- """
599
- # Third party modules
600
- from nexusformat.nexus import NXgroup
601
-
602
- nxobject_copy = nxobject.__class__()
603
- if not nxpath_prefix:
604
- if 'default' in nxobject.attrs:
605
- nxobject_copy.attrs['default'] = nxobject.attrs['default']
606
- else:
607
- for k, v in nxobject.attrs.items():
608
- nxobject_copy.attrs[k] = v
609
-
610
- if exclude_nxpaths is None:
611
- exclude_nxpaths = []
612
- for k, v in nxobject.items():
613
- nxpath = os_path.join(nxpath_prefix, k)
614
- if nxpath in exclude_nxpaths:
615
- continue
616
- if isinstance(v, NXgroup):
617
- nxobject_copy[k] = nxcopy(
618
- v, exclude_nxpaths=exclude_nxpaths,
619
- nxpath_prefix=os_path.join(nxpath_prefix, k))
620
- else:
621
- nxobject_copy[k] = v
622
-
623
- return nxobject_copy
624
-
625
613
 
626
614
  class SetNumexprThreads:
627
615
  """
@@ -667,8 +655,8 @@ class Tomo:
667
655
  """Reconstruct a set of tomographic images."""
668
656
 
669
657
  def __init__(
670
- self, interactive=False, num_core=-1, output_folder='.',
671
- save_figs='no', test_mode=False):
658
+ self, logger=None, outputdir='.', interactive=False, num_core=-1,
659
+ save_figs='no'):
672
660
  """
673
661
  Initialize Tomo.
674
662
 
@@ -677,42 +665,32 @@ class Tomo:
677
665
  :type interactive: bool, optional
678
666
  :param num_core: Number of processors.
679
667
  :type num_core: int
680
- :param output_folder: Output folder name, defaults to '.'.
681
- :type output_folder:: str, optional
668
+ :param outputdir: Output folder name, defaults to '.'.
669
+ :type outputdir:: str, optional
682
670
  :param save_figs: Safe figures to file ('yes' or 'only') and/or
683
671
  display figures ('yes' or 'no'), defaults to 'no'.
684
672
  :type save_figs: Literal['yes', 'no', 'only'], optional
685
- :param test_mode: Run in test mode (non-interactively), defaults
686
- to False.
687
- :type test_mode: bool, optional
688
673
  :raises ValueError: Invalid input parameter.
689
674
  """
690
675
  # System modules
691
- from logging import getLogger
692
676
  from multiprocessing import cpu_count
693
677
 
694
678
  self.__name__ = self.__class__.__name__
695
- self._logger = getLogger(self.__name__)
696
- self._logger.propagate = False
679
+ if logger is None:
680
+ # System modules
681
+ from logging import getLogger
682
+
683
+ self._logger = getLogger(self.__name__)
684
+ self._logger.propagate = False
685
+ else:
686
+ self._logger = logger
697
687
 
698
688
  if not isinstance(interactive, bool):
699
689
  raise ValueError(f'Invalid parameter interactive ({interactive})')
690
+ self._outputdir = outputdir
700
691
  self._interactive = interactive
701
692
  self._num_core = num_core
702
- self._output_folder = os_path.abspath(output_folder)
703
- if not os_path.isdir(self._output_folder):
704
- mkdir(self._output_folder)
705
- if self._interactive:
706
- self._test_mode = False
707
- else:
708
- if not isinstance(test_mode, bool):
709
- raise ValueError(f'Invalid parameter test_mode ({test_mode})')
710
- self._test_mode = test_mode
711
693
  self._test_config = {}
712
- if self._test_mode:
713
- if save_figs != 'only':
714
- self._logger.warning('Ignoring save_figs in test mode')
715
- save_figs = 'only'
716
694
  if save_figs == 'only':
717
695
  self._save_only = True
718
696
  self._save_figs = True
@@ -737,8 +715,15 @@ class Tomo:
737
715
  f'num_core = {self._num_core} is larger than the number '
738
716
  f'of available processors and reduced to {cpu_count()}')
739
717
  self._num_core = cpu_count()
718
+ # Tompy py uses numexpr with NUMEXPR_MAX_THREADS = 64
719
+ if self._num_core > 64:
720
+ self._logger.warning(
721
+ f'num_core = {self._num_core} is larger than the number '
722
+ f'of processors suitable to Tomopy and reduced to 64')
723
+ self._num_core = 64
740
724
 
741
- def reduce_data(self, nxroot, tool_config=None):
725
+ def reduce_data(
726
+ self, nxroot, tool_config=None, calibrate_center_rows=False):
742
727
  """
743
728
  Reduced the tomography images.
744
729
 
@@ -760,8 +745,9 @@ class Tomo:
760
745
 
761
746
  self._logger.info('Generate the reduced tomography images')
762
747
 
748
+ # Validate input parameter
763
749
  if isinstance(nxroot, NXroot):
764
- nxentry = nxroot[nxroot.attrs['default']]
750
+ nxentry = nxroot[nxroot.default]
765
751
  else:
766
752
  raise ValueError(
767
753
  f'Invalid parameter nxroot {type(nxroot)}:\n{nxroot}')
@@ -770,14 +756,22 @@ class Tomo:
770
756
  img_row_bounds = None
771
757
  else:
772
758
  delta_theta = tool_config.delta_theta
773
- img_row_bounds = tool_config.img_row_bounds
774
-
759
+ img_row_bounds = tuple(tool_config.img_row_bounds)
760
+ if img_row_bounds is not None:
761
+ if (nxentry.instrument.source.attrs['station']
762
+ in ('id1a3', 'id3a')):
763
+ self._logger.warning('Ignoring parameter img_row_bounds '
764
+ 'for id1a3 and id3a')
765
+ img_row_bounds = None
766
+ elif calibrate_center_rows:
767
+ self._logger.warning('Ignoring parameter img_row_bounds '
768
+ 'during rotation axis calibration')
769
+ img_row_bounds = None
775
770
  image_key = nxentry.instrument.detector.get('image_key', None)
776
771
  if image_key is None or 'data' not in nxentry.instrument.detector:
777
772
  raise ValueError(f'Unable to find image_key or data in '
778
773
  'instrument.detector '
779
774
  f'({nxentry.instrument.detector.tree})')
780
- image_key = np.asarray(image_key)
781
775
 
782
776
  # Create an NXprocess to store data reduction (meta)data
783
777
  reduced_data = NXprocess()
@@ -788,7 +782,7 @@ class Tomo:
788
782
  # Generate bright field
789
783
  reduced_data = self._gen_bright(nxentry, reduced_data, image_key)
790
784
 
791
- # Get rotation angles for image stacks
785
+ # Get rotation angles for image stacks (in degrees)
792
786
  thetas = self._gen_thetas(nxentry, image_key)
793
787
 
794
788
  # Get the image stack mask to remove bad images from stack
@@ -798,13 +792,13 @@ class Tomo:
798
792
  if delta_theta is not None:
799
793
  delta_theta = None
800
794
  self._logger.warning(
801
- 'Ignore delta_theta when an image mask is used')
795
+ 'Ignoring delta_theta when an image mask is used')
802
796
  np.random.seed(0)
803
797
  image_mask = np.where(np.random.rand(
804
798
  len(thetas)) < drop_fraction/100, 0, 1).astype(bool)
805
799
 
806
800
  # Set zoom and/or rotation angle interval to reduce memory
807
- # requirement
801
+ # requirement
808
802
  if image_mask is None:
809
803
  zoom_perc, delta_theta = self._set_zoom_or_delta_theta(
810
804
  thetas, delta_theta)
@@ -820,55 +814,66 @@ class Tomo:
820
814
  self._logger.debug(f'image_mask = {image_mask}')
821
815
  reduced_data.image_mask = image_mask
822
816
  thetas = thetas[image_mask]
823
- self._logger.debug(f'thetas = {thetas}')
824
- reduced_data.rotation_angle = thetas
825
- reduced_data.rotation_angle.units = 'degrees'
826
817
 
827
- # Set vertical detector bounds for image stack
818
+ # Set vertical detector bounds for image stack or rotation
819
+ # axis calibration rows
828
820
  img_row_bounds = self._set_detector_bounds(
829
821
  nxentry, reduced_data, image_key, thetas[0],
830
- img_row_bounds=img_row_bounds)
831
- self._logger.info(f'img_row_bounds = {img_row_bounds}')
822
+ img_row_bounds, calibrate_center_rows)
823
+ self._logger.debug(f'img_row_bounds = {img_row_bounds}')
824
+ if calibrate_center_rows:
825
+ calibrate_center_rows = tuple(sorted(img_row_bounds))
826
+ img_row_bounds = None
827
+ if img_row_bounds is None:
828
+ tbf_shape = reduced_data.data.bright_field.shape
829
+ img_row_bounds = (0, tbf_shape[0])
832
830
  reduced_data.img_row_bounds = img_row_bounds
833
831
  reduced_data.img_row_bounds.units = 'pixels'
832
+ reduced_data.img_row_bounds.attrs['long_name'] = \
833
+ 'image row boundaries in detector frame of reference'
834
+
835
+ # Store rotation angles for image stacks
836
+ self._logger.debug(f'thetas = {thetas}')
837
+ reduced_data.rotation_angle = thetas
838
+ reduced_data.rotation_angle.units = 'degrees'
834
839
 
835
840
  # Generate reduced tomography fields
836
- reduced_data = self._gen_tomo(nxentry, reduced_data, image_key)
841
+ reduced_data = self._gen_tomo(
842
+ nxentry, reduced_data, image_key, calibrate_center_rows)
837
843
 
838
844
  # Create a copy of the input Nexus object and remove raw and
839
- # any existing reduced data
840
- if isinstance(nxroot, NXroot):
841
- exclude_items = [
842
- f'{nxentry.nxname}/reduced_data/data',
843
- f'{nxentry.nxname}/instrument/detector/data',
844
- f'{nxentry.nxname}/instrument/detector/image_key',
845
- f'{nxentry.nxname}/instrument/detector/sequence_number',
846
- f'{nxentry.nxname}/sample/rotation_angle',
847
- f'{nxentry.nxname}/sample/x_translation',
848
- f'{nxentry.nxname}/sample/z_translation',
849
- f'{nxentry.nxname}/data/data',
850
- f'{nxentry.nxname}/data/image_key',
851
- f'{nxentry.nxname}/data/rotation_angle',
852
- f'{nxentry.nxname}/data/x_translation',
853
- f'{nxentry.nxname}/data/z_translation',
854
- ]
855
- nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
856
- nxentry = nxroot[nxroot.attrs['default']]
845
+ # any existing reduced data
846
+ exclude_items = [
847
+ f'{nxentry.nxname}/reduced_data/data',
848
+ f'{nxentry.nxname}/instrument/detector/data',
849
+ f'{nxentry.nxname}/instrument/detector/image_key',
850
+ f'{nxentry.nxname}/instrument/detector/sequence_number',
851
+ f'{nxentry.nxname}/sample/rotation_angle',
852
+ f'{nxentry.nxname}/sample/x_translation',
853
+ f'{nxentry.nxname}/sample/z_translation',
854
+ f'{nxentry.nxname}/data/data',
855
+ f'{nxentry.nxname}/data/image_key',
856
+ f'{nxentry.nxname}/data/rotation_angle',
857
+ f'{nxentry.nxname}/data/x_translation',
858
+ f'{nxentry.nxname}/data/z_translation',
859
+ ]
860
+ nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
857
861
 
858
862
  # Add the reduced data NXprocess
863
+ nxentry = nxroot[nxroot.default]
859
864
  nxentry.reduced_data = reduced_data
860
865
 
861
866
  if 'data' not in nxentry:
862
867
  nxentry.data = NXdata()
868
+ nxentry.data.set_default()
863
869
  nxentry.data.makelink(
864
870
  nxentry.reduced_data.data.tomo_fields, name='reduced_data')
865
- nxentry.data.makelink(
866
- nxentry.reduced_data.rotation_angle, name='rotation_angle')
871
+ nxentry.data.makelink(nxentry.reduced_data.rotation_angle)
867
872
  nxentry.data.attrs['signal'] = 'reduced_data'
868
873
 
869
- return nxroot
874
+ return nxroot, calibrate_center_rows
870
875
 
871
- def find_centers(self, nxroot, tool_config):
876
+ def find_centers(self, nxroot, tool_config, calibrate_center_rows=False):
872
877
  """
873
878
  Find the calibrated center axis info
874
879
 
@@ -883,106 +888,83 @@ class Tomo:
883
888
  :rtype: dict
884
889
  """
885
890
  # Third party modules
886
- from nexusformat.nexus import (
887
- NXentry,
888
- NXroot,
889
- )
891
+ from nexusformat.nexus import NXroot
890
892
  from yaml import safe_dump
891
893
 
892
894
  self._logger.info('Find the calibrated center axis info')
893
895
 
894
896
  if isinstance(nxroot, NXroot):
895
- nxentry = nxroot[nxroot.attrs['default']]
897
+ nxentry = nxroot[nxroot.default]
896
898
  else:
897
899
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
898
- center_rows = tool_config.center_rows
899
- center_stack_index = tool_config.center_stack_index
900
- if (center_stack_index is not None
901
- and (not isinstance(center_stack_index, int)
902
- or center_stack_index < 0)):
903
- raise ValueError(
904
- 'Invalid parameter center_stack_index '
905
- f'({center_stack_index})')
906
900
 
907
901
  # Check if reduced data is available
908
- if ('reduced_data' not in nxentry
909
- or 'reduced_data' not in nxentry.data):
902
+ if 'reduced_data' not in nxentry:
910
903
  raise ValueError(f'Unable to find valid reduced data in {nxentry}.')
911
904
 
912
- # Get full bright field
913
- tbf = np.asarray(nxentry.reduced_data.data.bright_field)
914
- tbf_shape = tbf.shape
915
-
916
- # Get image bounds
917
- img_row_bounds = tuple(
918
- nxentry.reduced_data.get('img_row_bounds', (0, tbf_shape[0])))
919
- img_column_bounds = tuple(
920
- nxentry.reduced_data.get('img_column_bounds', (0, tbf_shape[1])))
921
-
922
- # Select the image stack to calibrate the center axis
923
- # reduced data axes order: stack,theta,row,column
905
+ # Select the image stack to find the calibrated center axis
906
+ # reduced data axes order: stack,theta,row,column
924
907
  # Note: Nexus can't follow a link if the data it points to is
925
- # too big get the data from the actual place, not from
926
- # nxentry.data
908
+ # too big get the data from the actual place, not from
909
+ # nxentry.data
927
910
  num_tomo_stacks = nxentry.reduced_data.data.tomo_fields.shape[0]
928
- img_shape = nxentry.reduced_data.data.bright_field.shape
929
- num_row = int(img_row_bounds[1] - img_row_bounds[0])
911
+ self._logger.debug(f'num_tomo_stacks = {num_tomo_stacks}')
930
912
  if num_tomo_stacks == 1:
931
913
  center_stack_index = 0
932
- default = 'n'
933
914
  else:
934
- if self._test_mode:
935
- # Convert input value to offset 0
936
- center_stack_index = self._test_config['center_stack_index']
915
+ center_stack_index = tool_config.center_stack_index
916
+ if calibrate_center_rows:
917
+ center_stack_index = num_tomo_stacks//2
937
918
  elif self._interactive:
938
919
  if center_stack_index is None:
939
920
  center_stack_index = input_int(
940
921
  '\nEnter tomography stack index to calibrate the '
941
922
  'center axis', ge=0, lt=num_tomo_stacks,
942
- default=int(num_tomo_stacks/2))
923
+ default=num_tomo_stacks//2)
943
924
  else:
944
925
  if center_stack_index is None:
945
- center_stack_index = int(num_tomo_stacks/2)
926
+ center_stack_index = num_tomo_stacks//2
946
927
  self._logger.warning(
947
928
  'center_stack_index unspecified, use stack '
948
- f'{center_stack_index} to find centers')
949
- default = 'y'
929
+ f'{center_stack_index} to find center axis info')
950
930
 
951
931
  # Get thetas (in degrees)
952
- thetas = np.asarray(nxentry.reduced_data.rotation_angle)
953
-
954
- # Get effective pixel_size
955
- if 'zoom_perc' in nxentry.reduced_data:
956
- eff_pixel_size = float(
957
- 100. * (nxentry.instrument.detector.row_pixel_size
958
- / nxentry.reduced_data.attrs['zoom_perc']))
959
- else:
960
- eff_pixel_size = float(nxentry.instrument.detector.row_pixel_size)
961
-
962
- # Get cross sectional diameter
963
- cross_sectional_dim = img_shape[1]*eff_pixel_size
964
- self._logger.debug(f'cross_sectional_dim = {cross_sectional_dim}')
965
-
966
- # Determine center offset at sample row boundaries
967
- self._logger.info('Determine center offset at sample row boundaries')
932
+ thetas = nxentry.reduced_data.rotation_angle.nxdata
968
933
 
969
934
  # Select center rows
970
- if self._test_mode:
971
- center_rows = tuple(self._test_config['center_rows'])
935
+ if calibrate_center_rows:
936
+ center_rows = calibrate_center_rows
937
+ offset_center_rows = (0, 1)
972
938
  else:
973
939
  # Third party modules
974
940
  import matplotlib.pyplot as plt
975
941
 
942
+ # Get full bright field
943
+ tbf = nxentry.reduced_data.data.bright_field.nxdata
944
+ tbf_shape = tbf.shape
945
+
946
+ # Get image bounds
947
+ img_row_bounds = nxentry.reduced_data.get(
948
+ 'img_row_bounds', (0, tbf_shape[0]))
949
+ img_row_bounds = (int(img_row_bounds[0]), int(img_row_bounds[1]))
950
+ img_column_bounds = nxentry.reduced_data.get(
951
+ 'img_column_bounds', (0, tbf_shape[1]))
952
+ img_column_bounds = (
953
+ int(img_column_bounds[0]), int(img_column_bounds[1]))
954
+
955
+ center_rows = tool_config.center_rows
976
956
  if center_rows is None:
977
957
  if num_tomo_stacks == 1:
978
958
  # Add a small margin to avoid edge effects
979
- offset = min(5, int(0.1*num_row))
980
- center_rows = (offset, num_row-1-offset)
959
+ offset = min(
960
+ 5, int(0.1*(img_row_bounds[1] - img_row_bounds[0])))
961
+ center_rows = (
962
+ img_row_bounds[0]+offset, img_row_bounds[1]-1-offset)
981
963
  else:
982
964
  if not self._interactive:
983
965
  self._logger.warning('center_rows unspecified, find '
984
966
  'centers at reduced data bounds')
985
- center_rows = (0, num_row-1)
967
+ center_rows = (img_row_bounds[0], img_row_bounds[1]-1)
986
968
  fig, center_rows = select_image_indices(
987
969
  nxentry.reduced_data.data.tomo_fields[
988
970
  center_stack_index,0,:,:],
@@ -990,38 +972,60 @@ class Tomo:
990
972
  b=tbf[img_row_bounds[0]:img_row_bounds[1],
991
973
  img_column_bounds[0]:img_column_bounds[1]],
992
974
  preselected_indices=center_rows,
993
- title='Select or adjust two detector image row indices to '
994
- f'find center axis (in range [0, {num_row-1}])',
975
+ axis_index_offset=img_row_bounds[0],
976
+ title='Select two detector image row indices to find center '
977
+ f'axis (in range [{img_row_bounds[0]}, '
978
+ f'{img_row_bounds[1]-1}])',
995
979
  title_a=r'Tomography image at $\theta$ = '
996
980
  f'{round(thetas[0], 2)+0}',
997
981
  title_b='Bright field', interactive=self._interactive)
998
- if center_rows[1] == num_row:
982
+ if center_rows[1] == img_row_bounds[1]:
999
983
  center_rows = (center_rows[0], center_rows[1]-1)
984
+ offset_center_rows = (
985
+ center_rows[0] - img_row_bounds[0],
986
+ center_rows[1] - img_row_bounds[0])
1000
987
  # Plot results
1001
988
  if self._save_figs:
1002
989
  fig.savefig(
1003
- os_path.join(
1004
- self._output_folder, 'center_finding_rows.png'))
990
+ os_path.join(self._outputdir, 'center_finding_rows.png'))
1005
991
  plt.close()
1006
992
 
993
+ # Get effective pixel_size
994
+ if 'zoom_perc' in nxentry.reduced_data:
995
+ eff_pixel_size = float(
996
+ 100. * (nxentry.instrument.detector.row_pixel_size
997
+ / nxentry.reduced_data.attrs['zoom_perc']))
998
+ else:
999
+ eff_pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1000
+ self._logger.debug(f'eff_pixel_size = {eff_pixel_size}')
1001
+
1002
+ # Get cross sectional diameter
1003
+ cross_sectional_dim = \
1004
+ eff_pixel_size * nxentry.reduced_data.data.bright_field.shape[1]
1005
+ self._logger.debug(f'cross_sectional_dim = {cross_sectional_dim}')
1006
+
1007
1007
  # Find the center offsets at each of the center rows
1008
+ prev_center_offset = None
1008
1009
  center_offsets = []
1009
- for i, center_row in enumerate(center_rows):
1010
+ for row, offset_row in zip(center_rows, offset_center_rows):
1010
1011
  t0 = time()
1011
1012
  center_offsets.append(
1012
1013
  self._find_center_one_plane(
1013
- nxentry.reduced_data.data.tomo_fields[
1014
- center_stack_index,:,center_row,:],
1015
- center_row, thetas, eff_pixel_size, cross_sectional_dim,
1016
- path=self._output_folder, num_core=self._num_core,
1017
- search_range=tool_config.search_range,
1018
- search_step=tool_config.search_step,
1014
+ nxentry.reduced_data.data.tomo_fields, center_stack_index,
1015
+ row, offset_row, np.radians(thetas), eff_pixel_size,
1016
+ cross_sectional_dim, path=self._outputdir,
1017
+ num_core=self._num_core,
1018
+ center_offset_min=tool_config.center_offset_min,
1019
+ center_offset_max=tool_config.center_offset_max,
1020
+ center_search_range=tool_config.center_search_range,
1019
1021
  gaussian_sigma=tool_config.gaussian_sigma,
1020
- ring_width=tool_config.ring_width))
1022
+ ring_width=tool_config.ring_width,
1023
+ prev_center_offset=prev_center_offset))
1021
1024
  self._logger.info(
1022
- f'Finding center {i} took {time()-t0:.2f} seconds')
1023
- self._logger.debug(f'center_row {i} = {center_rows[i]:.2f}')
1024
- self._logger.debug(f'center_offset {i} = {center_offsets[i]:.2f}')
1025
+ f'Finding center row {row} took {time()-t0:.2f} seconds')
1026
+ self._logger.debug(f'center_row = {row:.2f}')
1027
+ self._logger.debug(f'center_offset = {center_offsets[-1]:.2f}')
1028
+ prev_center_offset = center_offsets[-1]
1025
1029
 
1026
1030
  center_config = {
1027
1031
  'center_rows': list(center_rows),
@@ -1029,12 +1033,14 @@ class Tomo:
1029
1033
  }
1030
1034
  if num_tomo_stacks > 1:
1031
1035
  center_config['center_stack_index'] = center_stack_index
1032
-
1033
- # Save test data to file
1034
- if self._test_mode:
1035
- with open(f'{self._output_folder}/center_config.yaml', 'w',
1036
- encoding='utf8') as f:
1037
- safe_dump(center_config, f)
1036
+ if tool_config.center_offset_min is not None:
1037
+ center_config['center_offset_min'] = tool_config.center_offset_min
1038
+ if tool_config.center_offset_max is not None:
1039
+ center_config['center_offset_max'] = tool_config.center_offset_max
1040
+ if tool_config.gaussian_sigma is not None:
1041
+ center_config['gaussian_sigma'] = tool_config.gaussian_sigma
1042
+ if tool_config.ring_width is not None:
1043
+ center_config['ring_width'] = tool_config.ring_width
1038
1044
 
1039
1045
  return center_config
1040
1046
 
@@ -1056,9 +1062,8 @@ class Tomo:
1056
1062
  """
1057
1063
  # Third party modules
1058
1064
  from nexusformat.nexus import (
1059
- nxgetconfig,
1060
1065
  NXdata,
1061
- NXentry,
1066
+ NXfield,
1062
1067
  NXprocess,
1063
1068
  NXroot,
1064
1069
  )
@@ -1066,21 +1071,20 @@ class Tomo:
1066
1071
  self._logger.info('Reconstruct the tomography data')
1067
1072
 
1068
1073
  if isinstance(nxroot, NXroot):
1069
- nxentry = nxroot[nxroot.attrs['default']]
1074
+ nxentry = nxroot[nxroot.default]
1070
1075
  else:
1071
1076
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
1072
1077
  if not isinstance(center_info, dict):
1073
1078
  raise ValueError(f'Invalid parameter center_info ({center_info})')
1074
1079
 
1075
1080
  # Check if reduced data is available
1076
- if ('reduced_data' not in nxentry
1077
- or 'reduced_data' not in nxentry.data):
1081
+ if 'reduced_data' not in nxentry:
1078
1082
  raise ValueError(f'Unable to find valid reduced data in {nxentry}.')
1079
1083
 
1080
1084
  # Create an NXprocess to store image reconstruction (meta)data
1081
1085
  nxprocess = NXprocess()
1082
1086
 
1083
- # Get rotation axis rows and centers
1087
+ # Get calibrated center axis rows and centers
1084
1088
  center_rows = center_info.get('center_rows')
1085
1089
  center_offsets = center_info.get('center_offsets')
1086
1090
  if center_rows is None or center_offsets is None:
@@ -1091,35 +1095,28 @@ class Tomo:
1091
1095
  / (center_rows[1]-center_rows[0])
1092
1096
 
1093
1097
  # Get thetas (in degrees)
1094
- thetas = np.asarray(nxentry.reduced_data.rotation_angle)
1098
+ thetas = nxentry.reduced_data.rotation_angle.nxdata
1095
1099
 
1096
1100
  # Reconstruct tomography data
1097
- # reduced data axes order: stack,theta,row,column
1098
- # reconstructed data: row/-z,y,x
1101
+ # - reduced data axes order: stack,theta,row,column
1102
+ # - reconstructed data axes order: row/-z,y,x
1099
1103
  # Note: Nexus can't follow a link if the data it points to is
1100
- # too big get the data from the actual place, not from
1101
- # nxentry.data
1104
+ # too big get the data from the actual place, not from
1105
+ # nxentry.data
1102
1106
  if 'zoom_perc' in nxentry.reduced_data:
1103
1107
  res_title = f'{nxentry.reduced_data.attrs["zoom_perc"]}p'
1104
1108
  else:
1105
1109
  res_title = 'fullres'
1106
- tomo_stacks = np.asarray(nxentry.reduced_data.data.tomo_fields)
1110
+ tomo_stacks = nxentry.reduced_data.data.tomo_fields
1107
1111
  num_tomo_stacks = tomo_stacks.shape[0]
1108
- tomo_recon_stacks = num_tomo_stacks*[np.array([])]
1112
+ tomo_recon_stacks = []
1113
+ img_row_bounds = tuple(nxentry.reduced_data.get(
1114
+ 'img_row_bounds', (0, tomo_stacks.shape[2])))
1115
+ center_rows -= img_row_bounds[0]
1109
1116
  for i in range(num_tomo_stacks):
1110
1117
  # Convert reduced data stack from theta,row,column to
1111
- # row,theta,column
1112
- t0 = time()
1113
- tomo_stack = tomo_stacks[i]
1114
- self._logger.info(
1115
- f'Reading reduced data stack {i} took {time()-t0:.2f} '
1116
- 'seconds')
1117
- if (len(tomo_stack.shape) != 3
1118
- or any(True for dim in tomo_stack.shape if not dim)):
1119
- raise RuntimeError(
1120
- f'Unable to load tomography stack {i} for '
1121
- 'reconstruction')
1122
- tomo_stack = np.swapaxes(tomo_stack, 0, 1)
1118
+ # row,theta,column
1119
+ tomo_stack = np.swapaxes(tomo_stacks[i,:,:,:], 0, 1)
1123
1120
  assert len(thetas) == tomo_stack.shape[1]
1124
1121
  assert 0 <= center_rows[0] < center_rows[1] < tomo_stack.shape[0]
1125
1122
  center_offsets = [
@@ -1129,46 +1126,42 @@ class Tomo:
1129
1126
  ]
1130
1127
  t0 = time()
1131
1128
  tomo_recon_stack = self._reconstruct_one_tomo_stack(
1132
- tomo_stack, thetas, center_offsets=center_offsets,
1129
+ tomo_stack, np.radians(thetas), center_offsets=center_offsets,
1133
1130
  num_core=self._num_core, algorithm='gridrec',
1134
1131
  secondary_iters=tool_config.secondary_iters,
1132
+ gaussian_sigma=tool_config.gaussian_sigma,
1135
1133
  remove_stripe_sigma=tool_config.remove_stripe_sigma,
1136
1134
  ring_width=tool_config.ring_width)
1137
1135
  self._logger.info(
1138
1136
  f'Reconstruction of stack {i} took {time()-t0:.2f} seconds')
1139
1137
 
1140
1138
  # Combine stacks
1141
- tomo_recon_stacks[i] = tomo_recon_stack
1139
+ tomo_recon_stacks.append(tomo_recon_stack)
1142
1140
 
1143
1141
  # Resize the reconstructed tomography data
1144
- # reconstructed data order in each stack: row/-z,y,x
1142
+ # - reconstructed axis data order in each stack: row/-z,y,x
1145
1143
  tomo_recon_shape = tomo_recon_stacks[0].shape
1146
- if self._test_mode:
1147
- x_bounds = tuple(self._test_config.get('x_bounds'))
1148
- y_bounds = tuple(self._test_config.get('y_bounds'))
1149
- z_bounds = (0, tomo_recon_shape[0])
1150
- else:
1151
- x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1152
- tomo_recon_stacks, x_bounds=tool_config.x_bounds,
1153
- y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
1144
+ x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1145
+ tomo_recon_stacks, x_bounds=tool_config.x_bounds,
1146
+ y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
1154
1147
  if x_bounds is None:
1155
1148
  x_range = (0, tomo_recon_shape[2])
1156
- x_slice = int(x_range[1]/2)
1149
+ x_slice = x_range[1]//2
1157
1150
  else:
1158
1151
  x_range = (min(x_bounds), max(x_bounds))
1159
- x_slice = int((x_bounds[0]+x_bounds[1]) / 2)
1152
+ x_slice = (x_bounds[0]+x_bounds[1])//2
1160
1153
  if y_bounds is None:
1161
1154
  y_range = (0, tomo_recon_shape[1])
1162
- y_slice = int(y_range[1] / 2)
1155
+ y_slice = y_range[1]//2
1163
1156
  else:
1164
1157
  y_range = (min(y_bounds), max(y_bounds))
1165
- y_slice = int((y_bounds[0]+y_bounds[1]) / 2)
1158
+ y_slice = (y_bounds[0]+y_bounds[1])//2
1166
1159
  if z_bounds is None:
1167
1160
  z_range = (0, tomo_recon_shape[0])
1168
- z_slice = int(z_range[1] / 2)
1161
+ z_slice = z_range[1]//2
1169
1162
  else:
1170
1163
  z_range = (min(z_bounds), max(z_bounds))
1171
- z_slice = int((z_bounds[0]+z_bounds[1]) / 2)
1164
+ z_slice = (z_bounds[0]+z_bounds[1])//2
1172
1165
  z_dim_org = tomo_recon_shape[0]
1173
1166
  for i, stack in enumerate(tomo_recon_stacks):
1174
1167
  tomo_recon_stacks[i] = stack[
@@ -1176,18 +1169,17 @@ class Tomo:
1176
1169
  x_range[0]:x_range[1]]
1177
1170
  tomo_recon_stacks = np.asarray(tomo_recon_stacks)
1178
1171
 
1179
- row_pixel_size = float(
1180
- nxentry.instrument.detector.row_pixel_size)
1181
- column_pixel_size = float(
1182
- nxentry.instrument.detector.column_pixel_size)
1172
+ detector = nxentry.instrument.detector
1173
+ row_pixel_size = float(detector.row_pixel_size)
1174
+ column_pixel_size = float(detector.column_pixel_size)
1183
1175
  if num_tomo_stacks == 1:
1184
1176
  # Convert the reconstructed tomography data from internal
1185
- # coordinate frame row/-z,y,x with the origin on the
1186
- # near-left-top corner to an z,y,x coordinate frame
1187
- # with the origin on the par file x,z values, halfway
1188
- # in the y-dimension.
1189
- # Here x is to the right, y along the beam direction
1190
- # and z upwards in the lab frame of reference
1177
+ # coordinate frame row/-z,y,x with the origin on the
1178
+ # near-left-top corner to an z,y,x coordinate frame with
1179
+ # the origin on the par file x,z values, halfway in the
1180
+ # y-dimension.
1181
+ # Here x is to the right, y along the beam direction and
1182
+ # z upwards in the lab frame of reference
1191
1183
  tomo_recon_stack = np.flip(tomo_recon_stacks[0], 0)
1192
1184
  z_range = (z_dim_org-z_range[1], z_dim_org-z_range[0])
1193
1185
 
@@ -1195,19 +1187,17 @@ class Tomo:
1195
1187
  x = column_pixel_size * (
1196
1188
  np.linspace(
1197
1189
  x_range[0], x_range[1], x_range[1]-x_range[0], False)
1198
- - 0.5*nxentry.instrument.detector.columns
1199
- + 0.5)
1190
+ - 0.5*detector.columns + 0.5)
1200
1191
  x = np.asarray(x + nxentry.reduced_data.x_translation[0])
1201
1192
  y = np.asarray(
1202
1193
  column_pixel_size * (
1203
1194
  np.linspace(
1204
1195
  y_range[0], y_range[1], y_range[1]-y_range[0], False)
1205
- - 0.5*nxentry.instrument.detector.columns
1206
- + 0.5))
1196
+ - 0.5*detector.columns + 0.5))
1207
1197
  z = row_pixel_size*(
1208
1198
  np.linspace(
1209
1199
  z_range[0], z_range[1], z_range[1]-z_range[0], False)
1210
- + nxentry.instrument.detector.rows
1200
+ + detector.rows
1211
1201
  - int(nxentry.reduced_data.img_row_bounds[1])
1212
1202
  + 0.5)
1213
1203
  z = np.asarray(z + nxentry.reduced_data.z_translation[0])
@@ -1223,7 +1213,7 @@ class Tomo:
1223
1213
  quick_imshow(
1224
1214
  tomo_recon_stack[:,:,x_index],
1225
1215
  title=f'recon {res_title} x={x[x_index]:.4f}',
1226
- origin='lower', extent=extent, path=self._output_folder,
1216
+ origin='lower', extent=extent, path=self._outputdir,
1227
1217
  save_fig=True, save_only=True)
1228
1218
  y_index = y_slice-y_range[0]
1229
1219
  extent = (
@@ -1234,7 +1224,7 @@ class Tomo:
1234
1224
  quick_imshow(
1235
1225
  tomo_recon_stack[:,y_index,:],
1236
1226
  title=f'recon {res_title} y={y[y_index]:.4f}',
1237
- origin='lower', extent=extent, path=self._output_folder,
1227
+ origin='lower', extent=extent, path=self._outputdir,
1238
1228
  save_fig=True, save_only=True)
1239
1229
  z_index = z_slice-z_range[0]
1240
1230
  extent = (
@@ -1245,16 +1235,8 @@ class Tomo:
1245
1235
  quick_imshow(
1246
1236
  tomo_recon_stack[z_index,:,:],
1247
1237
  title=f'recon {res_title} z={z[z_index]:.4f}',
1248
- origin='lower', extent=extent, path=self._output_folder,
1238
+ origin='lower', extent=extent, path=self._outputdir,
1249
1239
  save_fig=True, save_only=True)
1250
-
1251
- # Save test data to file
1252
- # reconstructed data order in each stack: z,y,x
1253
- if self._test_mode:
1254
- np.savetxt(
1255
- f'{self._output_folder}/recon_stack.txt',
1256
- tomo_recon_stacks[z_slice-z_range[0],:,:],
1257
- fmt='%.6e')
1258
1240
  else:
1259
1241
  # Plot a few reconstructed image slices
1260
1242
  if self._save_figs:
@@ -1263,94 +1245,84 @@ class Tomo:
1263
1245
  title = f'{basetitle} {res_title} xslice{x_slice}'
1264
1246
  quick_imshow(
1265
1247
  tomo_recon_stacks[i,:,:,x_slice-x_range[0]],
1266
- title=title, path=self._output_folder,
1267
- save_fig=True, save_only=True)
1248
+ title=title, path=self._outputdir, save_fig=True,
1249
+ save_only=True)
1268
1250
  title = f'{basetitle} {res_title} yslice{y_slice}'
1269
1251
  quick_imshow(
1270
1252
  tomo_recon_stacks[i,:,y_slice-y_range[0],:],
1271
- title=title, path=self._output_folder,
1272
- save_fig=True, save_only=True)
1253
+ title=title, path=self._outputdir, save_fig=True,
1254
+ save_only=True)
1273
1255
  title = f'{basetitle} {res_title} zslice{z_slice}'
1274
1256
  quick_imshow(
1275
1257
  tomo_recon_stacks[i,z_slice-z_range[0],:,:],
1276
- title=title, path=self._output_folder,
1277
- save_fig=True, save_only=True)
1278
-
1279
- # Save test data to file
1280
- # reconstructed data order in each stack: row/-z,y,x
1281
- if self._test_mode:
1282
- for i in range(tomo_recon_shape[0]):
1283
- np.savetxt(
1284
- f'{self._output_folder}/recon_stack_{i}.txt',
1285
- tomo_recon_stacks[i,z_slice-z_range[0],:,:],
1286
- fmt='%.6e')
1258
+ title=title, path=self._outputdir, save_fig=True,
1259
+ save_only=True)
1287
1260
 
1288
1261
  # Add image reconstruction to reconstructed data NXprocess
1289
- # reconstructed data order:
1290
- # - for one stack: z,y,x
1291
- # - for multiple stacks: row/-z,y,x
1292
- nxprocess.data = NXdata()
1293
- nxprocess.attrs['default'] = 'data'
1262
+ # reconstructed axis data order:
1263
+ # - for one stack: z,y,x
1264
+ # - for multiple stacks: row/-z,y,x
1294
1265
  for k, v in center_info.items():
1295
1266
  nxprocess[k] = v
1296
1267
  if k == 'center_rows' or k == 'center_offsets':
1297
1268
  nxprocess[k].units = 'pixels'
1269
+ if k == 'center_rows':
1270
+ nxprocess[k].attrs['long_name'] = \
1271
+ 'center row indices in detector frame of reference'
1298
1272
  if x_bounds is not None:
1299
1273
  nxprocess.x_bounds = x_bounds
1300
1274
  nxprocess.x_bounds.units = 'pixels'
1275
+ nxprocess.x_bounds.attrs['long_name'] = \
1276
+ 'x range indices in reduced data frame of reference'
1301
1277
  if y_bounds is not None:
1302
1278
  nxprocess.y_bounds = y_bounds
1303
1279
  nxprocess.y_bounds.units = 'pixels'
1280
+ nxprocess.y_bounds.attrs['long_name'] = \
1281
+ 'y range indices in reduced data frame of reference'
1304
1282
  if z_bounds is not None:
1305
1283
  nxprocess.z_bounds = z_bounds
1306
1284
  nxprocess.z_bounds.units = 'pixels'
1307
- nxprocess.data.attrs['signal'] = 'reconstructed_data'
1285
+ nxprocess.z_bounds.attrs['long_name'] = \
1286
+ 'z range indices in reduced data frame of reference'
1308
1287
  if num_tomo_stacks == 1:
1309
- nxprocess.data.reconstructed_data = tomo_recon_stack
1310
- nxprocess.data.attrs['axes'] = ['z', 'y', 'x']
1311
- nxprocess.data.attrs['x_indices'] = 2
1312
- nxprocess.data.attrs['y_indices'] = 1
1313
- nxprocess.data.attrs['z_indices'] = 0
1314
- nxprocess.data.x = x
1315
- nxprocess.data.x.units = \
1316
- nxentry.instrument.detector.column_pixel_size.units
1317
- nxprocess.data.y = y
1318
- nxprocess.data.y.units = \
1319
- nxentry.instrument.detector.column_pixel_size.units
1320
- nxprocess.data.z = z
1321
- nxprocess.data.z.units = \
1322
- nxentry.instrument.detector.row_pixel_size.units
1288
+ nxprocess.data = NXdata(
1289
+ NXfield(tomo_recon_stack, 'reconstructed_data'),
1290
+ (NXfield(
1291
+ z, 'z', attrs={'units': detector.row_pixel_size.units}),
1292
+ NXfield(
1293
+ y, 'y',
1294
+ attrs={'units': detector.column_pixel_size.units}),
1295
+ NXfield(
1296
+ x, 'x',
1297
+ attrs={'units': detector.column_pixel_size.units}),))
1323
1298
  else:
1324
- nxprocess.data.reconstructed_data = tomo_recon_stacks
1299
+ nxprocess.data = NXdata(
1300
+ NXfield(tomo_recon_stacks, 'reconstructed_data'))
1325
1301
 
1326
1302
  # Create a copy of the input Nexus object and remove reduced
1327
- # data
1303
+ # data
1328
1304
  exclude_items = [
1329
1305
  f'{nxentry.nxname}/reduced_data/data',
1330
1306
  f'{nxentry.nxname}/data/reduced_data',
1331
1307
  f'{nxentry.nxname}/data/rotation_angle',
1332
1308
  ]
1333
- nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1309
+ nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1334
1310
 
1335
1311
  # Add the reconstructed data NXprocess to the new Nexus object
1336
- nxentry_copy = nxroot_copy[nxroot_copy.attrs['default']]
1337
- nxentry_copy.reconstructed_data = nxprocess
1338
- if 'data' not in nxentry_copy:
1339
- nxentry_copy.data = NXdata()
1340
- nxentry_copy.attrs['default'] = 'data'
1341
- nxentry_copy.data.makelink(
1342
- nxprocess.data.reconstructed_data, name='reconstructed_data')
1343
- nxentry_copy.data.attrs['signal'] = 'reconstructed_data'
1312
+ nxentry = nxroot[nxroot.default]
1313
+ nxentry.reconstructed_data = nxprocess
1314
+ if 'data' not in nxentry:
1315
+ nxentry.data = NXdata()
1316
+ nxentry.data.set_default()
1317
+ nxentry.data.makelink(nxprocess.data.reconstructed_data)
1344
1318
  if num_tomo_stacks == 1:
1345
- nxentry_copy.data.attrs['axes'] = ['z', 'y', 'x']
1346
- nxentry_copy.data.attrs['x_indices'] = 2
1347
- nxentry_copy.data.attrs['y_indices'] = 1
1348
- nxentry_copy.data.attrs['z_indices'] = 0
1349
- nxentry_copy.data.makelink(nxprocess.data.x, name='x')
1350
- nxentry_copy.data.makelink(nxprocess.data.y, name='y')
1351
- nxentry_copy.data.makelink(nxprocess.data.z, name='z')
1319
+ nxentry.data.attrs['axes'] = ['z', 'y', 'x']
1320
+ nxentry.data.makelink(nxprocess.data.x)
1321
+ nxentry.data.makelink(nxprocess.data.y)
1322
+ nxentry.data.makelink(nxprocess.data.z)
1323
+ nxentry.data.attrs['signal'] = 'reconstructed_data'
1352
1324
 
1353
- return nxroot_copy
1325
+ return nxroot
1354
1326
 
1355
1327
  def combine_data(self, nxroot, tool_config):
1356
1328
  """Combine the reconstructed tomography stacks.
@@ -1368,7 +1340,7 @@ class Tomo:
1368
1340
  # Third party modules
1369
1341
  from nexusformat.nexus import (
1370
1342
  NXdata,
1371
- NXentry,
1343
+ NXfield,
1372
1344
  NXprocess,
1373
1345
  NXroot,
1374
1346
  )
@@ -1376,50 +1348,54 @@ class Tomo:
1376
1348
  self._logger.info('Combine the reconstructed tomography stacks')
1377
1349
 
1378
1350
  if isinstance(nxroot, NXroot):
1379
- nxentry = nxroot[nxroot.attrs['default']]
1351
+ nxentry = nxroot[nxroot.default]
1380
1352
  else:
1381
1353
  raise ValueError(f'Invalid parameter nxroot ({nxroot})')
1382
1354
 
1383
1355
  # Check if reconstructed image data is available
1384
- if ('reconstructed_data' not in nxentry
1385
- or 'reconstructed_data' not in nxentry.data):
1356
+ if 'reconstructed_data' not in nxentry:
1386
1357
  raise KeyError(
1387
1358
  f'Unable to find valid reconstructed image data in {nxentry}')
1388
1359
 
1389
1360
  # Create an NXprocess to store combined image reconstruction
1390
- # (meta)data
1361
+ # (meta)data
1391
1362
  nxprocess = NXprocess()
1392
1363
 
1393
- num_tomo_stacks = \
1394
- nxentry.reconstructed_data.data.reconstructed_data.shape[0]
1364
+ if nxentry.reconstructed_data.data.reconstructed_data.ndim == 3:
1365
+ num_tomo_stacks = 1
1366
+ else:
1367
+ num_tomo_stacks = \
1368
+ nxentry.reconstructed_data.data.reconstructed_data.shape[0]
1395
1369
  if num_tomo_stacks == 1:
1396
1370
  self._logger.info('Only one stack available: leaving combine_data')
1397
1371
  return nxroot
1398
1372
 
1399
1373
  # Get and combine the reconstructed stacks
1400
- # reconstructed data order: stack,row/-z,y,x
1374
+ # - reconstructed axis data order: stack,row/-z,y,x
1401
1375
  # Note: Nexus can't follow a link if the data it points to is
1402
- # too big. So get the data from the actual place, not from
1403
- # nxentry.data
1404
- # (load one stack at a time to reduce risk of hitting Nexus
1405
- # data access limit)
1376
+ # too big. So get the data from the actual place, not from
1377
+ # nxentry.data
1378
+ # Also load one stack at a time to reduce risk of hitting Nexus
1379
+ # data access limit
1406
1380
  t0 = time()
1407
1381
  tomo_recon_combined = \
1408
1382
  nxentry.reconstructed_data.data.reconstructed_data[0,:,:,:]
1383
+ # RV check this out more
1384
+ # tomo_recon_combined = np.concatenate(
1385
+ # [tomo_recon_combined]
1386
+ # + [nxentry.reconstructed_data.data.reconstructed_data[i,:,:,:]
1387
+ # for i in range(1, num_tomo_stacks)])
1409
1388
  tomo_recon_combined = np.concatenate(
1410
1389
  [nxentry.reconstructed_data.data.reconstructed_data[i,:,:,:]
1411
- for i in range(num_tomo_stacks-1,0,-1)]
1390
+ for i in range(num_tomo_stacks-1, 0, -1)]
1412
1391
  + [tomo_recon_combined])
1413
1392
  self._logger.info(
1414
1393
  f'Combining the reconstructed stacks took {time()-t0:.2f} seconds')
1394
+ tomo_shape = tomo_recon_combined.shape
1415
1395
 
1416
1396
  # Resize the combined tomography data stacks
1417
- # combined data order: row/-z,y,x
1418
- if self._test_mode:
1419
- x_bounds = None
1420
- y_bounds = None
1421
- z_bounds = tuple(self._test_config.get('z_bounds'))
1422
- elif self._interactive:
1397
+ # - combined axis data order: row/-z,y,x
1398
+ if self._interactive:
1423
1399
  x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(
1424
1400
  tomo_recon_combined, combine_data=True)
1425
1401
  else:
@@ -1428,76 +1404,73 @@ class Tomo:
1428
1404
  self._logger.warning(
1429
1405
  'x_bounds unspecified, reconstruct data for full x-range')
1430
1406
  elif not is_int_pair(
1431
- x_bounds, ge=0, le=tomo_recon_combined.shape[2]):
1407
+ x_bounds, ge=0, le=tomo_shape[2]):
1432
1408
  raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
1433
1409
  y_bounds = tool_config.y_bounds
1434
1410
  if y_bounds is None:
1435
1411
  self._logger.warning(
1436
1412
  'y_bounds unspecified, reconstruct data for full y-range')
1437
1413
  elif not is_int_pair(
1438
- y_bounds, ge=0, le=tomo_recon_combined.shape[1]):
1414
+ y_bounds, ge=0, le=tomo_shape[1]):
1439
1415
  raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
1440
1416
  z_bounds = tool_config.z_bounds
1441
1417
  if z_bounds is None:
1442
1418
  self._logger.warning(
1443
1419
  'z_bounds unspecified, reconstruct data for full z-range')
1444
1420
  elif not is_int_pair(
1445
- z_bounds, ge=0, le=tomo_recon_combined.shape[0]):
1421
+ z_bounds, ge=0, le=tomo_shape[0]):
1446
1422
  raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
1447
1423
  if x_bounds is None:
1448
- x_range = (0, tomo_recon_combined.shape[2])
1449
- x_slice = int(x_range[1]/2)
1424
+ x_range = (0, tomo_shape[2])
1425
+ x_slice = x_range[1]//2
1450
1426
  else:
1451
1427
  x_range = (min(x_bounds), max(x_bounds))
1452
- x_slice = int((x_bounds[0]+x_bounds[1]) / 2)
1428
+ x_slice = (x_bounds[0]+x_bounds[1])//2
1453
1429
  if y_bounds is None:
1454
- y_range = (0, tomo_recon_combined.shape[1])
1455
- y_slice = int(y_range[1]/2)
1430
+ y_range = (0, tomo_shape[1])
1431
+ y_slice = y_range[1]//2
1456
1432
  else:
1457
1433
  y_range = (min(y_bounds), max(y_bounds))
1458
- y_slice = int((y_bounds[0]+y_bounds[1]) / 2)
1434
+ y_slice = (y_bounds[0]+y_bounds[1])//2
1459
1435
  if z_bounds is None:
1460
- z_range = (0, tomo_recon_combined.shape[0])
1461
- z_slice = int(z_range[1]/2)
1436
+ z_range = (0, tomo_shape[0])
1437
+ z_slice = z_range[1]//2
1462
1438
  else:
1463
1439
  z_range = (min(z_bounds), max(z_bounds))
1464
- z_slice = int((z_bounds[0]+z_bounds[1]) / 2)
1465
- z_dim_org = tomo_recon_combined.shape[0]
1440
+ z_slice = (z_bounds[0]+z_bounds[1])//2
1441
+ z_dim_org = tomo_shape[0]
1466
1442
  tomo_recon_combined = tomo_recon_combined[
1467
1443
  z_range[0]:z_range[1],y_range[0]:y_range[1],x_range[0]:x_range[1]]
1468
1444
 
1469
1445
  # Convert the reconstructed tomography data from internal
1470
- # coordinate frame row/-z,y,x with the origin on the
1471
- # near-left-top corner to an z,y,x coordinate frame.
1472
- # Here x is to the right, y along the beam direction
1473
- # and z upwards in the lab frame of reference
1446
+ # coordinate frame row/-z,y,x with the origin on the
1447
+ # near-left-top corner to an z,y,x coordinate frame.
1448
+ # Here x is to the right, y along the beam direction and
1449
+ # z upwards in the lab frame of reference
1474
1450
  tomo_recon_combined = np.flip(tomo_recon_combined, 0)
1451
+ tomo_shape = tomo_recon_combined.shape
1475
1452
  z_range = (z_dim_org-z_range[1], z_dim_org-z_range[0])
1476
1453
 
1477
1454
  # Get coordinate axes
1478
- row_pixel_size = float(
1479
- nxentry.instrument.detector.row_pixel_size)
1480
- column_pixel_size = float(
1481
- nxentry.instrument.detector.column_pixel_size)
1455
+ detector = nxentry.instrument.detector
1456
+ row_pixel_size = float(detector.row_pixel_size)
1457
+ column_pixel_size = float(detector.column_pixel_size)
1482
1458
  x = column_pixel_size * (
1483
1459
  np.linspace(x_range[0], x_range[1], x_range[1]-x_range[0], False)
1484
- - 0.5*nxentry.instrument.detector.columns
1485
- + 0.5)
1460
+ - 0.5*detector.columns + 0.5)
1486
1461
  if nxentry.reconstructed_data.get('x_bounds', None) is not None:
1487
1462
  x += column_pixel_size*nxentry.reconstructed_data.x_bounds[0]
1488
1463
  x = np.asarray(x + nxentry.reduced_data.x_translation[0])
1489
1464
  y = column_pixel_size * (
1490
1465
  np.linspace(y_range[0], y_range[1], y_range[1]-y_range[0], False)
1491
- - 0.5*nxentry.instrument.detector.columns
1492
- + 0.5)
1466
+ - 0.5*detector.columns + 0.5)
1493
1467
  if nxentry.reconstructed_data.get('y_bounds', None) is not None:
1494
1468
  y += column_pixel_size*nxentry.reconstructed_data.y_bounds[0]
1495
1469
  y = np.asarray(y)
1496
1470
  z = row_pixel_size*(
1497
1471
  np.linspace(z_range[0], z_range[1], z_range[1]-z_range[0], False)
1498
1472
  - int(nxentry.reduced_data.img_row_bounds[0])
1499
- + 0.5*(nxentry.instrument.detector.rows)
1500
- -0.5)
1473
+ + 0.5*detector.rows - 0.5)
1501
1474
  z = np.asarray(z + nxentry.reduced_data.z_translation[0])
1502
1475
 
1503
1476
  # Plot a few combined image slices
@@ -1507,98 +1480,82 @@ class Tomo:
1507
1480
  y[-1],
1508
1481
  z[0],
1509
1482
  z[-1])
1510
- x_slice = int(tomo_recon_combined.shape[2]/2)
1483
+ x_slice = tomo_shape[2]//2
1511
1484
  quick_imshow(
1512
1485
  tomo_recon_combined[:,:,x_slice],
1513
1486
  title=f'recon combined x={x[x_slice]:.4f}', origin='lower',
1514
- extent=extent, path=self._output_folder, save_fig=True,
1487
+ extent=extent, path=self._outputdir, save_fig=True,
1515
1488
  save_only=True)
1516
1489
  extent = (
1517
1490
  x[0],
1518
1491
  x[-1],
1519
1492
  z[0],
1520
1493
  z[-1])
1521
- y_slice = int(tomo_recon_combined.shape[1]/2)
1494
+ y_slice = tomo_shape[1]//2
1522
1495
  quick_imshow(
1523
1496
  tomo_recon_combined[:,y_slice,:],
1524
1497
  title=f'recon combined y={y[y_slice]:.4f}', origin='lower',
1525
- extent=extent, path=self._output_folder, save_fig=True,
1498
+ extent=extent, path=self._outputdir, save_fig=True,
1526
1499
  save_only=True)
1527
1500
  extent = (
1528
1501
  x[0],
1529
1502
  x[-1],
1530
1503
  y[0],
1531
1504
  y[-1])
1532
- z_slice = int(tomo_recon_combined.shape[0]/2)
1505
+ z_slice = tomo_shape[0]//2
1533
1506
  quick_imshow(
1534
1507
  tomo_recon_combined[z_slice,:,:],
1535
1508
  title=f'recon combined z={z[z_slice]:.4f}', origin='lower',
1536
- extent=extent, path=self._output_folder, save_fig=True,
1509
+ extent=extent, path=self._outputdir, save_fig=True,
1537
1510
  save_only=True)
1538
1511
 
1539
- # Save test data to file
1540
- # combined data order: z,y,x
1541
- if self._test_mode:
1542
- z_slice = int(tomo_recon_combined.shape[0]/2)
1543
- np.savetxt(
1544
- f'{self._output_folder}/recon_combined.txt',
1545
- tomo_recon_combined[z_slice,:,:], fmt='%.6e')
1546
-
1547
1512
  # Add image reconstruction to reconstructed data NXprocess
1548
- # combined data order: z,y,x
1549
- nxprocess.data = NXdata()
1550
- nxprocess.attrs['default'] = 'data'
1551
- if x_bounds is not None:
1513
+ # - combined axis data order: z,y,x
1514
+ if x_bounds is not None and x_bounds != (0, tomo_shape[2]):
1552
1515
  nxprocess.x_bounds = x_bounds
1553
1516
  nxprocess.x_bounds.units = 'pixels'
1554
- if y_bounds is not None:
1517
+ nxprocess.x_bounds.attrs['long_name'] = \
1518
+ 'x range indices in reconstructed data frame of reference'
1519
+ if y_bounds is not None and y_bounds != (0, tomo_shape[1]):
1555
1520
  nxprocess.y_bounds = y_bounds
1556
1521
  nxprocess.y_bounds.units = 'pixels'
1557
- if z_bounds is not None:
1522
+ nxprocess.y_bounds.attrs['long_name'] = \
1523
+ 'y range indices in reconstructed data frame of reference'
1524
+ if z_bounds is not None and z_bounds != (0, tomo_shape[0]):
1558
1525
  nxprocess.z_bounds = z_bounds
1559
1526
  nxprocess.z_bounds.units = 'pixels'
1560
- nxprocess.data.combined_data = tomo_recon_combined
1561
- nxprocess.data.attrs['signal'] = 'combined_data'
1562
- nxprocess.data.attrs['axes'] = ['z', 'y', 'x']
1563
- nxprocess.data.attrs['x_indices'] = 2
1564
- nxprocess.data.attrs['y_indices'] = 1
1565
- nxprocess.data.attrs['z_indices'] = 0
1566
- nxprocess.data.x = x
1567
- nxprocess.data.x.units = \
1568
- nxentry.instrument.detector.column_pixel_size.units
1569
- nxprocess.data.y = y
1570
- nxprocess.data.y.units = \
1571
- nxentry.instrument.detector.column_pixel_size.units
1572
- nxprocess.data.z = z
1573
- nxprocess.data.z.units = \
1574
- nxentry.instrument.detector.row_pixel_size.units
1527
+ nxprocess.z_bounds.attrs['long_name'] = \
1528
+ 'z range indices in reconstructed data frame of reference'
1529
+ nxprocess.data = NXdata(
1530
+ NXfield(tomo_recon_combined, 'combined_data'),
1531
+ (NXfield(z, 'z', attrs={'units': detector.row_pixel_size.units}),
1532
+ NXfield(
1533
+ y, 'y', attrs={'units': detector.column_pixel_size.units}),
1534
+ NXfield(
1535
+ x, 'x', attrs={'units': detector.column_pixel_size.units}),))
1575
1536
 
1576
1537
  # Create a copy of the input Nexus object and remove
1577
- # reconstructed data
1538
+ # reconstructed data
1578
1539
  exclude_items = [
1579
1540
  f'{nxentry.nxname}/reconstructed_data/data',
1580
1541
  f'{nxentry.nxname}/data/reconstructed_data',
1581
1542
  ]
1582
- nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1543
+ nxroot = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1583
1544
 
1584
1545
  # Add the combined data NXprocess to the new Nexus object
1585
- nxentry_copy = nxroot_copy[nxroot_copy.attrs['default']]
1586
- nxentry_copy.combined_data = nxprocess
1587
- if 'data' not in nxentry_copy:
1588
- nxentry_copy.data = NXdata()
1589
- nxentry_copy.attrs['default'] = 'data'
1590
- nxentry_copy.data.makelink(
1591
- nxprocess.data.combined_data, name='combined_data')
1592
- nxentry_copy.data.attrs['signal'] = 'combined_data'
1593
- nxentry_copy.data.attrs['axes'] = ['z', 'y', 'x']
1594
- nxentry_copy.data.attrs['x_indices'] = 2
1595
- nxentry_copy.data.attrs['y_indices'] = 1
1596
- nxentry_copy.data.attrs['z_indices'] = 0
1597
- nxentry_copy.data.makelink(nxprocess.data.x, name='x')
1598
- nxentry_copy.data.makelink(nxprocess.data.y, name='y')
1599
- nxentry_copy.data.makelink(nxprocess.data.z, name='z')
1600
-
1601
- return nxroot_copy
1546
+ nxentry = nxroot[nxroot.default]
1547
+ nxentry.combined_data = nxprocess
1548
+ if 'data' not in nxentry:
1549
+ nxentry.data = NXdata()
1550
+ nxentry.data.set_default()
1551
+ nxentry.data.makelink(nxprocess.data.combined_data)
1552
+ nxentry.data.attrs['axes'] = ['z', 'y', 'x']
1553
+ nxentry.data.makelink(nxprocess.data.x)
1554
+ nxentry.data.makelink(nxprocess.data.y)
1555
+ nxentry.data.makelink(nxprocess.data.z)
1556
+ nxentry.data.attrs['signal'] = 'combined_data'
1557
+
1558
+ return nxroot
1602
1559
 
1603
1560
  def _gen_dark(self, nxentry, reduced_data, image_key):
1604
1561
  """Generate dark field."""
@@ -1609,8 +1566,7 @@ class Tomo:
1609
1566
  field_indices = [
1610
1567
  index for index, key in enumerate(image_key) if key == 2]
1611
1568
  if field_indices:
1612
- tdf_stack = np.asarray(
1613
- nxentry.instrument.detector.data[field_indices,:,:])
1569
+ tdf_stack = nxentry.instrument.detector.data[field_indices,:,:]
1614
1570
  else:
1615
1571
  self._logger.warning('Dark field unavailable')
1616
1572
  return reduced_data
@@ -1625,7 +1581,6 @@ class Tomo:
1625
1581
  raise RuntimeError(f'Invalid tdf_stack shape ({tdf_stack.shape})')
1626
1582
 
1627
1583
  # Remove dark field intensities above the cutoff
1628
- # tdf_cutoff = None
1629
1584
  tdf_cutoff = tdf.min() + 2 * (np.median(tdf)-tdf.min())
1630
1585
  self._logger.debug(f'tdf_cutoff = {tdf_cutoff}')
1631
1586
  if tdf_cutoff is not None:
@@ -1646,7 +1601,7 @@ class Tomo:
1646
1601
  if self._save_figs:
1647
1602
  quick_imshow(
1648
1603
  tdf, title='Dark field', name='dark_field',
1649
- path=self._output_folder, save_fig=True, save_only=True)
1604
+ path=self._outputdir, save_fig=True, save_only=True)
1650
1605
 
1651
1606
  # Add dark field to reduced data NXprocess
1652
1607
  reduced_data.data = NXdata()
@@ -1663,8 +1618,7 @@ class Tomo:
1663
1618
  field_indices = [
1664
1619
  index for index, key in enumerate(image_key) if key == 1]
1665
1620
  if field_indices:
1666
- tbf_stack = np.asarray(
1667
- nxentry.instrument.detector.data[field_indices,:,:])
1621
+ tbf_stack = nxentry.instrument.detector.data[field_indices,:,:]
1668
1622
  else:
1669
1623
  raise ValueError('Bright field unavailable')
1670
1624
 
@@ -1689,12 +1643,6 @@ class Tomo:
1689
1643
  else:
1690
1644
  raise RuntimeError(f'Invalid tbf_stack shape ({tbf_stack.shape})')
1691
1645
 
1692
- # Subtract dark field
1693
- if 'data' in reduced_data and 'dark_field' in reduced_data.data:
1694
- tbf -= np.asarray(reduced_data.data.dark_field)
1695
- else:
1696
- self._logger.warning('Dark field unavailable')
1697
-
1698
1646
  # Set any non-positive values to one
1699
1647
  # (avoid negative bright field values for spikes in dark field)
1700
1648
  tbf[tbf < 1] = 1
@@ -1703,7 +1651,7 @@ class Tomo:
1703
1651
  if self._save_figs:
1704
1652
  quick_imshow(
1705
1653
  tbf, title='Bright field', name='bright_field',
1706
- path=self._output_folder, save_fig=True, save_only=True)
1654
+ path=self._outputdir, save_fig=True, save_only=True)
1707
1655
 
1708
1656
  # Add bright field to reduced data NXprocess
1709
1657
  if 'data' not in reduced_data:
@@ -1712,8 +1660,9 @@ class Tomo:
1712
1660
 
1713
1661
  return reduced_data
1714
1662
 
1715
- def _set_detector_bounds(self, nxentry, reduced_data, image_key, theta,
1716
- img_row_bounds=None):
1663
+ def _set_detector_bounds(
1664
+ self, nxentry, reduced_data, image_key, theta, img_row_bounds,
1665
+ calibrate_center_rows):
1717
1666
  """
1718
1667
  Set vertical detector bounds for each image stack.Right now the
1719
1668
  range is the same for each set in the image stack.
@@ -1724,111 +1673,131 @@ class Tomo:
1724
1673
  # Local modules
1725
1674
  from CHAP.utils.general import is_index_range
1726
1675
 
1727
- if self._test_mode:
1728
- return tuple(self._test_config['img_row_bounds'])
1729
-
1730
1676
  # Get the first tomography image and the reference heights
1731
1677
  image_mask = reduced_data.get('image_mask')
1732
1678
  if image_mask is None:
1733
1679
  first_image_index = 0
1734
1680
  else:
1735
- raise RuntimeError('image_mask not tested yet')
1736
- image_mask = np.asarray(image_mask)
1737
1681
  first_image_index = int(np.argmax(image_mask))
1738
1682
  field_indices_all = [
1739
1683
  index for index, key in enumerate(image_key) if key == 0]
1740
1684
  if not field_indices_all:
1741
1685
  raise ValueError('Tomography field(s) unavailable')
1742
- z_translation_all = np.asarray(
1743
- nxentry.sample.z_translation)[field_indices_all]
1686
+ z_translation_all = nxentry.sample.z_translation[field_indices_all]
1744
1687
  z_translation_levels = sorted(list(set(z_translation_all)))
1745
1688
  num_tomo_stacks = len(z_translation_levels)
1746
- center_stack_index = int(num_tomo_stacks/2)
1689
+ center_stack_index = num_tomo_stacks//2
1747
1690
  z_translation = z_translation_levels[center_stack_index]
1748
1691
  try:
1749
1692
  field_indices = [
1750
1693
  field_indices_all[index]
1751
1694
  for index, z in enumerate(z_translation_all)
1752
1695
  if z == z_translation]
1753
- first_image = np.asarray(nxentry.instrument.detector.data[
1754
- field_indices[first_image_index]])
1696
+ first_image = nxentry.instrument.detector.data[
1697
+ field_indices[first_image_index]]
1755
1698
  except:
1756
1699
  raise RuntimeError('Unable to load the tomography images '
1757
1700
  f'for stack {i}')
1758
1701
 
1759
- # Select image bounds
1760
- tbf = np.asarray(reduced_data.data.bright_field)
1761
- if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1762
- pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1763
- # Try to get a fit from the bright field
1764
- row_sum = np.sum(tbf, 1)
1765
- fit = Fit.fit_data(
1766
- row_sum, 'rectangle', x=np.array(range(len(row_sum))),
1767
- form='atan', guess=True)
1768
- parameters = fit.best_values
1769
- row_low_fit = parameters.get('center1', None)
1770
- row_upp_fit = parameters.get('center2', None)
1771
- sig_low = parameters.get('sigma1', None)
1772
- sig_upp = parameters.get('sigma2', None)
1773
- have_fit = (fit.success and row_low_fit is not None
1774
- and row_upp_fit is not None and sig_low is not None
1775
- and sig_upp is not None
1776
- and 0 <= row_low_fit < row_upp_fit <= row_sum.size
1777
- and (sig_low+sig_upp) / (row_upp_fit-row_low_fit) < 0.1)
1778
- if num_tomo_stacks == 1:
1779
- if have_fit:
1780
- delta_z = (row_upp_fit-row_low_fit) * pixel_size
1702
+ # Set initial image bounds or rotation calibration rows
1703
+ tbf = reduced_data.data.bright_field.nxdata
1704
+ if (not isinstance(calibrate_center_rows, bool)
1705
+ and is_int_pair(calibrate_center_rows)):
1706
+ img_row_bounds = calibrate_center_rows
1707
+ else:
1708
+ if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1709
+ pixel_size = float(nxentry.instrument.detector.row_pixel_size)
1710
+ # Try to get a fit from the bright field
1711
+ row_sum = np.sum(tbf, 1)
1712
+ fit = Fit.fit_data(
1713
+ row_sum, 'rectangle', x=np.array(range(len(row_sum))),
1714
+ form='atan', guess=True)
1715
+ parameters = fit.best_values
1716
+ row_low_fit = parameters.get('center1', None)
1717
+ row_upp_fit = parameters.get('center2', None)
1718
+ sig_low = parameters.get('sigma1', None)
1719
+ sig_upp = parameters.get('sigma2', None)
1720
+ have_fit = (fit.success and row_low_fit is not None
1721
+ and row_upp_fit is not None and sig_low is not None
1722
+ and sig_upp is not None
1723
+ and 0 <= row_low_fit < row_upp_fit <= row_sum.size
1724
+ and (sig_low+sig_upp) / (row_upp_fit-row_low_fit) < 0.1)
1725
+ if num_tomo_stacks == 1:
1726
+ if have_fit:
1727
+ # Add a pixel margin for roundoff effects in fit
1728
+ row_low_fit += 1
1729
+ row_upp_fit -= 1
1730
+ delta_z = (row_upp_fit-row_low_fit) * pixel_size
1731
+ else:
1732
+ # Set a default range of 1 mm
1733
+ # RV can we get this from the slits?
1734
+ delta_z = 1.0
1781
1735
  else:
1782
- # Set a default range of 1 mm
1783
- # RV can we get this from the slits?
1784
- delta_z = 1.0
1785
- else:
1786
- # Get the default range from the reference heights
1787
- delta_z = z_translation_levels[1]-z_translation_levels[0]
1788
- for i in range(2, num_tomo_stacks):
1789
- delta_z = min(
1790
- delta_z,
1791
- z_translation_levels[i]-z_translation_levels[i-1])
1792
- self._logger.debug(f'delta_z = {delta_z}')
1793
- num_row_min = int((delta_z + 0.5*pixel_size) / pixel_size)
1794
- if num_row_min > tbf.shape[0]:
1795
- self._logger.warning(
1796
- 'Image bounds and pixel size prevent seamless '
1797
- 'stacking')
1798
- row_low = 0
1799
- row_upp = tbf.shape[0]
1800
- else:
1801
- self._logger.debug(f'num_row_min = {num_row_min}')
1802
- if have_fit:
1803
- # Center the default range relative to the fitted
1804
- # window
1805
- row_low = int((row_low_fit+row_upp_fit-num_row_min) / 2)
1806
- row_upp = row_low+num_row_min
1736
+ # Get the default range from the reference heights
1737
+ delta_z = z_translation_levels[1]-z_translation_levels[0]
1738
+ for i in range(2, num_tomo_stacks):
1739
+ delta_z = min(
1740
+ delta_z,
1741
+ z_translation_levels[i]-z_translation_levels[i-1])
1742
+ self._logger.debug(f'delta_z = {delta_z}')
1743
+ num_row_min = int((delta_z + 0.5*pixel_size) / pixel_size)
1744
+ if num_row_min > tbf.shape[0]:
1745
+ self._logger.warning(
1746
+ 'Image bounds and pixel size prevent seamless '
1747
+ 'stacking')
1748
+ row_low = 0
1749
+ row_upp = tbf.shape[0]
1807
1750
  else:
1808
- # Center the default range
1809
- row_low = int((tbf.shape[0]-num_row_min) / 2)
1810
- row_upp = row_low+num_row_min
1811
- img_row_bounds = (row_low, row_upp)
1751
+ self._logger.debug(f'num_row_min = {num_row_min}')
1752
+ if have_fit:
1753
+ # Center the default range relative to the fitted
1754
+ # window
1755
+ row_low = int((row_low_fit+row_upp_fit-num_row_min)/2)
1756
+ row_upp = row_low+num_row_min
1757
+ else:
1758
+ # Center the default range
1759
+ row_low = int((tbf.shape[0]-num_row_min)/2)
1760
+ row_upp = row_low+num_row_min
1761
+ img_row_bounds = (row_low, row_upp)
1762
+ if calibrate_center_rows:
1763
+ # Add a small margin to avoid edge effects
1764
+ offset = int(min(5, 0.1*(row_upp-row_low)))
1765
+ img_row_bounds = (row_low+offset, row_upp-1-offset)
1766
+ else:
1767
+ if num_tomo_stacks > 1:
1768
+ raise NotImplementedError(
1769
+ 'Selecting image bounds or calibrating rotation axis '
1770
+ 'for multiple stacks on FMB')
1771
+ # For FMB: use the first tomography image to select range
1772
+ # RV revisit if they do tomography with multiple stacks
1773
+ if img_row_bounds is None and not self._interactive:
1774
+ if calibrate_center_rows:
1775
+ self._logger.warning(
1776
+ 'calibrate_center_rows unspecified, find rotation '
1777
+ 'axis at detector bounds (with a small margin)')
1778
+ # Add a small margin to avoid edge effects
1779
+ offset = min(5, 0.1*first_image.shape[0])
1780
+ img_row_bounds = (
1781
+ offset, first_image.shape[0]-1-offset)
1782
+ else:
1783
+ self._logger.warning(
1784
+ 'img_row_bounds unspecified, reduce data for '
1785
+ 'entire detector range')
1786
+ img_row_bounds = (0, first_image.shape[0])
1787
+ if calibrate_center_rows:
1788
+ title='Select two detector image row indices to '\
1789
+ 'calibrate rotation axis (in range '\
1790
+ f'[0, {first_image.shape[0]}])'
1812
1791
  else:
1813
- if num_tomo_stacks > 1:
1814
- raise NotImplementedError(
1815
- 'Selecting image bounds for multiple stacks on FMB')
1816
- # For FMB: use the first tomography image to select range
1817
- # RV revisit if they do tomography with multiple stacks
1818
- if img_row_bounds is None:
1819
- if not self._interactive:
1820
- self._logger.warning(
1821
- 'img_row_bounds unspecified, reduce data for entire '
1822
- 'detector range')
1823
- img_row_bounds = (0, first_image.shape[0])
1792
+ title='Select detector image row bounds for data '\
1793
+ f'reduction (in range [0, {first_image.shape[0]}])'
1824
1794
  fig, img_row_bounds = select_image_indices(
1825
1795
  first_image, 0, b=tbf, preselected_indices=img_row_bounds,
1826
- title='Select or adjust detector image row bounds for data '
1827
- f'reduction (in range {[0, first_image.shape[0]]})',
1796
+ title=title,
1828
1797
  title_a=r'Tomography image at $\theta$ = 'f'{round(theta, 2)+0}',
1829
1798
  title_b='Bright field',
1830
1799
  interactive=self._interactive)
1831
- if (num_tomo_stacks > 1
1800
+ if not calibrate_center_rows and (num_tomo_stacks > 1
1832
1801
  and (img_row_bounds[1]-img_row_bounds[0]+1)
1833
1802
  < int((delta_z - 0.5*pixel_size) / pixel_size)):
1834
1803
  self._logger.warning(
@@ -1836,19 +1805,22 @@ class Tomo:
1836
1805
 
1837
1806
  # Plot results
1838
1807
  if self._save_figs:
1839
- fig.savefig(
1840
- os_path.join(self._output_folder, 'detector_image_bounds.png'))
1808
+ if calibrate_center_rows:
1809
+ fig.savefig(os_path.join(
1810
+ self._outputdir, 'rotation_calibration_rows.png'))
1811
+ else:
1812
+ fig.savefig(os_path.join(
1813
+ self._outputdir, 'detector_image_bounds.png'))
1841
1814
  plt.close()
1842
1815
 
1843
1816
  return img_row_bounds
1844
1817
 
1845
1818
  def _gen_thetas(self, nxentry, image_key):
1846
1819
  """Get the rotation angles for the image stacks."""
1847
- # Get the rotation angles
1820
+ # Get the rotation angles (in degrees)
1848
1821
  field_indices_all = [
1849
1822
  index for index, key in enumerate(image_key) if key == 0]
1850
- z_translation_all = np.asarray(
1851
- nxentry.sample.z_translation)[field_indices_all]
1823
+ z_translation_all = nxentry.sample.z_translation[field_indices_all]
1852
1824
  z_translation_levels = sorted(list(set(z_translation_all)))
1853
1825
  thetas = None
1854
1826
  for i, z_translation in enumerate(z_translation_levels):
@@ -1856,22 +1828,20 @@ class Tomo:
1856
1828
  field_indices_all[index]
1857
1829
  for index, z in enumerate(z_translation_all)
1858
1830
  if z == z_translation]
1859
- sequence_numbers = np.asarray(
1860
- nxentry.instrument.detector.sequence_number)[field_indices]
1831
+ sequence_numbers = \
1832
+ nxentry.instrument.detector.sequence_number[field_indices]
1861
1833
  assert (list(sequence_numbers)
1862
1834
  == list(range((len(sequence_numbers)))))
1863
1835
  if thetas is None:
1864
- thetas = np.asarray(
1865
- nxentry.sample.rotation_angle)[
1866
- field_indices][sequence_numbers]
1836
+ thetas = nxentry.sample.rotation_angle[
1837
+ field_indices][sequence_numbers]
1867
1838
  else:
1868
1839
  assert all(
1869
- thetas[i] == np.asarray(
1870
- nxentry.sample.rotation_angle)[
1871
- field_indices[index]]
1840
+ thetas[i] == nxentry.sample.rotation_angle[
1841
+ field_indices[index]]
1872
1842
  for i, index in enumerate(sequence_numbers))
1873
1843
 
1874
- return thetas
1844
+ return np.asarray(thetas)
1875
1845
 
1876
1846
  def _set_zoom_or_delta_theta(self, thetas, delta_theta=None):
1877
1847
  """
@@ -1881,9 +1851,6 @@ class Tomo:
1881
1851
  # Local modules
1882
1852
  from CHAP.utils.general import index_nearest
1883
1853
 
1884
- if self._test_mode:
1885
- return tuple(self._test_config['delta_theta'])
1886
-
1887
1854
  # if input_yesno(
1888
1855
  # '\nDo you want to zoom in to reduce memory '
1889
1856
  # 'requirement (y/n)?', 'n'):
@@ -1901,7 +1868,7 @@ class Tomo:
1901
1868
  if self._interactive:
1902
1869
  if delta_theta is None:
1903
1870
  delta_theta = thetas[1]-thetas[0]
1904
- print(f'Available \u03b8 range: [{thetas[0]}, {thetas[-1]}]')
1871
+ print(f'\nAvailable \u03b8 range: [{thetas[0]}, {thetas[-1]}]')
1905
1872
  print(f'Current \u03b8 interval: {delta_theta}')
1906
1873
  if input_yesno(
1907
1874
  'Do you want to change the \u03b8 interval to reduce the '
@@ -1916,109 +1883,139 @@ class Tomo:
1916
1883
 
1917
1884
  return zoom_perc, delta_theta
1918
1885
 
1919
- def _gen_tomo(self, nxentry, reduced_data, image_key):
1886
+ def _gen_tomo(
1887
+ self, nxentry, reduced_data, image_key, calibrate_center_rows):
1920
1888
  """Generate tomography fields."""
1921
1889
  # Third party modules
1922
1890
  from numexpr import evaluate
1923
1891
  from scipy.ndimage import zoom
1924
1892
 
1925
- # Get full bright field
1926
- tbf = np.asarray(reduced_data.data.bright_field)
1893
+ # Get dark field
1894
+ if 'dark_field' in reduced_data.data:
1895
+ tdf = reduced_data.data.dark_field.nxdata
1896
+ else:
1897
+ self._logger.warning('Dark field unavailable')
1898
+ tdf = None
1899
+
1900
+ # Get bright field
1901
+ tbf = reduced_data.data.bright_field.nxdata
1927
1902
  tbf_shape = tbf.shape
1928
1903
 
1904
+ # Subtract dark field
1905
+ if tdf is not None:
1906
+ try:
1907
+ with SetNumexprThreads(self._num_core):
1908
+ evaluate('tbf-tdf', out=tbf)
1909
+ except TypeError as e:
1910
+ sys_exit(
1911
+ f'\nA {type(e).__name__} occured while subtracting '
1912
+ 'the dark field with num_expr.evaluate()'
1913
+ '\nTry reducing the detector range'
1914
+ f'\n(currently img_row_bounds = {img_row_bounds}, and '
1915
+ f'img_column_bounds = {img_column_bounds})\n')
1916
+
1929
1917
  # Get image bounds
1930
- img_row_bounds = tuple(
1931
- reduced_data.get('img_row_bounds', (0, tbf_shape[0])))
1918
+ img_row_bounds = tuple(reduced_data.get('img_row_bounds'))
1932
1919
  img_column_bounds = tuple(
1933
1920
  reduced_data.get('img_column_bounds', (0, tbf_shape[1])))
1934
1921
 
1935
- # Get resized dark field
1936
- if 'dark_field' in reduced_data.data:
1937
- tdf = np.asarray(
1938
- reduced_data.data.dark_field)[
1922
+ # Check if this run is a rotation axis calibration
1923
+ # and resize dark and bright fields accordingly
1924
+ if calibrate_center_rows:
1925
+ if tdf is not None:
1926
+ tdf = tdf[calibrate_center_rows,:]
1927
+ tbf = tbf[calibrate_center_rows,:]
1928
+ else:
1929
+ if (img_row_bounds != (0, tbf.shape[0])
1930
+ or img_column_bounds != (0, tbf.shape[1])):
1931
+ if tdf is not None:
1932
+ tdf = tdf[
1933
+ img_row_bounds[0]:img_row_bounds[1],
1934
+ img_column_bounds[0]:img_column_bounds[1]]
1935
+ tbf = tbf[
1939
1936
  img_row_bounds[0]:img_row_bounds[1],
1940
1937
  img_column_bounds[0]:img_column_bounds[1]]
1941
- else:
1942
- self._logger.warning('Dark field unavailable')
1943
- tdf = None
1944
-
1945
- # Resize bright field
1946
- if (img_row_bounds != (0, tbf.shape[0])
1947
- or img_column_bounds != (0, tbf.shape[1])):
1948
- tbf = tbf[
1949
- img_row_bounds[0]:img_row_bounds[1],
1950
- img_column_bounds[0]:img_column_bounds[1]]
1951
1938
 
1952
1939
  # Get thetas (in degrees)
1953
- thetas = np.asarray(reduced_data.rotation_angle)
1940
+ thetas = reduced_data.rotation_angle.nxdata
1954
1941
 
1955
1942
  # Get or create image mask
1956
1943
  image_mask = reduced_data.get('image_mask')
1957
1944
  if image_mask is None:
1958
- image_mask = np.ones(len(thetas), dtype=bool)
1945
+ image_mask = [True]*len(thetas)
1959
1946
  else:
1960
- image_mask = np.asarray(image_mask)
1947
+ image_mask = list(image_mask)
1961
1948
 
1962
1949
  # Get the tomography images
1963
1950
  field_indices_all = [
1964
1951
  index for index, key in enumerate(image_key) if key == 0]
1965
1952
  if not field_indices_all:
1966
1953
  raise ValueError('Tomography field(s) unavailable')
1967
- z_translation_all = np.asarray(
1968
- nxentry.sample.z_translation)[field_indices_all]
1954
+ z_translation_all = nxentry.sample.z_translation[
1955
+ field_indices_all]
1969
1956
  z_translation_levels = sorted(list(set(z_translation_all)))
1970
1957
  num_tomo_stacks = len(z_translation_levels)
1958
+ if calibrate_center_rows:
1959
+ center_stack_index = num_tomo_stacks//2
1971
1960
  tomo_stacks = num_tomo_stacks*[np.array([])]
1972
1961
  horizontal_shifts = []
1973
1962
  vertical_shifts = []
1974
- tomo_stacks = []
1975
1963
  for i, z_translation in enumerate(z_translation_levels):
1964
+ if calibrate_center_rows and i != center_stack_index:
1965
+ continue
1976
1966
  try:
1977
1967
  field_indices = [
1978
- field_indices_all[index]
1979
- for index, z in enumerate(z_translation_all)
1968
+ field_indices_all[i]
1969
+ for i, z in enumerate(z_translation_all)
1980
1970
  if z == z_translation]
1981
- field_indices_masked = np.asarray(field_indices)[image_mask]
1982
- horizontal_shift = list(set(np.asarray(
1983
- nxentry.sample.x_translation)[field_indices_masked]))
1971
+ field_indices_masked = [
1972
+ v for i, v in enumerate(field_indices) if image_mask[i]]
1973
+ horizontal_shift = list(
1974
+ set(nxentry.sample.x_translation[field_indices_masked]))
1984
1975
  assert len(horizontal_shift) == 1
1985
1976
  horizontal_shifts += horizontal_shift
1986
- vertical_shift = list(set(np.asarray(
1987
- nxentry.sample.z_translation)[field_indices_masked]))
1977
+ vertical_shift = list(
1978
+ set(nxentry.sample.z_translation[field_indices_masked]))
1988
1979
  assert len(vertical_shift) == 1
1989
1980
  vertical_shifts += vertical_shift
1990
- sequence_numbers = np.asarray(
1991
- nxentry.instrument.detector.sequence_number)[
1992
- field_indices]
1981
+ sequence_numbers = \
1982
+ nxentry.instrument.detector.sequence_number[field_indices]
1993
1983
  assert (list(sequence_numbers)
1994
1984
  == list(range((len(sequence_numbers)))))
1995
- tomo_stack = np.asarray(
1996
- nxentry.instrument.detector.data)[field_indices_masked]
1985
+ tomo_stack = nxentry.instrument.detector.data[
1986
+ field_indices_masked]
1997
1987
  except:
1998
1988
  raise RuntimeError('Unable to load the tomography images '
1999
1989
  f'for stack {i}')
2000
- tomo_stacks.append(tomo_stack)
2001
- if not i:
2002
- tomo_stack_shape = tomo_stack.shape
2003
- else:
2004
- assert tomo_stack_shape == tomo_stack.shape
1990
+ tomo_stacks[i] = tomo_stack
1991
+ if not calibrate_center_rows:
1992
+ if not i:
1993
+ tomo_stack_shape = tomo_stack.shape
1994
+ else:
1995
+ assert tomo_stack_shape == tomo_stack.shape
2005
1996
 
2006
1997
  row_pixel_size = float(nxentry.instrument.detector.row_pixel_size)
2007
1998
  column_pixel_size = float(
2008
1999
  nxentry.instrument.detector.column_pixel_size)
2009
- reduced_tomo_stacks = []
2000
+ reduced_tomo_stacks = num_tomo_stacks*[np.array([])]
2001
+ tomo_stack_shape = None
2010
2002
  for i, tomo_stack in enumerate(tomo_stacks):
2003
+ if not tomo_stack.size:
2004
+ continue
2011
2005
  # Resize the tomography images
2012
2006
  # Right now the range is the same for each set in the stack
2013
- assert len(thetas) == tomo_stack.shape[0]
2014
- if (img_row_bounds != (0, tomo_stack.shape[1])
2015
- or img_column_bounds != (0, tomo_stack.shape[2])):
2016
- tomo_stack = tomo_stack[
2017
- :,img_row_bounds[0]:img_row_bounds[1],
2018
- img_column_bounds[0]:img_column_bounds[1]].astype(
2007
+ if calibrate_center_rows:
2008
+ tomo_stack = tomo_stack[:,calibrate_center_rows,:].astype(
2019
2009
  'float64', copy=False)
2020
2010
  else:
2021
- tomo_stack = tomo_stack.astype('float64', copy=False)
2011
+ if (img_row_bounds != (0, tomo_stack.shape[1])
2012
+ or img_column_bounds != (0, tomo_stack.shape[2])):
2013
+ tomo_stack = tomo_stack[
2014
+ :,img_row_bounds[0]:img_row_bounds[1],
2015
+ img_column_bounds[0]:img_column_bounds[1]].astype(
2016
+ 'float64', copy=False)
2017
+ else:
2018
+ tomo_stack = tomo_stack.astype('float64', copy=False)
2022
2019
 
2023
2020
  # Subtract dark field
2024
2021
  if tdf is not None:
@@ -2059,7 +2056,7 @@ class Tomo:
2059
2056
 
2060
2057
  # Downsize tomography stack to smaller size
2061
2058
  tomo_stack = tomo_stack.astype('float32', copy=False)
2062
- if not self._test_mode and (self._save_figs or self._save_only):
2059
+ if self._save_figs or self._save_only:
2063
2060
  theta = round(thetas[0], 2)
2064
2061
  if len(tomo_stacks) == 1:
2065
2062
  title = r'Reduced data, $\theta$ = 'f'{theta}'
@@ -2069,7 +2066,7 @@ class Tomo:
2069
2066
  name = f'reduced_data_stack_{i}_theta_{theta}'
2070
2067
  quick_imshow(
2071
2068
  tomo_stack[0,:,:], title=title, name=name,
2072
- path=self._output_folder, save_fig=self._save_figs,
2069
+ path=self._outputdir, save_fig=self._save_figs,
2073
2070
  save_only=self._save_only, block=self._block)
2074
2071
  zoom_perc = 100
2075
2072
  if zoom_perc != 100:
@@ -2081,31 +2078,31 @@ class Tomo:
2081
2078
  tomo_zoom_list.append(tomo_zoom)
2082
2079
  tomo_stack = np.stack(tomo_zoom_list)
2083
2080
  self._logger.info(f'Zooming in took {time()-t0:.2f} seconds')
2081
+ title = f'red stack {zoom_perc}p theta ' \
2082
+ f'{round(thetas[0], 2)+0}'
2083
+ quick_imshow(
2084
+ tomo_stack[0,:,:], title=title,
2085
+ path=self._outputdir, save_fig=self._save_figs,
2086
+ save_only=self._save_only, block=self._block)
2084
2087
  del tomo_zoom_list
2085
- if not self._test_mode:
2086
- title = f'red stack {zoom_perc}p theta ' \
2087
- f'{round(thetas[0], 2)+0}'
2088
- quick_imshow(
2089
- tomo_stack[0,:,:], title=title,
2090
- path=self._output_folder, save_fig=self._save_figs,
2091
- save_only=self._save_only, block=self._block)
2092
-
2093
- # Save test data to file
2094
- if self._test_mode:
2095
- row_index = int(tomo_stack.shape[1]/2)
2096
- np.savetxt(
2097
- f'{self._output_folder}/red_stack_{i}.txt',
2098
- tomo_stack[:,row_index,:], fmt='%.6e')
2099
2088
 
2100
2089
  # Combine resized stacks
2101
- reduced_tomo_stacks.append(tomo_stack)
2090
+ reduced_tomo_stacks[i] = tomo_stack
2091
+ if tomo_stack_shape is None:
2092
+ tomo_stack_shape = tomo_stack.shape
2093
+ else:
2094
+ assert tomo_stack_shape == tomo_stack.shape
2095
+
2096
+ for i, stack in enumerate(reduced_tomo_stacks):
2097
+ if not stack.size:
2098
+ reduced_tomo_stacks[i] = np.zeros(tomo_stack_shape)
2102
2099
 
2103
2100
  # Add tomo field info to reduced data NXprocess
2104
- reduced_data.x_translation = np.asarray(horizontal_shifts)
2101
+ reduced_data.x_translation = horizontal_shifts
2105
2102
  reduced_data.x_translation.units = 'mm'
2106
- reduced_data.z_translation = np.asarray(vertical_shifts)
2103
+ reduced_data.z_translation = vertical_shifts
2107
2104
  reduced_data.z_translation.units = 'mm'
2108
- reduced_data.data.tomo_fields = np.asarray(reduced_tomo_stacks)
2105
+ reduced_data.data.tomo_fields = reduced_tomo_stacks
2109
2106
  reduced_data.data.attrs['signal'] = 'tomo_fields'
2110
2107
 
2111
2108
  if tdf is not None:
@@ -2115,235 +2112,467 @@ class Tomo:
2115
2112
  return reduced_data
2116
2113
 
2117
2114
  def _find_center_one_plane(
2118
- self, sinogram, row, thetas, eff_pixel_size, cross_sectional_dim,
2119
- path=None, num_core=1, search_range=None, search_step=None,
2120
- gaussian_sigma=None, ring_width=None):
2121
- """Find center for a single tomography plane."""
2115
+ self, tomo_stacks, stack_index, row, offset_row, thetas,
2116
+ eff_pixel_size, cross_sectional_dim, path=None, num_core=1,
2117
+ center_offset_min=-50, center_offset_max=50,
2118
+ center_search_range=None, gaussian_sigma=None, ring_width=None,
2119
+ prev_center_offset=None):
2120
+ """
2121
+ Find center for a single tomography plane.
2122
+
2123
+ tomo_stacks data axes order: stack,theta,row,column
2124
+ thetas in radians
2125
+ """
2122
2126
  # Third party modules
2123
2127
  import matplotlib.pyplot as plt
2124
- from tomopy import find_center_vo
2128
+ from tomopy import (
2129
+ # find_center,
2130
+ find_center_vo,
2131
+ find_center_pc,
2132
+ )
2125
2133
 
2126
2134
  if not gaussian_sigma:
2127
2135
  gaussian_sigma = None
2128
2136
  if not ring_width:
2129
2137
  ring_width = None
2130
- # Try automatic center finding routines for initial value
2131
- # sinogram index order: theta,column
2132
- # need column,theta for iradon, so take transpose
2133
- sinogram = np.asarray(sinogram)
2134
- sinogram_t = sinogram.T
2135
- center = sinogram.shape[1]/2
2136
-
2137
- # Try using Nghia Vo’s method
2138
+
2139
+ # Get the sinogram for the selected plane
2140
+ sinogram = tomo_stacks[stack_index,:,offset_row,:]
2141
+ center_offset_range = sinogram.shape[1]/2
2142
+
2143
+ # Try Nghia Vo's method to find the center
2138
2144
  t0 = time()
2145
+ if center_offset_min is None:
2146
+ center_offset_min = -50
2147
+ if center_offset_max is None:
2148
+ center_offset_max = 50
2139
2149
  if num_core > NUM_CORE_TOMOPY_LIMIT:
2140
2150
  self._logger.debug(
2141
- f'Running find_center_vo on {NUM_CORE_TOMOPY_LIMIT} cores ...')
2151
+ f'Running find_center_vo on {NUM_CORE_TOMOPY_LIMIT} '
2152
+ 'cores ...')
2142
2153
  tomo_center = find_center_vo(
2143
- sinogram, ncore=NUM_CORE_TOMOPY_LIMIT)
2154
+ sinogram, ncore=NUM_CORE_TOMOPY_LIMIT, smin=center_offset_min,
2155
+ smax=center_offset_max)
2144
2156
  else:
2145
- tomo_center = find_center_vo(sinogram, ncore=num_core)
2157
+ tomo_center = find_center_vo(
2158
+ sinogram, ncore=num_core, smin=center_offset_min,
2159
+ smax=center_offset_max)
2146
2160
  self._logger.info(
2147
- f'Finding center using Nghia Vos method took {time()-t0:.2f} '
2161
+ f'Finding center using Nghia Vo\'s method took {time()-t0:.2f} '
2148
2162
  'seconds')
2149
- center_offset_vo = float(tomo_center-center)
2163
+ center_offset_vo = float(tomo_center-center_offset_range)
2150
2164
  self._logger.info(
2151
- f'Center at row {row} using Nghia Vos method = '
2165
+ f'Center at row {row} using Nghia Vo\'s method = '
2152
2166
  f'{center_offset_vo:.2f}')
2153
2167
 
2168
+ selected_center_offset = center_offset_vo
2154
2169
  if self._interactive or self._save_figs:
2155
2170
 
2156
- # Reconstruct the plane for Nghia Vo’s center offset
2171
+ # Try Guizar-Sicairos's phase correlation method to find
2172
+ # the center
2157
2173
  t0 = time()
2158
- recon_plane = self._reconstruct_one_plane(
2159
- sinogram_t, center_offset_vo, thetas, eff_pixel_size,
2160
- cross_sectional_dim, False, num_core, gaussian_sigma,
2161
- ring_width)
2174
+ tomo_center = find_center_pc(
2175
+ tomo_stacks[stack_index,0,:,:],
2176
+ tomo_stacks[stack_index,-1,:,:])
2177
+ self._logger.info(
2178
+ 'Finding center using Guizar-Sicairos\'s phase correlation '
2179
+ f'method took {time()-t0:.2f} seconds')
2180
+ center_offset_pc = float(tomo_center-center_offset_range)
2162
2181
  self._logger.info(
2163
- f'Reconstructing row {row} took {time()-t0:.2f} seconds')
2164
- recon_edges = [self._get_edges_one_plane(recon_plane)]
2165
- fig, accept, selected_center_offset = self._select_center_offset(
2166
- recon_edges, row, center_offset_vo)
2182
+ f'Center at row {row} using Guizar-Sicairos\'s image entropy '
2183
+ f'method = {center_offset_pc:.2f}')
2184
+
2185
+ # Try Donath's image entropy method to find the center
2186
+ # Skip this method, it seems flawed somehow or I'm doing something wrong
2187
+ # t0 = time()
2188
+ # tomo_center = find_center(
2189
+ # tomo_stacks[stack_index,:,:,:], thetas,
2190
+ # ind=offset_row)
2191
+ # self._logger.info(
2192
+ # 'Finding center using Donath\'s image entropy method took '
2193
+ # f'{time()-t0:.2f} seconds')
2194
+ # center_offset_ie = float(tomo_center-center_offset_range)
2195
+ # self._logger.info(
2196
+ # f'Center at row {row} using Donath\'s image entropy method = '
2197
+ # f'{center_offset_ie:.2f}')
2198
+
2199
+ # Reconstruct the plane for the Nghia Vo's center
2200
+ t0 = time()
2201
+ center_offsets = [center_offset_vo]
2202
+ fig_titles = [f'Vo\'s method: center offset = '
2203
+ f'{center_offset_vo:.2f}']
2204
+ recon_planes = [self._reconstruct_planes(
2205
+ sinogram, center_offset_vo, thetas, num_core=num_core,
2206
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width)]
2207
+ self._logger.info(
2208
+ f'Reconstructing row {row} with center at '
2209
+ f'{center_offset_vo} took {time()-t0:.2f} seconds')
2210
+
2211
+ # Reconstruct the plane for the Guizar-Sicairos's center
2212
+ t0 = time()
2213
+ center_offsets.append(center_offset_pc)
2214
+ fig_titles.append(f'Guizar-Sicairos\'s method: center offset = '
2215
+ f'{center_offset_pc:.2f}')
2216
+ recon_planes.append(self._reconstruct_planes(
2217
+ sinogram, center_offset_pc, thetas, num_core=num_core,
2218
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2219
+ self._logger.info(
2220
+ f'Reconstructing row {row} with center at '
2221
+ f'{center_offset_pc} took {time()-t0:.2f} seconds')
2222
+
2223
+ # Reconstruct the plane for the Donath's center
2224
+ # t0 = time()
2225
+ # center_offsets.append(center_offset_ie)
2226
+ # fig_titles.append(f'Donath\'s method: center offset = '
2227
+ # f'{center_offset_ie:.2f}')
2228
+ # recon_planes.append(self._reconstruct_planes(
2229
+ # sinogram, center_offset_ie, thetas, num_core=num_core,
2230
+ # gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2231
+ # self._logger.info(
2232
+ # f'Reconstructing row {row} with center at '
2233
+ # f'{center_offset_ie} took {time()-t0:.2f} seconds')
2234
+
2235
+ # Reconstruct the plane at the previous row's center
2236
+ if (prev_center_offset is not None
2237
+ and prev_center_offset not in center_offsets):
2238
+ t0 = time()
2239
+ center_offsets.append(prev_center_offset)
2240
+ fig_titles.append(f'Previous row\'s: center offset = '
2241
+ f'{prev_center_offset:.2f}')
2242
+ recon_planes.append(self._reconstruct_planes(
2243
+ sinogram, prev_center_offset, thetas, num_core=num_core,
2244
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2245
+ self._logger.info(
2246
+ f'Reconstructing row {row} with center at '
2247
+ f'{prev_center_offset} took {time()-t0:.2f} seconds')
2248
+
2249
+ # t0 = time()
2250
+ # recon_edges = []
2251
+ # for recon_plane in recon_planes:
2252
+ # recon_edges.append(self._get_edges_one_plane(recon_plane))
2253
+ # print(f'\nGetting edges for row {row} with centers at '
2254
+ # f'{center_offsets} took {time()-t0:.2f} seconds\n')
2255
+
2256
+ # Select the best center
2257
+ fig, accept, selected_center_offset = \
2258
+ self._select_center_offset(
2259
+ recon_planes, row, center_offsets, default_offset_index=0,
2260
+ fig_titles=fig_titles, search_button=False,
2261
+ include_all_bad=True)
2262
+
2167
2263
  # Plot results
2168
2264
  if self._save_figs:
2169
2265
  fig.savefig(
2170
2266
  os_path.join(
2171
- self._output_folder,
2172
- f'edges_default_center_row_{row}.png'))
2267
+ self._outputdir,
2268
+ f'recon_row_{row}_default_centers.png'))
2173
2269
  plt.close()
2174
2270
 
2175
-
2176
- # Perform center finding search
2177
- center_offsets = [center_offset_vo]
2178
- step_size = 4
2179
- indices = 3*[-1]
2180
- prev_index = None
2181
- up = True
2182
- while not accept and step_size:
2183
- selected_center_offset = round(selected_center_offset)
2271
+ # Create reconstructions for a specified search range
2272
+ if self._interactive:
2273
+ if (center_search_range is None
2274
+ and input_yesno('\nDo you want to reconstruct images '
2275
+ 'for a range of rotation centers', 'n')):
2276
+ center_search_range = input_num_list(
2277
+ 'Enter up to 3 numbers (start, end, step), '
2278
+ '(range, step), or range', remove_duplicates=False,
2279
+ sort=False)
2280
+ if center_search_range is not None:
2281
+ if len(center_search_range) != 3:
2282
+ search_range = center_search_range[0]
2283
+ if len(center_search_range) == 1:
2284
+ step = search_range
2285
+ else:
2286
+ step = center_search_range[1]
2287
+ if selected_center_offset == 'all bad':
2288
+ center_search_range = [
2289
+ - search_range/2, search_range/2, step]
2290
+ else:
2291
+ center_search_range = [
2292
+ selected_center_offset - search_range/2,
2293
+ selected_center_offset + search_range/2,
2294
+ step]
2295
+ center_search_range[1] += 1 # Make upper bound inclusive
2296
+ search_center_offsets = list(np.arange(*center_search_range))
2297
+ search_recon_planes = self._reconstruct_planes(
2298
+ sinogram, search_center_offsets, thetas, num_core=num_core,
2299
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width)
2300
+ for i, center in enumerate(search_center_offsets):
2301
+ title = f'Reconstruction for row {row}, center offset: ' \
2302
+ f'{center:.2f}'
2303
+ name = f'recon_row_{row}_center_{center:.2f}.png'
2304
+ if self._interactive:
2305
+ save_only = False
2306
+ block = True
2307
+ else:
2308
+ save_only = True
2309
+ block = False
2310
+ quick_imshow(
2311
+ search_recon_planes[i], title=title, row_label='y',
2312
+ column_label='x', path=self._outputdir, name=name,
2313
+ save_only=save_only, save_fig=True, block=block)
2314
+ center_offsets.append(center)
2315
+ recon_planes.append(search_recon_planes[i])
2316
+
2317
+ # Perform an interactive center finding search
2318
+ calibrate_interactively = False
2319
+ if self._interactive:
2320
+ if selected_center_offset == 'all bad':
2321
+ calibrate_interactively = input_yesno(
2322
+ '\nDo you want to perform an interactive search to '
2323
+ 'calibrate the rotation center (y/n)?', 'n')
2324
+ else:
2325
+ calibrate_interactively = input_yesno(
2326
+ '\nDo you want to perform an interactive search to '
2327
+ 'calibrate the rotation center around the selected value '
2328
+ f'of {selected_center_offset} (y/n)?', 'n')
2329
+ if calibrate_interactively:
2330
+ include_all_bad = True
2331
+ low = None
2332
+ upp = None
2333
+ if selected_center_offset == 'all bad':
2334
+ selected_center_offset = None
2335
+ selected_center_offset = input_num(
2336
+ '\nEnter the initial center offset in the center calibration '
2337
+ 'search', ge=-center_offset_range, le=center_offset_range,
2338
+ default=selected_center_offset)
2339
+ max_step_size = min(
2340
+ center_offset_range+selected_center_offset,
2341
+ center_offset_range-selected_center_offset-1)
2342
+ max_step_size = 1 << int(np.log2(max_step_size))-1
2343
+ step_size = input_int(
2344
+ '\nEnter the intial step size in the center calibration '
2345
+ 'search (will be truncated to the nearest lower power of 2)',
2346
+ ge=2, le=max_step_size, default=4)
2347
+ step_size = 1 << int(np.log2(step_size))
2348
+ selected_center_offset_prev = round(selected_center_offset)
2349
+ while step_size:
2184
2350
  preselected_offsets = (
2185
- selected_center_offset-step_size,
2186
- selected_center_offset,
2187
- selected_center_offset+step_size)
2351
+ selected_center_offset_prev-step_size,
2352
+ selected_center_offset_prev,
2353
+ selected_center_offset_prev+step_size)
2354
+ indices = []
2188
2355
  for i, preselected_offset in enumerate(preselected_offsets):
2189
2356
  if preselected_offset in center_offsets:
2190
- indices[i] = center_offsets.index(preselected_offset)
2357
+ indices.append(
2358
+ center_offsets.index(preselected_offset))
2191
2359
  else:
2192
- recon_plane = self._reconstruct_one_plane(
2193
- sinogram_t, preselected_offset, thetas,
2194
- eff_pixel_size, cross_sectional_dim, False,
2195
- num_core, gaussian_sigma, ring_width)
2196
- indices[i] = len(center_offsets)
2360
+ indices.append(len(center_offsets))
2197
2361
  center_offsets.append(preselected_offset)
2198
- recon_edges.append(
2199
- self._get_edges_one_plane(recon_plane))
2362
+ recon_planes.append(self._reconstruct_planes(
2363
+ sinogram, preselected_offset, thetas,
2364
+ num_core=num_core, gaussian_sigma=gaussian_sigma,
2365
+ ring_width=ring_width))
2200
2366
  fig, accept, selected_center_offset = \
2201
2367
  self._select_center_offset(
2202
- [recon_edges[i] for i in indices],
2203
- row, preselected_offsets, center_offset_vo)
2204
- index = preselected_offsets.index(selected_center_offset)
2205
- if index != 1 and prev_index in (None, index) and up:
2206
- step_size *=2
2207
- else:
2208
- step_size = int(step_size/2)
2209
- up = False
2210
- prev_index = index
2368
+ [recon_planes[i] for i in indices],
2369
+ row, preselected_offsets, default_offset_index=1,
2370
+ include_all_bad=include_all_bad)
2211
2371
  # Plot results
2212
2372
  if self._save_figs:
2213
2373
  fig.savefig(
2214
2374
  os_path.join(
2215
- self._output_folder,
2216
- f'edges_center_{row}_{min(preselected_offsets)}_'\
2375
+ self._outputdir,
2376
+ f'recon_row_{row}_center_range_'
2377
+ f'{min(preselected_offsets)}_'\
2217
2378
  f'{max(preselected_offsets)}.png'))
2218
2379
  plt.close()
2380
+ if accept and input_yesno(
2381
+ f'Accept center offset {selected_center_offset} '
2382
+ f'for row {row}? (y/n)', 'y'):
2383
+ break
2384
+ if selected_center_offset == 'all bad':
2385
+ step_size *=2
2386
+ else:
2387
+ if selected_center_offset == preselected_offsets[0]:
2388
+ upp = preselected_offsets[1]
2389
+ elif selected_center_offset == preselected_offsets[1]:
2390
+ low = preselected_offsets[0]
2391
+ upp = preselected_offsets[2]
2392
+ else:
2393
+ low = preselected_offsets[1]
2394
+ if None in (low, upp):
2395
+ step_size *= 2
2396
+ else:
2397
+ step_size = step_size//2
2398
+ include_all_bad = False
2399
+ selected_center_offset_prev = round(selected_center_offset)
2400
+ if step_size > max_step_size:
2401
+ self._logger.warning(
2402
+ 'Exceeding maximum step size of {max_step_size}')
2403
+ step_size = max_step_size
2404
+
2405
+ # Collect info for the currently selected center
2406
+ recon_planes = [recon_planes[
2407
+ center_offsets.index(selected_center_offset)]]
2408
+ center_offsets = [selected_center_offset]
2409
+ fig_titles = [f'Reconstruction for center offset = '
2410
+ f'{selected_center_offset:.2f}']
2411
+
2412
+ # Try Nghia Vo's method with the selected center
2413
+ step_size = min(step_size, 10)
2414
+ center_offset_min = selected_center_offset-step_size
2415
+ center_offset_max = selected_center_offset+step_size
2416
+ if num_core > NUM_CORE_TOMOPY_LIMIT:
2417
+ self._logger.debug(
2418
+ f'Running find_center_vo on {NUM_CORE_TOMOPY_LIMIT} '
2419
+ 'cores ...')
2420
+ tomo_center = find_center_vo(
2421
+ sinogram, ncore=NUM_CORE_TOMOPY_LIMIT,
2422
+ smin=center_offset_min, smax=center_offset_max)
2423
+ else:
2424
+ tomo_center = find_center_vo(
2425
+ sinogram, ncore=num_core, smin=center_offset_min,
2426
+ smax=center_offset_max)
2427
+ center_offset_vo = float(tomo_center-center_offset_range)
2428
+ self._logger.info(
2429
+ f'Center at row {row} using Nghia Vo\'s method = '
2430
+ f'{center_offset_vo:.2f}')
2431
+
2432
+ # Reconstruct the plane for the Nghia Vo's center
2433
+ center_offsets.append(center_offset_vo)
2434
+ fig_titles.append(f'Vo\'s method: center offset = '
2435
+ f'{center_offset_vo:.2f}')
2436
+ recon_planes.append(self._reconstruct_planes(
2437
+ sinogram, center_offset_vo, thetas, num_core=num_core,
2438
+ gaussian_sigma=gaussian_sigma, ring_width=ring_width))
2439
+
2440
+ # Select the best center
2441
+ fig, accept, selected_center_offset = \
2442
+ self._select_center_offset(
2443
+ recon_planes, row, center_offsets, default_offset_index=0,
2444
+ fig_titles=fig_titles, search_button=False)
2219
2445
 
2220
- # Select center location
2221
- if self._interactive:
2222
- center_offset = selected_center_offset
2223
- else:
2224
- center_offset = center_offset_vo
2225
-
2226
- del sinogram_t
2227
- if recon_plane is not None:
2228
- del recon_plane
2446
+ # Plot results
2447
+ if self._save_figs:
2448
+ fig.savefig(
2449
+ os_path.join(
2450
+ self._outputdir,
2451
+ f'recon_row_{row}_center_'
2452
+ f'{selected_center_offset:.2f}.png'))
2453
+ plt.close()
2229
2454
 
2230
- return float(center_offset)
2455
+ del sinogram
2456
+ del recon_planes
2231
2457
 
2232
- def _reconstruct_one_plane(
2233
- self, tomo_plane_t, center_offset, thetas, eff_pixel_size,
2234
- cross_sectional_dim, plot_sinogram=False, num_core=1,
2458
+ # Return the center location
2459
+ if self._interactive:
2460
+ if selected_center_offset == 'all bad':
2461
+ print('\nUnable to successfully calibrate center axis')
2462
+ selected_center_offset = input_num(
2463
+ 'Enter the center offset for row {row}',
2464
+ ge=-center_offset_range, le=center_offset_range)
2465
+ return float(selected_center_offset)
2466
+ return float(center_offset_vo)
2467
+
2468
+ def _reconstruct_planes(
2469
+ self, tomo_planes, center_offset, thetas, num_core=1,
2235
2470
  gaussian_sigma=None, ring_width=None):
2236
- """Invert the sinogram for a single tomography plane."""
2471
+ """Invert the sinogram for a single or multiple tomography
2472
+ planes using tomopy's recon routine."""
2237
2473
  # Third party modules
2238
2474
  from scipy.ndimage import gaussian_filter
2239
- from skimage.transform import iradon
2240
- from tomopy import misc
2241
-
2242
- # tomo_plane_t index order: column,theta
2243
- two_offset = 2 * int(np.round(center_offset))
2244
- two_offset_abs = np.abs(two_offset)
2245
- # Add 10% slack to max_rad to avoid edge effects
2246
- max_rad = int(0.55 * (cross_sectional_dim/eff_pixel_size))
2247
- if max_rad > 0.5*tomo_plane_t.shape[0]:
2248
- max_rad = 0.5*tomo_plane_t.shape[0]
2249
- dist_from_edge = max(1, int(np.floor(
2250
- (tomo_plane_t.shape[0] - two_offset_abs) / 2.) - max_rad))
2251
- if two_offset >= 0:
2252
- self._logger.debug(
2253
- f'sinogram range = [{two_offset+dist_from_edge}, '
2254
- f'{-dist_from_edge}]')
2255
- sinogram = tomo_plane_t[
2256
- two_offset+dist_from_edge:-dist_from_edge,:]
2257
- else:
2258
- self._logger.debug(
2259
- f'sinogram range = [{dist_from_edge}, '
2260
- f'{two_offset-dist_from_edge}]')
2261
- sinogram = tomo_plane_t[dist_from_edge:two_offset-dist_from_edge,:]
2262
- if plot_sinogram:
2263
- quick_imshow(
2264
- sinogram.T,
2265
- title=f'Sinogram for a center offset of {center_offset:.2f}',
2266
- name=f'sinogram_center_offset{center_offset:.2f}',
2267
- path=self._output_folder, save_fig=self._save_figs,
2268
- save_only=self._save_only, block=self._block, aspect='auto')
2475
+ from tomopy import (
2476
+ misc,
2477
+ recon,
2478
+ )
2269
2479
 
2270
- # Inverting sinogram
2271
- t0 = time()
2272
- recon_sinogram = iradon(sinogram, theta=thetas, circle=True)
2273
- self._logger.info(f'Inverting sinogram took {time()-t0:.2f} seconds')
2274
- del sinogram
2480
+ # Reconstruct the planes
2481
+ # tomo_planes axis data order: (row,)theta,column
2482
+ # thetas in radians
2483
+ if isinstance(center_offset, (int, float)):
2484
+ tomo_planes = np.expand_dims(tomo_planes, 0)
2485
+ center_offset = center_offset + tomo_planes.shape[2]/2
2486
+ elif is_num_series(center_offset):
2487
+ tomo_planes = np.array([tomo_planes]*len(center_offset))
2488
+ center_offset = np.asarray(center_offset) + tomo_planes.shape[2]/2
2489
+ else:
2490
+ raise ValueError(
2491
+ f'Invalid parameter center_offset ({center_offset})')
2492
+ recon_planes = recon(
2493
+ tomo_planes, thetas, center=center_offset, sinogram_order=True,
2494
+ algorithm='gridrec', ncore=num_core)
2275
2495
 
2276
2496
  # Performing Gaussian filtering and removing ring artifacts
2277
2497
  if gaussian_sigma is not None and gaussian_sigma:
2278
- recon_sinogram = gaussian_filter(
2279
- recon_sinogram, gaussian_sigma, mode='nearest')
2280
- recon_clean = np.expand_dims(recon_sinogram, axis=0)
2281
- del recon_sinogram
2498
+ recon_planes = gaussian_filter(
2499
+ recon_planes, gaussian_sigma, mode='nearest')
2282
2500
  if ring_width is not None and ring_width:
2283
- recon_clean = misc.corr.remove_ring(
2284
- recon_clean, rwidth=ring_width, ncore=num_core)
2285
-
2286
- return recon_clean
2287
-
2288
- def _get_edges_one_plane(self, recon_plane):
2289
- """
2290
- Create an "edges plot" image for a single reconstructed
2291
- tomography data plane.
2292
- """
2293
- # Third party modules
2294
- from skimage.restoration import denoise_tv_chambolle
2295
-
2296
- vis_parameters = None # self._config.get('vis_parameters')
2297
- if vis_parameters is None:
2298
- weight = 0.1
2299
- else:
2300
- weight = vis_parameters.get('denoise_weight', 0.1)
2301
- if not is_num(weight, ge=0.):
2302
- self._logger.warning(
2303
- f'Invalid weight ({weight}) in _get_edges_one_plane, '
2304
- 'set to a default of 0.1')
2305
- weight = 0.1
2306
- return denoise_tv_chambolle(recon_plane, weight=weight)[0]
2501
+ recon_planes = misc.corr.remove_ring(
2502
+ recon_planes, rwidth=ring_width, ncore=num_core)
2503
+
2504
+ # Apply a circular mask
2505
+ recon_planes = misc.corr.circ_mask(recon_planes, axis=0)
2506
+
2507
+ return np.squeeze(recon_planes)
2508
+
2509
+ # def _get_edges_one_plane(self, recon_plane):
2510
+ # """
2511
+ # Create an "edges plot" image for a single reconstructed
2512
+ # tomography data plane.
2513
+ # """
2514
+ # # Third party modules
2515
+ # from skimage.restoration import denoise_tv_chambolle
2516
+ #
2517
+ # vis_parameters = None # RV self._config.get('vis_parameters')
2518
+ # if vis_parameters is None:
2519
+ # weight = 0.1
2520
+ # else:
2521
+ # weight = vis_parameters.get('denoise_weight', 0.1)
2522
+ # if not is_num(weight, ge=0.):
2523
+ # self._logger.warning(
2524
+ # f'Invalid weight ({weight}) in _get_edges_one_plane, '
2525
+ # 'set to a default of 0.1')
2526
+ # weight = 0.1
2527
+ # return denoise_tv_chambolle(recon_plane, weight=weight)
2307
2528
 
2308
2529
  def _select_center_offset(
2309
- self, recon_edges, row, preselected_offsets,
2310
- center_offset_vo=None):
2311
- """Select a center offset value from an "edges plot" image
2530
+ self, recon_planes, row, preselected_offsets,
2531
+ default_offset_index=0, fig_titles=None, search_button=True,
2532
+ include_all_bad=False):
2533
+ """Select a center offset value from reconstructed images
2312
2534
  for a single reconstructed tomography data plane."""
2313
2535
  # Third party modules
2314
2536
  import matplotlib.pyplot as plt
2315
2537
  from matplotlib.widgets import RadioButtons, Button
2316
- from matplotlib.widgets import Button
2317
2538
 
2318
2539
  def select_offset(offset):
2319
2540
  """Callback function for the "Select offset" input."""
2320
- selected_offset.append(
2321
- ((False,
2322
- preselected_offsets[preselected_offsets.index(
2323
- float(radio_btn.value_selected))])))
2324
- plt.close()
2541
+ pass
2325
2542
 
2326
- def reject(event):
2327
- """Callback function for the "Reject" button."""
2328
- selected_offset.append((False, preselected_offsets[0]))
2543
+ def search(event):
2544
+ """Callback function for the "Search" button."""
2545
+ if num_plots == 1:
2546
+ selected_offset.append(
2547
+ (False, preselected_offsets[default_offset_index]))
2548
+ else:
2549
+ offset = radio_btn.value_selected
2550
+ if offset in ('both bad', 'all bad'):
2551
+ selected_offset.append((False, 'all bad'))
2552
+ else:
2553
+ selected_offset.append((False, float(offset)))
2329
2554
  plt.close()
2330
2555
 
2331
2556
  def accept(event):
2332
2557
  """Callback function for the "Accept" button."""
2333
2558
  if num_plots == 1:
2334
- selected_offset.append((True, preselected_offsets[0]))
2335
- else:
2336
2559
  selected_offset.append(
2337
- ((False,
2338
- preselected_offsets[preselected_offsets.index(
2339
- float(radio_btn.value_selected))])))
2560
+ (True, preselected_offsets[default_offset_index]))
2561
+ else:
2562
+ offset = radio_btn.value_selected
2563
+ if offset in ('both bad', 'all bad'):
2564
+ selected_offset.append((False, 'all bad'))
2565
+ else:
2566
+ selected_offset.append((True, float(offset)))
2340
2567
  plt.close()
2341
2568
 
2342
- if not isinstance(recon_edges, (tuple, list)):
2343
- recon_edges = [recon_edges]
2569
+ if not isinstance(recon_planes, (tuple, list)):
2570
+ recon_planes = [recon_planes]
2344
2571
  if not isinstance(preselected_offsets, (tuple, list)):
2345
2572
  preselected_offsets = [preselected_offsets]
2346
- assert len(recon_edges) == len(preselected_offsets)
2573
+ assert len(recon_planes) == len(preselected_offsets)
2574
+ if fig_titles is not None:
2575
+ assert len(fig_titles) == len(preselected_offsets)
2347
2576
 
2348
2577
  selected_offset = []
2349
2578
 
@@ -2355,55 +2584,62 @@ class Tomo:
2355
2584
  'horizontalalignment': 'center',
2356
2585
  'verticalalignment': 'bottom'}
2357
2586
 
2358
- num_plots = len(recon_edges)
2587
+ num_plots = len(recon_planes)
2359
2588
  if num_plots == 1:
2360
2589
  fig, axs = plt.subplots(figsize=(11, 8.5))
2361
2590
  axs = [axs]
2362
- vmax = np.max(recon_edges[0][:,:])
2591
+ vmax = np.max(recon_planes[0][:,:])
2363
2592
  else:
2364
2593
  fig, axs = plt.subplots(ncols=num_plots, figsize=(17, 8.5))
2365
2594
  axs = list(axs)
2366
- vmax = np.max(recon_edges[1][:,:])
2367
- for i, (ax, recon_edge, preselected_offset) in enumerate(zip(
2368
- axs, recon_edges, preselected_offsets)):
2369
- ax.imshow(recon_edge, vmin=-vmax, vmax=vmax, cmap='gray')
2370
- if num_plots == 1:
2371
- ax.set_title(
2372
- f'Reconstruction for row {row}, center offset: ' \
2373
- f'{preselected_offset:.2f}', fontsize='x-large')
2374
- else:
2375
- ax.set_title(
2376
- f'Center offset: {preselected_offset}',
2377
- fontsize='x-large')
2595
+ vmax = np.max(recon_planes[1][:,:])
2596
+ for i, (ax, recon_plane, preselected_offset) in enumerate(zip(
2597
+ axs, recon_planes, preselected_offsets)):
2598
+ ax.imshow(recon_plane, vmin=-vmax, vmax=vmax, cmap='gray')
2599
+ if fig_titles is None:
2600
+ if num_plots == 1:
2601
+ ax.set_title(
2602
+ f'Reconstruction for row {row}, center offset: ' \
2603
+ f'{preselected_offset:.2f}', fontsize='x-large')
2604
+ else:
2605
+ ax.set_title(
2606
+ f'Center offset: {preselected_offset}',
2607
+ fontsize='x-large')
2378
2608
  ax.set_xlabel('x', fontsize='x-large')
2379
2609
  if not i:
2380
2610
  ax.set_ylabel('y', fontsize='x-large')
2611
+ if fig_titles is not None:
2612
+ for (ax, fig_title) in zip(axs, fig_titles):
2613
+ ax.set_title(fig_title, fontsize='x-large')
2381
2614
 
2382
- if len(recon_edges) > 1:
2383
- fig_title = plt.figtext(
2384
- *title_pos,
2385
- f'Reconstructions for row {row} (default center offset: '
2386
- f'{center_offset_vo})',
2387
- **title_props)
2615
+ fig_title = plt.figtext(
2616
+ *title_pos, f'Reconstruction for row {row}', **title_props)
2617
+ if num_plots == 1:
2388
2618
  fig_subtitle = plt.figtext(
2389
2619
  *subtitle_pos,
2390
- 'Select the best offset or press "Accept" to accept the '
2391
- f'default value of {preselected_offsets[1]}',
2620
+ 'Press "Accept" to accept this value or "Reject" if not',
2392
2621
  **subtitle_props)
2393
2622
  else:
2394
- fig_title = plt.figtext(
2395
- *title_pos,
2396
- 'Press "Accept" to accept this value or "Reject" to start a '
2397
- 'center calibration search',
2398
- **title_props)
2623
+ if search_button:
2624
+ fig_subtitle = plt.figtext(
2625
+ *subtitle_pos,
2626
+ 'Select the best offset and press "Accept" to accept or '
2627
+ '"Search" to continue the search',
2628
+ **subtitle_props)
2629
+ else:
2630
+ fig_subtitle = plt.figtext(
2631
+ *subtitle_pos,
2632
+ 'Select the best offset and press "Accept" to accept',
2633
+ **subtitle_props)
2399
2634
 
2400
2635
  if not self._interactive:
2401
2636
 
2402
- selected_offset.append((True, preselected_offsets[0]))
2637
+ selected_offset.append(
2638
+ (True, preselected_offsets[default_offset_index]))
2403
2639
 
2404
2640
  else:
2405
2641
 
2406
- fig.subplots_adjust(bottom=0.2)
2642
+ fig.subplots_adjust(bottom=0.25, top=0.85)
2407
2643
 
2408
2644
  if num_plots == 1:
2409
2645
 
@@ -2412,18 +2648,30 @@ class Tomo:
2412
2648
  plt.axes([0.15, 0.05, 0.15, 0.075]), 'Reject')
2413
2649
  reject_cid = reject_btn.on_clicked(reject)
2414
2650
 
2415
-
2416
2651
  else:
2417
2652
 
2418
2653
  # Setup RadioButtons
2419
2654
  select_text = plt.figtext(
2420
2655
  0.225, 0.175, 'Select offset', fontsize='x-large',
2421
2656
  horizontalalignment='center', verticalalignment='center')
2657
+ if include_all_bad:
2658
+ if num_plots == 2:
2659
+ labels = (*preselected_offsets, 'both bad')
2660
+ else:
2661
+ labels = (*preselected_offsets, 'all bad')
2662
+ else:
2663
+ labels = preselected_offsets
2422
2664
  radio_btn = RadioButtons(
2423
2665
  plt.axes([0.175, 0.05, 0.1, 0.1]),
2424
- labels = preselected_offsets, active=1)
2666
+ labels = labels, active=default_offset_index)
2425
2667
  radio_cid = radio_btn.on_clicked(select_offset)
2426
2668
 
2669
+ # Setup "Search" button
2670
+ if search_button:
2671
+ search_btn = Button(
2672
+ plt.axes([0.4125, 0.05, 0.15, 0.075]), 'Search')
2673
+ search_cid = search_btn.on_clicked(search)
2674
+
2427
2675
  # Setup "Accept" button
2428
2676
  accept_btn = Button(
2429
2677
  plt.axes([0.7, 0.05, 0.15, 0.075]), 'Accept')
@@ -2439,6 +2687,9 @@ class Tomo:
2439
2687
  else:
2440
2688
  radio_btn.disconnect(radio_cid)
2441
2689
  radio_btn.ax.remove()
2690
+ if search_button:
2691
+ search_btn.disconnect(search_cid)
2692
+ search_btn.ax.remove()
2442
2693
  accept_btn.disconnect(accept_cid)
2443
2694
  accept_btn.ax.remove()
2444
2695
 
@@ -2446,16 +2697,20 @@ class Tomo:
2446
2697
  fig_title.remove()
2447
2698
  else:
2448
2699
  fig_title.set_in_layout(True)
2449
- fig_subtitle.remove()
2450
- select_text.remove()
2700
+ if self._interactive:
2701
+ select_text.remove()
2702
+ fig_subtitle.remove()
2451
2703
  fig.tight_layout(rect=(0, 0, 1, 0.95))
2704
+ if not selected_offset:# and num_plots == 1:
2705
+ selected_offset.append(
2706
+ (True, preselected_offsets[default_offset_index]))
2452
2707
 
2453
2708
  return fig, *selected_offset[0]
2454
2709
 
2455
2710
  def _reconstruct_one_tomo_stack(
2456
2711
  self, tomo_stack, thetas, center_offsets=None, num_core=1,
2457
- algorithm='gridrec', secondary_iters=0, remove_stripe_sigma=None,
2458
- ring_width=None):
2712
+ algorithm='gridrec', secondary_iters=0, gaussian_sigma=None,
2713
+ remove_stripe_sigma=None, ring_width=None):
2459
2714
  """Reconstruct a single tomography stack."""
2460
2715
  # Third party modules
2461
2716
  from tomopy import (
@@ -2465,16 +2720,10 @@ class Tomo:
2465
2720
  recon,
2466
2721
  )
2467
2722
 
2468
- # tomo_stack order: row,theta,column
2469
- # input thetas must be in degrees
2723
+ # tomo_stack axis data order: row,theta,column
2724
+ # thetas in radians
2470
2725
  # centers_offset: tomography axis shift in pixels relative
2471
- # to column center
2472
- # RV should we remove stripes?
2473
- # https://tomopy.readthedocs.io/en/latest/api/tomopy.prep.stripe.html
2474
- # RV should we remove rings?
2475
- # https://tomopy.readthedocs.io/en/latest/api/tomopy.misc.corr.html
2476
- # RV add an option to do (extra) secondary iterations later or
2477
- # to do some sort of convergence test?
2726
+ # to column center
2478
2727
  if center_offsets is None:
2479
2728
  centers = np.zeros((tomo_stack.shape[0]))
2480
2729
  elif len(center_offsets) == 2:
@@ -2488,26 +2737,10 @@ class Tomo:
2488
2737
  centers = center_offsets
2489
2738
  centers += tomo_stack.shape[2]/2
2490
2739
 
2491
- # tomo_recon_stack = []
2492
- # eff_pixel_size = 0.05
2493
- # cross_sectional_dim = 20.0
2494
- # gaussian_sigma = 0.05
2495
- # ring_width = 1
2496
- # for i in range(tomo_stack.shape[0]):
2497
- # sinogram_t = tomo_stack[i,:,:].T
2498
- # recon_plane = self._reconstruct_one_plane(
2499
- # sinogram_t, centers[i], thetas, eff_pixel_size,
2500
- # cross_sectional_dim, False, num_core, gaussian_sigma,
2501
- # ring_width)
2502
- # tomo_recon_stack.append(recon_plane[0,:,:])
2503
- # tomo_recon_stack = np.asarray(tomo_recon_stack)
2504
- # return tomo_recon_stack
2505
-
2506
2740
  # Remove horizontal stripe
2507
2741
  # RV prep.stripe.remove_stripe_fw seems flawed for hollow brick
2508
- # accross multiple stacks
2742
+ # accross multiple stacks
2509
2743
  if remove_stripe_sigma is not None and remove_stripe_sigma:
2510
- self._logger.warning('Ignoring remove_stripe_sigma')
2511
2744
  if num_core > NUM_CORE_TOMOPY_LIMIT:
2512
2745
  tomo_stack = prep.stripe.remove_stripe_fw(
2513
2746
  tomo_stack, sigma=remove_stripe_sigma,
@@ -2520,7 +2753,7 @@ class Tomo:
2520
2753
  self._logger.debug('Performing initial image reconstruction')
2521
2754
  t0 = time()
2522
2755
  tomo_recon_stack = recon(
2523
- tomo_stack, np.radians(thetas), centers, sinogram_order=True,
2756
+ tomo_stack, thetas, centers, sinogram_order=True,
2524
2757
  algorithm=algorithm, ncore=num_core)
2525
2758
  self._logger.info(
2526
2759
  f'Performing initial image reconstruction took {time()-t0:.2f} '
@@ -2558,9 +2791,9 @@ class Tomo:
2558
2791
  }
2559
2792
  t0 = time()
2560
2793
  tomo_recon_stack = recon(
2561
- tomo_stack, np.radians(thetas), centers,
2562
- init_recon=tomo_recon_stack, options=options,
2563
- sinogram_order=True, algorithm=astra, ncore=num_core)
2794
+ tomo_stack, thetas, centers, init_recon=tomo_recon_stack,
2795
+ options=options, sinogram_order=True, algorithm=astra,
2796
+ ncore=num_core)
2564
2797
  self._logger.info(
2565
2798
  f'Performing secondary iterations took {time()-t0:.2f} '
2566
2799
  'seconds')
@@ -2571,6 +2804,11 @@ class Tomo:
2571
2804
  tomo_recon_stack, rwidth=ring_width, out=tomo_recon_stack,
2572
2805
  ncore=num_core)
2573
2806
 
2807
+ # Performing Gaussian filtering
2808
+ if gaussian_sigma is not None and gaussian_sigma:
2809
+ tomo_recon_stack = misc.corr.gaussian_filter(
2810
+ tomo_recon_stack, sigma=gaussian_sigma, ncore=num_core)
2811
+
2574
2812
  return tomo_recon_stack
2575
2813
 
2576
2814
  def _resize_reconstructed_data(
@@ -2644,7 +2882,7 @@ class Tomo:
2644
2882
  if self._save_figs:
2645
2883
  fig.savefig(
2646
2884
  os_path.join(
2647
- self._output_folder, 'reconstructed_data_xy_roi.png'))
2885
+ self._outputdir, 'reconstructed_data_xy_roi.png'))
2648
2886
  plt.close()
2649
2887
 
2650
2888
  # Selecting z bounds (in xy-plane)
@@ -2676,7 +2914,7 @@ class Tomo:
2676
2914
  if self._save_figs:
2677
2915
  fig.savefig(
2678
2916
  os_path.join(
2679
- self._output_folder, 'reconstructed_data_z_roi.png'))
2917
+ self._outputdir, 'reconstructed_data_z_roi.png'))
2680
2918
  plt.close()
2681
2919
 
2682
2920
  return x_bounds, y_bounds, z_bounds
@@ -2689,7 +2927,7 @@ class TomoSimFieldProcessor(Processor):
2689
2927
  tomography detector images.
2690
2928
  """
2691
2929
 
2692
- def process(self, data, **kwargs):
2930
+ def process(self, data):
2693
2931
  """
2694
2932
  Process the input configuration and return a
2695
2933
  `nexusformat.nexus.NXroot` object with the simulated
@@ -2743,30 +2981,30 @@ class TomoSimFieldProcessor(Processor):
2743
2981
  f'({detector_size[0]*pixel_size[0]})')
2744
2982
 
2745
2983
  # Get the rotation angles (start at a arbitrarily choose angle
2746
- # and add thetas for a full 360 degrees rotation series)
2984
+ # and add thetas for a full 360 degrees rotation series)
2747
2985
  if station in ('id1a3', 'id3a'):
2748
2986
  theta_start = 0.
2749
2987
  else:
2750
2988
  theta_start = -17
2751
- #RV theta_end = theta_start + 360.
2989
+ # RV theta_end = theta_start + 360.
2752
2990
  theta_end = theta_start + 180.
2753
2991
  thetas = list(
2754
2992
  np.arange(theta_start, theta_end+0.5*theta_step, theta_step))
2755
2993
 
2756
2994
  # Get the number of horizontal stacks bases on the diagonal
2757
- # of the square and for now don't allow more than one
2995
+ # of the square and for now don't allow more than one
2758
2996
  num_tomo_stack = 1 + int((sample_size[1]*np.sqrt(2)-pixel_size[1])
2759
2997
  / (detector_size[1]*pixel_size[1]))
2760
2998
  if num_tomo_stack > 1:
2761
2999
  raise ValueError('Sample is too wide for the detector')
2762
3000
 
2763
3001
  # Create the x-ray path length through a solid square
2764
- # crosssection for a set of rotation angles.
3002
+ # crosssection for a set of rotation angles.
2765
3003
  path_lengths_solid = self._create_pathlength_solid_square(
2766
3004
  sample_size[1], thetas, pixel_size[1], detector_size[1])
2767
3005
 
2768
3006
  # Create the x-ray path length through a hollow square
2769
- # crosssection for a set of rotation angles.
3007
+ # crosssection for a set of rotation angles.
2770
3008
  path_lengths_hollow = None
2771
3009
  if sample_type in ('square_pipe', 'hollow_cube', 'hollow_brick'):
2772
3010
  path_lengths_hollow = path_lengths_solid \
@@ -2887,10 +3125,6 @@ class TomoSimFieldProcessor(Processor):
2887
3125
  nxdetector.z_translation = vertical_shifts
2888
3126
  nxdetector.starting_image_index = starting_image_index
2889
3127
  nxdetector.starting_image_offset = starting_image_offset
2890
- # nxdetector.path_lengths_solid = path_lengths_solid
2891
- # nxdetector.path_lengths_hollow = path_lengths_hollow
2892
- # nxdetector.intensities_solid = intensities_solid
2893
- # nxdetector.intensities_hollow = intensities_hollow
2894
3128
 
2895
3129
  return nxroot
2896
3130
 
@@ -2944,7 +3178,7 @@ class TomoDarkFieldProcessor(Processor):
2944
3178
  tomography data set created by TomoSimProcessor.
2945
3179
  """
2946
3180
 
2947
- def process(self, data, num_image=5, **kwargs):
3181
+ def process(self, data, num_image=5):
2948
3182
  """
2949
3183
  Process the input configuration and return a
2950
3184
  `nexusformat.nexus.NXroot` object with the simulated
@@ -3017,7 +3251,7 @@ class TomoBrightFieldProcessor(Processor):
3017
3251
  tomography data set created by TomoSimProcessor.
3018
3252
  """
3019
3253
 
3020
- def process(self, data, num_image=5, **kwargs):
3254
+ def process(self, data, num_image=5):
3021
3255
  """
3022
3256
  Process the input configuration and return a
3023
3257
  `nexusformat.nexus.NXroot` object with the simulated
@@ -3034,7 +3268,6 @@ class TomoBrightFieldProcessor(Processor):
3034
3268
  """
3035
3269
  # Third party modules
3036
3270
  from nexusformat.nexus import (
3037
- NeXusError,
3038
3271
  NXroot,
3039
3272
  NXentry,
3040
3273
  NXinstrument,
@@ -3070,9 +3303,9 @@ class TomoBrightFieldProcessor(Processor):
3070
3303
  dtype=np.int64)
3071
3304
  bright_field = np.concatenate((dummy_fields, bright_field))
3072
3305
  num_image += num_dummy_start
3073
- # Add 10% to slit size to make the bright beam slightly taller
3074
- # than the vertical displacements between stacks
3075
- slit_size = 1.10*source.slit_size
3306
+ # Add 20% to slit size to make the bright beam slightly taller
3307
+ # than the vertical displacements between stacks
3308
+ slit_size = 1.2*source.slit_size
3076
3309
  if slit_size < float(detector.row_pixel_size*detector_size[0]):
3077
3310
  img_row_coords = float(detector.row_pixel_size) \
3078
3311
  * (0.5 + np.asarray(range(int(detector_size[0])))
@@ -3106,7 +3339,7 @@ class TomoSpecProcessor(Processor):
3106
3339
  simulated tomography data set created by TomoSimProcessor.
3107
3340
  """
3108
3341
 
3109
- def process(self, data, scan_numbers=[1], **kwargs):
3342
+ def process(self, data, scan_numbers=[1]):
3110
3343
  """
3111
3344
  Process the input configuration and return a list of strings
3112
3345
  representing a plain text SPEC file.
@@ -3124,17 +3357,14 @@ class TomoSpecProcessor(Processor):
3124
3357
  from json import dumps
3125
3358
  from datetime import datetime
3126
3359
 
3127
- # Third party modules
3128
3360
  from nexusformat.nexus import (
3129
- NeXusError,
3130
- NXcollection,
3131
3361
  NXentry,
3132
3362
  NXroot,
3133
3363
  NXsubentry,
3134
3364
  )
3135
3365
 
3136
3366
  # Get and validate the TomoSimField, TomoDarkField, or
3137
- # TomoBrightField configuration object in data
3367
+ # TomoBrightField configuration object in data
3138
3368
  configs = {}
3139
3369
  nxroot = get_nxroot(data, 'tomo.models.TomoDarkField')
3140
3370
  if nxroot is not None:
@@ -3163,7 +3393,7 @@ class TomoSpecProcessor(Processor):
3163
3393
  raise ValueError('Inconsistent sample_type among scans')
3164
3394
  detector = nxroot.entry.instrument.detector
3165
3395
  if 'z_translation' in detector:
3166
- num_stack = np.asarray(detector.z_translation).size
3396
+ num_stack = detector.z_translation.size
3167
3397
  else:
3168
3398
  num_stack = 1
3169
3399
  data_shape = detector.data.shape
@@ -3197,9 +3427,9 @@ class TomoSpecProcessor(Processor):
3197
3427
  if station in ('id1a3', 'id3a'):
3198
3428
  spec_file.append('#O0 ramsx ramsz')
3199
3429
  else:
3200
- #RV Fix main code to use independent dim info
3430
+ # RV Fix main code to use independent dim info
3201
3431
  spec_file.append('#O0 GI_samx GI_samz GI_samphi')
3202
- spec_file.append('#o0 samx samz samphi') #RV do I need this line?
3432
+ spec_file.append('#o0 samx samz samphi') # RV do I need this line?
3203
3433
  spec_file.append('')
3204
3434
 
3205
3435
  # Create the SPEC file scan info (and image and parfile data for SMB)
@@ -3211,10 +3441,10 @@ class TomoSpecProcessor(Processor):
3211
3441
  for schema, nxroot in configs.items():
3212
3442
  detector = nxroot.entry.instrument.detector
3213
3443
  if 'z_translation' in detector:
3214
- z_translations = list(np.asarray(detector.z_translation))
3444
+ z_translations = list(detector.z_translation.nxdata)
3215
3445
  else:
3216
3446
  z_translations = [0.]
3217
- thetas = np.asarray(detector.thetas)
3447
+ thetas = detector.thetas
3218
3448
  num_theta = thetas.size
3219
3449
  if schema == 'tomo.models.TomoDarkField':
3220
3450
  if station in ('id1a3', 'id3a'):
@@ -3253,9 +3483,11 @@ class TomoSpecProcessor(Processor):
3253
3483
  spec_file.append('#N 1')
3254
3484
  spec_file.append('#L ome')
3255
3485
  if scan_type == 'ts1':
3256
- image_sets.append(np.asarray(detector.data)[n])
3486
+ #image_sets.append(detector.data.nxdata[n])
3487
+ image_sets.append(detector.data[n])
3257
3488
  else:
3258
- image_sets.append(np.asarray(detector.data))
3489
+ #image_sets.append(detector.data.nxdata)
3490
+ image_sets.append(detector.data)
3259
3491
  par_file.append(
3260
3492
  f'{datetime.now().strftime("%Y%m%d")} '
3261
3493
  f'{datetime.now().strftime("%H%M%S")} '
@@ -3345,7 +3577,7 @@ class TomoSpecProcessor(Processor):
3345
3577
 
3346
3578
  nxroot = NXroot()
3347
3579
  nxroot[sample_type] = nxentry
3348
- nxroot.attrs['default'] = sample_type
3580
+ nxroot[sample_type].set_default()
3349
3581
 
3350
3582
  return nxroot
3351
3583