Skip to content

Commit

Permalink
Merge pull request #3346 from ericpre/add_example_converting_color_image
Browse files Browse the repository at this point in the history
Add example converting color image and fix navigator event disconnection
  • Loading branch information
jlaehne committed Apr 12, 2024
2 parents e5d0fe5 + a3b7a6c commit d30b15d
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -31,6 +31,7 @@ setup/windows/requires/*
setup/windows/reccomends/*
doc/auto_examples
examples/io/*.msa
examples/io/rgb*.jpg
doc/gh-pages
doc/log.txt
*Thumbs.db
Expand Down
81 changes: 81 additions & 0 deletions examples/io/convert_color_image.py
@@ -0,0 +1,81 @@
"""
Adjust contrast and save RGB images
===================================
This example shows how to adjust the contrast and intensities using
scikit-image and save it as an RGB image.
When saving an RGB image to ``jpg``, only 8 bits are supported and the image
intensity needs to be rescaled to 0-255 before converting to 8 bits,
otherwise, the intensities will be cropped at the value of 255.
"""

import hyperspy.api as hs
import numpy as np
import skimage as ski

#%%
#
# Adjust contrast
# ###############
#
# In hyperspy, color images are defined as Signal1D with the signal dimension
# corresponding to the color channel (red, green and blue)

# The dtype can be changed to a custom dtype, which is convenient to visualise
# the color image
s = hs.signals.Signal1D(ski.data.astronaut())
s.change_dtype("rgb8")
print(s)

#%%
# Display the color image
s.plot()

#%%
# Processing is usually performed on standard dtype (e.g. ``uint8``, ``uint16``), because
# most functions from scikit-image, numpy, scipy, etc. only support standard ``dtype``.
# Convert from RGB to unsigned integer 16 bits
s.change_dtype("uint8")
print(s)

#%%
# Adjust contrast (gamma correction)
s.data = ski.exposure.adjust_gamma(s.data, gamma=0.2)

#%%
#
# Save to ``jpg``
# ###############
#
# Change dtype back to custom dtype ``rgb8``
s.change_dtype("rgb8")

#%%
# Save as jpg
s.save("rgb8_image.jpg", overwrite=True)


#%%
#
# Save ``rgb16`` image to ``jpg``
# ###############################
#
# The last part of this example shows how to save ``rgb16`` to a ``jpg`` file
#
# Create a signal with ``rgb16`` dtype
s2 = hs.signals.Signal1D(ski.data.astronaut().astype("uint16") * 100)

#%%
# To save a color image to ``jpg``, the signal needs to be converted to ``rgb8`` because
# ``jpg`` only support 8-bit RGB
# Rescale intensity to fit the unsigned integer 8 bits (2**8 = 256 intensity level)
s2.data = ski.exposure.rescale_intensity(s2.data, out_range=(0, 255))

#%%
# Now that the values have been rescaled to the 0-255 range, we can convert the data type
# to unsigned integer 8 bit and then ``rgb8`` to be able to save the RGB image in ``jpg`` format
s2.change_dtype("uint8")
s2.change_dtype("rgb8")
s2.save("rgb16_image_saved_as_jpg.jpg", overwrite=True)
33 changes: 33 additions & 0 deletions hyperspy/signal.py
Expand Up @@ -3075,6 +3075,10 @@ def get_dynamic_explorer_wrapper(*args, **kwargs):
else:
return navigator(as_numpy=True)

# function to disconnect when closing the navigator
function_to_disconnect = None
connected_event_copy = self.events.data_changed.connected.copy()

if not isinstance(navigator, BaseSignal) and navigator == "auto":
if self.navigator is not None:
navigator = self.navigator
Expand All @@ -3100,6 +3104,11 @@ def get_dynamic_explorer_wrapper(*args, **kwargs):
s=self,
axis=self.axes_manager.signal_axes,
)
# Sets are not ordered, to retrieve the function to disconnect
# take the difference with the previous copy
function_to_disconnect = list(
self.events.data_changed.connected - connected_event_copy
)[0]
if navigator.axes_manager.navigation_dimension == 1:
navigator = interactive(
f=navigator.as_signal1D,
Expand Down Expand Up @@ -3178,6 +3187,7 @@ def is_shape_compatible(navigation_shape, shape):
self._plot.plot(**kwargs)
self.events.data_changed.connect(self.update_plot, [])

# Disconnect event when closing signal
p = (
self._plot.signal_plot
if self._plot.signal_plot
Expand All @@ -3186,6 +3196,17 @@ def is_shape_compatible(navigation_shape, shape):
p.events.closed.connect(
lambda: self.events.data_changed.disconnect(self.update_plot), []
)
# Disconnect events to the navigator when closing navigator
if function_to_disconnect is not None:
self._plot.navigator_plot.events.closed.connect(
lambda: self.events.data_changed.disconnect(function_to_disconnect), []
)
self._plot.navigator_plot.events.closed.connect(
lambda: self.axes_manager.events.any_axis_changed.disconnect(
function_to_disconnect
),
[],
)

if plot_markers:
if self.metadata.has_item("Markers"):
Expand Down Expand Up @@ -5677,10 +5698,16 @@ def change_dtype(self, dtype, rechunk=False):
"Only signals with dtype uint16 can be converted to "
"rgb16 images"
)
replot = self._plot is not None and self._plot.is_active
if replot:
# Close the figure to avoid error with events
self._plot.close()
self.data = rgb_tools.regular_array2rgbx(self.data)
self.axes_manager.remove(-1)
self.axes_manager._set_signal_dimension(2)
self._assign_subclass()
if replot:
self.plot()
return
else:
dtype = np.dtype(dtype)
Expand All @@ -5689,6 +5716,10 @@ def change_dtype(self, dtype, rechunk=False):

if ddtype != dtype:
raise ValueError("It is only possibile to change to %s." % ddtype)
replot = self._plot is not None and self._plot.is_active
if replot:
# Close the figure to avoid error with events
self._plot.close()
self.data = rgb_tools.rgbx2regular_array(self.data)
self.axes_manager._append_axis(
size=self.data.shape[-1],
Expand All @@ -5699,6 +5730,8 @@ def change_dtype(self, dtype, rechunk=False):
)
self.axes_manager._set_signal_dimension(1)
self._assign_subclass()
if replot:
self.plot()
return
else:
self.data = self.data.astype(dtype)
Expand Down
18 changes: 18 additions & 0 deletions hyperspy/tests/drawing/test_plot_signal.py
Expand Up @@ -136,6 +136,24 @@ def test_plot_data_changed_event(sdim):
return plt.gcf()


@pytest.mark.parametrize("ndim", [0, 1, 2])
@pytest.mark.parametrize("sdim", [1, 2])
def test_plot_event_close(sdim, ndim):
# check that the events are correctly disconnected after closing
dim = sdim + ndim
nav_event = 1 if sdim + ndim >= 3 else 0
data = np.arange(5**dim).reshape((5,) * dim)
s = hs.signals.Signal1D(data) if sdim == 1 else hs.signals.Signal2D(data)
assert len(s.events.data_changed.connected) == 0
assert len(s.axes_manager.events.any_axis_changed.connected) == 0
s.plot()
assert len(s.events.data_changed.connected) == 1 + nav_event
assert len(s.axes_manager.events.any_axis_changed.connected) == nav_event
s._plot.close()
assert len(s.events.data_changed.connected) == 0
assert len(s.axes_manager.events.any_axis_changed.connected) == 0


def _get_figure(test_plot, data_type, plot_type):
if plot_type == "sig":
plot = "signal_plot"
Expand Down
11 changes: 11 additions & 0 deletions hyperspy/tests/signals/test_rgb.py
Expand Up @@ -56,6 +56,17 @@ def test_wrong_rgb(self):
with pytest.raises(AttributeError):
self.im.change_dtype("rgb8")

def test_change_dtype_with_plot(self):
# Check that changing dtype with a plot open
# doesn't raise any error
s = self.s
s.plot()
assert s._plot.navigator_plot is not None
s.change_dtype("rgba8")
assert s._plot.navigator_plot is None
s.change_dtype("uint8")
assert s._plot.navigator_plot is not None


class TestRGBA16:
def setup_method(self, method):
Expand Down
1 change: 1 addition & 0 deletions upcoming_changes/3346.bugfix.rst
@@ -0,0 +1 @@
Fix navigator event disconnection and fix plot when changing dtype from/to rbgx.
1 change: 1 addition & 0 deletions upcoming_changes/3346.enhancements.rst
@@ -0,0 +1 @@
Add example showing how to handle RGB images.

0 comments on commit d30b15d

Please sign in to comment.