Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vacuum mask eels #2183

Merged
merged 16 commits into from Sep 11, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions hyperspy/_signals/eds_tem.py
Expand Up @@ -437,6 +437,11 @@ def vacuum_mask(self, threshold=1.0, closing=True, opening=False):
opnening: bool
If true, applied a morphologic opening to the mask

Return
ericpre marked this conversation as resolved.
Show resolved Hide resolved
------
mask: signal
The mask of the region

Examples
--------
>>> # Simulate a spectrum image with vacuum region
Expand All @@ -447,11 +452,6 @@ def vacuum_mask(self, threshold=1.0, closing=True, opening=False):
>>> si = hs.stack([s]*3 + [s_vac])
>>> si.vacuum_mask().data
array([False, False, False, True], dtype=bool)

Return
------
mask: signal
The mask of the region
"""
from scipy.ndimage.morphology import binary_dilation, binary_erosion
mask = (self.max(-1) <= threshold)
Expand Down
116 changes: 82 additions & 34 deletions hyperspy/_signals/eels.py
Expand Up @@ -24,7 +24,7 @@
import traits.api as t
from scipy import constants

from hyperspy.signal import BaseSetMetadataItems
from hyperspy.signal import BaseSetMetadataItems, BaseSignal
from hyperspy._signals.signal1d import (Signal1D, LazySignal1D)
from hyperspy.misc.elements import elements as elements_db
import hyperspy.axes
Expand Down Expand Up @@ -163,10 +163,10 @@ def estimate_zero_loss_peak_centre(self, mask=None):

Parameters
----------
mask : Signal1D of bool data type.
mask : Signal1D of bool data type or bool array
It must have signal_dimension = 0 and navigation_shape equal to the
current signal. Where mask is True the shift is not computed
and set to nan.
navigation shape of the current signal. Where mask is True the
shift is not computed and set to nan.

Returns
-------
Expand All @@ -187,12 +187,14 @@ def estimate_zero_loss_peak_centre(self, mask=None):
"""
self._check_signal_dimension_equals_one()
self._check_navigation_mask(mask)
if isinstance(mask, BaseSignal):
mask = mask.data
zlpc = self.valuemax(-1)
if mask is not None:
if zlpc._lazy:
zlpc.data = da.where(mask.data, np.nan, zlpc.data)
zlpc.data = da.where(mask, np.nan, zlpc.data)
else:
zlpc.data[mask.data] = np.nan
zlpc.data[mask] = np.nan
zlpc.set_signal_type("")
title = self.metadata.General.title
zlpc.metadata.General.title = "ZLP(%s)" % title
Expand Down Expand Up @@ -232,10 +234,10 @@ def align_zero_loss_peak(
subpixel : bool
If True, perform the alignment with subpixel accuracy
using cross-correlation.
mask : Signal1D of bool data type.
It must have signal_dimension = 0 and navigation_shape equal to the
current signal. Where mask is True the shift is not computed
and set to nan.
mask : Signal1D of bool data type or bool array.
It must have signal_dimension = 0 and navigation_shape equal to
the shape of the current signal. Where mask is True the shift is
not computed and set to nan.
signal_range : tuple of integers, tuple of floats. Optional
Will only search for the ZLP within the signal_range. If given
in integers, the range will be in index values. If given floats,
Expand Down Expand Up @@ -788,21 +790,21 @@ def fourier_ratio_deconvolution(self, ll,
'after_fourier_ratio_deconvolution')
return cl

def richardson_lucy_deconvolution(self, psf, iterations=15, mask=None,
def richardson_lucy_deconvolution(self, psf, iterations=15,
ericpre marked this conversation as resolved.
Show resolved Hide resolved
show_progressbar=None,
parallel=None):
"""1D Richardson-Lucy Poissonian deconvolution of
the spectrum by the given kernel.

Parameters
----------
iterations: int
Number of iterations of the deconvolution. Note that
increasing the value will increase the noise amplification.
psf: EELSSpectrum
psf : EELSSpectrum
It must have the same signal dimension as the current
spectrum and a spatial dimension of 0 or the same as the
current spectrum.
iterations : int
Number of iterations of the deconvolution. Note that
increasing the value will increase the noise amplification.
%s
%s

Expand All @@ -818,8 +820,6 @@ def richardson_lucy_deconvolution(self, psf, iterations=15, mask=None,
show_progressbar = preferences.General.show_progressbar
self._check_signal_dimension_equals_one()
psf_size = psf.axes_manager.signal_axes[0].size
kernel = psf()
imax = kernel.argmax()
maxval = self.axes_manager.navigation_size
show_progressbar = show_progressbar and (maxval > 0)

Expand All @@ -833,6 +833,7 @@ def deconv_function(signal, kernel=None,
result *= np.convolve(kernel[::-1], signal /
first)[mimax:mimax + psf_size]
return result

ds = self.map(deconv_function, kernel=psf, iterations=iterations,
psf_size=psf_size, show_progressbar=show_progressbar,
parallel=parallel, ragged=False, inplace=False)
Expand Down Expand Up @@ -892,29 +893,30 @@ def set_microscope_parameters(self,
"Acquisition_instrument.TEM.Detector.EELS.collection_angle",
collection_angle)
set_microscope_parameters.__doc__ = \
"""
Set the microscope parameters that are necessary to calculate
the GOS.

If not all of them are defined, in interactive mode
raises an UI item to fill the values

beam_energy: float
The energy of the electron beam in keV
convengence_angle : float
The microscope convergence semi-angle in mrad.
collection_angle : float
The collection semi-angle in mrad.
{}
{}
""".format(TOOLKIT_DT, DISPLAY_DT)
"""
Set the microscope parameters that are necessary to calculate
the GOS. If not all of them are defined, in interactive mode
raises an UI item to fill the values.

Parameters
----------

beam_energy: float
The energy of the electron beam in keV.
convengence_angle : float
The microscope convergence semi-angle in mrad.
collection_angle : float
The collection semi-angle in mrad.
{}
{}
""".format(TOOLKIT_DT, DISPLAY_DT)

def power_law_extrapolation(self,
window_size=20,
extrapolation_size=1024,
add_noise=False,
fix_neg_r=False):
"""Extrapolate the spectrum to the right using a powerlaw
"""Extrapolate the spectrum to the right using a powerlaw.


Parameters
Expand Down Expand Up @@ -1365,6 +1367,52 @@ def rebin(self, new_shape=None, scale=None, crop=True, out=None):
return m
rebin.__doc__ = hyperspy.signal.BaseSignal.rebin.__doc__

def vacuum_mask(self, threshold=10.0, start_energy=None,
closing=True, opening=False):
"""
Generate mask of the vacuum region

Parameters
----------
threshold: float
For a given pixel, mean value in the energy axis below which the
ericpre marked this conversation as resolved.
Show resolved Hide resolved
pixel is considered as vacuum.
start_energy: float, None
Energy from which the intensity is averaged up to the end.
ericpre marked this conversation as resolved.
Show resolved Hide resolved
If None, set the value to 15.0 eV when the zero-loss peak and the
15.0 eV energy channel are in the spectrum, otherwise, take the
last quarter of the spectrum.
closing: bool
If true, applied a morphologic closing to the mask
ericpre marked this conversation as resolved.
Show resolved Hide resolved
opnening: bool
ericpre marked this conversation as resolved.
Show resolved Hide resolved
If true, applied a morphologic opening to the mask
ericpre marked this conversation as resolved.
Show resolved Hide resolved

Return
------
mask: signal
The mask of the region
ericpre marked this conversation as resolved.
Show resolved Hide resolved
"""
signal_axis = self.axes_manager.signal_axes[0]
if start_energy is None:
# zero-loss peak present
if signal_axis.low_value < 0.0:
start_energy = 15.0
if signal_axis.high_value < start_energy:
start_energy = 0.25 * signal_axis.high_value
else:
start_energy = 0.25 * signal_axis.high_value

mask = (self.isig[start_energy:].mean(-1) <= threshold)

from scipy.ndimage.morphology import binary_dilation, binary_erosion
if closing:
mask.data = binary_dilation(mask.data, border_value=0)
mask.data = binary_erosion(mask.data, border_value=1)
if opening:
mask.data = binary_erosion(mask.data, border_value=1)
mask.data = binary_dilation(mask.data, border_value=0)
return mask


class EELSSpectrum(EELSSpectrum_mixin, Signal1D):

Expand Down
15 changes: 9 additions & 6 deletions hyperspy/_signals/signal1d.py
Expand Up @@ -338,10 +338,10 @@ def spikes_removal_tool(self, signal_mask=None,

Parameters
----------
signal_mask : boolean array
signal_mask: boolean array or signal of bool
Restricts the operation to the signal locations not marked
as True (masked)
navigation_mask : boolean array
navigation_mask: boolean array or signal of bool
Restricts the operation to the navigation locations not
marked as True (masked)
%s
Expand All @@ -353,6 +353,12 @@ def spikes_removal_tool(self, signal_mask=None,

"""
self._check_signal_dimension_equals_one()
self._check_signal_mask(signal_mask)
if isinstance(signal_mask, BaseSignal):
signal_mask = signal_mask.data
self._check_navigation_mask(navigation_mask)
if isinstance(navigation_mask, BaseSignal):
navigation_mask = navigation_mask.data
sr = SpikesRemoval(self,
navigation_mask=navigation_mask,
signal_mask=signal_mask)
Expand Down Expand Up @@ -543,10 +549,7 @@ def _check_navigation_mask(self, mask):
elif mask.axes_manager.signal_dimension not in (0, 1):
raise ValueError("mask must be a BaseSignal "
"with signal_dimension equal to 1")
elif (mask.axes_manager.navigation_dimension !=
self.axes_manager.navigation_dimension):
raise ValueError("mask must be a BaseSignal with the same "
"navigation_dimension as the current signal.")
super()._check_navigation_mask(mask)

def estimate_shift1D(self,
start=None,
Expand Down
7 changes: 6 additions & 1 deletion hyperspy/learn/mva.py
Expand Up @@ -129,7 +129,7 @@ def decomposition(self,
auto_transpose : bool
If True, automatically transposes the data to boost performance.
Only has effect when using the svd of fast_svd algorithms.
navigation_mask : boolean numpy array
navigation_mask : boolean numpy array or BaseSignal
The navigation locations marked as True are not used in the
decompostion.
signal_mask : boolean numpy array
Expand Down Expand Up @@ -161,6 +161,8 @@ def decomposition(self,
plot_decomposition_factors, plot_decomposition_loadings, plot_lev

"""
from hyperspy.signal import BaseSignal

to_return = None
# Check if it is the wrong data type
if self.data.dtype.char not in ['e', 'f', 'd']: # If not float
Expand Down Expand Up @@ -201,6 +203,9 @@ def decomposition(self,
# Transform the data in a line spectrum
self._unfolded4decomposition = self.unfold()
try:
if isinstance(navigation_mask, BaseSignal):
self._check_navigation_mask(mask=navigation_mask)
navigation_mask = navigation_mask.data
tjof2 marked this conversation as resolved.
Show resolved Hide resolved
if hasattr(navigation_mask, 'ravel'):
navigation_mask = navigation_mask.ravel()

Expand Down
5 changes: 4 additions & 1 deletion hyperspy/misc/eds/utils.py
Expand Up @@ -363,7 +363,7 @@ def quantification_cliff_lorimer(intensities,
kfactors: list of float
The list of kfactor in same order as intensities eg. kfactors =
[1, 1.47, 1.72] for ['Al_Ka','Cr_Ka', 'Ni_Ka']
mask: array of bool
mask: array of bool of signal of bool
The mask with the dimension of intensities[0]. If a pixel is True,
the composition is set to zero.

Expand Down Expand Up @@ -391,6 +391,9 @@ def quantification_cliff_lorimer(intensities,
intens[index[0], i] = 1.
intens = intens.reshape(dim)
if mask is not None:
from hyperspy.signals import BaseSignal
if isinstance(mask, BaseSignal):
mask = mask.data
for i in range(dim[0]):
intens[i][mask] = 0
return intens
Expand Down
15 changes: 14 additions & 1 deletion hyperspy/signal.py
Expand Up @@ -3698,7 +3698,7 @@ def map(self, function,
choice is made while processing. None is not allowed for Lazy
signals!
keyword arguments : any valid keyword argument
All extra keyword arguments are passed to the
All extra keyword arguments are passed to the function.

Notes
-----
Expand Down Expand Up @@ -4920,6 +4920,19 @@ def T(self):
"""
return self.transpose()

def _check_navigation_mask(self, mask):
if mask is not None:
if (mask.axes_manager.navigation_shape !=
self.axes_manager.navigation_shape):
raise ValueError("mask must be a signal with the same "
"navigation shape as the current signal.")

def _check_signal_mask(self, mask):
if mask is not None:
if (mask.axes_manager.signal_shape !=
self.axes_manager.signal_shape):
raise ValueError("mask must be a signal with the same "
"signal shape as the current signal.")

ARITHMETIC_OPERATORS = (
"__add__",
Expand Down
27 changes: 27 additions & 0 deletions hyperspy/tests/signal/test_eels.py
Expand Up @@ -249,6 +249,7 @@ def test_running(self, extrapolate_lowloss):
extrapolate_lowloss=extrapolate_lowloss)


@lazifyTestClass
class TestRebin:
def setup_method(self, method):
# Create an empty spectrum
Expand Down Expand Up @@ -285,3 +286,29 @@ def test_offset_after_rebin(self):
assert s2.axes_manager[0].offset == 1.5
assert s2.axes_manager[1].offset == 2.5
assert s2.axes_manager[2].offset == s.axes_manager[2].offset


@lazifyTestClass
class TestVacuumMask:

def setup_method(self, method):
s = signals.EELSSpectrum(np.array([np.linspace(0.001, 0.5, 20)] * 100).T)
np.random.seed(0) # Same random every time
s.add_poissonian_noise()
s.inav[:10] += 20
self.signal = s

def test_vacuum_mask(self):
s = self.signal
assert not s.vacuum_mask().data[0]
assert not s.vacuum_mask().data[9]
assert s.vacuum_mask().data[10]
assert s.vacuum_mask().data[-1]

def test_vacuum_mask_threshold(self):
s = self.signal
assert s.vacuum_mask(threshold=20).data[0]
assert not s.vacuum_mask(threshold=20).data[1]
assert not s.vacuum_mask(threshold=20).data[9]
assert s.vacuum_mask(threshold=20).data[10]
assert s.vacuum_mask(threshold=20).data[-1]