NeuNorm 1.6.12__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.
NeuNorm/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Neutron Imaging Normalization package"""
2
+ try:
3
+ from ._version import __version__ # noqa: F401
4
+ except ImportError:
5
+ __version__ = "unknown"
6
+
7
+
8
+ class DataType:
9
+ sample = "sample"
10
+ ob = "ob"
11
+ df = "df"
12
+ normalized = "normalized"
NeuNorm/_utilities.py ADDED
@@ -0,0 +1,37 @@
1
+ import os
2
+ import numpy as np
3
+
4
+ im_ext = [
5
+ ".fits",
6
+ ".tiff",
7
+ ".tif",
8
+ ".hdf",
9
+ ".h4",
10
+ ".hdf4",
11
+ ".he2",
12
+ "h5",
13
+ ".hdf5",
14
+ ".he5",
15
+ ]
16
+
17
+
18
+ def get_sorted_list_images(folder=""):
19
+ """return the list of images sorted that have the correct format
20
+
21
+ Parameters:
22
+ folder: string of the path containing the images
23
+
24
+ Return:
25
+ sorted list of only images that can be read by program
26
+ """
27
+ filenames = [
28
+ name for name in os.listdir(folder) if name.lower().endswith(tuple(im_ext))
29
+ ]
30
+ filenames.sort()
31
+ return filenames
32
+
33
+
34
+ def average_df(df=[]):
35
+ """if more than 1 DF have been provided, we need to average them"""
36
+ mean_average = np.mean(df, axis=0)
37
+ return mean_average
NeuNorm/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.6.12"
NeuNorm/exporter.py ADDED
@@ -0,0 +1,13 @@
1
+ from PIL import Image
2
+ from astropy.io import fits
3
+
4
+
5
+ def make_tif(data=[], metadata=[], file_name=""):
6
+ """create tif file"""
7
+ new_image = Image.fromarray(data)
8
+ new_image.save(file_name, tiffinfo=metadata)
9
+
10
+
11
+ def make_fits(data=[], file_name=""):
12
+ """create fits file"""
13
+ fits.writeto(file_name, data, overwrite=True)
NeuNorm/loader.py ADDED
@@ -0,0 +1,53 @@
1
+ from astropy.io import fits
2
+ import numpy as np
3
+ import h5py
4
+ from PIL import Image
5
+
6
+
7
+ def load_hdf(file_name):
8
+ """load HDF image
9
+
10
+ Parameters
11
+ ----------
12
+ full file name of HDF5 file
13
+ """
14
+
15
+ hdf = h5py.File(file_name, "r")["entry"]["data"]["data"].value
16
+ tmp = []
17
+ for iScan in hdf:
18
+ tmp.append(iScan)
19
+ return tmp
20
+
21
+
22
+ def load_fits(file_name):
23
+ """load fits image
24
+
25
+ Parameters
26
+ ----------
27
+ full file name of fits image
28
+ """
29
+ tmp = []
30
+ try:
31
+ tmp = fits.open(file_name, ignore_missing_end=True)[0].data
32
+ if len(tmp.shape) == 3:
33
+ tmp = tmp.reshape(tmp.shape[1:])
34
+ return tmp
35
+ except OSError:
36
+ raise OSError("Unable to read the FITS file provided!")
37
+
38
+
39
+ def load_tiff(file_name):
40
+ """load tiff image
41
+
42
+ Parameters:
43
+ -----------
44
+ full file name of tiff image
45
+ """
46
+ try:
47
+ _image = Image.open(file_name)
48
+ metadata = dict(_image.tag_v2)
49
+ data = np.asarray(_image)
50
+ _image.close()
51
+ return [data, metadata]
52
+ except OSError as e:
53
+ raise OSError(f"Unable to read the TIFF file provided!: {e}")
@@ -0,0 +1,928 @@
1
+ """Normalization module for NeuNorm"""
2
+ #!/usr/bin/env python
3
+ from pathlib import Path
4
+ import numpy as np
5
+ import os
6
+ import logging
7
+ import copy
8
+ from scipy.ndimage import convolve
9
+ from tqdm.auto import tqdm
10
+
11
+ from NeuNorm.loader import load_tiff, load_fits
12
+ from NeuNorm.exporter import make_fits, make_tif
13
+ from NeuNorm.roi import ROI
14
+ from NeuNorm._utilities import get_sorted_list_images, average_df
15
+ from NeuNorm import DataType
16
+
17
+
18
+ class Normalization:
19
+ working_data_type = np.float32
20
+
21
+ def __init__(self):
22
+ self.shape = {"width": np.nan, "height": np.nan}
23
+ self.dict_image = {
24
+ "data": None,
25
+ "oscilation": None,
26
+ "file_name": None,
27
+ "metadata": None,
28
+ "shape": copy.deepcopy(self.shape),
29
+ }
30
+ self.dict_ob = {
31
+ "data": None,
32
+ "oscilation": None,
33
+ "metadata": None,
34
+ "file_name": None,
35
+ "data_mean": None,
36
+ "shape": copy.deepcopy(self.shape),
37
+ }
38
+ self.dict_df = {
39
+ "data": None,
40
+ "metadata": None,
41
+ "data_average": None,
42
+ "file_name": None,
43
+ "shape": copy.deepcopy(self.shape),
44
+ }
45
+
46
+ __roi_dict = {"x0": np.nan, "x1": np.nan, "y0": np.nan, "y1": np.nan}
47
+ self.roi = {
48
+ "normalization": copy.deepcopy(__roi_dict),
49
+ "crop": copy.deepcopy(__roi_dict),
50
+ }
51
+
52
+ self.__exec_process_status = {
53
+ "df_correction": False,
54
+ "normalization": False,
55
+ "crop": False,
56
+ "oscillation": False,
57
+ "bin": False,
58
+ }
59
+
60
+ self.data = {}
61
+ self.data["sample"] = self.dict_image
62
+ self.data["ob"] = self.dict_ob
63
+ self.data["df"] = self.dict_df
64
+ self.data["normalized"] = None
65
+ self.export_file_name = None
66
+
67
+ def load(
68
+ self,
69
+ file="",
70
+ folder="",
71
+ data=None,
72
+ data_type="sample",
73
+ auto_gamma_filter=True,
74
+ manual_gamma_filter=False,
75
+ notebook=False,
76
+ manual_gamma_threshold=0.1,
77
+ check_shape=True,
78
+ ):
79
+ """
80
+ Function to read individual files, entire files from folder, list of files or event data arrays.
81
+ Data are also gamma filtered if requested.
82
+
83
+ Parameters:
84
+ file: list - full path to a single file, or list of files
85
+ folder: string - full path to folder containing files to load
86
+ data: numpy array - 2D array of data to load
87
+ data_type: string - 'sample', 'ob' or 'df (default 'sample')
88
+ auto_gamma_filter: boolean - will correct the gamma filter automatically (highest count possible
89
+ for the data type will be replaced by the average of the 9 neighboring pixels) (default True)
90
+ manual_gamma_filter: boolean - apply or not gamma filtering to the data loaded (default False)
91
+ notebooks: boolean - turn on this option if you run the library from a
92
+ notebook to have a progress bar displayed showing you the progress of the loading (default False)
93
+ manual_gamma_threshold: float between 0 and 1 - manual gamma coefficient to use (default 0.1)
94
+
95
+ Warning:
96
+ Algorithm won't be allowed to run if any of the main algorithm have been run already, such as
97
+ oscillation, crop, binning, df_correction.
98
+
99
+ """
100
+
101
+ list_exec_flag = [_flag for _flag in self.__exec_process_status.values()]
102
+ if True in list_exec_flag:
103
+ raise IOError(
104
+ "Operation not allowed as you already worked on this data set!"
105
+ )
106
+
107
+ if not file == "":
108
+ if isinstance(file, str):
109
+ self.load_file(
110
+ file=file,
111
+ data_type=data_type,
112
+ auto_gamma_filter=auto_gamma_filter,
113
+ manual_gamma_filter=manual_gamma_filter,
114
+ manual_gamma_threshold=manual_gamma_threshold,
115
+ check_shape=check_shape,
116
+ )
117
+ elif isinstance(file, list):
118
+ # use tqdm to handle the progress bar
119
+ if notebook:
120
+ for _file in tqdm(file, desc=f"Loading {data_type}", leave=False):
121
+ self.load_file(
122
+ file=_file,
123
+ data_type=data_type,
124
+ auto_gamma_filter=auto_gamma_filter,
125
+ manual_gamma_filter=manual_gamma_filter,
126
+ manual_gamma_threshold=manual_gamma_threshold,
127
+ check_shape=check_shape,
128
+ )
129
+ else:
130
+ for _file in file:
131
+ self.load_file(
132
+ file=_file,
133
+ data_type=data_type,
134
+ auto_gamma_filter=auto_gamma_filter,
135
+ manual_gamma_filter=manual_gamma_filter,
136
+ manual_gamma_threshold=manual_gamma_threshold,
137
+ check_shape=check_shape,
138
+ )
139
+
140
+ elif not folder == "":
141
+ # load all files from folder
142
+ list_images = get_sorted_list_images(folder=folder)
143
+ # use tqdm to handle the progress bar
144
+ if notebook:
145
+ for _image in tqdm(
146
+ list_images, desc=f"Loading {data_type}", leave=False
147
+ ):
148
+ full_path_image = os.path.join(folder, _image)
149
+ self.load_file(
150
+ file=full_path_image,
151
+ data_type=data_type,
152
+ auto_gamma_filter=auto_gamma_filter,
153
+ manual_gamma_filter=manual_gamma_filter,
154
+ manual_gamma_threshold=manual_gamma_threshold,
155
+ check_shape=check_shape,
156
+ )
157
+ else:
158
+ for _image in list_images:
159
+ full_path_image = os.path.join(folder, _image)
160
+ self.load_file(
161
+ file=full_path_image,
162
+ data_type=data_type,
163
+ auto_gamma_filter=auto_gamma_filter,
164
+ manual_gamma_filter=manual_gamma_filter,
165
+ manual_gamma_threshold=manual_gamma_threshold,
166
+ check_shape=check_shape,
167
+ )
168
+
169
+ elif data is not None:
170
+ self.load_data(data=data, data_type=data_type, notebook=notebook)
171
+
172
+ def calculate_how_long_its_going_to_take(
173
+ self, index_we_are=-1, time_it_took_so_far=0, total_number_of_loop=1
174
+ ):
175
+ """Estimate how long the loading is going to take according to the time it already took to load the
176
+ first images.
177
+
178
+ Parameters:
179
+ index_we_are: int - index where we are in the list of files to load (default -1)
180
+ time_it_took_so_far: float - time it took so far to load the data (default 0)
181
+ total_number_of_loop: int - total number of files to load (default 1)
182
+
183
+ Returns:
184
+ string
185
+ """
186
+ time_per_loop = time_it_took_so_far / index_we_are
187
+ total_time_it_will_take = time_per_loop * total_number_of_loop
188
+ time_left = total_time_it_will_take - time_per_loop * index_we_are
189
+
190
+ # convert to nice format h mn and seconds
191
+ m, s = divmod(time_left, 60)
192
+ h, m = divmod(m, 60)
193
+
194
+ if h == 0:
195
+ if m == 0:
196
+ return "%02ds" % (s)
197
+ else:
198
+ return "%02dmn %02ds" % (m, s)
199
+ else:
200
+ return "%dh %02dmn %02ds" % (h, m, s)
201
+
202
+ def load_data(self, data=None, data_type="sample", notebook=False):
203
+ """Function to save the data already loaded as arrays
204
+
205
+ Paramters:
206
+ data: np array 2D or 3D
207
+ data_type: string - 'sample', 'ob' or 'df' (default 'sample')
208
+ notebook: boolean - turn on this option if you run the library from a
209
+ notebook to have a progress bar displayed showing you the progress of the loading (default False)
210
+ """
211
+ if len(np.shape(data)) > 2:
212
+ # use tqdm to handle the progress bar
213
+ if notebook:
214
+ for _data in tqdm(data, desc=f"Loading {data_type}", leave=False):
215
+ _data = _data.astype(self.working_data_type)
216
+ self.__load_individual_data(data=_data, data_type=data_type)
217
+ else:
218
+ for _data in data:
219
+ _data = _data.astype(self.working_data_type)
220
+ self.__load_individual_data(data=_data, data_type=data_type)
221
+
222
+ else:
223
+ data = data.astype(self.working_data_type)
224
+ self.__load_individual_data(data=data, data_type=data_type)
225
+
226
+ def __load_individual_data(self, data=None, data_type="sample"):
227
+ """method that loads the data one at a time
228
+
229
+ Parameters:
230
+ data: np array
231
+ data_type: string - 'data', 'ob' or 'df' (default 'sample')
232
+ """
233
+ if self.data[data_type]["data"] is None:
234
+ self.data[data_type]["data"] = [data]
235
+ else:
236
+ self.data[data_type]["data"].append(data)
237
+ index = len(self.data[data_type]["data"])
238
+ if self.data[data_type]["file_name"] is None:
239
+ self.data[data_type]["file_name"] = ["image_{:04}".format(index)]
240
+ else:
241
+ self.data[data_type]["file_name"].append("image_{:04}".format(index))
242
+ if self.data[data_type]["metadata"] is None:
243
+ self.data[data_type]["metadata"] = [""]
244
+ else:
245
+ self.data[data_type]["metadata"].append("")
246
+ self.save_or_check_shape(data=data, data_type=data_type)
247
+
248
+ def load_file(
249
+ self,
250
+ file="",
251
+ data_type="sample",
252
+ auto_gamma_filter=True,
253
+ manual_gamma_filter=False,
254
+ manual_gamma_threshold=0.1,
255
+ check_shape=True,
256
+ ):
257
+ """
258
+ Function to read data from the specified path, it can read FITS, TIFF and HDF.
259
+
260
+ Parameters
261
+ file : string - full path of the input file with his extension.
262
+ data_type: string - 'sample', 'df' or 'ob' (default 'sample')
263
+ manual_gamma_filter: boolean - apply or not gamma filtering (default False)
264
+ manual_gamma_threshold: float (between 0 and 1) - manual gamma threshold
265
+ auto_gamma_filter: boolean - flag to turn on or off the auto gamma filering (default True)
266
+
267
+ Raises:
268
+ OSError: if file does not exist
269
+ NotImplementedError: if file is HDF5
270
+ OSError: if any other any file format requested
271
+
272
+ """
273
+ my_file = Path(file)
274
+ if my_file.is_file():
275
+ metadata = {}
276
+ if file.lower().endswith(".fits"):
277
+ data = np.array(load_fits(my_file))
278
+ elif file.lower().endswith((".tiff", ".tif")):
279
+ [data, metadata] = load_tiff(my_file)
280
+ data = np.array(data)
281
+ elif file.lower().endswith(
282
+ (".hdf", ".h4", ".hdf4", ".he2", "h5", ".hdf5", ".he5")
283
+ ):
284
+ raise NotImplementedError
285
+ # data = np.array(load_hdf(my_file))
286
+ else:
287
+ raise OSError(
288
+ "file extension not yet implemented....Do it your own way!"
289
+ )
290
+
291
+ if auto_gamma_filter:
292
+ data = self._auto_gamma_filtering(data=data)
293
+ elif manual_gamma_filter:
294
+ data = self._manual_gamma_filtering(
295
+ data=data, manual_gamma_threshold=manual_gamma_threshold
296
+ )
297
+
298
+ data = np.squeeze(data)
299
+
300
+ if self.data[data_type]["data"] is None:
301
+ self.data[data_type]["data"] = [data]
302
+ else:
303
+ self.data[data_type]["data"].append(data)
304
+
305
+ if self.data[data_type]["metadata"] is None:
306
+ self.data[data_type]["metadata"] = [metadata]
307
+ else:
308
+ self.data[data_type]["metadata"].append(metadata)
309
+
310
+ if self.data[data_type]["file_name"] is None:
311
+ self.data[data_type]["file_name"] = [file]
312
+ else:
313
+ self.data[data_type]["file_name"].append(file)
314
+
315
+ if check_shape:
316
+ self.save_or_check_shape(data=data, data_type=data_type)
317
+
318
+ else:
319
+ raise OSError("The file name does not exist")
320
+
321
+ def _auto_gamma_filtering(self, data=None):
322
+ """perform the automatic gamma filtering
323
+
324
+ This algorithm check the data format of the input data file (ex: int16, int32...)
325
+ and will determine the maxixum value for this data type. Any pixel that have a value
326
+ above the max value - 5 (just to give it a little bit of range) will be considered as
327
+ being gamma pixels. Those pixels will be replaced by the average value of the 8 pixels
328
+ surrounding this pixel
329
+
330
+ Parameters:
331
+ data: np array
332
+
333
+ Returns:
334
+ np array of the data cleaned
335
+
336
+ Raises:
337
+ ValueError if array is empty
338
+ """
339
+ if data is None:
340
+ raise ValueError("Data array is empty!")
341
+
342
+ # we may be dealing with a float time, that means it does not need any gamma filtering
343
+
344
+ try:
345
+ data_type = data.dtype
346
+ if data_type in [float, "float32"]:
347
+ max = np.finfo(data_type).max
348
+ else:
349
+ max = np.iinfo(data.dtype).max
350
+ except Exception as error:
351
+ logging.warning(f"Use default max value for data type: {error}")
352
+ return data
353
+
354
+ manual_gamma_threshold = max - 5
355
+ new_data = np.array(data, self.working_data_type)
356
+
357
+ data_gamma_filtered = np.copy(new_data)
358
+ gamma_indexes = np.where(new_data > manual_gamma_threshold)
359
+
360
+ mean_kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]]) / 8.0
361
+ convolved_data = convolve(data_gamma_filtered, mean_kernel, mode="constant")
362
+
363
+ data_gamma_filtered[gamma_indexes] = convolved_data[gamma_indexes]
364
+
365
+ return data_gamma_filtered
366
+
367
+ def _manual_gamma_filtering(self, data=None, manual_gamma_threshold=0.1):
368
+ """perform manual gamma filtering on the data
369
+
370
+ This algoritm uses the manual_gamma_threshold value to estimate if a pixel is a gamma or not.
371
+ 1. mean value of data array is calculated
372
+ 2. pixel is considered gamma if its value times the manual gamma threshold is bigger than the mean value
373
+ 3. if pixel is gamma, its value is replaced by the mean value of the 8 pixels surrounding it.
374
+
375
+ Parameters:
376
+ data: numpy 2D array
377
+ manual_gamma_threshold: float - coefficient between 0 and 1 used to estimate the threshold of the
378
+ gamma pixels (default 0.1)
379
+
380
+ Returns:
381
+ numpy 2D array
382
+
383
+ Raises:
384
+ ValueError if data is empty
385
+ """
386
+ if data is None:
387
+ raise ValueError("Data array is empty!")
388
+
389
+ data_gamma_filtered = np.copy(data)
390
+ mean_counts = np.mean(data_gamma_filtered)
391
+ gamma_indexes = np.where(
392
+ manual_gamma_threshold * data_gamma_filtered > mean_counts
393
+ )
394
+
395
+ mean_kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]]) / 8.0
396
+ convolved_data = convolve(data_gamma_filtered, mean_kernel, mode="constant")
397
+
398
+ data_gamma_filtered[gamma_indexes] = convolved_data[gamma_indexes]
399
+
400
+ return data_gamma_filtered
401
+
402
+ def save_or_check_shape(self, data=None, data_type="sample"):
403
+ """save the shape for the first data loaded (of each type) otherwise
404
+ check if the size match
405
+
406
+ Parameters:
407
+ data: np array of the data to check or save shape (default [])
408
+ data_type: string - 'ob', 'df' or 'sample' (default 'sample')
409
+
410
+ Raises:
411
+ IOError if size do not match
412
+ """
413
+ [height, width] = np.shape(data)
414
+ if np.isnan(self.data[data_type]["shape"]["height"]):
415
+ _shape = copy.deepcopy(self.shape)
416
+ _shape["height"] = height
417
+ _shape["width"] = width
418
+ self.data[data_type]["shape"] = _shape
419
+ else:
420
+ _prev_width = self.data[data_type]["shape"]["width"]
421
+ _prev_height = self.data[data_type]["shape"]["height"]
422
+
423
+ if (not (_prev_width == width)) or (not (_prev_height == height)):
424
+ raise IOError(
425
+ "Shape of {} do not match previous loaded data set!".format(
426
+ data_type
427
+ )
428
+ )
429
+
430
+ def normalization(
431
+ self,
432
+ roi=None,
433
+ force=False,
434
+ force_mean_ob=False,
435
+ force_median_ob=False,
436
+ notebook=False,
437
+ use_only_sample=False,
438
+ ):
439
+ """normalization of the data
440
+
441
+ Parameters:
442
+ roi: ROI object or list of ROI objects - object defines the region of the sample and OB that have to match
443
+ in intensity
444
+ force: boolean - True will force the normalization to occur, even if it had been
445
+ run before with the same data set (default False)
446
+ notebook: boolean - turn on this option if you run the library from a
447
+ notebook to have a progress bar displayed showing you the progress of the loading (default False)
448
+ use_only_sample - turn on this option to normalize the sample data using the ROI on the sample. each pixel
449
+ counts will be divided by the average counts of all the ROI of the same image
450
+
451
+ Return:
452
+ True - status of the normalization (True if every went ok, this is mostly used for the unit test)
453
+
454
+ Raises:
455
+ IOError: if no sample loaded
456
+ IOError: if no OB loaded and use_only_sample if False
457
+ IOError: if use_only_sample is True and no ROI provided
458
+ IOError: if size of sample and OB do not match
459
+
460
+ """
461
+ if not force:
462
+ # does nothing if normalization has already been run
463
+ if self.__exec_process_status["normalization"]:
464
+ return
465
+ self.__exec_process_status["normalization"] = True
466
+
467
+ # make sure we loaded some sample data
468
+ if self.data["sample"]["data"] is None:
469
+ raise IOError("No normalization available as no data have been loaded")
470
+
471
+ # make sure we loaded some ob data
472
+ if not use_only_sample:
473
+ if self.data["ob"]["data"] is None:
474
+ raise IOError("No normalization available as no OB have been loaded")
475
+
476
+ # make sure the data loaded have the same size
477
+ if not self.data_loaded_have_matching_shape():
478
+ raise ValueError("Data loaded do not have the same shape!")
479
+
480
+ if notebook:
481
+ from ipywidgets import widgets
482
+ from IPython.display import display
483
+
484
+ # make sure, if provided, roi has the right type and fits into the images
485
+ b_list_roi = False
486
+
487
+ if not use_only_sample:
488
+ if roi:
489
+ b_list_roi = self.check_roi_format(roi)
490
+
491
+ if b_list_roi:
492
+ _sample_corrected_normalized = self.calculate_corrected_normalized(
493
+ data_type=DataType.sample, roi=roi
494
+ )
495
+ _ob_corrected_normalized = self.calculate_corrected_normalized(
496
+ data_type=DataType.ob, roi=roi
497
+ )
498
+
499
+ else:
500
+ _x0 = roi.x0
501
+ _y0 = roi.y0
502
+ _x1 = roi.x1
503
+ _y1 = roi.y1
504
+
505
+ _sample_corrected_normalized = [
506
+ _sample / np.mean(_sample[_y0 : _y1 + 1, _x0 : _x1 + 1])
507
+ for _sample in self.data["sample"]["data"]
508
+ ]
509
+ _ob_corrected_normalized = [
510
+ _ob / np.mean(_ob[_y0 : _y1 + 1, _x0 : _x1 + 1])
511
+ for _ob in self.data["ob"]["data"]
512
+ ]
513
+
514
+ else:
515
+ _sample_corrected_normalized = copy.deepcopy(
516
+ self.data["sample"]["data"]
517
+ )
518
+ _ob_corrected_normalized = copy.deepcopy(self.data["ob"]["data"])
519
+
520
+ self.data[DataType.sample]["data"] = _sample_corrected_normalized
521
+ self.data[DataType.ob]["data"] = _ob_corrected_normalized
522
+
523
+ # if the number of sample and ob do not match, use mean or median of obs
524
+ nbr_sample = len(self.data["sample"]["file_name"])
525
+ nbr_ob = len(self.data["ob"]["file_name"])
526
+ if (
527
+ (nbr_sample != nbr_ob) or force_mean_ob or force_median_ob
528
+ ): # work with mean ob
529
+ if force_median_ob:
530
+ _ob_corrected_normalized = np.nanmedian(
531
+ _ob_corrected_normalized, axis=0
532
+ )
533
+ elif force_mean_ob:
534
+ _ob_corrected_normalized = np.nanmean(
535
+ _ob_corrected_normalized, axis=0
536
+ )
537
+ else:
538
+ _ob_corrected_normalized = np.nanmedian(
539
+ _ob_corrected_normalized, axis=0
540
+ )
541
+
542
+ self.data["ob"]["data_mean"] = _ob_corrected_normalized
543
+ _working_ob = copy.deepcopy(_ob_corrected_normalized)
544
+ _working_ob[_working_ob == 0] = np.nan
545
+
546
+ if notebook:
547
+ # turn on progress bar
548
+ _message = "Normalization"
549
+ box1 = widgets.HBox(
550
+ [
551
+ widgets.Label(_message, layout=widgets.Layout(width="20%")),
552
+ widgets.IntProgress(max=len(self.data["sample"]["data"])),
553
+ ]
554
+ )
555
+ display(box1)
556
+ w1 = box1.children[1]
557
+
558
+ normalized_data = []
559
+ for _index, _sample in enumerate(self.data["sample"]["data"]):
560
+ _norm = np.divide(_sample, _working_ob)
561
+ _norm[np.isnan(_norm)] = 0
562
+ _norm[np.isinf(_norm)] = 0
563
+ normalized_data.append(_norm)
564
+
565
+ if notebook:
566
+ w1.value = _index + 1
567
+
568
+ else: # 1 ob for each sample
569
+ # produce normalized data
570
+ sample_ob = zip(
571
+ self.data[DataType.sample]["data"], self.data[DataType.ob]["data"]
572
+ )
573
+
574
+ if notebook:
575
+ # turn on progress bar
576
+ _message = "Normalization"
577
+ box1 = widgets.HBox(
578
+ [
579
+ widgets.Label(_message, layout=widgets.Layout(width="20%")),
580
+ widgets.IntProgress(max=len(self.data["sample"]["data"])),
581
+ ]
582
+ )
583
+ display(box1)
584
+ w1 = box1.children[1]
585
+
586
+ normalized_data = []
587
+ for _index, [_sample, _ob] in enumerate(sample_ob):
588
+ _working_ob = copy.deepcopy(_ob)
589
+ _working_ob[_working_ob == 0] = np.nan
590
+ _norm = np.divide(_sample, _working_ob)
591
+ _norm[np.isnan(_norm)] = 0
592
+ _norm[np.isinf(_norm)] = 0
593
+ normalized_data.append(_norm)
594
+
595
+ if notebook:
596
+ w1.value = _index + 1
597
+
598
+ self.data["normalized"] = normalized_data
599
+
600
+ else: # use_sample_only with ROI
601
+ normalized_data = self.calculate_corrected_normalized_without_ob(roi=roi)
602
+ self.data["normalized"] = normalized_data
603
+
604
+ return True
605
+
606
+ def calculate_corrected_normalized_without_ob(self, roi=None):
607
+ if not roi:
608
+ raise ValueError(
609
+ "You need to provide at least 1 ROI using this use_only_sample mode!"
610
+ )
611
+
612
+ b_list_roi = self.check_roi_format(roi)
613
+
614
+ if b_list_roi:
615
+ normalized_data = []
616
+ for _sample in self.data["sample"]["data"]:
617
+ total_counts_of_rois = 0
618
+ total_number_of_pixels = 0
619
+ for _roi in roi:
620
+ _x0 = _roi.x0
621
+ _y0 = _roi.y0
622
+ _x1 = _roi.x1
623
+ _y1 = _roi.y1
624
+ total_number_of_pixels += (_y1 - _y0 + 1) * (_x1 - _x0 + 1)
625
+ total_counts_of_rois += np.sum(
626
+ _sample[_y0 : _y1 + 1, _x0 : _x1 + 1]
627
+ )
628
+
629
+ full_sample_mean = total_counts_of_rois / total_number_of_pixels
630
+ normalized_data.append(_sample / full_sample_mean)
631
+
632
+ else:
633
+ _x0 = roi.x0
634
+ _y0 = roi.y0
635
+ _x1 = roi.x1
636
+ _y1 = roi.y1
637
+
638
+ normalized_data = [
639
+ _sample / np.mean(_sample[_y0 : _y1 + 1, _x0 : _x1 + 1])
640
+ for _sample in self.data["sample"]["data"]
641
+ ]
642
+
643
+ return normalized_data
644
+
645
+ def calculate_corrected_normalized(self, data_type=DataType.sample, roi=None):
646
+ corrected_normalized = []
647
+ for _sample in self.data[data_type]["data"]:
648
+ total_counts_of_rois = 0
649
+ total_number_of_pixels = 0
650
+ for _roi in roi:
651
+ _x0 = _roi.x0
652
+ _y0 = _roi.y0
653
+ _x1 = _roi.x1
654
+ _y1 = _roi.y1
655
+ total_number_of_pixels += (_y1 - _y0 + 1) * (_x1 - _x0 + 1)
656
+ total_counts_of_rois += np.sum(_sample[_y0 : _y1 + 1, _x0 : _x1 + 1])
657
+
658
+ full_sample_mean = total_counts_of_rois / total_number_of_pixels
659
+ corrected_normalized.append(_sample / full_sample_mean)
660
+ return corrected_normalized
661
+
662
+ def check_roi_format(self, roi):
663
+ b_list_roi = False
664
+ if isinstance(roi, list):
665
+ for _roi in roi:
666
+ if not type(_roi) == ROI:
667
+ raise ValueError("roi must be a ROI object!")
668
+ if not self.__roi_fit_into_sample(roi=_roi):
669
+ raise ValueError("roi does not fit into sample image!")
670
+ b_list_roi = True
671
+
672
+ elif not type(roi) == ROI:
673
+ raise ValueError("roi must be a ROI object!")
674
+ else:
675
+ if not self.__roi_fit_into_sample(roi=roi):
676
+ raise ValueError("roi does not fit into sample image!")
677
+
678
+ return b_list_roi
679
+
680
+ def data_loaded_have_matching_shape(self):
681
+ """check that data loaded have the same shape
682
+
683
+ Returns:
684
+ bool: result of the check
685
+ """
686
+ _shape_sample = self.data["sample"]["shape"]
687
+ _shape_ob = self.data["ob"]["shape"]
688
+
689
+ if not (_shape_sample == _shape_ob):
690
+ return False
691
+
692
+ _shape_df = self.data["df"]["shape"]
693
+ if not np.isnan(_shape_df["height"]):
694
+ if not (_shape_sample == _shape_df):
695
+ return False
696
+
697
+ return True
698
+
699
+ def __roi_fit_into_sample(self, roi=None):
700
+ """check if roi is within the dimension of the image
701
+
702
+ Returns:
703
+ bool: True if roi is within the image dimension
704
+
705
+ """
706
+ [sample_height, sample_width] = np.shape(self.data["sample"]["data"][0])
707
+
708
+ [_x0, _y0, _x1, _y1] = [roi.x0, roi.y0, roi.x1, roi.y1]
709
+ if (_x0 < 0) or (_x1 >= sample_width):
710
+ return False
711
+
712
+ if (_y0 < 0) or (_y1 >= sample_height):
713
+ return False
714
+
715
+ return True
716
+
717
+ def df_correction(self, force=False):
718
+ """dark field correction of sample and ob
719
+
720
+ Parameters
721
+ force: boolean - that if True will force the df correction to occur, even if it had been
722
+ run before with the same data set (default False)
723
+
724
+ sample_df_corrected = sample - DF
725
+ ob_df_corrected = OB - DF
726
+
727
+ """
728
+ if not force:
729
+ if self.__exec_process_status["df_correction"]:
730
+ return
731
+ self.__exec_process_status["df_correction"] = True
732
+
733
+ if self.data["sample"]["data"] is not None:
734
+ self.__df_correction(data_type="sample")
735
+
736
+ if self.data["ob"]["data"] is not None:
737
+ self.__df_correction(data_type="ob")
738
+
739
+ def __df_correction(self, data_type="sample"):
740
+ """dark field correction
741
+
742
+ Parameters:
743
+ data_type: string ['sample','ob]
744
+
745
+ Raises:
746
+ KeyError: if data type is not 'sample' or 'ob'
747
+ IOError: if sample and df or ob and df do not have the same shape
748
+ """
749
+ if data_type not in ["sample", "ob"]:
750
+ raise KeyError("Wrong data type passed. Must be either 'sample' or 'ob'!")
751
+
752
+ if self.data["df"]["data"] is None:
753
+ return
754
+
755
+ if self.data["df"]["data_average"] is None:
756
+ _df = self.data["df"]["data"]
757
+ if len(_df) > 1:
758
+ _df = average_df(df=_df)
759
+ self.data["df"]["data_average"] = np.squeeze(_df)
760
+
761
+ else:
762
+ _df = np.squeeze(self.data["df"]["data_average"])
763
+
764
+ if np.shape(self.data[data_type]["data"][0]) != np.shape(
765
+ self.data["df"]["data"][0]
766
+ ):
767
+ raise IOError("{} and df data must have the same shape!".format(data_type))
768
+
769
+ _data_df_corrected = [_data - _df for _data in self.data[data_type]["data"]]
770
+ _data_df_corrected = [np.squeeze(_data) for _data in _data_df_corrected]
771
+ self.data[data_type]["data"] = _data_df_corrected
772
+
773
+ def crop(self, roi=None, force=False):
774
+ """Cropping the sample and ob normalized data
775
+
776
+ Parameters:
777
+ roi: ROI object that defines the region to crop
778
+ force: Boolean - that force or not the algorithm to be run more than once
779
+ with the same data set (default False)
780
+
781
+ Returns:
782
+ True (for unit test purpose)
783
+
784
+ Raises:
785
+ ValueError if sample and ob data have not been normalized yet
786
+ """
787
+ if (self.data["sample"]["data"] is None) or (self.data["ob"]["data"] is None):
788
+ raise IOError("We need sample and ob Data !")
789
+
790
+ if not type(roi) == ROI:
791
+ raise ValueError("roi must be of type ROI")
792
+
793
+ if not force:
794
+ if self.__exec_process_status["crop"]:
795
+ return
796
+ self.__exec_process_status["crop"] = True
797
+
798
+ _x0 = roi.x0
799
+ _y0 = roi.y0
800
+ _x1 = roi.x1
801
+ _y1 = roi.y1
802
+
803
+ new_sample = [
804
+ _data[_y0 : _y1 + 1, _x0 : _x1 + 1] for _data in self.data["sample"]["data"]
805
+ ]
806
+ self.data["sample"]["data"] = new_sample
807
+
808
+ new_ob = [
809
+ _data[_y0 : _y1 + 1, _x0 : _x1 + 1] for _data in self.data["ob"]["data"]
810
+ ]
811
+ self.data["ob"]["data"] = new_ob
812
+
813
+ if self.data["df"]["data"] is not None:
814
+ new_df = [
815
+ _data[_y0 : _y1 + 1, _x0 : _x1 + 1] for _data in self.data["df"]["data"]
816
+ ]
817
+ self.data["df"]["data"] = new_df
818
+
819
+ if self.data["normalized"] is not None:
820
+ new_normalized = [
821
+ _data[_y0 : _y1 + 1, _x0 : _x1 + 1] for _data in self.data["normalized"]
822
+ ]
823
+ self.data["normalized"] = new_normalized
824
+
825
+ return True
826
+
827
+ def export(self, folder="./", data_type="normalized", file_type="tif"):
828
+ """export all the data from the type specified into a folder
829
+
830
+ Parameters:
831
+ folder: String - where to create all the images. Folder must exist otherwise an error is
832
+ raised (default is './')
833
+ data_type: String - Must be one of the following 'sample','ob','df','normalized' (default is 'normalized').
834
+ file_type: String - format in which to export the data. Must be either 'tif' or 'fits' (default is 'tif')
835
+
836
+ Raises:
837
+ IOError if the folder does not exist
838
+ KeyError if data_type can not be found in the list ['normalized','sample','ob','df']
839
+
840
+ """
841
+ if not os.path.exists(folder):
842
+ raise IOError("Folder '{}' does not exist!".format(folder))
843
+
844
+ if data_type not in ["normalized", "sample", "ob", "df"]:
845
+ raise KeyError("data_type '{}' is wrong".format(data_type))
846
+
847
+ prefix = ""
848
+ if data_type == "normalized":
849
+ data = self.get_normalized_data()
850
+ prefix = "normalized"
851
+ data_type = "sample"
852
+ else:
853
+ data = self.data[data_type]["data"]
854
+
855
+ if data is None:
856
+ return False
857
+
858
+ metadata = self.data[data_type]["metadata"]
859
+
860
+ list_file_name_raw = self.data[data_type]["file_name"]
861
+ self.__create_list_file_names(
862
+ initial_list=list_file_name_raw,
863
+ output_folder=folder,
864
+ prefix=prefix,
865
+ suffix=file_type,
866
+ )
867
+
868
+ self.__export_data(
869
+ data=data,
870
+ metadata=metadata,
871
+ output_file_names=self._export_file_name,
872
+ suffix=file_type,
873
+ )
874
+
875
+ def __export_data(self, data=[], metadata=[], output_file_names=[], suffix="tif"):
876
+ """save the list of files with the data specified
877
+
878
+ Parameters:
879
+ data: numpy array that contains the array of data to save (default [])
880
+ output_file_names: numpy array of string of full file names (default [])
881
+ suffix: String - format in which the file will be created (default 'tif')
882
+ """
883
+ name_data_metadata_array = zip(output_file_names, data, metadata)
884
+ for _file_name, _data, _metadata in name_data_metadata_array:
885
+ if suffix in ["tif", "tiff"]:
886
+ make_tif(data=_data, metadata=_metadata, file_name=_file_name)
887
+ elif suffix == "fits":
888
+ make_fits(data=_data, file_name=_file_name)
889
+
890
+ def __create_list_file_names(
891
+ self, initial_list=[], output_folder="", prefix="", suffix=""
892
+ ):
893
+ """create a list of the new file name used to export the images
894
+
895
+ Parameters:
896
+ initial_list: array of full file name
897
+ ex: ['/users/me/image001.tif',/users/me/image002.tif',/users/me/image003.tif']
898
+ output_folder: String (default is ./ as specified by calling function) where we want to create the data
899
+ prefix: String. what to add to the output file name in front of base name
900
+ ex: 'normalized' will produce 'normalized_image001.tif'
901
+ suffix: String. extension to file. 'tif' for TIFF and 'fits' for FITS
902
+ """
903
+ _base_name = [os.path.basename(_file) for _file in initial_list]
904
+ _raw_name = [os.path.splitext(_file)[0] for _file in _base_name]
905
+ _prefix = ""
906
+ if prefix:
907
+ _prefix = prefix + "_"
908
+ full_file_names = [
909
+ os.path.join(output_folder, _prefix + _file + "." + suffix)
910
+ for _file in _raw_name
911
+ ]
912
+ self._export_file_name = full_file_names
913
+
914
+ def get_normalized_data(self):
915
+ """return the normalized data"""
916
+ return self.data["normalized"]
917
+
918
+ def get_sample_data(self):
919
+ """return the sample data"""
920
+ return self.data["sample"]["data"]
921
+
922
+ def get_ob_data(self):
923
+ """return the ob data"""
924
+ return self.data["ob"]["data"]
925
+
926
+ def get_df_data(self):
927
+ """return the df data"""
928
+ return self.data["df"]["data"]
NeuNorm/roi.py ADDED
@@ -0,0 +1,35 @@
1
+ import numpy as np
2
+
3
+
4
+ class ROI(object):
5
+ """class that list the type of ROI available"""
6
+
7
+ x0 = np.nan
8
+ y0 = np.nan
9
+ x1 = np.nan
10
+ y1 = np.nan
11
+
12
+ def __init__(
13
+ self, x0=np.nan, y0=np.nan, x1=np.nan, y1=np.nan, width=np.nan, height=np.nan
14
+ ):
15
+ if np.isnan(x0) or np.isnan(y0):
16
+ raise ValueError("x0 and y0 must be provided!")
17
+
18
+ self.x0 = x0
19
+ self.y0 = y0
20
+
21
+ if not np.isnan(y1):
22
+ self.y1 = np.max([y0, y1])
23
+ self.y0 = np.min([y0, y1])
24
+ elif not np.isnan(height):
25
+ self.y1 = y0 + height
26
+ else:
27
+ raise ValueError("You must defined either y1 or height!")
28
+
29
+ if not np.isnan(x1):
30
+ self.x1 = np.max([x0, x1])
31
+ self.x0 = np.min([x0, x1])
32
+ elif not np.isnan(width):
33
+ self.x1 = x0 + width
34
+ else:
35
+ raise ValueError("you must defined either x1 or width!")
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2017, Oak Ridge National Laboratory
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.2
2
+ Name: NeuNorm
3
+ Version: 1.6.12
4
+ Summary: neutron normalization data
5
+ Author-email: Jean Bilheux <bilheuxjm@ornl.gov>
6
+ Maintainer-email: Jean Bilheux <bilheuxjm@ornl.gov>, Chen Zhang <zhangc@ornl.gov>
7
+ License: BSD-3-Clause license
8
+ Project-URL: homepage, https://github.com/neutrons/python_project_template/
9
+ Keywords: neutron,normalization,imaging
10
+ Requires-Python: >=3.10
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pillow
14
+ Requires-Dist: pathlib
15
+ Requires-Dist: astropy
16
+ Requires-Dist: scipy
@@ -0,0 +1,12 @@
1
+ NeuNorm/__init__.py,sha256=4g6jYEZo5zwXqV609fsLeEAtOWWP3I4synH03O2pApA,247
2
+ NeuNorm/_utilities.py,sha256=DE0X6FhIFqfIoA6EQwx_KKNV0gqjAA-OlkpASCG74y4,740
3
+ NeuNorm/_version.py,sha256=1ThuVeMS5KlxvVTyorbbzJi1pftu6GNdFZ7WRJWlGVc,23
4
+ NeuNorm/exporter.py,sha256=AYcGRmeM6wEdxRGhUD0kEZFTsdSzazZR8x4sVP4R2q4,332
5
+ NeuNorm/loader.py,sha256=kyApFIk-D4Hf4B9D7_hRjuO8tptgwKT7THhh4ewqCYc,1143
6
+ NeuNorm/normalization.py,sha256=ib_D5jY04qZb64gUNcsmheAwUU2g3vbiNWLnlOzbfqA,35528
7
+ NeuNorm/roi.py,sha256=lcaTnH8kq5UmoXm7zGJOba7qzsanPj0zPI4rh12xL2M,932
8
+ neunorm-1.6.12.dist-info/LICENSE,sha256=nT1_bF7wnQfD8Obx20W8Umce2Fc03aPyZRG1rL6rgR0,1529
9
+ neunorm-1.6.12.dist-info/METADATA,sha256=Yx5uKuzT6HJb2FsZP3z9hT8feS4T692dVPnPMyMw0Sw,520
10
+ neunorm-1.6.12.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
11
+ neunorm-1.6.12.dist-info/top_level.txt,sha256=zwenKNgm2fVtus0Segjs7dVG4zz9ENAbhRvn8o9bvko,8
12
+ neunorm-1.6.12.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ NeuNorm