From 142e67da10d4493065d391751c269aabaf0d9c77 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 10 Jul 2024 03:02:00 -0400 Subject: [PATCH 1/3] add colorbar to hlut tool --- fastplotlib/widgets/histogram_lut.py | 83 +++++++++++++++++++++++++--- fastplotlib/widgets/image.py | 2 +- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 02c21aa38..b7c2cbace 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -1,8 +1,9 @@ import weakref +from math import ceil import numpy as np -from pygfx import Group +import pygfx from ..graphics import LineGraphic, ImageGraphic, TextGraphic from ..graphics._base import Graphic @@ -46,7 +47,7 @@ def __init__( self._histogram_line = LineGraphic(line_data) - bounds = (edges[0], edges[-1]) + bounds = (edges[0] * self._scale_factor, edges[-1] * self._scale_factor) limits = (edges_flanked[0], edges_flanked[-1]) size = 120 # since it's scaled to 100 origin = (hist_scaled.max() / 2, 0) @@ -63,8 +64,8 @@ def __init__( # there will be a small difference with the histogram edges so this makes them both line up exactly self._linear_region_selector.selection = ( - self._image_graphic.vmin, - self._image_graphic.vmax, + self._image_graphic.vmin * self._scale_factor, + self._image_graphic.vmax * self._scale_factor, ) self._vmin = self.image_graphic.vmin @@ -94,7 +95,7 @@ def __init__( self._text_vmax.world_object.material.pick_write = False - widget_wo = Group() + widget_wo = pygfx.Group() widget_wo.add( self._histogram_line.world_object, self._linear_region_selector.world_object, @@ -114,7 +115,39 @@ def __init__( self._linear_region_handler, "selection" ) - self.image_graphic.add_event_handler(self._image_cmap_handler, "vmin", "vmax") + self.image_graphic.add_event_handler(self._image_cmap_handler, "vmin", "vmax", "cmap") + + # colorbar for grayscale images + if self.image_graphic.data.value.ndim != 3: + self._colorbar: ImageGraphic = self._make_colorbar(edges_flanked) + + self.world_object.add(self._colorbar.world_object) + else: + self._colorbar = None + self._cmap = None + + def _make_colorbar(self, edges_flanked) -> ImageGraphic: + # use the histogram edge values as data for an + # image with 2 columns, this will be our colorbar! + colorbar_data = np.column_stack( + [np.linspace(edges_flanked[0], edges_flanked[-1], ceil(np.ptp(edges_flanked)))] * 2 + ).astype(np.float32) + + colorbar_data /= self._scale_factor + + cbar = ImageGraphic( + data=colorbar_data, + vmin=self.vmin, + vmax=self.vmax, + cmap=self.image_graphic.cmap, + interpolation="linear", + offset=(-55, edges_flanked[0], -1) + ) + + cbar.world_object.world.scale_x = 20 + self._cmap = self.image_graphic.cmap + + return cbar def _get_vmin_vmax_str(self) -> tuple[str, str]: if self.vmin < 0.001 or self.vmin > 99_999: @@ -135,6 +168,7 @@ def _fpl_add_plot_area_hook(self, plot_area): self._histogram_line._fpl_add_plot_area_hook(plot_area) self._plot_area.auto_scale() + self._plot_area.controller.enabled = True def _calculate_histogram(self, data): if data.ndim > 2: @@ -205,6 +239,22 @@ def _linear_region_handler(self, ev): def _image_cmap_handler(self, ev): setattr(self, ev.type, ev.info["value"]) + @property + def cmap(self) -> str: + return self._cmap + + @cmap.setter + def cmap(self, name: str): + if self._colorbar is None: + return + + self.image_graphic.block_events = True + self.image_graphic.cmap = name + + self._cmap = name + self._colorbar.cmap = name + self.image_graphic.block_events = False + @property def vmin(self) -> float: return self._vmin @@ -226,6 +276,8 @@ def vmin(self, value: float): self._linear_region_selector.block_events = False self._vmin = value + if self._colorbar is not None: + self._colorbar.vmin = value vmin_str, vmax_str = self._get_vmin_vmax_str() self._text_vmin.offset = (-120, self._linear_region_selector.selection[0], 0) @@ -253,6 +305,8 @@ def vmax(self, value: float): self._linear_region_selector.block_events = False self._vmax = value + if self._colorbar is not None: + self._colorbar.vmax = value vmin_str, vmax_str = self._get_vmin_vmax_str() self._text_vmax.offset = (-120, self._linear_region_selector.selection[1], 0) @@ -284,6 +338,17 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._data = weakref.proxy(data) + if self._colorbar is not None: + self.world_object.remove(self._colorbar.world_object) + + if self.image_graphic.data.value.ndim != 3: + self._colorbar: ImageGraphic = self._make_colorbar(edges_flanked) + + self.world_object.add(self._colorbar.world_object) + else: + self._colorbar = None + self._cmap = None + # reset plotarea dims self._plot_area.auto_scale() @@ -300,14 +365,14 @@ def image_graphic(self, graphic): if self._image_graphic is not None: # cleanup events from current image graphic - self._image_graphic.remove_event_handler(self._image_cmap_handler) + self._image_graphic.remove_event_handler(self._image_cmap_handler, "vmin", "vmax", "cmap") self._image_graphic = graphic - self.image_graphic.add_event_handler(self._image_cmap_handler) + self.image_graphic.add_event_handler(self._image_cmap_handler, "vmin", "vmax", "cmap") def disconnect_image_graphic(self): - self._image_graphic.remove_event_handler(self._image_cmap_handler) + self._image_graphic.remove_event_handler(self._image_cmap_handler, "vmin", "vmax", "cmap") del self._image_graphic # self._image_graphic = None diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index df9b46b55..b2742de18 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -126,7 +126,7 @@ def managed_graphics(self) -> list[ImageGraphic]: def cmap(self) -> list[str]: cmaps = list() for g in self.managed_graphics: - cmaps.append(g.cmap.name) + cmaps.append(g.cmap) return cmaps From 67b2b75d4451a7bf362aa458d2c067eb3ed3c3ab Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 10 Jul 2024 03:48:56 -0400 Subject: [PATCH 2/3] pause_events contextmanager --- fastplotlib/graphics/__init__.py | 2 + fastplotlib/graphics/utils.py | 23 ++++++++++ fastplotlib/widgets/histogram_lut.py | 64 +++++++++++----------------- 3 files changed, 50 insertions(+), 39 deletions(-) create mode 100644 fastplotlib/graphics/utils.py diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index ff96baa4c..abea09a9c 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -3,6 +3,7 @@ from .image import ImageGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack +from .utils import pause_events __all__ = [ @@ -12,4 +13,5 @@ "TextGraphic", "LineCollection", "LineStack", + "pause_events" ] diff --git a/fastplotlib/graphics/utils.py b/fastplotlib/graphics/utils.py new file mode 100644 index 000000000..e64e634e1 --- /dev/null +++ b/fastplotlib/graphics/utils.py @@ -0,0 +1,23 @@ +from contextlib import contextmanager + +from ._base import Graphic + + +@contextmanager +def pause_events(*graphics: Graphic): + if not all([isinstance(g, Graphic) for g in graphics]): + raise TypeError( + f"`pause_events` only takes Graphic instances as arguments, " + f"you have passed the following types:\n{[type(g) for g in graphics]}" + ) + + original_vals = [g.block_events for g in graphics] + + try: + for g in graphics: + g.block_events = True + yield + + finally: + for g, value in zip(graphics, original_vals): + g.block_events = value diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index b7c2cbace..bc8b5b962 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -1,11 +1,11 @@ -import weakref from math import ceil +import weakref import numpy as np import pygfx -from ..graphics import LineGraphic, ImageGraphic, TextGraphic +from ..graphics import LineGraphic, ImageGraphic, TextGraphic, pause_events from ..graphics._base import Graphic from ..graphics.selectors import LinearRegionSelector @@ -248,12 +248,11 @@ def cmap(self, name: str): if self._colorbar is None: return - self.image_graphic.block_events = True - self.image_graphic.cmap = name + with pause_events(self.image_graphic): + self.image_graphic.cmap = name - self._cmap = name - self._colorbar.cmap = name - self.image_graphic.block_events = False + self._cmap = name + self._colorbar.cmap = name @property def vmin(self) -> float: @@ -261,19 +260,14 @@ def vmin(self) -> float: @vmin.setter def vmin(self, value: float): - self.image_graphic.block_events = True - self._linear_region_selector.block_events = True - - # must use world coordinate values directly from selection() - # otherwise the linear region bounds jump to the closest bin edges - self._linear_region_selector.selection = ( - value * self._scale_factor, - self._linear_region_selector.selection[1], - ) - self.image_graphic.vmin = value - - self.image_graphic.block_events = False - self._linear_region_selector.block_events = False + with pause_events(self.image_graphic, self._linear_region_selector): + # must use world coordinate values directly from selection() + # otherwise the linear region bounds jump to the closest bin edges + self._linear_region_selector.selection = ( + value * self._scale_factor, + self._linear_region_selector.selection[1], + ) + self.image_graphic.vmin = value self._vmin = value if self._colorbar is not None: @@ -289,20 +283,15 @@ def vmax(self) -> float: @vmax.setter def vmax(self, value: float): - self.image_graphic.block_events = True - self._linear_region_selector.block_events = True - - # must use world coordinate values directly from selection() - # otherwise the linear region bounds jump to the closest bin edges - self._linear_region_selector.selection = ( - self._linear_region_selector.selection[0], - value * self._scale_factor, - ) - - self.image_graphic.vmax = value + with pause_events(self.image_graphic, self._linear_region_selector): + # must use world coordinate values directly from selection() + # otherwise the linear region bounds jump to the closest bin edges + self._linear_region_selector.selection = ( + self._linear_region_selector.selection[0], + value * self._scale_factor, + ) - self.image_graphic.block_events = False - self._linear_region_selector.block_events = False + self.image_graphic.vmax = value self._vmax = value if self._colorbar is not None: @@ -329,12 +318,9 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._linear_region_selector.limits = limits self._linear_region_selector.selection = bounds else: - # don't change the current selection - self.image_graphic.block_events = True - self._linear_region_selector.block_events = True - self._linear_region_selector.limits = limits - self.image_graphic.block_events = False - self._linear_region_selector.block_events = False + with pause_events(self.image_graphic, self._linear_region_selector): + # don't change the current selection + self._linear_region_selector.limits = limits self._data = weakref.proxy(data) From 323c57c556da9650c64186ae2ebd84393863cf5c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 10 Jul 2024 04:21:39 -0400 Subject: [PATCH 3/3] cleanup --- fastplotlib/graphics/utils.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/fastplotlib/graphics/utils.py b/fastplotlib/graphics/utils.py index e64e634e1..2f9f98d38 100644 --- a/fastplotlib/graphics/utils.py +++ b/fastplotlib/graphics/utils.py @@ -13,11 +13,9 @@ def pause_events(*graphics: Graphic): original_vals = [g.block_events for g in graphics] - try: - for g in graphics: - g.block_events = True - yield + for g in graphics: + g.block_events = True + yield - finally: - for g, value in zip(graphics, original_vals): - g.block_events = value + for g, value in zip(graphics, original_vals): + g.block_events = value