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