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