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

Fix restoring intensity when showing a multi-channel image with deselected channels #576

Merged
merged 6 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
python -u -m magmap.tests.test_cv_nd
python -u -m magmap.tests.test_detector
python -u -m magmap.tests.test_libmag
python -u -m magmap.tests.test_np_io
# TODO: add UI testing
#python -u -m magmap.tests.test_visualizer
# TODO: add image artifacts
Expand Down
4 changes: 2 additions & 2 deletions docs/release/release_v1.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@
- "Merge" option in the ROI panel to merge channels using additive blending (#492, #552)
- "Blend" option in the image adjustment panel to visualize alignment in overlaid images (#89, #450)
- Image adjustment channels are radio buttons for easier selection (#212)
- Fixed synchronization between the ROI Editor and image adjustment controls after initialization (#142)
- Fixed synchronization with image adjustment controls (#142, #576)
- Fixed redundant triggers when adjusting the displayed image (#474)
- Fixed intensity sliders to cover the full range (#572)
- Fixed intensity sliders to cover the full range (#572, #576)
- Images are rotated by dynamic transformation (#214, #471, #505)
- Smoother, faster interactions with main plots, including atlas label name display, label editing, and pan and zoom navigation (#317, #335, #359, #367)
- Atlas labels adapt better in zoomed images to stay within each plot (#317)
Expand Down
26 changes: 22 additions & 4 deletions magmap/gui/plot_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from skimage import draw

from magmap.gui import image_viewer, pixel_display
from magmap.io import libmag
from magmap.io import libmag, np_io
from magmap.settings import config
from magmap.atlas import ontology
from magmap.plot import plot_support
Expand Down Expand Up @@ -512,7 +512,8 @@ def show_overview(self):
# with settings for each channel within this main image
imgs2d = [self._get_img2d(0, self.img3d, self.max_intens_proj)]
self._channels = [config.channel]
cmaps = [config.cmaps]
cmaps = [config.cmaps] # for all channels in image, even if not shown
# rest of settings are only for channels in config.channel
alphas = list(self.alpha_img3d)
alpha_is_default = True
alpha_blends = [None]
Expand All @@ -521,6 +522,7 @@ def show_overview(self):
vmins = [None]
brightnesses = [None]
contrasts = [None]

if self._plot_ax_imgs:
# use vmin/vmax from norm values in previously displayed images
# if available; None specifies auto-scaling
Expand Down Expand Up @@ -585,13 +587,29 @@ def show_overview(self):
shapes.append(self._img3d_shapes[imgi][1:3])
vmaxs.append(None)
vmins.append(None)


def make_abs_chls(vals):
# `overlay_images` requires channel-specific settings to be given
# for all channels of first (intensity) image in absolute rather
# than relative position
if libmag.is_seq(vals[0]):
# initialize for all channels and slot in channel-specific vals
vals_chls = [None] * nchls
for chl, v in zip(config.channel, vals[0]):
vals_chls[chl] = v
vals = list(vals)
vals[0] = vals_chls
return vals

# overlay all images and set labels for footer value on mouseover;
# if first time showing image, need to check for images with single
# value since they fail to update on subsequent updates for unclear
# reasons
nchls = np_io.get_num_channels(self.img3d, True)
ax_imgs = self.overlayer.overlay_images(
imgs2d, self._channels, cmaps, alphas, vmins, vmaxs,
imgs2d, self._channels, cmaps,
# expand for all intensity channels
make_abs_chls(alphas), make_abs_chls(vmins), make_abs_chls(vmaxs),
check_single=(self._ax_img_labels is None),
alpha_blends=alpha_blends)

Expand Down
49 changes: 2 additions & 47 deletions magmap/gui/visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,7 @@ class Visualization(HasTraits):
editor=EnumEditor(
name="object._imgadj_color_names.selections",
completion_mode="popup", evaluate=True)),
# use large range sliders to adjust min/max range
# use large range sliders to adjust min/max range and brightness
HGroup(
Item("_imgadj_min", label="Min", editor=RangeEditor(
low_name="_imgadj_min_low", high_name="_imgadj_min_high",
Expand All @@ -881,7 +881,7 @@ class Visualization(HasTraits):
HGroup(
Item("_imgadj_brightness", label="Brightness", editor=RangeEditor(
low_name="_imgadj_brightness_low",
high_name="_imgadj_brightness_high", mode="slider",
high_name="_imgadj_brightness_high", mode="xslider",
format_str="%.1g"))
),
HGroup(
Expand Down Expand Up @@ -1372,9 +1372,6 @@ def update_imgadj_for_img(self):
self._imgadj_contrast = plot_ax_img.contrast
self._imgadj_alpha = plot_ax_img.alpha

# populate intensity limits, auto-scaling, and current val (if not auto)
self._adapt_imgadj_limits(plot_ax_img)

if plot_ax_img.vmin is None:
self._imgadj_min_auto = True
else:
Expand All @@ -1390,51 +1387,10 @@ def update_imgadj_for_img(self):
# respond to triggers
self._imgadj_ignore_update = False

def _adapt_imgadj_limits(self, plot_ax_img: "plot_editor.PlotAxImg"):
"""Adapt image adjustment slider limits based on values in the
given plotted image.

Args:
plot_ax_img: Plotted image.

"""
if plot_ax_img is None: return
norm = plot_ax_img.ax_img.norm
input_img = plot_ax_img.input_img
inten_lim = (np.amin(input_img), np.amax(input_img))

if inten_lim[0] < self._imgadj_max_low:
# ensure that lower limit is beyond current plane's lower limit;
# bottom out at 0 unless current low is < 0
low_thresh = inten_lim[0] if norm.vmin is None else min(
inten_lim[0], norm.vmin)
low = 2 * low_thresh if low_thresh < 0 else 0
self._imgadj_min_low = low
self._imgadj_max_low = low

high = None
if inten_lim[1] > self._imgadj_max_high:
# ensure that upper limit is beyond current plane's limits;
# cap at 0 if current high is < 0 in case image is fully neg
high_thresh = inten_lim[1] if norm.vmax is None else max(
inten_lim[1], norm.vmax)
high = 2 * high_thresh if high_thresh > 0 else 0
elif (0 < inten_lim[1] < 0.1 * self._imgadj_max_high
and self._imgadj_max_high >= 0):
# reduce upper limit if current max is comparatively very small
high = 10 * inten_lim[1]
if high is not None:
# make brightness symmetric around upper limit
self._imgadj_min_high = high
self._imgadj_max_high = high
self._imgadj_brightness_low = -high
self._imgadj_brightness_high = high

def _set_inten_min_to_curr(self, plot_ax_img):
"""Set min intensity to current image value."""
if plot_ax_img is not None:
vmin = plot_ax_img.ax_img.norm.vmin
self._adapt_imgadj_limits(plot_ax_img)
if self._imgadj_min != vmin:
self._imgadj_min_ignore_update = True
self._imgadj_min = vmin
Expand All @@ -1443,7 +1399,6 @@ def _set_inten_max_to_curr(self, plot_ax_img):
"""Set max intensity to current image value."""
if plot_ax_img is not None:
vmax = plot_ax_img.ax_img.norm.vmax
self._adapt_imgadj_limits(plot_ax_img)
if self._imgadj_max != vmax:
self._imgadj_max_ignore_update = True
self._imgadj_max = vmax
Expand Down
15 changes: 9 additions & 6 deletions magmap/io/np_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,18 +546,21 @@ def add_metadata():
blobs.scaling = scaling


def get_num_channels(image5d):
"""Get the number of channels in a 5D image.
def get_num_channels(img: np.ndarray, is_3d: bool = False) -> int:
"""Get the number of image channels based on expected dimensions.

Args:
image5d (:obj:`np.ndarray`): Numpy arry in the order, `t,z,y,x[,c]`.
img: Numpy array.
is_3d: True if ``img` is a 3D+ array with ``z,y,x[,c]`` order.
Otherwise, assumes that the image is in 5D (4D+channel),
`t,z,y,x[,c]` order.

Returns:
int: Number of channels inferred based on the presence and length
of the 5th dimension.
Inferred number of channels.

"""
return 1 if image5d is None or image5d.ndim <= 4 else image5d.shape[4]
chl_dim = 3 if is_3d else 4
return 1 if img is None or img.ndim <= chl_dim else img.shape[chl_dim]


def write_raw_file(arr, path):
Expand Down
51 changes: 29 additions & 22 deletions magmap/plot/plot_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ def overlay_images(
self, imgs2d: Sequence[np.ndarray],
channels: Optional[List[List[int]]],
cmaps: Sequence[Union[
str, "colors.Colormap", colormaps.DiscreteColormap]],
str, "colors.Colormap", Sequence["colors.Colormap"]]],
alphas: Optional[Union[
float, Sequence[Union[float, Sequence[float]]]]] = None,
vmins: Optional[Union[
Expand All @@ -596,35 +596,42 @@ def overlay_images(
imgs2d: Sequence of 2D images to display,
where the first image may be 2D+channel.
channels: A nested list of channels to display for
each image, or None to use :attr:``config.channel`` for the
each image, or None to use
:attr:``magmap.settings.config.channel`` for the
first image and 0 for all subsequent images.
cmaps: Either a single colormap for all images or a list of
colormaps corresponding to each image. Colormaps of type
:class:`colormaps.DiscreteColormap` will have their
normalization object applied as well. If a color is given for
:obj:`config.AtlasLabels.BINARY` in :attr:`config.atlas_labels`,
images with :class:`colormaps.DiscreteColormap` will be
cmaps: Either a single colormap for all images or a sequence of
colormaps corresponding to each image. If a sequence, the first
value should be another sequence corresponding to all channels,
including channels not included in ``channels``. Colormaps of
type :class:`magmap.plot.colormaps.DiscreteColormap` will have
their normalization object applied as well. If a color is
given for :obj:`magmap.settings.config.AtlasLabels.BINARY`
in :attr:`magmap.settings.config.atlas_labels`, images with
:class:`magmap.plot.colormaps.DiscreteColormap` will be
converted to NaN for foreground to use this color.
alphas: Either a single alpha for all images or a list of
alphas corresponding to each image. Defaults to None to use
:attr:`config.alphas`, filling with 0.9 for any additional
values required and :attr:`config.plot_labels` for the first value.
vmins: A list of vmins for each image; defaults to None to use
:attr:``config.vmins`` for the first image and None for all others.
vmaxs: A list of vmaxs for each image; defaults to None to use
:attr:``config.vmax_overview`` for the first image and None
for all others.
alphas: Image opacity, given in the same format as for ``cmaps``.
``None`` to use :attr:`magmap.settings.config.alphas`,
filling with 0.9 for any additional values required and
:attr:`magmap.settings.config.plot_labels` for the first value.
vmins: Minimum intensities, given in the same format as for
``cmaps``. ``None`` to use
:attr:``magmap.settings.config.vmins`` for the first
image and None for all others.
vmaxs: Maximum intensities, given in the same format as for
``cmaps``. ``None`` to use
:attr:``magmap.settings.config.vmax_overview`` for
the first image and None for all others.
check_single: True to check for images with a single unique
value displayed with a :class:`colormaps.DiscreteColormap`, which
value displayed with a
:class:`magmap.plot.colormaps.DiscreteColormap`, which
will not update for unclear reasons. If found, the final value
will be incremented by one as a workaround to allow updates.
Defaults to False.
alpha_blends: Opacity blending values for each image in ``imgs2d``;
defaults to None.
alpha_blends: Opacity blending values for each image in ``imgs2d``.

Returns:
Nested list containing a list of Matplotlib image objects
Nested list containing a list of Matplotlib image objects
corresponding to display of each ``imgs2d`` image.

"""
ax_imgs = []
num_imgs2d = len(imgs2d)
Expand Down
33 changes: 33 additions & 0 deletions magmap/tests/test_np_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# MagellanMapper unit testing for np_io
"""Unit testing for the MagellanMapper np_io module.
"""

import unittest

import numpy as np

from magmap.io import np_io


class TestLibmag(unittest.TestCase):

def test_get_num_channels(self):
# test 5D image
img5d = np.zeros((1, 3, 4, 5, 6))
self.assertEqual(np_io.get_num_channels(img5d), 6)

# test 4D+channel image
img5d_no_chl = np.zeros((1, 3, 4, 5))
self.assertEqual(np_io.get_num_channels(img5d_no_chl), 1)

# test 3D+channel image
img3d = np.zeros((3, 4, 5, 6))
self.assertEqual(np_io.get_num_channels(img3d, True), 6)

# test 3D image
img3d_no_chl = np.zeros((3, 4, 5))
self.assertEqual(np_io.get_num_channels(img3d_no_chl, True), 1)


if __name__ == "__main__":
unittest.main()