ChessAnalysisPipeline 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

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

Potentially problematic release.


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

CHAP/tomo/processor.py ADDED
@@ -0,0 +1,2007 @@
1
+ #!/usr/bin/env python
2
+ #-*- coding: utf-8 -*-
3
+ #pylint: disable=
4
+ '''
5
+ File : processor.py
6
+ Author : Rolf Verberg <rolfverberg AT gmail dot com>
7
+ Description: Module for Processors used only by tomography experiments
8
+ '''
9
+
10
+ # system modules
11
+ from os import mkdir
12
+ from os import path as os_path
13
+ from time import time
14
+
15
+ # third party modules
16
+ from nexusformat.nexus import NXobject
17
+ import numpy as np
18
+
19
+ # local modules
20
+ from CHAP.common.utils.general import clear_plot, clear_imshow, quick_plot, quick_imshow
21
+ from CHAP.processor import Processor
22
+
23
+ num_core_tomopy_limit = 24
24
+
25
+
26
+ class TomoDataProcessor(Processor):
27
+ '''Class representing the processes to reconstruct a set of Tomographic images returning
28
+ either a dictionary or a `nexusformat.nexus.NXroot` object containing the (meta) data after
29
+ processing each individual step.
30
+ '''
31
+
32
+ def _process(self, data):
33
+ '''Process the output of a `Reader` that contains a map or a `nexusformat.nexus.NXroot`
34
+ object and one that contains the step specific instructions and return either a dictionary
35
+ or a `nexusformat.nexus.NXroot` returning the processed result.
36
+
37
+ :param data: Result of `Reader.read` where at least one item is of type
38
+ `nexusformat.nexus.NXroot` or has the value `'MapConfig'` for the `'schema'` key,
39
+ and at least one item has the value `'TomoReduceConfig'` for the `'schema'` key.
40
+ :type data: list[dict[str,object]]
41
+ :return: processed (meta)data
42
+ :rtype: dict or nexusformat.nexus.NXroot
43
+ '''
44
+
45
+ tomo = Tomo(save_figs='only')
46
+ nxroot = None
47
+ center_config = None
48
+
49
+ # Get and validate the relevant configuration objects in data
50
+ configs = self.get_configs(data)
51
+
52
+ # Setup the pipeline for a tomography reconstruction
53
+ if 'setup' in configs:
54
+ configs.pop('nxroot', None)
55
+ nxroot = self.get_nxroot(configs.pop('map'), configs.pop('setup'))
56
+ else:
57
+ nxroot = configs.pop('nxroot', None)
58
+
59
+ # Reduce tomography images
60
+ if 'reduce' in configs:
61
+ tool_config = configs.pop('reduce')
62
+ if nxroot is None:
63
+ map_config = configs.pop('map')
64
+ nxroot = self.get_nxroot(map_config, tool_config)
65
+ nxroot = tomo.gen_reduced_data(nxroot, img_x_bounds=tool_config.img_x_bounds)
66
+
67
+ # Find rotation axis centers for the tomography stacks
68
+ # Pass tool_config directly to tomo.find_centers?
69
+ if 'find_center' in configs:
70
+ tool_config = configs.pop('find_center')
71
+ center_rows = [tool_config.lower_row, tool_config.upper_row]
72
+ if (None in center_rows or tool_config.lower_center_offset is None or
73
+ tool_config.upper_center_offset is None):
74
+ center_config = tomo.find_centers(nxroot, center_rows=center_rows,
75
+ center_stack_index=tool_config.center_stack_index)
76
+ else:
77
+ #RV make a convert to dict in basemodel?
78
+ center_config = {'lower_row': tool_config.lower_row,
79
+ 'lower_center_offset': tool_config.lower_center_offset,
80
+ 'upper_row': tool_config.upper_row,
81
+ 'upper_center_offset': tool_config.upper_center_offset}
82
+
83
+ # Reconstruct tomography stacks
84
+ # Pass tool_config and center_config directly to tomo.reconstruct_data
85
+ if 'reconstruct' in configs:
86
+ tool_config = configs.pop('reconstruct')
87
+ nxroot = tomo.reconstruct_data(nxroot, center_config, x_bounds=tool_config.x_bounds,
88
+ y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
89
+ center_config = None
90
+
91
+ # Combine reconstructed tomography stacks
92
+ if 'combine' in configs:
93
+ tool_config = configs.pop('combine')
94
+ nxroot = tomo.combine_data(nxroot, x_bounds=tool_config.x_bounds,
95
+ y_bounds=tool_config.y_bounds, z_bounds=tool_config.z_bounds)
96
+
97
+ if center_config is not None:
98
+ return center_config
99
+ else:
100
+ return nxroot
101
+
102
+ def get_configs(self, data):
103
+ '''Get instances of the configuration objects needed by this
104
+ `Processor` from a returned value of `Reader.read`
105
+
106
+ :param data: Result of `Reader.read` where at least one item
107
+ is of type `nexusformat.nexus.NXroot` or has the value
108
+ `'MapConfig'` for the `'schema'` key, and at least one item
109
+ has the value `'TomoSetupConfig'`, or `'TomoReduceConfig'`,
110
+ or `'TomoFindCenterConfig'`, or `'TomoReconstructConfig'`,
111
+ or `'TomoCombineConfig'` for the `'schema'` key.
112
+ :type data: list[dict[str,object]]
113
+ :raises Exception: If valid config objects cannot be constructed
114
+ from `data`.
115
+ :return: valid instances of the configuration objects with field
116
+ values taken from `data`.
117
+ :rtype: dict
118
+ '''
119
+ #:rtype: dict{'map': MapConfig, 'reduce': TomoReduceConfig} RV: Is there a way to denote optional items?
120
+ from CHAP.common.models.map import MapConfig
121
+ from CHAP.tomo.models import TomoSetupConfig, TomoReduceConfig, TomoFindCenterConfig, \
122
+ TomoReconstructConfig, TomoCombineConfig
123
+ from nexusformat.nexus import NXroot
124
+
125
+ configs = {}
126
+ if isinstance(data, list):
127
+ for item in data:
128
+ if isinstance(item, dict):
129
+ schema = item.get('schema')
130
+ if isinstance(item.get('data'), NXroot):
131
+ configs['nxroot'] = item.get('data')
132
+ if schema == 'MapConfig':
133
+ configs['map'] = MapConfig(**(item.get('data')))
134
+ if schema == 'TomoSetupConfig':
135
+ configs['setup'] = TomoSetupConfig(**(item.get('data')))
136
+ if schema == 'TomoReduceConfig':
137
+ configs['reduce'] = TomoReduceConfig(**(item.get('data')))
138
+ elif schema == 'TomoFindCenterConfig':
139
+ configs['find_center'] = TomoFindCenterConfig(**(item.get('data')))
140
+ elif schema == 'TomoReconstructConfig':
141
+ configs['reconstruct'] = TomoReconstructConfig(**(item.get('data')))
142
+ elif schema == 'TomoCombineConfig':
143
+ configs['combine'] = TomoCombineConfig(**(item.get('data')))
144
+
145
+ return configs
146
+
147
+ def get_nxroot(self, map_config, tool_config):
148
+ '''Get a map of the collected tomography data from the scans in `map_config`. The
149
+ data will be reduced based on additional parameters included in `tool_config`.
150
+ The data will be returned along with relevant metadata in the form of a NeXus structure.
151
+
152
+ :param map_config: the map configuration
153
+ :type map_config: MapConfig
154
+ :param tool_config: the tomography image reduction configuration
155
+ :type tool_config: TomoReduceConfig
156
+ :return: a map of the collected tomography data along with the data reduction configuration
157
+ :rtype: nexusformat.nexus.NXroot
158
+ '''
159
+ from CHAP.common import MapProcessor
160
+ from CHAP.common.models.map import import_scanparser
161
+ from CHAP.common.utils.general import index_nearest
162
+ from copy import deepcopy
163
+ from nexusformat.nexus import NXcollection, NXdata, NXdetector, NXinstrument, NXsample, \
164
+ NXsource, NXsubentry, NXroot
165
+
166
+ include_raw_data = getattr(tool_config, "include_raw_data", False)
167
+
168
+ # Construct NXroot
169
+ nxroot = NXroot()
170
+
171
+ # Construct base NXentry and add to NXroot
172
+ nxentry = MapProcessor.get_nxentry(map_config)
173
+ nxroot[map_config.title] = nxentry
174
+ nxroot.attrs['default'] = map_config.title
175
+ nxentry.definition = 'NXtomo'
176
+ if 'data' in nxentry:
177
+ del nxentry['data']
178
+
179
+ # Add an NXinstrument to the NXentry
180
+ nxinstrument = NXinstrument()
181
+ nxentry.instrument = nxinstrument
182
+
183
+ # Add an NXsource to the NXinstrument
184
+ nxsource = NXsource()
185
+ nxinstrument.source = nxsource
186
+ nxsource.type = 'Synchrotron X-ray Source'
187
+ nxsource.name = 'CHESS'
188
+ nxsource.probe = 'x-ray'
189
+
190
+ # Tag the NXsource with the runinfo (as an attribute)
191
+ # nxsource.attrs['cycle'] = map_config.cycle
192
+ # nxsource.attrs['btr'] = map_config.btr
193
+ nxsource.attrs['station'] = map_config.station
194
+ nxsource.attrs['experiment_type'] = map_config.experiment_type
195
+
196
+ # Add an NXdetector to the NXinstrument (don't fill in data fields yet)
197
+ nxdetector = NXdetector()
198
+ nxinstrument.detector = nxdetector
199
+ nxdetector.local_name = tool_config.detector.prefix
200
+ pixel_size = tool_config.detector.pixel_size
201
+ if len(pixel_size) == 1:
202
+ nxdetector.x_pixel_size = pixel_size[0]/tool_config.detector.lens_magnification
203
+ nxdetector.y_pixel_size = pixel_size[0]/tool_config.detector.lens_magnification
204
+ else:
205
+ nxdetector.x_pixel_size = pixel_size[0]/tool_config.detector.lens_magnification
206
+ nxdetector.y_pixel_size = pixel_size[1]/tool_config.detector.lens_magnification
207
+ nxdetector.x_pixel_size.attrs['units'] = 'mm'
208
+ nxdetector.y_pixel_size.attrs['units'] = 'mm'
209
+
210
+ if include_raw_data:
211
+ # Add an NXsample to NXentry (don't fill in data fields yet)
212
+ nxsample = NXsample()
213
+ nxentry.sample = nxsample
214
+ nxsample.name = map_config.sample.name
215
+ nxsample.description = map_config.sample.description
216
+
217
+ # Add NXcollection's to NXentry to hold metadata about the spec scans in the map
218
+ # Also obtain the data fields in NXsample and NXdetector if requested
219
+ import_scanparser(map_config.station, map_config.experiment_type)
220
+ image_keys = []
221
+ sequence_numbers = []
222
+ image_stacks = []
223
+ rotation_angles = []
224
+ x_translations = []
225
+ z_translations = []
226
+ for scans in map_config.spec_scans:
227
+ for scan_number in scans.scan_numbers:
228
+ scanparser = scans.get_scanparser(scan_number)
229
+ if map_config.station in ('id1a3', 'id3a'):
230
+ scan_type = scanparser.scan_type
231
+ if scan_type == 'df1':
232
+ image_key = 2
233
+ field_name = 'dark_field'
234
+ elif scan_type == 'bf1':
235
+ image_key = 1
236
+ field_name = 'bright_field'
237
+ elif scan_type == 'ts1':
238
+ image_key = 0
239
+ field_name = 'tomo_fields'
240
+ else:
241
+ raise RuntimeError('Invalid scan type: {scan_type}')
242
+ elif map_config.station in ('id3b'):
243
+ if scans.spec_file.endswith('_dark'):
244
+ image_key = 2
245
+ field_name = 'dark_field'
246
+ elif scans.spec_file.endswith('_flat'):
247
+ #RV not yet tested with an actual fmb run
248
+ image_key = 1
249
+ field_name = 'bright_field'
250
+ else:
251
+ image_key = 0
252
+ field_name = 'tomo_fields'
253
+ else:
254
+ raise RuntimeError(f'Invalid station: {station}')
255
+
256
+ # Create an NXcollection for each field type
257
+ if field_name in nxentry.spec_scans:
258
+ nxcollection = nxentry.spec_scans[field_name]
259
+ if nxcollection.attrs['spec_file'] != str(scans.spec_file):
260
+ raise RuntimeError(f'Multiple SPEC files for a single field type not yet '+
261
+ f'implemented; field name: {field_name}, '+
262
+ f'SPEC file: {str(scans.spec_file)}')
263
+ else:
264
+ nxcollection = NXcollection()
265
+ nxentry.spec_scans[field_name] = nxcollection
266
+ nxcollection.attrs['spec_file'] = str(scans.spec_file)
267
+ nxcollection.attrs['date'] = scanparser.spec_scan.file_date
268
+
269
+ # Get thetas
270
+ image_offset = scanparser.starting_image_offset
271
+ if map_config.station in ('id1a3', 'id3a'):
272
+ theta_vals = scanparser.theta_vals
273
+ thetas = np.linspace(theta_vals.get('start'), theta_vals.get('end'),
274
+ theta_vals.get('num'))
275
+ else:
276
+ if len(scans.scan_numbers) != 1:
277
+ raise RuntimeError('Multiple scans not yet implemented for '+
278
+ f'{map_config.station}')
279
+ scan_number = scans.scan_numbers[0]
280
+ thetas = []
281
+ for dim in map_config.independent_dimensions:
282
+ if dim.label != 'theta':
283
+ continue
284
+ for index in range(scanparser.spec_scan_npts):
285
+ thetas.append(dim.get_value(scans, scan_number, index))
286
+ if not len(thetas):
287
+ raise RuntimeError(f'Unable to obtain thetas for {field_name}')
288
+ if thetas[image_offset] <= 0.0 and thetas[-1] >= 180.0:
289
+ image_offset = index_nearest(thetas, 0.0)
290
+ thetas = thetas[image_offset:index_nearest(thetas, 180.0)]
291
+ elif thetas[-1]-thetas[image_offset] >= 180:
292
+ thetas = thetas[image_offset:index_nearest(thetas, thetas[0]+180.0)]
293
+ else:
294
+ thetas = thetas[image_offset:]
295
+
296
+ # x and z translations
297
+ x_translation = scanparser.horizontal_shift
298
+ z_translation = scanparser.vertical_shift
299
+
300
+ # Add an NXsubentry to the NXcollection for each scan
301
+ entry_name = f'scan_{scan_number}'
302
+ nxsubentry = NXsubentry()
303
+ nxcollection[entry_name] = nxsubentry
304
+ nxsubentry.start_time = scanparser.spec_scan.date
305
+ nxsubentry.spec_command = scanparser.spec_command
306
+ # Add an NXinstrument to the scan's NXsubentry
307
+ nxsubentry.instrument = NXinstrument()
308
+ # Add an NXdetector to the NXinstrument to the scan's NXsubentry
309
+ nxsubentry.instrument.detector = deepcopy(nxdetector)
310
+ nxsubentry.instrument.detector.frame_start_number = image_offset
311
+ nxsubentry.instrument.detector.image_key = image_key
312
+ # Add an NXsample to the scan's NXsubentry
313
+ nxsubentry.sample = NXsample()
314
+ nxsubentry.sample.rotation_angle = thetas
315
+ nxsubentry.sample.rotation_angle.units = 'degrees'
316
+ nxsubentry.sample.x_translation = x_translation
317
+ nxsubentry.sample.x_translation.units = 'mm'
318
+ nxsubentry.sample.z_translation = z_translation
319
+ nxsubentry.sample.z_translation.units = 'mm'
320
+
321
+ if include_raw_data:
322
+ num_image = len(thetas)
323
+ image_keys += num_image*[image_key]
324
+ sequence_numbers += list(range(num_image))
325
+ image_stacks.append(scanparser.get_detector_data(tool_config.detector.prefix,
326
+ scan_step_index=(image_offset, image_offset+num_image)))
327
+ rotation_angles += list(thetas)
328
+ x_translations += num_image*[x_translation]
329
+ z_translations += num_image*[z_translation]
330
+
331
+ if include_raw_data:
332
+ # Add image data to NXdetector
333
+ nxinstrument.detector.image_key = image_keys
334
+ nxinstrument.detector.sequence_number = sequence_numbers
335
+ nxinstrument.detector.data = np.concatenate([image for image in image_stacks])
336
+
337
+ # Add image data to NXsample
338
+ nxsample.rotation_angle = rotation_angles
339
+ nxsample.rotation_angle.attrs['units'] = 'degrees'
340
+ nxsample.x_translation = x_translations
341
+ nxsample.x_translation.attrs['units'] = 'mm'
342
+ nxsample.z_translation = z_translations
343
+ nxsample.z_translation.attrs['units'] = 'mm'
344
+
345
+ # Add an NXdata to NXentry
346
+ nxdata = NXdata()
347
+ nxentry.data = nxdata
348
+ nxdata.makelink(nxentry.instrument.detector.data, name='data')
349
+ nxdata.makelink(nxentry.instrument.detector.image_key)
350
+ nxdata.makelink(nxentry.sample.rotation_angle)
351
+ nxdata.makelink(nxentry.sample.x_translation)
352
+ nxdata.makelink(nxentry.sample.z_translation)
353
+ # nxdata.attrs['axes'] = ['field', 'row', 'column']
354
+ # nxdata.attrs['field_indices'] = 0
355
+ # nxdata.attrs['row_indices'] = 1
356
+ # nxdata.attrs['column_indices'] = 2
357
+
358
+ return(nxroot)
359
+
360
+
361
+ def nxcopy(nxobject:NXobject, exclude_nxpaths:list[str]=[], nxpath_prefix:str='') -> NXobject:
362
+ '''Function that returns a copy of a nexus object, optionally exluding certain child items.
363
+
364
+ :param nxobject: the original nexus object to return a "copy" of
365
+ :type nxobject: nexusformat.nexus.NXobject
366
+ :param exlude_nxpaths: a list of paths to child nexus objects that
367
+ should be exluded from the returned "copy", defaults to `[]`
368
+ :type exclude_nxpaths: list[str], optional
369
+ :param nxpath_prefix: For use in recursive calls from inside this
370
+ function only!
371
+ :type nxpath_prefix: str
372
+ :return: a copy of `nxobject` with some children optionally exluded.
373
+ :rtype: NXobject
374
+ '''
375
+ from nexusformat.nexus import NXgroup
376
+
377
+ nxobject_copy = nxobject.__class__()
378
+ if not len(nxpath_prefix):
379
+ if 'default' in nxobject.attrs:
380
+ nxobject_copy.attrs['default'] = nxobject.attrs['default']
381
+ else:
382
+ for k, v in nxobject.attrs.items():
383
+ nxobject_copy.attrs[k] = v
384
+
385
+ for k, v in nxobject.items():
386
+ nxpath = os_path.join(nxpath_prefix, k)
387
+
388
+ if nxpath in exclude_nxpaths:
389
+ continue
390
+
391
+ if isinstance(v, NXgroup):
392
+ nxobject_copy[k] = nxcopy(v, exclude_nxpaths=exclude_nxpaths,
393
+ nxpath_prefix=os_path.join(nxpath_prefix, k))
394
+ else:
395
+ nxobject_copy[k] = v
396
+
397
+ return(nxobject_copy)
398
+
399
+
400
+ class set_numexpr_threads:
401
+ def __init__(self, num_core):
402
+ from multiprocessing import cpu_count
403
+
404
+ if num_core is None or num_core < 1 or num_core > cpu_count():
405
+ self.num_core = cpu_count()
406
+ else:
407
+ self.num_core = num_core
408
+
409
+ def __enter__(self):
410
+ import numexpr as ne
411
+
412
+ self.num_core_org = ne.set_num_threads(min(self.num_core, ne.MAX_THREADS))
413
+
414
+ def __exit__(self, exc_type, exc_value, traceback):
415
+ import numexpr as ne
416
+
417
+ ne.set_num_threads(self.num_core_org)
418
+
419
+
420
+ class Tomo:
421
+ """Processing tomography data with misalignment.
422
+ """
423
+ def __init__(self, galaxy_flag=False, num_core=-1, output_folder='.', save_figs=None,
424
+ test_mode=False):
425
+ """Initialize with optional config input file or dictionary
426
+ """
427
+ from logging import getLogger
428
+
429
+ from multiprocessing import cpu_count
430
+
431
+ self.__name__ = self.__class__.__name__
432
+ self.logger = getLogger(self.__name__)
433
+ self.logger.propagate = False
434
+
435
+ if not isinstance(galaxy_flag, bool):
436
+ raise ValueError(f'Invalid parameter galaxy_flag ({galaxy_flag})')
437
+ self.galaxy_flag = galaxy_flag
438
+ self.num_core = num_core
439
+ if self.galaxy_flag:
440
+ if output_folder != '.':
441
+ self.logger.warning('Ignoring output_folder in galaxy mode')
442
+ self.output_folder = '.'
443
+ if test_mode != False:
444
+ self.logger.warning('Ignoring test_mode in galaxy mode')
445
+ self.test_mode = False
446
+ if save_figs is not None:
447
+ self.logger.warning('Ignoring save_figs in galaxy mode')
448
+ save_figs = 'only'
449
+ else:
450
+ self.output_folder = os_path.abspath(output_folder)
451
+ if not os_path.isdir(output_folder):
452
+ mkdir(os_path.abspath(output_folder))
453
+ if not isinstance(test_mode, bool):
454
+ raise ValueError(f'Invalid parameter test_mode ({test_mode})')
455
+ self.test_mode = test_mode
456
+ if save_figs is None:
457
+ save_figs = 'no'
458
+ self.test_config = {}
459
+ if self.test_mode:
460
+ if save_figs != 'only':
461
+ self.logger.warning('Ignoring save_figs in test mode')
462
+ save_figs = 'only'
463
+ if save_figs == 'only':
464
+ self.save_only = True
465
+ self.save_figs = True
466
+ elif save_figs == 'yes':
467
+ self.save_only = False
468
+ self.save_figs = True
469
+ elif save_figs == 'no':
470
+ self.save_only = False
471
+ self.save_figs = False
472
+ else:
473
+ raise ValueError(f'Invalid parameter save_figs ({save_figs})')
474
+ if self.save_only:
475
+ self.block = False
476
+ else:
477
+ self.block = True
478
+ if self.num_core == -1:
479
+ self.num_core = cpu_count()
480
+ if not isinstance(self.num_core, int) or self.num_core < 0:
481
+ raise ValueError(f'Invalid parameter num_core ({num_core})')
482
+ if self.num_core > cpu_count():
483
+ self.logger.warning(f'num_core = {self.num_core} is larger than the number of '
484
+ f'available processors and reduced to {cpu_count()}')
485
+ self.num_core= cpu_count()
486
+
487
+ def read(self, filename):
488
+ extension = os_path.splitext(filename)[1]
489
+ if extension == '.yml' or extension == '.yaml':
490
+ with open(filename, 'r') as f:
491
+ config = safe_load(f)
492
+ # if len(config) > 1:
493
+ # raise ValueError(f'Multiple root entries in {filename} not yet implemented')
494
+ # if len(list(config.values())[0]) > 1:
495
+ # raise ValueError(f'Multiple sample maps in {filename} not yet implemented')
496
+ return(config)
497
+ elif extension == '.nxs':
498
+ with NXFile(filename, mode='r') as nxfile:
499
+ nxroot = nxfile.readfile()
500
+ return(nxroot)
501
+ else:
502
+ raise ValueError(f'Invalid filename extension ({extension})')
503
+
504
+ def write(self, data, filename):
505
+ extension = os_path.splitext(filename)[1]
506
+ if extension == '.yml' or extension == '.yaml':
507
+ with open(filename, 'w') as f:
508
+ safe_dump(data, f)
509
+ elif extension == '.nxs':
510
+ data.save(filename, mode='w')
511
+ elif extension == '.nc':
512
+ data.to_netcdf(os_path=filename)
513
+ else:
514
+ raise ValueError(f'Invalid filename extension ({extension})')
515
+
516
+ def gen_reduced_data(self, data, img_x_bounds=None):
517
+ """Generate the reduced tomography images.
518
+ """
519
+ from nexusformat.nexus import NXdata, NXprocess, NXroot
520
+
521
+ from CHAP.common.models.map import import_scanparser
522
+
523
+ self.logger.info('Generate the reduced tomography images')
524
+ if img_x_bounds is not None:
525
+ if not isinstance(img_x_bounds, (tuple, list)):
526
+ raise ValueError(f'Invalid parameter img_x_bounds ({img_x_bounds})')
527
+ img_x_bounds = tuple(img_x_bounds)
528
+
529
+ # Create plot galaxy path directory if needed
530
+ if self.galaxy_flag and not os_path.exists('tomo_reduce_plots'):
531
+ mkdir('tomo_reduce_plots')
532
+
533
+ if isinstance(data, dict):
534
+ # Create Nexus format object from input dictionary
535
+ wf = TomoWorkflow(**data)
536
+ if len(wf.sample_maps) > 1:
537
+ raise ValueError(f'Multiple sample maps not yet implemented')
538
+ nxroot = NXroot()
539
+ t0 = time()
540
+ for sample_map in wf.sample_maps:
541
+ self.logger.info(f'Start constructing the {sample_map.title} map.')
542
+ import_scanparser(sample_map.station)
543
+ sample_map.construct_nxentry(nxroot, include_raw_data=False)
544
+ self.logger.info(f'Constructed all sample maps in {time()-t0:.2f} seconds.')
545
+ nxentry = nxroot[nxroot.attrs['default']]
546
+ # Get test mode configuration info
547
+ if self.test_mode:
548
+ self.test_config = data['sample_maps'][0]['test_mode']
549
+ elif isinstance(data, NXroot):
550
+ nxentry = data[data.attrs['default']]
551
+ else:
552
+ raise ValueError(f'Invalid parameter data ({data})')
553
+
554
+ # Create an NXprocess to store data reduction (meta)data
555
+ reduced_data = NXprocess()
556
+
557
+ # Generate dark field
558
+ if 'dark_field' in nxentry['spec_scans']:
559
+ reduced_data = self._gen_dark(nxentry, reduced_data)
560
+
561
+ # Generate bright field
562
+ reduced_data = self._gen_bright(nxentry, reduced_data)
563
+
564
+ # Set vertical detector bounds for image stack
565
+ img_x_bounds = self._set_detector_bounds(nxentry, reduced_data, img_x_bounds=img_x_bounds)
566
+ self.logger.info(f'img_x_bounds = {img_x_bounds}')
567
+ reduced_data['img_x_bounds'] = img_x_bounds
568
+
569
+ # Set zoom and/or theta skip to reduce memory the requirement
570
+ zoom_perc, num_theta_skip = self._set_zoom_or_skip()
571
+ if zoom_perc is not None:
572
+ reduced_data.attrs['zoom_perc'] = zoom_perc
573
+ if num_theta_skip is not None:
574
+ reduced_data.attrs['num_theta_skip'] = num_theta_skip
575
+
576
+ # Generate reduced tomography fields
577
+ reduced_data = self._gen_tomo(nxentry, reduced_data)
578
+
579
+ # Create a copy of the input Nexus object and remove raw and any existing reduced data
580
+ if isinstance(data, NXroot):
581
+ exclude_items = [f'{nxentry._name}/reduced_data/data',
582
+ f'{nxentry._name}/instrument/detector/data',
583
+ f'{nxentry._name}/instrument/detector/image_key',
584
+ f'{nxentry._name}/instrument/detector/sequence_number',
585
+ f'{nxentry._name}/sample/rotation_angle',
586
+ f'{nxentry._name}/sample/x_translation',
587
+ f'{nxentry._name}/sample/z_translation',
588
+ f'{nxentry._name}/data/data',
589
+ f'{nxentry._name}/data/image_key',
590
+ f'{nxentry._name}/data/rotation_angle',
591
+ f'{nxentry._name}/data/x_translation',
592
+ f'{nxentry._name}/data/z_translation']
593
+ nxroot = nxcopy(data, exclude_nxpaths=exclude_items)
594
+ nxentry = nxroot[nxroot.attrs['default']]
595
+
596
+ # Add the reduced data NXprocess
597
+ nxentry.reduced_data = reduced_data
598
+
599
+ if 'data' not in nxentry:
600
+ nxentry.data = NXdata()
601
+ nxentry.attrs['default'] = 'data'
602
+ nxentry.data.makelink(nxentry.reduced_data.data.tomo_fields, name='reduced_data')
603
+ nxentry.data.makelink(nxentry.reduced_data.rotation_angle, name='rotation_angle')
604
+ nxentry.data.attrs['signal'] = 'reduced_data'
605
+
606
+ return(nxroot)
607
+
608
+ def find_centers(self, nxroot, center_rows=None, center_stack_index=None):
609
+ """Find the calibrated center axis info
610
+ """
611
+ from nexusformat.nexus import NXentry, NXroot
612
+
613
+ from CHAP.common.utils.general import is_int_pair
614
+
615
+ self.logger.info('Find the calibrated center axis info')
616
+
617
+ if not isinstance(nxroot, NXroot):
618
+ raise ValueError(f'Invalid parameter nxroot ({nxroot})')
619
+ nxentry = nxroot[nxroot.attrs['default']]
620
+ if not isinstance(nxentry, NXentry):
621
+ raise ValueError(f'Invalid nxentry ({nxentry})')
622
+ if self.galaxy_flag:
623
+ if center_rows is not None:
624
+ center_rows = tuple(center_rows)
625
+ if not is_int_pair(center_rows):
626
+ raise ValueError(f'Invalid parameter center_rows ({center_rows})')
627
+ elif center_rows is not None:
628
+ # self.logger.warning(f'Ignoring parameter center_rows ({center_rows})')
629
+ # center_rows = None
630
+ if not isinstance(center_rows, (tuple, list)) or len(center_rows) != 2:
631
+ raise ValueError(f'Invalid parameter center_rows ({center_rows})')
632
+ if self.galaxy_flag:
633
+ if center_stack_index is not None and (not isinstance(center_stack_index, int) or
634
+ center_stack_index < 0):
635
+ raise ValueError(f'Invalid parameter center_stack_index ({center_stack_index})')
636
+
637
+ # Create plot galaxy path directory and path if needed
638
+ if self.galaxy_flag:
639
+ if not os_path.exists('tomo_find_centers_plots'):
640
+ mkdir('tomo_find_centers_plots')
641
+ path = 'tomo_find_centers_plots'
642
+ else:
643
+ path = self.output_folder
644
+
645
+ # Check if reduced data is available
646
+ if ('reduced_data' not in nxentry or 'reduced_data' not in nxentry.data):
647
+ raise KeyError(f'Unable to find valid reduced data in {nxentry}.')
648
+
649
+ # Select the image stack to calibrate the center axis
650
+ # reduced data axes order: stack,theta,row,column
651
+ # Note: Nexus cannot follow a link if the data it points to is too big,
652
+ # so get the data from the actual place, not from nxentry.data
653
+ tomo_fields_shape = nxentry.reduced_data.data.tomo_fields.shape
654
+ if len(tomo_fields_shape) != 4 or any(True for dim in tomo_fields_shape if not dim):
655
+ raise KeyError('Unable to load the required reduced tomography stack')
656
+ num_tomo_stacks = tomo_fields_shape[0]
657
+ if num_tomo_stacks == 1:
658
+ center_stack_index = 0
659
+ default = 'n'
660
+ else:
661
+ if self.test_mode:
662
+ center_stack_index = self.test_config['center_stack_index']-1 # make offset 0
663
+ elif self.galaxy_flag:
664
+ if center_stack_index is None:
665
+ center_stack_index = int(num_tomo_stacks/2)
666
+ if center_stack_index >= num_tomo_stacks:
667
+ raise ValueError(f'Invalid parameter center_stack_index ({center_stack_index})')
668
+ else:
669
+ if center_stack_index is None:
670
+ center_stack_index = input_int('\nEnter tomography stack index to calibrate '
671
+ 'the center axis', ge=1, le=num_tomo_stacks,
672
+ default=int(1+num_tomo_stacks/2))
673
+ else:
674
+ if (not isinstance(center_stack_index, int) or
675
+ not 0 < center_stack_index <= num_tomo_stacks):
676
+ raise ValueError('Invalid parameter center_stack_index '+
677
+ f'({center_stack_index})')
678
+ center_stack_index -= 1
679
+ default = 'y'
680
+
681
+ # Get thetas (in degrees)
682
+ thetas = np.asarray(nxentry.reduced_data.rotation_angle)
683
+
684
+ # Get effective pixel_size
685
+ if 'zoom_perc' in nxentry.reduced_data:
686
+ eff_pixel_size = 100.*(nxentry.instrument.detector.x_pixel_size/
687
+ nxentry.reduced_data.attrs['zoom_perc'])
688
+ else:
689
+ eff_pixel_size = nxentry.instrument.detector.x_pixel_size
690
+
691
+ # Get cross sectional diameter
692
+ cross_sectional_dim = tomo_fields_shape[3]*eff_pixel_size
693
+ self.logger.debug(f'cross_sectional_dim = {cross_sectional_dim}')
694
+
695
+ # Determine center offset at sample row boundaries
696
+ self.logger.info('Determine center offset at sample row boundaries')
697
+
698
+ # Lower row center
699
+ if self.test_mode:
700
+ lower_row = self.test_config['lower_row']
701
+ elif self.galaxy_flag:
702
+ if center_rows is None:
703
+ lower_row = 0
704
+ else:
705
+ lower_row = min(center_rows)
706
+ if not 0 <= lower_row < tomo_fields_shape[2]-1:
707
+ raise ValueError(f'Invalid parameter center_rows ({center_rows})')
708
+ else:
709
+ if center_rows is not None and center_rows[0] is not None:
710
+ lower_row = center_rows[0]
711
+ if lower_row == -1:
712
+ lower_row = 0
713
+ if not 0 <= lower_row < tomo_fields_shape[2]-1:
714
+ raise ValueError(f'Invalid parameter center_rows ({center_rows})')
715
+ else:
716
+ lower_row = select_one_image_bound(
717
+ nxentry.reduced_data.data.tomo_fields[center_stack_index,0,:,:],
718
+ 0, bound=0, title=f'theta={round(thetas[0], 2)+0}',
719
+ bound_name='row index to find lower center', default=default,
720
+ raise_error=True)
721
+ self.logger.debug('Finding center...')
722
+ t0 = time()
723
+ lower_center_offset = self._find_center_one_plane(
724
+ #np.asarray(nxentry.reduced_data.data.tomo_fields[center_stack_index,:,lower_row,:]),
725
+ nxentry.reduced_data.data.tomo_fields[center_stack_index,:,lower_row,:],
726
+ lower_row, thetas, eff_pixel_size, cross_sectional_dim, path=path,
727
+ num_core=self.num_core)
728
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
729
+ self.logger.debug(f'lower_row = {lower_row:.2f}')
730
+ self.logger.debug(f'lower_center_offset = {lower_center_offset:.2f}')
731
+
732
+ # Upper row center
733
+ if self.test_mode:
734
+ upper_row = self.test_config['upper_row']
735
+ elif self.galaxy_flag:
736
+ if center_rows is None:
737
+ upper_row = tomo_fields_shape[2]-1
738
+ else:
739
+ upper_row = max(center_rows)
740
+ if not lower_row < upper_row < tomo_fields_shape[2]:
741
+ raise ValueError(f'Invalid parameter center_rows ({center_rows})')
742
+ else:
743
+ if center_rows is not None and center_rows[1] is not None:
744
+ upper_row = center_rows[1]
745
+ if upper_row == -1:
746
+ upper_row = tomo_fields_shape[2]-1
747
+ if not lower_row < upper_row < tomo_fields_shape[2]:
748
+ raise ValueError(f'Invalid parameter center_rows ({center_rows})')
749
+ else:
750
+ upper_row = select_one_image_bound(
751
+ nxentry.reduced_data.data.tomo_fields[center_stack_index,0,:,:],
752
+ 0, bound=tomo_fields_shape[2]-1, title=f'theta={round(thetas[0], 2)+0}',
753
+ bound_name='row index to find upper center', default=default,
754
+ raise_error=True)
755
+ self.logger.debug('Finding center...')
756
+ t0 = time()
757
+ upper_center_offset = self._find_center_one_plane(
758
+ #np.asarray(nxentry.reduced_data.data.tomo_fields[center_stack_index,:,upper_row,:]),
759
+ nxentry.reduced_data.data.tomo_fields[center_stack_index,:,upper_row,:],
760
+ upper_row, thetas, eff_pixel_size, cross_sectional_dim, path=path,
761
+ num_core=self.num_core)
762
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
763
+ self.logger.debug(f'upper_row = {upper_row:.2f}')
764
+ self.logger.debug(f'upper_center_offset = {upper_center_offset:.2f}')
765
+
766
+ center_config = {'lower_row': lower_row, 'lower_center_offset': lower_center_offset,
767
+ 'upper_row': upper_row, 'upper_center_offset': upper_center_offset}
768
+ if num_tomo_stacks > 1:
769
+ center_config['center_stack_index'] = center_stack_index+1 # save as offset 1
770
+
771
+ # Save test data to file
772
+ if self.test_mode:
773
+ with open(f'{self.output_folder}/center_config.yaml', 'w') as f:
774
+ safe_dump(center_config, f)
775
+
776
+ return(center_config)
777
+
778
+ def reconstruct_data(self, nxroot, center_info, x_bounds=None, y_bounds=None, z_bounds=None):
779
+ """Reconstruct the tomography data.
780
+ """
781
+ from nexusformat.nexus import NXdata, NXentry, NXprocess, NXroot
782
+
783
+ from CHAP.common.utils.general import is_int_pair
784
+
785
+ self.logger.info('Reconstruct the tomography data')
786
+
787
+ if not isinstance(nxroot, NXroot):
788
+ raise ValueError(f'Invalid parameter nxroot ({nxroot})')
789
+ nxentry = nxroot[nxroot.attrs['default']]
790
+ if not isinstance(nxentry, NXentry):
791
+ raise ValueError(f'Invalid nxentry ({nxentry})')
792
+ if not isinstance(center_info, dict):
793
+ raise ValueError(f'Invalid parameter center_info ({center_info})')
794
+ if x_bounds is not None:
795
+ if not isinstance(x_bounds, (tuple, list)):
796
+ raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
797
+ x_bounds = tuple(x_bounds)
798
+ if y_bounds is not None:
799
+ if not isinstance(y_bounds, (tuple, list)):
800
+ raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
801
+ y_bounds = tuple(y_bounds)
802
+ if z_bounds is not None:
803
+ if not isinstance(z_bounds, (tuple, list)):
804
+ raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
805
+ z_bounds = tuple(z_bounds)
806
+
807
+ # Create plot galaxy path directory and path if needed
808
+ if self.galaxy_flag:
809
+ if not os_path.exists('tomo_reconstruct_plots'):
810
+ mkdir('tomo_reconstruct_plots')
811
+ path = 'tomo_reconstruct_plots'
812
+ else:
813
+ path = self.output_folder
814
+
815
+ # Check if reduced data is available
816
+ if ('reduced_data' not in nxentry or 'reduced_data' not in nxentry.data):
817
+ raise KeyError(f'Unable to find valid reduced data in {nxentry}.')
818
+
819
+ # Create an NXprocess to store image reconstruction (meta)data
820
+ nxprocess = NXprocess()
821
+
822
+ # Get rotation axis rows and centers
823
+ lower_row = center_info.get('lower_row')
824
+ lower_center_offset = center_info.get('lower_center_offset')
825
+ upper_row = center_info.get('upper_row')
826
+ upper_center_offset = center_info.get('upper_center_offset')
827
+ if (lower_row is None or lower_center_offset is None or upper_row is None or
828
+ upper_center_offset is None):
829
+ raise KeyError(f'Unable to find valid calibrated center axis info in {center_info}.')
830
+ center_slope = (upper_center_offset-lower_center_offset)/(upper_row-lower_row)
831
+
832
+ # Get thetas (in degrees)
833
+ thetas = np.asarray(nxentry.reduced_data.rotation_angle)
834
+
835
+ # Reconstruct tomography data
836
+ # reduced data axes order: stack,theta,row,column
837
+ # reconstructed data order in each stack: row/z,x,y
838
+ # Note: Nexus cannot follow a link if the data it points to is too big,
839
+ # so get the data from the actual place, not from nxentry.data
840
+ if 'zoom_perc' in nxentry.reduced_data:
841
+ res_title = f'{nxentry.reduced_data.attrs["zoom_perc"]}p'
842
+ else:
843
+ res_title = 'fullres'
844
+ load_error = False
845
+ num_tomo_stacks = nxentry.reduced_data.data.tomo_fields.shape[0]
846
+ tomo_recon_stacks = num_tomo_stacks*[np.array([])]
847
+ for i in range(num_tomo_stacks):
848
+ # Convert reduced data stack from theta,row,column to row,theta,column
849
+ self.logger.debug(f'Reading reduced data stack {i+1}...')
850
+ t0 = time()
851
+ tomo_stack = np.asarray(nxentry.reduced_data.data.tomo_fields[i])
852
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
853
+ if len(tomo_stack.shape) != 3 or any(True for dim in tomo_stack.shape if not dim):
854
+ raise ValueError(f'Unable to load tomography stack {i+1} for reconstruction')
855
+ tomo_stack = np.swapaxes(tomo_stack, 0, 1)
856
+ assert(len(thetas) == tomo_stack.shape[1])
857
+ assert(0 <= lower_row < upper_row < tomo_stack.shape[0])
858
+ center_offsets = [lower_center_offset-lower_row*center_slope,
859
+ upper_center_offset+(tomo_stack.shape[0]-1-upper_row)*center_slope]
860
+ t0 = time()
861
+ self.logger.debug(f'Running _reconstruct_one_tomo_stack on {self.num_core} cores ...')
862
+ tomo_recon_stack = self._reconstruct_one_tomo_stack(tomo_stack, thetas,
863
+ center_offsets=center_offsets, num_core=self.num_core, algorithm='gridrec')
864
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
865
+ self.logger.info(f'Reconstruction of stack {i+1} took {time()-t0:.2f} seconds')
866
+
867
+ # Combine stacks
868
+ tomo_recon_stacks[i] = tomo_recon_stack
869
+
870
+ # Resize the reconstructed tomography data
871
+ # reconstructed data order in each stack: row/z,x,y
872
+ if self.test_mode:
873
+ x_bounds = tuple(self.test_config.get('x_bounds'))
874
+ y_bounds = tuple(self.test_config.get('y_bounds'))
875
+ z_bounds = None
876
+ elif self.galaxy_flag:
877
+ if x_bounds is not None and not is_int_pair(x_bounds, ge=0,
878
+ lt=tomo_recon_stacks[0].shape[1]):
879
+ raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
880
+ if y_bounds is not None and not is_int_pair(y_bounds, ge=0,
881
+ lt=tomo_recon_stacks[0].shape[1]):
882
+ raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
883
+ z_bounds = None
884
+ else:
885
+ x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(tomo_recon_stacks,
886
+ x_bounds=x_bounds, y_bounds=y_bounds, z_bounds=z_bounds)
887
+ if x_bounds is None:
888
+ x_range = (0, tomo_recon_stacks[0].shape[1])
889
+ x_slice = int(x_range[1]/2)
890
+ else:
891
+ x_range = (min(x_bounds), max(x_bounds))
892
+ x_slice = int((x_bounds[0]+x_bounds[1])/2)
893
+ if y_bounds is None:
894
+ y_range = (0, tomo_recon_stacks[0].shape[2])
895
+ y_slice = int(y_range[1]/2)
896
+ else:
897
+ y_range = (min(y_bounds), max(y_bounds))
898
+ y_slice = int((y_bounds[0]+y_bounds[1])/2)
899
+ if z_bounds is None:
900
+ z_range = (0, tomo_recon_stacks[0].shape[0])
901
+ z_slice = int(z_range[1]/2)
902
+ else:
903
+ z_range = (min(z_bounds), max(z_bounds))
904
+ z_slice = int((z_bounds[0]+z_bounds[1])/2)
905
+
906
+ # Plot a few reconstructed image slices
907
+ if self.save_figs:
908
+ for i, stack in enumerate(tomo_recon_stacks):
909
+ if num_tomo_stacks == 1:
910
+ basetitle = 'recon'
911
+ else:
912
+ basetitle = f'recon stack {i+1}'
913
+ title = f'{basetitle} {res_title} xslice{x_slice}'
914
+ quick_imshow(stack[z_range[0]:z_range[1],x_slice,y_range[0]:y_range[1]],
915
+ title=title, path=path, save_fig=True, save_only=True)
916
+ title = f'{basetitle} {res_title} yslice{y_slice}'
917
+ quick_imshow(stack[z_range[0]:z_range[1],x_range[0]:x_range[1],y_slice],
918
+ title=title, path=path, save_fig=True, save_only=True)
919
+ title = f'{basetitle} {res_title} zslice{z_slice}'
920
+ quick_imshow(stack[z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]],
921
+ title=title, path=path, save_fig=True, save_only=True)
922
+
923
+ # Save test data to file
924
+ # reconstructed data order in each stack: row/z,x,y
925
+ if self.test_mode:
926
+ for i, stack in enumerate(tomo_recon_stacks):
927
+ np.savetxt(f'{self.output_folder}/recon_stack_{i+1}.txt',
928
+ stack[z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]], fmt='%.6e')
929
+
930
+ # Add image reconstruction to reconstructed data NXprocess
931
+ # reconstructed data order in each stack: row/z,x,y
932
+ nxprocess.data = NXdata()
933
+ nxprocess.attrs['default'] = 'data'
934
+ for k, v in center_info.items():
935
+ nxprocess[k] = v
936
+ if x_bounds is not None:
937
+ nxprocess.x_bounds = x_bounds
938
+ if y_bounds is not None:
939
+ nxprocess.y_bounds = y_bounds
940
+ if z_bounds is not None:
941
+ nxprocess.z_bounds = z_bounds
942
+ nxprocess.data['reconstructed_data'] = np.asarray([stack[z_range[0]:z_range[1],
943
+ x_range[0]:x_range[1],y_range[0]:y_range[1]] for stack in tomo_recon_stacks])
944
+ nxprocess.data.attrs['signal'] = 'reconstructed_data'
945
+
946
+ # Create a copy of the input Nexus object and remove reduced data
947
+ exclude_items = [f'{nxentry._name}/reduced_data/data', f'{nxentry._name}/data/reduced_data']
948
+ nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
949
+
950
+ # Add the reconstructed data NXprocess to the new Nexus object
951
+ nxentry_copy = nxroot_copy[nxroot_copy.attrs['default']]
952
+ nxentry_copy.reconstructed_data = nxprocess
953
+ if 'data' not in nxentry_copy:
954
+ nxentry_copy.data = NXdata()
955
+ nxentry_copy.attrs['default'] = 'data'
956
+ nxentry_copy.data.makelink(nxprocess.data.reconstructed_data, name='reconstructed_data')
957
+ nxentry_copy.data.attrs['signal'] = 'reconstructed_data'
958
+
959
+ return(nxroot_copy)
960
+
961
+ def combine_data(self, nxroot, x_bounds=None, y_bounds=None, z_bounds=None):
962
+ """Combine the reconstructed tomography stacks.
963
+ """
964
+ from nexusformat.nexus import NXdata, NXentry, NXprocess, NXroot
965
+
966
+ from CHAP.common.utils.general import is_int_pair
967
+
968
+ self.logger.info('Combine the reconstructed tomography stacks')
969
+
970
+ if not isinstance(nxroot, NXroot):
971
+ raise ValueError(f'Invalid parameter nxroot ({nxroot})')
972
+ nxentry = nxroot[nxroot.attrs['default']]
973
+ if not isinstance(nxentry, NXentry):
974
+ raise ValueError(f'Invalid nxentry ({nxentry})')
975
+ if x_bounds is not None:
976
+ if not isinstance(x_bounds, (tuple, list)):
977
+ raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
978
+ x_bounds = tuple(x_bounds)
979
+ if y_bounds is not None:
980
+ if not isinstance(y_bounds, (tuple, list)):
981
+ raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
982
+ y_bounds = tuple(y_bounds)
983
+ if z_bounds is not None:
984
+ if not isinstance(z_bounds, (tuple, list)):
985
+ raise ValueError(f'Invalid parameter z_bounds ({z_bounds})')
986
+ z_bounds = tuple(z_bounds)
987
+
988
+ # Create plot galaxy path directory and path if needed
989
+ if self.galaxy_flag:
990
+ if not os_path.exists('tomo_combine_plots'):
991
+ mkdir('tomo_combine_plots')
992
+ path = 'tomo_combine_plots'
993
+ else:
994
+ path = self.output_folder
995
+
996
+ # Check if reconstructed image data is available
997
+ if ('reconstructed_data' not in nxentry or 'reconstructed_data' not in nxentry.data):
998
+ raise KeyError(f'Unable to find valid reconstructed image data in {nxentry}.')
999
+
1000
+ # Create an NXprocess to store combined image reconstruction (meta)data
1001
+ nxprocess = NXprocess()
1002
+
1003
+ # Get the reconstructed data
1004
+ # reconstructed data order: stack,row(z),x,y
1005
+ # Note: Nexus cannot follow a link if the data it points to is too big,
1006
+ # so get the data from the actual place, not from nxentry.data
1007
+ num_tomo_stacks = nxentry.reconstructed_data.data.reconstructed_data.shape[0]
1008
+ if num_tomo_stacks == 1:
1009
+ self.logger.info('Only one stack available: leaving combine_data')
1010
+ return(None)
1011
+
1012
+ # Combine the reconstructed stacks
1013
+ # (load one stack at a time to reduce risk of hitting Nexus data access limit)
1014
+ t0 = time()
1015
+ self.logger.debug(f'Combining the reconstructed stacks ...')
1016
+ tomo_recon_combined = np.asarray(nxentry.reconstructed_data.data.reconstructed_data[0])
1017
+ if num_tomo_stacks > 2:
1018
+ tomo_recon_combined = np.concatenate([tomo_recon_combined]+
1019
+ [nxentry.reconstructed_data.data.reconstructed_data[i]
1020
+ for i in range(1, num_tomo_stacks-1)])
1021
+ if num_tomo_stacks > 1:
1022
+ tomo_recon_combined = np.concatenate([tomo_recon_combined]+
1023
+ [nxentry.reconstructed_data.data.reconstructed_data[num_tomo_stacks-1]])
1024
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1025
+ self.logger.info(f'Combining the reconstructed stacks took {time()-t0:.2f} seconds')
1026
+
1027
+ # Resize the combined tomography data stacks
1028
+ # combined data order: row/z,x,y
1029
+ if self.test_mode:
1030
+ x_bounds = None
1031
+ y_bounds = None
1032
+ z_bounds = tuple(self.test_config.get('z_bounds'))
1033
+ elif self.galaxy_flag:
1034
+ if x_bounds is not None and not is_int_pair(x_bounds, ge=0,
1035
+ lt=tomo_recon_stacks[0].shape[1]):
1036
+ raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
1037
+ if y_bounds is not None and not is_int_pair(y_bounds, ge=0,
1038
+ lt=tomo_recon_stacks[0].shape[1]):
1039
+ raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
1040
+ z_bounds = None
1041
+ else:
1042
+ if x_bounds is None and x_bounds in nxentry.reconstructed_data:
1043
+ x_bounds = (-1, -1)
1044
+ if y_bounds is None and y_bounds in nxentry.reconstructed_data:
1045
+ y_bounds = (-1, -1)
1046
+ x_bounds, y_bounds, z_bounds = self._resize_reconstructed_data(tomo_recon_combined,
1047
+ z_only=True)
1048
+ if x_bounds is None:
1049
+ x_range = (0, tomo_recon_combined.shape[1])
1050
+ x_slice = int(x_range[1]/2)
1051
+ else:
1052
+ x_range = x_bounds
1053
+ x_slice = int((x_bounds[0]+x_bounds[1])/2)
1054
+ if y_bounds is None:
1055
+ y_range = (0, tomo_recon_combined.shape[2])
1056
+ y_slice = int(y_range[1]/2)
1057
+ else:
1058
+ y_range = y_bounds
1059
+ y_slice = int((y_bounds[0]+y_bounds[1])/2)
1060
+ if z_bounds is None:
1061
+ z_range = (0, tomo_recon_combined.shape[0])
1062
+ z_slice = int(z_range[1]/2)
1063
+ else:
1064
+ z_range = z_bounds
1065
+ z_slice = int((z_bounds[0]+z_bounds[1])/2)
1066
+
1067
+ # Plot a few combined image slices
1068
+ if self.save_figs:
1069
+ quick_imshow(tomo_recon_combined[z_range[0]:z_range[1],x_slice,y_range[0]:y_range[1]],
1070
+ title=f'recon combined xslice{x_slice}', path=path, save_fig=True,
1071
+ save_only=True)
1072
+ quick_imshow(tomo_recon_combined[z_range[0]:z_range[1],x_range[0]:x_range[1],y_slice],
1073
+ title=f'recon combined yslice{y_slice}', path=path, save_fig=True,
1074
+ save_only=True)
1075
+ quick_imshow(tomo_recon_combined[z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]],
1076
+ title=f'recon combined zslice{z_slice}', path=path, save_fig=True,
1077
+ save_only=True)
1078
+
1079
+ # Save test data to file
1080
+ # combined data order: row/z,x,y
1081
+ if self.test_mode:
1082
+ np.savetxt(f'{self.output_folder}/recon_combined.txt', tomo_recon_combined[
1083
+ z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]], fmt='%.6e')
1084
+
1085
+ # Add image reconstruction to reconstructed data NXprocess
1086
+ # combined data order: row/z,x,y
1087
+ nxprocess.data = NXdata()
1088
+ nxprocess.attrs['default'] = 'data'
1089
+ if x_bounds is not None:
1090
+ nxprocess.x_bounds = x_bounds
1091
+ if y_bounds is not None:
1092
+ nxprocess.y_bounds = y_bounds
1093
+ if z_bounds is not None:
1094
+ nxprocess.z_bounds = z_bounds
1095
+ nxprocess.data['combined_data'] = tomo_recon_combined[
1096
+ z_range[0]:z_range[1],x_range[0]:x_range[1],y_range[0]:y_range[1]]
1097
+ nxprocess.data.attrs['signal'] = 'combined_data'
1098
+
1099
+ # Create a copy of the input Nexus object and remove reconstructed data
1100
+ exclude_items = [f'{nxentry._name}/reconstructed_data/data',
1101
+ f'{nxentry._name}/data/reconstructed_data']
1102
+ nxroot_copy = nxcopy(nxroot, exclude_nxpaths=exclude_items)
1103
+
1104
+ # Add the combined data NXprocess to the new Nexus object
1105
+ nxentry_copy = nxroot_copy[nxroot_copy.attrs['default']]
1106
+ nxentry_copy.combined_data = nxprocess
1107
+ if 'data' not in nxentry_copy:
1108
+ nxentry_copy.data = NXdata()
1109
+ nxentry_copy.attrs['default'] = 'data'
1110
+ nxentry_copy.data.makelink(nxprocess.data.combined_data, name='combined_data')
1111
+ nxentry_copy.data.attrs['signal'] = 'combined_data'
1112
+
1113
+ return(nxroot_copy)
1114
+
1115
+ def _gen_dark(self, nxentry, reduced_data):
1116
+ """Generate dark field.
1117
+ """
1118
+ from nexusformat.nexus import NXdata
1119
+
1120
+ from CHAP.common.models.map import get_scanparser, import_scanparser
1121
+
1122
+ # Get the dark field images
1123
+ image_key = nxentry.instrument.detector.get('image_key', None)
1124
+ if image_key and 'data' in nxentry.instrument.detector:
1125
+ field_indices = [index for index, key in enumerate(image_key) if key == 2]
1126
+ tdf_stack = nxentry.instrument.detector.data[field_indices,:,:]
1127
+ # RV the default NXtomo form does not accomodate bright or dark field stacks
1128
+ else:
1129
+ import_scanparser(nxentry.instrument.source.attrs['station'],
1130
+ nxentry.instrument.source.attrs['experiment_type'])
1131
+ dark_field_scans = nxentry.spec_scans.dark_field
1132
+ detector_prefix = str(nxentry.instrument.detector.local_name)
1133
+ tdf_stack = []
1134
+ for nxsubentry_name, nxsubentry in dark_field_scans.items():
1135
+ scan_number = int(nxsubentry_name.split('_')[-1])
1136
+ scanparser = get_scanparser(dark_field_scans.attrs['spec_file'], scan_number)
1137
+ image_offset = int(nxsubentry.instrument.detector.frame_start_number)
1138
+ num_image = len(nxsubentry.sample.rotation_angle)
1139
+ tdf_stack.append(scanparser.get_detector_data(detector_prefix,
1140
+ (image_offset, image_offset+num_image)))
1141
+ if isinstance(tdf_stack, list):
1142
+ assert(len(tdf_stack) == 1) # TODO
1143
+ tdf_stack = tdf_stack[0]
1144
+
1145
+ # Take median
1146
+ if tdf_stack.ndim == 2:
1147
+ tdf = tdf_stack
1148
+ elif tdf_stack.ndim == 3:
1149
+ tdf = np.median(tdf_stack, axis=0)
1150
+ del tdf_stack
1151
+ else:
1152
+ raise ValueError(f'Invalid tdf_stack shape ({tdf_stack.shape})')
1153
+
1154
+ # Remove dark field intensities above the cutoff
1155
+ #RV tdf_cutoff = None
1156
+ tdf_cutoff = tdf.min()+2*(np.median(tdf)-tdf.min())
1157
+ self.logger.debug(f'tdf_cutoff = {tdf_cutoff}')
1158
+ if tdf_cutoff is not None:
1159
+ if not isinstance(tdf_cutoff, (int, float)) or tdf_cutoff < 0:
1160
+ self.logger.warning(f'Ignoring illegal value of tdf_cutoff {tdf_cutoff}')
1161
+ else:
1162
+ tdf[tdf > tdf_cutoff] = np.nan
1163
+ self.logger.debug(f'tdf_cutoff = {tdf_cutoff}')
1164
+
1165
+ # Remove nans
1166
+ tdf_mean = np.nanmean(tdf)
1167
+ self.logger.debug(f'tdf_mean = {tdf_mean}')
1168
+ np.nan_to_num(tdf, copy=False, nan=tdf_mean, posinf=tdf_mean, neginf=0.)
1169
+
1170
+ # Plot dark field
1171
+ if self.save_figs:
1172
+ if self.galaxy_flag:
1173
+ quick_imshow(tdf, title='dark field', path='tomo_reduce_plots', save_fig=True,
1174
+ save_only=True)
1175
+ else:
1176
+ quick_imshow(tdf, title='dark field', path=self.output_folder, save_fig=True,
1177
+ save_only=True)
1178
+
1179
+ # Add dark field to reduced data NXprocess
1180
+ reduced_data.data = NXdata()
1181
+ reduced_data.data['dark_field'] = tdf
1182
+
1183
+ return(reduced_data)
1184
+
1185
+ def _gen_bright(self, nxentry, reduced_data):
1186
+ """Generate bright field.
1187
+ """
1188
+ from nexusformat.nexus import NXdata
1189
+
1190
+ from CHAP.common.models.map import get_scanparser, import_scanparser
1191
+
1192
+ # Get the bright field images
1193
+ image_key = nxentry.instrument.detector.get('image_key', None)
1194
+ if image_key and 'data' in nxentry.instrument.detector:
1195
+ field_indices = [index for index, key in enumerate(image_key) if key == 1]
1196
+ tbf_stack = nxentry.instrument.detector.data[field_indices,:,:]
1197
+ # RV the default NXtomo form does not accomodate bright or dark field stacks
1198
+ else:
1199
+ import_scanparser(nxentry.instrument.source.attrs['station'],
1200
+ nxentry.instrument.source.attrs['experiment_type'])
1201
+ bright_field_scans = nxentry.spec_scans.bright_field
1202
+ detector_prefix = str(nxentry.instrument.detector.local_name)
1203
+ tbf_stack = []
1204
+ for nxsubentry_name, nxsubentry in bright_field_scans.items():
1205
+ scan_number = int(nxsubentry_name.split('_')[-1])
1206
+ scanparser = get_scanparser(bright_field_scans.attrs['spec_file'], scan_number)
1207
+ image_offset = int(nxsubentry.instrument.detector.frame_start_number)
1208
+ num_image = len(nxsubentry.sample.rotation_angle)
1209
+ tbf_stack.append(scanparser.get_detector_data(detector_prefix,
1210
+ (image_offset, image_offset+num_image)))
1211
+ if isinstance(tbf_stack, list):
1212
+ assert(len(tbf_stack) == 1) # TODO
1213
+ tbf_stack = tbf_stack[0]
1214
+
1215
+ # Take median if more than one image
1216
+ """Median or mean: It may be best to try the median because of some image
1217
+ artifacts that arise due to crinkles in the upstream kapton tape windows
1218
+ causing some phase contrast images to appear on the detector.
1219
+ One thing that also may be useful in a future implementation is to do a
1220
+ brightfield adjustment on EACH frame of the tomo based on a ROI in the
1221
+ corner of the frame where there is no sample but there is the direct X-ray
1222
+ beam because there is frame to frame fluctuations from the incoming beam.
1223
+ We don’t typically account for them but potentially could.
1224
+ """
1225
+ from nexusformat.nexus import NXdata
1226
+
1227
+ if tbf_stack.ndim == 2:
1228
+ tbf = tbf_stack
1229
+ elif tbf_stack.ndim == 3:
1230
+ tbf = np.median(tbf_stack, axis=0)
1231
+ del tbf_stack
1232
+ else:
1233
+ raise ValueError(f'Invalid tbf_stack shape ({tbf_stacks.shape})')
1234
+
1235
+ # Subtract dark field
1236
+ if 'data' in reduced_data and 'dark_field' in reduced_data.data:
1237
+ tbf -= reduced_data.data.dark_field
1238
+ else:
1239
+ self.logger.warning('Dark field unavailable')
1240
+
1241
+ # Set any non-positive values to one
1242
+ # (avoid negative bright field values for spikes in dark field)
1243
+ tbf[tbf < 1] = 1
1244
+
1245
+ # Plot bright field
1246
+ if self.save_figs:
1247
+ if self.galaxy_flag:
1248
+ quick_imshow(tbf, title='bright field', path='tomo_reduce_plots', save_fig=True,
1249
+ save_only=True)
1250
+ else:
1251
+ quick_imshow(tbf, title='bright field', path=self.output_folder, save_fig=True,
1252
+ save_only=True)
1253
+
1254
+ # Add bright field to reduced data NXprocess
1255
+ if 'data' not in reduced_data:
1256
+ reduced_data.data = NXdata()
1257
+ reduced_data.data['bright_field'] = tbf
1258
+
1259
+ return(reduced_data)
1260
+
1261
+ def _set_detector_bounds(self, nxentry, reduced_data, img_x_bounds=None):
1262
+ """Set vertical detector bounds for each image stack.
1263
+ Right now the range is the same for each set in the image stack.
1264
+ """
1265
+ from CHAP.common.models.map import get_scanparser, import_scanparser
1266
+ from CHAP.common.utils.general import is_index_range
1267
+
1268
+ if self.test_mode:
1269
+ return(tuple(self.test_config['img_x_bounds']))
1270
+
1271
+ # Get the first tomography image and the reference heights
1272
+ image_key = nxentry.instrument.detector.get('image_key', None)
1273
+ if image_key and 'data' in nxentry.instrument.detector:
1274
+ field_indices = [index for index, key in enumerate(image_key) if key == 0]
1275
+ first_image = np.asarray(nxentry.instrument.detector.data[field_indices[0],:,:])
1276
+ theta = float(nxentry.sample.rotation_angle[field_indices[0]])
1277
+ z_translation_all = nxentry.sample.z_translation[field_indices]
1278
+ vertical_shifts = sorted(list(set(z_translation_all)))
1279
+ num_tomo_stacks = len(vertical_shifts)
1280
+ else:
1281
+ import_scanparser(nxentry.instrument.source.attrs['station'],
1282
+ nxentry.instrument.source.attrs['experiment_type'])
1283
+ tomo_field_scans = nxentry.spec_scans.tomo_fields
1284
+ num_tomo_stacks = len(tomo_field_scans.keys())
1285
+ center_stack_index = int(num_tomo_stacks/2)
1286
+ detector_prefix = str(nxentry.instrument.detector.local_name)
1287
+ vertical_shifts = []
1288
+ for i, nxsubentry in enumerate(tomo_field_scans.items()):
1289
+ scan_number = int(nxsubentry[0].split('_')[-1])
1290
+ scanparser = get_scanparser(tomo_field_scans.attrs['spec_file'], scan_number)
1291
+ image_offset = int(nxsubentry[1].instrument.detector.frame_start_number)
1292
+ vertical_shifts.append(nxsubentry[1].sample.z_translation)
1293
+ if i == center_stack_index:
1294
+ first_image = scanparser.get_detector_data(detector_prefix, image_offset)
1295
+ theta = float(nxsubentry[1].sample.rotation_angle[0])
1296
+
1297
+ # Select image bounds
1298
+ title = f'tomography image at theta={round(theta, 2)+0}'
1299
+ if img_x_bounds is not None:
1300
+ if not is_index_range(img_x_bounds, ge=0, le=first_image.shape[0]):
1301
+ raise ValueError(f'Invalid parameter img_x_bounds ({img_x_bounds})')
1302
+ #RV TODO make interactive upon request?
1303
+ return(img_x_bounds)
1304
+ if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
1305
+ pixel_size = nxentry.instrument.detector.x_pixel_size
1306
+ # Try to get a fit from the bright field
1307
+ tbf = np.asarray(reduced_data.data.bright_field)
1308
+ tbf_shape = tbf.shape
1309
+ x_sum = np.sum(tbf, 1)
1310
+ x_sum_min = x_sum.min()
1311
+ x_sum_max = x_sum.max()
1312
+ fit = Fit.fit_data(x_sum, 'rectangle', x=np.array(range(len(x_sum))), form='atan',
1313
+ guess=True)
1314
+ parameters = fit.best_values
1315
+ x_low_fit = parameters.get('center1', None)
1316
+ x_upp_fit = parameters.get('center2', None)
1317
+ sig_low = parameters.get('sigma1', None)
1318
+ sig_upp = parameters.get('sigma2', None)
1319
+ have_fit = fit.success and x_low_fit is not None and x_upp_fit is not None and \
1320
+ sig_low is not None and sig_upp is not None and \
1321
+ 0 <= x_low_fit < x_upp_fit <= x_sum.size and \
1322
+ (sig_low+sig_upp)/(x_upp_fit-x_low_fit) < 0.1
1323
+ if have_fit:
1324
+ # Set a 5% margin on each side
1325
+ margin = 0.05*(x_upp_fit-x_low_fit)
1326
+ x_low_fit = max(0, x_low_fit-margin)
1327
+ x_upp_fit = min(tbf_shape[0], x_upp_fit+margin)
1328
+ if num_tomo_stacks == 1:
1329
+ if have_fit:
1330
+ # Set the default range to enclose the full fitted window
1331
+ x_low = int(x_low_fit)
1332
+ x_upp = int(x_upp_fit)
1333
+ else:
1334
+ # Center a default range of 1 mm (RV: can we get this from the slits?)
1335
+ num_x_min = int((1.0-0.5*pixel_size)/pixel_size)
1336
+ x_low = int(0.5*(tbf_shape[0]-num_x_min))
1337
+ x_upp = x_low+num_x_min
1338
+ else:
1339
+ # Get the default range from the reference heights
1340
+ delta_z = vertical_shifts[1]-vertical_shifts[0]
1341
+ for i in range(2, num_tomo_stacks):
1342
+ delta_z = min(delta_z, vertical_shifts[i]-vertical_shifts[i-1])
1343
+ self.logger.debug(f'delta_z = {delta_z}')
1344
+ num_x_min = int((delta_z-0.5*pixel_size)/pixel_size)
1345
+ self.logger.debug(f'num_x_min = {num_x_min}')
1346
+ if num_x_min > tbf_shape[0]:
1347
+ self.logger.warning('Image bounds and pixel size prevent seamless stacking')
1348
+ if have_fit:
1349
+ # Center the default range relative to the fitted window
1350
+ x_low = int(0.5*(x_low_fit+x_upp_fit-num_x_min))
1351
+ x_upp = x_low+num_x_min
1352
+ else:
1353
+ # Center the default range
1354
+ x_low = int(0.5*(tbf_shape[0]-num_x_min))
1355
+ x_upp = x_low+num_x_min
1356
+ if self.galaxy_flag:
1357
+ img_x_bounds = (x_low, x_upp)
1358
+ else:
1359
+ tmp = np.copy(tbf)
1360
+ tmp_max = tmp.max()
1361
+ tmp[x_low,:] = tmp_max
1362
+ tmp[x_upp-1,:] = tmp_max
1363
+ quick_imshow(tmp, title='bright field')
1364
+ tmp = np.copy(first_image)
1365
+ tmp_max = tmp.max()
1366
+ tmp[x_low,:] = tmp_max
1367
+ tmp[x_upp-1,:] = tmp_max
1368
+ quick_imshow(tmp, title=title)
1369
+ del tmp
1370
+ quick_plot((range(x_sum.size), x_sum),
1371
+ ([x_low, x_low], [x_sum_min, x_sum_max], 'r-'),
1372
+ ([x_upp, x_upp], [x_sum_min, x_sum_max], 'r-'),
1373
+ title='sum over theta and y')
1374
+ print(f'lower bound = {x_low} (inclusive)')
1375
+ print(f'upper bound = {x_upp} (exclusive)]')
1376
+ accept = input_yesno('Accept these bounds (y/n)?', 'y')
1377
+ clear_imshow('bright field')
1378
+ clear_imshow(title)
1379
+ clear_plot('sum over theta and y')
1380
+ if accept:
1381
+ img_x_bounds = (x_low, x_upp)
1382
+ else:
1383
+ while True:
1384
+ mask, img_x_bounds = draw_mask_1d(x_sum, title='select x data range',
1385
+ legend='sum over theta and y')
1386
+ if len(img_x_bounds) == 1:
1387
+ break
1388
+ else:
1389
+ print(f'Choose a single connected data range')
1390
+ img_x_bounds = tuple(img_x_bounds[0])
1391
+ if (num_tomo_stacks > 1 and img_x_bounds[1]-img_x_bounds[0]+1 <
1392
+ int((delta_z-0.5*pixel_size)/pixel_size)):
1393
+ self.logger.warning('Image bounds and pixel size prevent seamless stacking')
1394
+ else:
1395
+ if num_tomo_stacks > 1:
1396
+ raise NotImplementedError('Selecting image bounds for multiple stacks on FMB')
1397
+ # For FMB: use the first tomography image to select range
1398
+ # RV: revisit if they do tomography with multiple stacks
1399
+ x_sum = np.sum(first_image, 1)
1400
+ x_sum_min = x_sum.min()
1401
+ x_sum_max = x_sum.max()
1402
+ if self.galaxy_flag:
1403
+ if img_x_bounds is None:
1404
+ img_x_bounds = (0, first_image.shape[0])
1405
+ else:
1406
+ print('Select vertical data reduction range from first tomography image')
1407
+ img_x_bounds = select_image_bounds(first_image, 0, title=title)
1408
+ if img_x_bounds is None:
1409
+ raise ValueError('Unable to select image bounds')
1410
+
1411
+ # Plot results
1412
+ if self.save_figs:
1413
+ if self.galaxy_flag:
1414
+ path = 'tomo_reduce_plots'
1415
+ else:
1416
+ path = self.output_folder
1417
+ x_low = img_x_bounds[0]
1418
+ x_upp = img_x_bounds[1]
1419
+ tmp = np.copy(first_image)
1420
+ tmp_max = tmp.max()
1421
+ tmp[x_low,:] = tmp_max
1422
+ tmp[x_upp-1,:] = tmp_max
1423
+ quick_imshow(tmp, title=title, path=path, save_fig=True, save_only=True)
1424
+ quick_plot((range(x_sum.size), x_sum),
1425
+ ([x_low, x_low], [x_sum_min, x_sum_max], 'r-'),
1426
+ ([x_upp, x_upp], [x_sum_min, x_sum_max], 'r-'),
1427
+ title='sum over theta and y', path=path, save_fig=True, save_only=True)
1428
+ del tmp
1429
+
1430
+ return(img_x_bounds)
1431
+
1432
+ def _set_zoom_or_skip(self):
1433
+ """Set zoom and/or theta skip to reduce memory the requirement for the analysis.
1434
+ """
1435
+ # if input_yesno('\nDo you want to zoom in to reduce memory requirement (y/n)?', 'n'):
1436
+ # zoom_perc = input_int(' Enter zoom percentage', ge=1, le=100)
1437
+ # else:
1438
+ # zoom_perc = None
1439
+ zoom_perc = None
1440
+ # if input_yesno('Do you want to skip thetas to reduce memory requirement (y/n)?', 'n'):
1441
+ # num_theta_skip = input_int(' Enter the number skip theta interval', ge=0,
1442
+ # lt=num_theta)
1443
+ # else:
1444
+ # num_theta_skip = None
1445
+ num_theta_skip = None
1446
+ self.logger.debug(f'zoom_perc = {zoom_perc}')
1447
+ self.logger.debug(f'num_theta_skip = {num_theta_skip}')
1448
+
1449
+ return(zoom_perc, num_theta_skip)
1450
+
1451
+ def _gen_tomo(self, nxentry, reduced_data):
1452
+ """Generate tomography fields.
1453
+ """
1454
+ import numexpr as ne
1455
+ import scipy.ndimage as spi
1456
+
1457
+ from CHAP.common.models.map import get_scanparser, import_scanparser
1458
+
1459
+ # Get full bright field
1460
+ tbf = np.asarray(reduced_data.data.bright_field)
1461
+ tbf_shape = tbf.shape
1462
+
1463
+ # Get image bounds
1464
+ img_x_bounds = tuple(reduced_data.get('img_x_bounds', (0, tbf_shape[0])))
1465
+ img_y_bounds = tuple(reduced_data.get('img_y_bounds', (0, tbf_shape[1])))
1466
+
1467
+ # Get resized dark field
1468
+ # if 'dark_field' in data:
1469
+ # tbf = np.asarray(reduced_data.data.dark_field[
1470
+ # img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]])
1471
+ # else:
1472
+ # self.logger.warning('Dark field unavailable')
1473
+ # tdf = None
1474
+ tdf = None
1475
+
1476
+ # Resize bright field
1477
+ if img_x_bounds != (0, tbf.shape[0]) or img_y_bounds != (0, tbf.shape[1]):
1478
+ tbf = tbf[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
1479
+
1480
+ # Get the tomography images
1481
+ image_key = nxentry.instrument.detector.get('image_key', None)
1482
+ if image_key and 'data' in nxentry.instrument.detector:
1483
+ field_indices_all = [index for index, key in enumerate(image_key) if key == 0]
1484
+ z_translation_all = nxentry.sample.z_translation[field_indices_all]
1485
+ z_translation_levels = sorted(list(set(z_translation_all)))
1486
+ num_tomo_stacks = len(z_translation_levels)
1487
+ tomo_stacks = num_tomo_stacks*[np.array([])]
1488
+ horizontal_shifts = []
1489
+ vertical_shifts = []
1490
+ thetas = None
1491
+ tomo_stacks = []
1492
+ for i, z_translation in enumerate(z_translation_levels):
1493
+ field_indices = [field_indices_all[index]
1494
+ for index, z in enumerate(z_translation_all) if z == z_translation]
1495
+ horizontal_shift = list(set(nxentry.sample.x_translation[field_indices]))
1496
+ assert(len(horizontal_shift) == 1)
1497
+ horizontal_shifts += horizontal_shift
1498
+ vertical_shift = list(set(nxentry.sample.z_translation[field_indices]))
1499
+ assert(len(vertical_shift) == 1)
1500
+ vertical_shifts += vertical_shift
1501
+ sequence_numbers = nxentry.instrument.detector.sequence_number[field_indices]
1502
+ if thetas is None:
1503
+ thetas = np.asarray(nxentry.sample.rotation_angle[field_indices]) \
1504
+ [sequence_numbers]
1505
+ else:
1506
+ assert(all(thetas[i] == nxentry.sample.rotation_angle[field_indices[index]]
1507
+ for i, index in enumerate(sequence_numbers)))
1508
+ assert(list(set(sequence_numbers)) == [i for i in range(len(sequence_numbers))])
1509
+ if list(sequence_numbers) == [i for i in range(len(sequence_numbers))]:
1510
+ tomo_stack = np.asarray(nxentry.instrument.detector.data[field_indices])
1511
+ else:
1512
+ raise ValueError('Unable to load the tomography images')
1513
+ tomo_stacks.append(tomo_stack)
1514
+ else:
1515
+ import_scanparser(nxentry.instrument.source.attrs['station'],
1516
+ nxentry.instrument.source.attrs['experiment_type'])
1517
+ tomo_field_scans = nxentry.spec_scans.tomo_fields
1518
+ num_tomo_stacks = len(tomo_field_scans.keys())
1519
+ center_stack_index = int(num_tomo_stacks/2)
1520
+ detector_prefix = str(nxentry.instrument.detector.local_name)
1521
+ thetas = None
1522
+ tomo_stacks = []
1523
+ horizontal_shifts = []
1524
+ vertical_shifts = []
1525
+ for nxsubentry_name, nxsubentry in tomo_field_scans.items():
1526
+ scan_number = int(nxsubentry_name.split('_')[-1])
1527
+ scanparser = get_scanparser(tomo_field_scans.attrs['spec_file'], scan_number)
1528
+ image_offset = int(nxsubentry.instrument.detector.frame_start_number)
1529
+ if thetas is None:
1530
+ thetas = np.asarray(nxsubentry.sample.rotation_angle)
1531
+ num_image = len(thetas)
1532
+ tomo_stacks.append(scanparser.get_detector_data(detector_prefix,
1533
+ (image_offset, image_offset+num_image)))
1534
+ horizontal_shifts.append(nxsubentry.sample.x_translation)
1535
+ vertical_shifts.append(nxsubentry.sample.z_translation)
1536
+
1537
+ reduced_tomo_stacks = []
1538
+ if self.galaxy_flag:
1539
+ path = 'tomo_reduce_plots'
1540
+ else:
1541
+ path = self.output_folder
1542
+ for i, tomo_stack in enumerate(tomo_stacks):
1543
+ # Resize the tomography images
1544
+ # Right now the range is the same for each set in the image stack.
1545
+ if img_x_bounds != (0, tbf.shape[0]) or img_y_bounds != (0, tbf.shape[1]):
1546
+ t0 = time()
1547
+ tomo_stack = tomo_stack[:,img_x_bounds[0]:img_x_bounds[1],
1548
+ img_y_bounds[0]:img_y_bounds[1]].astype('float64')
1549
+ self.logger.debug(f'Resizing tomography images took {time()-t0:.2f} seconds')
1550
+
1551
+ # Subtract dark field
1552
+ if tdf is not None:
1553
+ t0 = time()
1554
+ with set_numexpr_threads(self.num_core):
1555
+ ne.evaluate('tomo_stack-tdf', out=tomo_stack)
1556
+ self.logger.debug(f'Subtracting dark field took {time()-t0:.2f} seconds')
1557
+
1558
+ # Normalize
1559
+ t0 = time()
1560
+ with set_numexpr_threads(self.num_core):
1561
+ ne.evaluate('tomo_stack/tbf', out=tomo_stack, truediv=True)
1562
+ self.logger.debug(f'Normalizing took {time()-t0:.2f} seconds')
1563
+
1564
+ # Remove non-positive values and linearize data
1565
+ t0 = time()
1566
+ cutoff = 1.e-6
1567
+ with set_numexpr_threads(self.num_core):
1568
+ ne.evaluate('where(tomo_stack<cutoff, cutoff, tomo_stack)', out=tomo_stack)
1569
+ with set_numexpr_threads(self.num_core):
1570
+ ne.evaluate('-log(tomo_stack)', out=tomo_stack)
1571
+ self.logger.debug('Removing non-positive values and linearizing data took '+
1572
+ f'{time()-t0:.2f} seconds')
1573
+
1574
+ # Get rid of nans/infs that may be introduced by normalization
1575
+ t0 = time()
1576
+ np.where(np.isfinite(tomo_stack), tomo_stack, 0.)
1577
+ self.logger.debug(f'Remove nans/infs took {time()-t0:.2f} seconds')
1578
+
1579
+ # Downsize tomography stack to smaller size
1580
+ # TODO use theta_skip as well
1581
+ tomo_stack = tomo_stack.astype('float32')
1582
+ if not self.test_mode:
1583
+ if len(tomo_stacks) == 1:
1584
+ title = f'red fullres theta {round(thetas[0], 2)+0}'
1585
+ else:
1586
+ title = f'red stack {i+1} fullres theta {round(thetas[0], 2)+0}'
1587
+ quick_imshow(tomo_stack[0,:,:], title=title, path=path, save_fig=self.save_figs,
1588
+ save_only=self.save_only, block=self.block)
1589
+ # if not self.block:
1590
+ # clear_imshow(title)
1591
+ if False and zoom_perc != 100:
1592
+ t0 = time()
1593
+ self.logger.debug(f'Zooming in ...')
1594
+ tomo_zoom_list = []
1595
+ for j in range(tomo_stack.shape[0]):
1596
+ tomo_zoom = spi.zoom(tomo_stack[j,:,:], 0.01*zoom_perc)
1597
+ tomo_zoom_list.append(tomo_zoom)
1598
+ tomo_stack = np.stack([tomo_zoom for tomo_zoom in tomo_zoom_list])
1599
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1600
+ self.logger.info(f'Zooming in took {time()-t0:.2f} seconds')
1601
+ del tomo_zoom_list
1602
+ if not self.test_mode:
1603
+ title = f'red stack {zoom_perc}p theta {round(thetas[0], 2)+0}'
1604
+ quick_imshow(tomo_stack[0,:,:], title=title, path=path, save_fig=self.save_figs,
1605
+ save_only=self.save_only, block=self.block)
1606
+ # if not self.block:
1607
+ # clear_imshow(title)
1608
+
1609
+ # Save test data to file
1610
+ if self.test_mode:
1611
+ # row_index = int(tomo_stack.shape[0]/2)
1612
+ # np.savetxt(f'{self.output_folder}/red_stack_{i+1}.txt', tomo_stack[row_index,:,:],
1613
+ # fmt='%.6e')
1614
+ row_index = int(tomo_stack.shape[1]/2)
1615
+ np.savetxt(f'{self.output_folder}/red_stack_{i+1}.txt', tomo_stack[:,row_index,:],
1616
+ fmt='%.6e')
1617
+
1618
+ # Combine resized stacks
1619
+ reduced_tomo_stacks.append(tomo_stack)
1620
+
1621
+ # Add tomo field info to reduced data NXprocess
1622
+ reduced_data['rotation_angle'] = thetas
1623
+ reduced_data['x_translation'] = np.asarray(horizontal_shifts)
1624
+ reduced_data['z_translation'] = np.asarray(vertical_shifts)
1625
+ reduced_data.data['tomo_fields'] = np.asarray(reduced_tomo_stacks)
1626
+
1627
+ if tdf is not None:
1628
+ del tdf
1629
+ del tbf
1630
+
1631
+ return(reduced_data)
1632
+
1633
+ def _find_center_one_plane(self, sinogram, row, thetas, eff_pixel_size, cross_sectional_dim,
1634
+ path=None, tol=0.1, num_core=1):
1635
+ """Find center for a single tomography plane.
1636
+ """
1637
+ import tomopy
1638
+
1639
+ # Try automatic center finding routines for initial value
1640
+ # sinogram index order: theta,column
1641
+ # need column,theta for iradon, so take transpose
1642
+ sinogram = np.asarray(sinogram)
1643
+ sinogram_T = sinogram.T
1644
+ center = sinogram.shape[1]/2
1645
+
1646
+ # Try using Nghia Vo’s method
1647
+ t0 = time()
1648
+ if num_core > num_core_tomopy_limit:
1649
+ self.logger.debug(f'Running find_center_vo on {num_core_tomopy_limit} cores ...')
1650
+ tomo_center = tomopy.find_center_vo(sinogram, ncore=num_core_tomopy_limit)
1651
+ else:
1652
+ self.logger.debug(f'Running find_center_vo on {num_core} cores ...')
1653
+ tomo_center = tomopy.find_center_vo(sinogram, ncore=num_core)
1654
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1655
+ self.logger.info(f'Finding the center using Nghia Vo’s method took {time()-t0:.2f} seconds')
1656
+ center_offset_vo = tomo_center-center
1657
+ self.logger.info(f'Center at row {row} using Nghia Vo’s method = {center_offset_vo:.2f}')
1658
+ t0 = time()
1659
+ self.logger.debug(f'Running _reconstruct_one_plane on {self.num_core} cores ...')
1660
+ recon_plane = self._reconstruct_one_plane(sinogram_T, tomo_center, thetas,
1661
+ eff_pixel_size, cross_sectional_dim, False, num_core)
1662
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1663
+ self.logger.info(f'Reconstructing row {row} took {time()-t0:.2f} seconds')
1664
+
1665
+ title = f'edges row{row} center offset{center_offset_vo:.2f} Vo'
1666
+ self._plot_edges_one_plane(recon_plane, title, path=path)
1667
+
1668
+ # Try using phase correlation method
1669
+ # if input_yesno('Try finding center using phase correlation (y/n)?', 'n'):
1670
+ # t0 = time()
1671
+ # self.logger.debug(f'Running find_center_pc ...')
1672
+ # tomo_center = tomopy.find_center_pc(sinogram, sinogram, tol=0.1, rotc_guess=tomo_center)
1673
+ # error = 1.
1674
+ # while error > tol:
1675
+ # prev = tomo_center
1676
+ # tomo_center = tomopy.find_center_pc(sinogram, sinogram, tol=tol,
1677
+ # rotc_guess=tomo_center)
1678
+ # error = np.abs(tomo_center-prev)
1679
+ # self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1680
+ # self.logger.info('Finding the center using the phase correlation method took '+
1681
+ # f'{time()-t0:.2f} seconds')
1682
+ # center_offset = tomo_center-center
1683
+ # print(f'Center at row {row} using phase correlation = {center_offset:.2f}')
1684
+ # t0 = time()
1685
+ # self.logger.debug(f'Running _reconstruct_one_plane on {self.num_core} cores ...')
1686
+ # recon_plane = self._reconstruct_one_plane(sinogram_T, tomo_center, thetas,
1687
+ # eff_pixel_size, cross_sectional_dim, False, num_core)
1688
+ # self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1689
+ # self.logger.info(f'Reconstructing row {row} took {time()-t0:.2f} seconds')
1690
+ #
1691
+ # title = f'edges row{row} center_offset{center_offset:.2f} PC'
1692
+ # self._plot_edges_one_plane(recon_plane, title, path=path)
1693
+
1694
+ # Select center location
1695
+ # if input_yesno('Accept a center location (y) or continue search (n)?', 'y'):
1696
+ if True:
1697
+ # center_offset = input_num(' Enter chosen center offset', ge=-center, le=center,
1698
+ # default=center_offset_vo)
1699
+ center_offset = center_offset_vo
1700
+ del sinogram_T
1701
+ del recon_plane
1702
+ return float(center_offset)
1703
+
1704
+ # perform center finding search
1705
+ while True:
1706
+ center_offset_low = input_int('\nEnter lower bound for center offset', ge=-center,
1707
+ le=center)
1708
+ center_offset_upp = input_int('Enter upper bound for center offset',
1709
+ ge=center_offset_low, le=center)
1710
+ if center_offset_upp == center_offset_low:
1711
+ center_offset_step = 1
1712
+ else:
1713
+ center_offset_step = input_int('Enter step size for center offset search', ge=1,
1714
+ le=center_offset_upp-center_offset_low)
1715
+ num_center_offset = 1+int((center_offset_upp-center_offset_low)/center_offset_step)
1716
+ center_offsets = np.linspace(center_offset_low, center_offset_upp, num_center_offset)
1717
+ for center_offset in center_offsets:
1718
+ if center_offset == center_offset_vo:
1719
+ continue
1720
+ t0 = time()
1721
+ self.logger.debug(f'Running _reconstruct_one_plane on {num_core} cores ...')
1722
+ recon_plane = self._reconstruct_one_plane(sinogram_T, center_offset+center, thetas,
1723
+ eff_pixel_size, cross_sectional_dim, False, num_core)
1724
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1725
+ self.logger.info(f'Reconstructing center_offset {center_offset} took '+
1726
+ f'{time()-t0:.2f} seconds')
1727
+ title = f'edges row{row} center_offset{center_offset:.2f}'
1728
+ self._plot_edges_one_plane(recon_plane, title, path=path)
1729
+ if input_int('\nContinue (0) or end the search (1)', ge=0, le=1):
1730
+ break
1731
+
1732
+ del sinogram_T
1733
+ del recon_plane
1734
+ center_offset = input_num(' Enter chosen center offset', ge=-center, le=center)
1735
+ return float(center_offset)
1736
+
1737
+ def _reconstruct_one_plane(self, tomo_plane_T, center, thetas, eff_pixel_size,
1738
+ cross_sectional_dim, plot_sinogram=True, num_core=1):
1739
+ """Invert the sinogram for a single tomography plane.
1740
+ """
1741
+ import scipy.ndimage as spi
1742
+ from skimage.transform import iradon
1743
+ import tomopy
1744
+
1745
+ # tomo_plane_T index order: column,theta
1746
+ assert(0 <= center < tomo_plane_T.shape[0])
1747
+ center_offset = center-tomo_plane_T.shape[0]/2
1748
+ two_offset = 2*int(np.round(center_offset))
1749
+ two_offset_abs = np.abs(two_offset)
1750
+ max_rad = int(0.55*(cross_sectional_dim/eff_pixel_size)) # 10% slack to avoid edge effects
1751
+ if max_rad > 0.5*tomo_plane_T.shape[0]:
1752
+ max_rad = 0.5*tomo_plane_T.shape[0]
1753
+ dist_from_edge = max(1, int(np.floor((tomo_plane_T.shape[0]-two_offset_abs)/2.)-max_rad))
1754
+ if two_offset >= 0:
1755
+ self.logger.debug(f'sinogram range = [{two_offset+dist_from_edge}, {-dist_from_edge}]')
1756
+ sinogram = tomo_plane_T[two_offset+dist_from_edge:-dist_from_edge,:]
1757
+ else:
1758
+ self.logger.debug(f'sinogram range = [{dist_from_edge}, {two_offset-dist_from_edge}]')
1759
+ sinogram = tomo_plane_T[dist_from_edge:two_offset-dist_from_edge,:]
1760
+ if not self.galaxy_flag and plot_sinogram:
1761
+ quick_imshow(sinogram.T, f'sinogram center offset{center_offset:.2f}', aspect='auto',
1762
+ path=self.output_folder, save_fig=self.save_figs, save_only=self.save_only,
1763
+ block=self.block)
1764
+
1765
+ # Inverting sinogram
1766
+ t0 = time()
1767
+ recon_sinogram = iradon(sinogram, theta=thetas, circle=True)
1768
+ self.logger.debug(f'Inverting sinogram took {time()-t0:.2f} seconds')
1769
+ del sinogram
1770
+
1771
+ # Performing Gaussian filtering and removing ring artifacts
1772
+ recon_parameters = None#self.config.get('recon_parameters')
1773
+ if recon_parameters is None:
1774
+ sigma = 1.0
1775
+ ring_width = 15
1776
+ else:
1777
+ sigma = recon_parameters.get('gaussian_sigma', 1.0)
1778
+ if not is_num(sigma, ge=0.0):
1779
+ self.logger.warning(f'Invalid gaussian_sigma ({sigma}) in _reconstruct_one_plane, '+
1780
+ 'set to a default value of 1.0')
1781
+ sigma = 1.0
1782
+ ring_width = recon_parameters.get('ring_width', 15)
1783
+ if not isinstance(ring_width, int) or ring_width < 0:
1784
+ self.logger.warning(f'Invalid ring_width ({ring_width}) in '+
1785
+ '_reconstruct_one_plane, set to a default value of 15')
1786
+ ring_width = 15
1787
+ t0 = time()
1788
+ recon_sinogram = spi.gaussian_filter(recon_sinogram, sigma, mode='nearest')
1789
+ recon_clean = np.expand_dims(recon_sinogram, axis=0)
1790
+ del recon_sinogram
1791
+ recon_clean = tomopy.misc.corr.remove_ring(recon_clean, rwidth=ring_width, ncore=num_core)
1792
+ self.logger.debug(f'Filtering and removing ring artifacts took {time()-t0:.2f} seconds')
1793
+
1794
+ return recon_clean
1795
+
1796
+ def _plot_edges_one_plane(self, recon_plane, title, path=None):
1797
+ from skimage.restoration import denoise_tv_chambolle
1798
+
1799
+ vis_parameters = None#self.config.get('vis_parameters')
1800
+ if vis_parameters is None:
1801
+ weight = 0.1
1802
+ else:
1803
+ weight = vis_parameters.get('denoise_weight', 0.1)
1804
+ if not is_num(weight, ge=0.0):
1805
+ self.logger.warning(f'Invalid weight ({weight}) in _plot_edges_one_plane, '+
1806
+ 'set to a default value of 0.1')
1807
+ weight = 0.1
1808
+ edges = denoise_tv_chambolle(recon_plane, weight=weight)
1809
+ vmax = np.max(edges[0,:,:])
1810
+ vmin = -vmax
1811
+ if path is None:
1812
+ path = self.output_folder
1813
+ quick_imshow(edges[0,:,:], f'{title} coolwarm', path=path, cmap='coolwarm',
1814
+ save_fig=self.save_figs, save_only=self.save_only, block=self.block)
1815
+ quick_imshow(edges[0,:,:], f'{title} gray', path=path, cmap='gray', vmin=vmin, vmax=vmax,
1816
+ save_fig=self.save_figs, save_only=self.save_only, block=self.block)
1817
+ del edges
1818
+
1819
+ def _reconstruct_one_tomo_stack(self, tomo_stack, thetas, center_offsets=[], num_core=1,
1820
+ algorithm='gridrec'):
1821
+ """Reconstruct a single tomography stack.
1822
+ """
1823
+ import tomopy
1824
+
1825
+ # tomo_stack order: row,theta,column
1826
+ # input thetas must be in degrees
1827
+ # centers_offset: tomography axis shift in pixels relative to column center
1828
+ # RV should we remove stripes?
1829
+ # https://tomopy.readthedocs.io/en/latest/api/tomopy.prep.stripe.html
1830
+ # RV should we remove rings?
1831
+ # https://tomopy.readthedocs.io/en/latest/api/tomopy.misc.corr.html
1832
+ # RV: Add an option to do (extra) secondary iterations later or to do some sort of convergence test?
1833
+ if not len(center_offsets):
1834
+ centers = np.zeros((tomo_stack.shape[0]))
1835
+ elif len(center_offsets) == 2:
1836
+ centers = np.linspace(center_offsets[0], center_offsets[1], tomo_stack.shape[0])
1837
+ else:
1838
+ if center_offsets.size != tomo_stack.shape[0]:
1839
+ raise ValueError('center_offsets dimension mismatch in reconstruct_one_tomo_stack')
1840
+ centers = center_offsets
1841
+ centers += tomo_stack.shape[2]/2
1842
+
1843
+ # Get reconstruction parameters
1844
+ recon_parameters = None#self.config.get('recon_parameters')
1845
+ if recon_parameters is None:
1846
+ sigma = 2.0
1847
+ secondary_iters = 0
1848
+ ring_width = 15
1849
+ else:
1850
+ sigma = recon_parameters.get('stripe_fw_sigma', 2.0)
1851
+ if not is_num(sigma, ge=0):
1852
+ self.logger.warning(f'Invalid stripe_fw_sigma ({sigma}) in '+
1853
+ '_reconstruct_one_tomo_stack, set to a default value of 2.0')
1854
+ ring_width = 15
1855
+ secondary_iters = recon_parameters.get('secondary_iters', 0)
1856
+ if not isinstance(secondary_iters, int) or secondary_iters < 0:
1857
+ self.logger.warning(f'Invalid secondary_iters ({secondary_iters}) in '+
1858
+ '_reconstruct_one_tomo_stack, set to a default value of 0 (skip them)')
1859
+ ring_width = 0
1860
+ ring_width = recon_parameters.get('ring_width', 15)
1861
+ if not isinstance(ring_width, int) or ring_width < 0:
1862
+ self.logger.warning(f'Invalid ring_width ({ring_width}) in '+
1863
+ '_reconstruct_one_plane, set to a default value of 15')
1864
+ ring_width = 15
1865
+
1866
+ # Remove horizontal stripe
1867
+ t0 = time()
1868
+ if num_core > num_core_tomopy_limit:
1869
+ self.logger.debug('Running remove_stripe_fw on {num_core_tomopy_limit} cores ...')
1870
+ tomo_stack = tomopy.prep.stripe.remove_stripe_fw(tomo_stack, sigma=sigma,
1871
+ ncore=num_core_tomopy_limit)
1872
+ else:
1873
+ self.logger.debug(f'Running remove_stripe_fw on {num_core} cores ...')
1874
+ tomo_stack = tomopy.prep.stripe.remove_stripe_fw(tomo_stack, sigma=sigma,
1875
+ ncore=num_core)
1876
+ self.logger.debug(f'... tomopy.prep.stripe.remove_stripe_fw took {time()-t0:.2f} seconds')
1877
+
1878
+ # Perform initial image reconstruction
1879
+ self.logger.debug('Performing initial image reconstruction')
1880
+ t0 = time()
1881
+ self.logger.debug(f'Running recon on {num_core} cores ...')
1882
+ tomo_recon_stack = tomopy.recon(tomo_stack, np.radians(thetas), centers,
1883
+ sinogram_order=True, algorithm=algorithm, ncore=num_core)
1884
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1885
+ self.logger.info(f'Performing initial image reconstruction took {time()-t0:.2f} seconds')
1886
+
1887
+ # Run optional secondary iterations
1888
+ if secondary_iters > 0:
1889
+ self.logger.debug(f'Running {secondary_iters} secondary iterations')
1890
+ #options = {'method':'SIRT_CUDA', 'proj_type':'cuda', 'num_iter':secondary_iters}
1891
+ #RV: doesn't work for me:
1892
+ #"Error: CUDA error 803: system has unsupported display driver/cuda driver combination."
1893
+ #options = {'method':'SIRT', 'proj_type':'linear', 'MinConstraint': 0, 'num_iter':secondary_iters}
1894
+ #SIRT did not finish while running overnight
1895
+ #options = {'method':'SART', 'proj_type':'linear', 'num_iter':secondary_iters}
1896
+ options = {'method':'SART', 'proj_type':'linear', 'MinConstraint': 0,
1897
+ 'num_iter':secondary_iters}
1898
+ t0 = time()
1899
+ self.logger.debug(f'Running recon on {num_core} cores ...')
1900
+ tomo_recon_stack = tomopy.recon(tomo_stack, np.radians(thetas), centers,
1901
+ init_recon=tomo_recon_stack, options=options, sinogram_order=True,
1902
+ algorithm=tomopy.astra, ncore=num_core)
1903
+ self.logger.debug(f'... done in {time()-t0:.2f} seconds')
1904
+ self.logger.info(f'Performing secondary iterations took {time()-t0:.2f} seconds')
1905
+
1906
+ # Remove ring artifacts
1907
+ t0 = time()
1908
+ tomopy.misc.corr.remove_ring(tomo_recon_stack, rwidth=ring_width, out=tomo_recon_stack,
1909
+ ncore=num_core)
1910
+ self.logger.debug(f'Removing ring artifacts took {time()-t0:.2f} seconds')
1911
+
1912
+ return tomo_recon_stack
1913
+
1914
+ def _resize_reconstructed_data(self, data, x_bounds=None, y_bounds=None, z_bounds=None,
1915
+ z_only=False):
1916
+ """Resize the reconstructed tomography data.
1917
+ """
1918
+ # Data order: row(z),x,y or stack,row(z),x,y
1919
+ if isinstance(data, list):
1920
+ for stack in data:
1921
+ assert(stack.ndim == 3)
1922
+ num_tomo_stacks = len(data)
1923
+ tomo_recon_stacks = data
1924
+ else:
1925
+ assert(data.ndim == 3)
1926
+ num_tomo_stacks = 1
1927
+ tomo_recon_stacks = [data]
1928
+
1929
+ if x_bounds == (-1, -1):
1930
+ x_bounds = None
1931
+ elif not z_only and x_bounds is None:
1932
+ # Selecting x bounds (in yz-plane)
1933
+ tomosum = 0
1934
+ [tomosum := tomosum+np.sum(tomo_recon_stacks[i], axis=(0,2))
1935
+ for i in range(num_tomo_stacks)]
1936
+ select_x_bounds = input_yesno('\nDo you want to change the image x-bounds (y/n)?', 'y')
1937
+ if not select_x_bounds:
1938
+ x_bounds = None
1939
+ else:
1940
+ accept = False
1941
+ index_ranges = None
1942
+ while not accept:
1943
+ mask, x_bounds = draw_mask_1d(tomosum, current_index_ranges=index_ranges,
1944
+ title='select x data range', legend='recon stack sum yz')
1945
+ while len(x_bounds) != 1:
1946
+ print('Please select exactly one continuous range')
1947
+ mask, x_bounds = draw_mask_1d(tomosum, title='select x data range',
1948
+ legend='recon stack sum yz')
1949
+ x_bounds = x_bounds[0]
1950
+ accept = True
1951
+ self.logger.debug(f'x_bounds = {x_bounds}')
1952
+
1953
+ if y_bounds == (-1, -1):
1954
+ y_bounds = None
1955
+ elif not z_only and y_bounds is None:
1956
+ # Selecting y bounds (in xz-plane)
1957
+ tomosum = 0
1958
+ [tomosum := tomosum+np.sum(tomo_recon_stacks[i], axis=(0,1))
1959
+ for i in range(num_tomo_stacks)]
1960
+ select_y_bounds = input_yesno('\nDo you want to change the image y-bounds (y/n)?', 'y')
1961
+ if not select_y_bounds:
1962
+ y_bounds = None
1963
+ else:
1964
+ accept = False
1965
+ index_ranges = None
1966
+ while not accept:
1967
+ mask, y_bounds = draw_mask_1d(tomosum, current_index_ranges=index_ranges,
1968
+ title='select x data range', legend='recon stack sum xz')
1969
+ while len(y_bounds) != 1:
1970
+ print('Please select exactly one continuous range')
1971
+ mask, y_bounds = draw_mask_1d(tomosum, title='select x data range',
1972
+ legend='recon stack sum xz')
1973
+ y_bounds = y_bounds[0]
1974
+ accept = True
1975
+ self.logger.debug(f'y_bounds = {y_bounds}')
1976
+
1977
+ # Selecting z bounds (in xy-plane) (only valid for a single image stack)
1978
+ if z_bounds == (-1, -1):
1979
+ z_bounds = None
1980
+ elif z_bounds is None and num_tomo_stacks != 1:
1981
+ tomosum = 0
1982
+ [tomosum := tomosum+np.sum(tomo_recon_stacks[i], axis=(1,2))
1983
+ for i in range(num_tomo_stacks)]
1984
+ select_z_bounds = input_yesno('Do you want to change the image z-bounds (y/n)?', 'n')
1985
+ if not select_z_bounds:
1986
+ z_bounds = None
1987
+ else:
1988
+ accept = False
1989
+ index_ranges = None
1990
+ while not accept:
1991
+ mask, z_bounds = draw_mask_1d(tomosum, current_index_ranges=index_ranges,
1992
+ title='select x data range', legend='recon stack sum xy')
1993
+ while len(z_bounds) != 1:
1994
+ print('Please select exactly one continuous range')
1995
+ mask, z_bounds = draw_mask_1d(tomosum, title='select x data range',
1996
+ legend='recon stack sum xy')
1997
+ z_bounds = z_bounds[0]
1998
+ accept = True
1999
+ self.logger.debug(f'z_bounds = {z_bounds}')
2000
+
2001
+ return(x_bounds, y_bounds, z_bounds)
2002
+
2003
+
2004
+ if __name__ == '__main__':
2005
+ from CHAP.processor import main
2006
+ main()
2007
+