import scipp as sc
from easyimaging import Measurement
from easyimaging.datasets import iron_alpha_scitiff
# If you use your own file, simply specify the path like: 'some_path/some_file.tiff'
scitiff_path = iron_alpha_scitiff()
Downloading file 'iron_alpha_scitiff/iron_alpha_v1.tiff' from 'https://raw.githubusercontent.com/easyscience/imaging/refs/heads/master/data/iron_alpha_scitiff/iron_alpha_v1.tiff' to '/home/runner/.cache/easyimaging'.
The Measurement class can be created in 3 different ways. The preffered way is by loading a scitiff which holds all the neccesary meta-data, using the from_scitiff method.
measurement = Measurement.from_scitiff(filename=scitiff_path)
Alternatively, the Measurement class can also be created using a regular Tiff stack, using the from_tiff_stack method, and supplying the list of time-of-flights for each frame in the Tiff stack and optionally the x- and y-positions of the pixels.
from easyimaging.datasets import iron_alpha_tiff
tiff_path = iron_alpha_tiff()
Downloading file 'iron_alpha_tiff/iron_alpha_v1.tiff' from 'https://raw.githubusercontent.com/easyscience/imaging/refs/heads/master/data/iron_alpha_tiff/iron_alpha_v1.tiff' to '/home/runner/.cache/easyimaging'.
measurement = Measurement.from_tiff_stack(
filename=tiff_path,
time_of_flights=sc.arange('t', 0, 1501, 1, unit='s'), # mandatory
# x_positions=sc.arange('x', 0, 50, 1, unit='mm'), # optional
# y_positions=sc.arange('y', 0, 50, 1, unit='mm'), # optional
)
The x_positions and y_positions of the pixels can also be set or overwritten later with the method set_physical_coord_positions
measurement.set_physical_coord_positions(
x_positions=sc.arange('x', -8, 8, 1, unit='cm'),
y_positions=sc.arange('y', -8, 8, 1, unit='cm'),
)
Finally the Measurement class can also be created by directly passing an appropriate scipp DataArray. This is useful if working in the same notebook as the reduction is performed. Please be aware that there are strict requirements on the format, coordinates and meta-data of this DataArray. Here is an example:
tof = sc.arange('t', 0, 10, 1, unit='s')
x = sc.arange('x', 0, 7, 1, unit='m') # optional
y = sc.arange('y', 0, 7, 1, unit='m') # optional
data = sc.ones(dims=['x', 'y', 't'], shape=[6, 6, 10])
scipp_array = sc.DataArray(data=data, coords={'tof': tof, 'x': x, 'y': y})
Note that the x and y coordinates, corresponding to the previous x_positions and y_positions, is 1 element longer than the data. That is because we work with bin-edges rathar than bin-centers in the Measurement class. You can also provide a bin-center list, but it will internally be converted to bin-edges assuming even bin-sizes.
Another point to note is that the time-of-flight coordinate is called 'tof' but it has to have the dimension 't'.
With this correct DataArray, we can then construct the Measurement class
measurement = Measurement(data_array=scipp_array)
To continue to show off the Measurement class, we will use the preffered creation method, using the scitiff file.
measurement = Measurement.from_scitiff(filename=scitiff_path)
The Measurements x_positions, y_positions and time_of_flights can all be viewed
display(measurement.x_positions) # If available
display(measurement.y_positions) # If available
display(measurement.time_of_flights)
- (x: 17)float32mm-7.0382814, -6.158496, ..., 6.158496, 7.0382814
Values:
array([-7.0382814 , -6.158496 , -5.2787113 , -4.398926 , -3.5191407 , -2.6393557 , -1.7595701 , -0.87978506, 0. , 0.87978506, 1.7595701 , 2.6393552 , 3.5191412 , 4.3989263 , 5.2787113 , 6.158496 , 7.0382814 ], dtype=float32)
- (y: 17)float32mm-7.0382814, -6.158496, ..., 6.158496, 7.0382814
Values:
array([-7.0382814 , -6.158496 , -5.2787113 , -4.398926 , -3.5191407 , -2.6393557 , -1.7595701 , -0.87978506, 0. , 0.87978506, 1.7595701 , 2.6393552 , 3.5191412 , 4.3989263 , 5.2787113 , 6.158496 , 7.0382814 ], dtype=float32)
- (t: 1501)float64s0.022, 0.022, ..., 0.092, 0.093
Values:
array([0.02158728, 0.02163456, 0.02168185, ..., 0.09242233, 0.09246961, 0.0925169 ], shape=(1501,))
The Measurement class has a number of plotting methods relying on the underlying plopp library from scipp. The simplest plot is a plot of the Tiff stack
measurement.plot()
By default, this plot is summed over the time-of-flight frames, but a specific time-of-flight can also be plotted
measurement.plot(time_of_flight=0)
Pixels with invalid data values, like infinity or NaN, will appear as red.
For further customization of the plot, see https://scipp.github.io/plopp/plotting/image-plot.html and https://scipp.github.io/plopp/generated/plopp.plot.html. Any of the arguments supported here is also supported in the plot method.
In case of invalid data-points, it can often be beneficial to rebin the data. When rebinning, invalid data-points are ignored when pixels are averaged, yielding clean data-sets without errenous data-points. EasyImaging only supports rebinning by divisible whole integers, to avoid having to "guess" how much of a transmission belongs to each bin.
measurement.rebin(dimensions={'x': 2, 'y': 2})
measurement.plot(time_of_flight=0)
Further rebinning will rebin the reduced data-set again, yielding an even rougher binning. Rebinning can be reverted and the original data recovered by using the revert_rebin method
measurement.revert_rebin()
A more useful plotter to inspect the different time-of-flight frames is the slicer_plot, which is only available in Jupyter notebooks or similar environments with an interactive graphical backend. The slicer_plot has a time-of-flight slider to choose the desired frame to plot.
To run it, first add the interactive matplotlib widget to your Jupyter notebook:
%matplotlib widget
Then the slicer plot can be called
measurement.slicer_plot()
Just like with the regular plot method, any arguments accepted by the underlying plopp backend is supported to further customize the plot, see https://scipp.github.io/plopp/plotting/slicer-plot.html and https://scipp.github.io/plopp/generated/plopp.slicer.html for supported arguments.
To get the time-of-flight spectrum, the Measurement provides the spectrum method.
measurement.spectrum()
- t: 1500
- Ltotal()int64m61
Values:
array(61) - c()stringintensities
Values:
'intensities' - tof(t [bin-edge])float64s0.022, 0.022, ..., 0.092, 0.093
Values:
array([0.02158728, 0.02163456, 0.02168185, ..., 0.09242233, 0.09246961, 0.0925169 ], shape=(1501,)) - wavelength(t [bin-edge])float64Å1.4, 1.403, ..., 5.997, 6.0
Values:
array([1.4 , 1.40306667, 1.40613333, ..., 5.99386667, 5.99693333, 6. ], shape=(1501,))
- (t)float32𝟙0.5531428, 0.56375575, ..., 0.63517606, 0.63194895σ = 0.002992472, 0.003154994, ..., 0.006249289, 0.006158047
Values:
array([0.5531428 , 0.56375575, 0.5592331 , ..., 0.6236488 , 0.63517606, 0.63194895], shape=(1500,), dtype=float32)
Variances (σ²):
array([8.9548885e-06, 9.9539866e-06, 9.6888207e-06, ..., 3.6184254e-05, 3.9053615e-05, 3.7921542e-05], shape=(1500,), dtype=float32)
By default the spectrum method adds up all the pixels of the Measurement.
The method returns a scipp DataArray with uncertainties and time-of-flights.
It is also possible to plot the spectrum using the spectrum_plot method.
measurement.spectrum_plot()
Which also accepts plopp supported arguments for plot customization, see: https://scipp.github.io/plopp/plotting/line-plot.html and https://scipp.github.io/plopp/generated/plopp.plot.html
It is also possible to inspect the spectra of individual pixels using the spectrum_inspector plot method. This is also an interactive plot which is only supported in Jupyter notebooks and similar. In the plotted Measurement, individual pixels can be clicked (after selecting the point-clicker in the menu on the left) to show their spectrum in the spectrum plot underneath.
measurement.spectrum_inspector()
Where again the plot(s) can be customized, see https://scipp.github.io/plopp/plotting/inspector-plot.html and https://scipp.github.io/plopp/generated/plopp.inspector.html
It is also possible to define a region of interest (ROI) to provide the spectrum of a specific designated area. To do this, first the ROI has to be created
from easyimaging.regions_of_interest.rectangle_roi import RectangleROI
It is possible to define the ROI in code by hand, or draw it by hand (as shown further down)
roi = RectangleROI(
x_pixel_range=(15, 35), # mandatory
y_pixel_range=(15, 35), # mandatory
x_range=(sc.scalar(-2, unit='mm'), sc.scalar(2, unit='mm')), # optional
y_range=(sc.scalar(-2, unit='mm'), sc.scalar(2, unit='mm')), # optional
unique_name='center_roi', # custom name for the ROI, optional, has to be unique in computer memory
)
The ROI needs the x and y pixel ranges, as they are used as fallback for when the ROI is used on a Measurement without x_positions and y_positions. The x_range and y_range are optional physical ranges which are prioritized when used on a Measurement with x_positions and y_positions.
This ROI can then be passed to the spectrum method and the spectrum_plot method to only sum up the spectrum of this region.
measurement.spectrum(roi=roi)
- t: 1500
- Ltotal()int64m61
Values:
array(61) - c()stringintensities
Values:
'intensities' - tof(t [bin-edge])float64s0.022, 0.022, ..., 0.092, 0.093
Values:
array([0.02158728, 0.02163456, 0.02168185, ..., 0.09242233, 0.09246961, 0.0925169 ], shape=(1501,)) - wavelength(t [bin-edge])float64Å1.4, 1.403, ..., 5.997, 6.0
Values:
array([1.4 , 1.40306667, 1.40613333, ..., 5.99386667, 5.99693333, 6. ], shape=(1501,))
- (t)float32𝟙0.36840674, 0.3425866, ..., 0.46642005, 0.45523798σ = 0.0058153444, 0.0056076236, ..., 0.013006445, 0.012870439
Values:
array([0.36840674, 0.3425866 , 0.35694158, ..., 0.43976402, 0.46642005, 0.45523798], shape=(1500,), dtype=float32)
Variances (σ²):
array([3.3818233e-05, 3.1445445e-05, 3.3600576e-05, ..., 1.4671838e-04, 1.6916762e-04, 1.6564821e-04], shape=(1500,), dtype=float32)
measurement.spectrum_plot(roi=roi)
In general, an ROI should be added to a Measurement class to be used later in fitting
measurement.regions_of_interest.append(roi)
When the ROI is added to a Measurement, it can also be used in spectrum and spectrum_plot methods by simply passing its unique name
measurement.spectrum_plot(roi='center_roi')
As prevously promised, the ROI can also be drawn easily by hand, using the roi_creator tool. After opening the tool, click the bottom option in the control panel on the left to activate the ROI drawing tool.
Controls when ROI drawing tool is activated:
- Left-click to make new ROI's
- Left-click and hold on ROI vertices to resize ROI's
- Right-click and hold to drag/move the entire ROI
- Middle-click to delete an ROI
measurement.roi_creator()
The ROI's drawn with the roi_creator tool will appear in the Measurements list of regions of interest with default unique_names.
measurement.regions_of_interest
EasyList of length 1 of type(s) [<class 'easyimaging.regions_of_interest.rectangle_roi.RectangleROI'>]
To easier identify specific ROI's, rename ROI's with memorable unique_names. Be aware that each unique_name can only exist once in memory, if a desired unique_name is currently occupied by another object, you might have to restart the notebook to clear the memory.
measurement.regions_of_interest[1].unique_name = 'best_roi' # Requires that an ROI was drawn using the roi_creator tool.