I’ve been trying to build a tool that helps you manipulate heatpmaps / pictures. I’ve simplified my code to just the minimum that reproduces the issues.
This tool offsets the pictures to put the clicked point on the top left corner. (finer relative controls are also provided through sliders)
The issues is that the callback ends up called twice on every click for unknown reasons. A larger issue is that if I try to “memorize” the requested offsets to allow more than the first click to work (see commented lines), then the callback is called 60+ times (85 usually) and the app crashes.
I’d like to understand why…
A tangential question, if anyone knows; when set to the “line” mode, I’d like a line to be dynamically drawn to the hovering cursor until the second click. polygon doesn’t seem to work in this setup, any ideas?
import numpy as np
import param
import panel as pn
import holoviews as hv
pn.extension()
hv.extension('bokeh')
class ImageClicker(pn.viewable.Viewer):
x_offset = param.Integer(default=0, bounds=(-400, 400), step=1)
y_offset = param.Integer(default=0, bounds=(-400, 400), step=1)
tool_mode = param.Selector(default="Offset", objects=["None", "Offset", "Line"], doc="")
offset = param.Integer(default=0)
def __init__(self, results=None, **params):
super().__init__(**params)
# Initialize image array
self.raster_row = 400
self.raster_col = 400
array = np.zeros([self.raster_row, self.raster_col])
array[150:250, 150:250] = 1
array.flatten()
# Randomly roll the array
self.array = np.roll(array, 58742)
# initialize internal values
self.delta_offset = int(0)
self.tool_offset = int(0)
# Panel for display
self._main_display = pn.pane.HoloViews(
sizing_mode="stretch_both",
min_width=500,
#max_width= 3000,
#min_height=self._min_height,
)
# Controls for relative offsets
offset_ctrl = pn.Column(
pn.Param(self.param.x_offset, widgets={
"x_offset": {"type": pn.widgets.EditableIntSlider, "tooltips": True,
"width": 200}}),
pn.Param(self.param.y_offset, widgets={
"y_offset": {"type": pn.widgets.EditableIntSlider, "tooltips": True,
"width": 200}}),
pn.Param(self.param.tool_mode, widgets={
"tool_mode": {"type": pn.widgets.Select, "width": 200}}),
sizing_mode="stretch_both")
self.display = pn.pane.Markdown("")
self.global_tab = pn.Column(offset_ctrl,self.display, sizing_mode="stretch_width", height=500)
# Set Tap dynamics
self.tap_image = hv.streams.Tap()
self._main_display_tools = hv.DynamicMap(self._react_to_click, streams=[self.tap_image])
self.image_display = hv.DynamicMap(
pn.bind(self._display_image,
self.param.offset)
)
# Setup figure
self._main_display.object = (
(self.image_display * self._main_display_tools).opts(width=500, height = 500)
)
# App layout
self.layout=pn.Row(self.global_tab, self._main_display, sizing_mode='stretch_both')
# some counters to help out
self.count = 0
self.current_point = None
def _react_to_click(self, x, y):
if hasattr(self, 'tool_offset'):
if x is None:
x = 0
if y is None:
y=0
if self.tool_mode == "None":
# Do nothing
return hv.Points([])
elif self.tool_mode =="Offset":
dy = self.raster_row - int(y)
# This results in 60+ calls somehow
# Uncomment me!!
#self.tool_offset += self.convert_offset(-int(x), -dy)
# This results in 2 calls
# Comment me!!
self.tool_offset = self.convert_offset(-int(x), -dy)
self.offset = int(self.delta_offset+self.tool_offset)
self.count += 1
self.display.object=str(self.count )
return hv.Points([])
elif self.tool_mode == "Line":
if self.current_point is None:
self.current_point = [x, y]
return hv.Points((x, y))
else:
# Calculate distance between self.current_point and [x,y]
# ...
self.current_point = None
return hv.Points([])
else:
return hv.Points([])
@param.depends('x_offset', 'y_offset', watch=True)
def update_offset(self):
self.delta_offset = self.convert_offset(self.x_offset, -self.y_offset)
self.offset = int(self.delta_offset+self.tool_offset)
def convert_offset(self, x_offset, y_offset):
n_pixel_per_row = self.raster_col
return int(x_offset+n_pixel_per_row*y_offset)
def _display_image(self, offset):
array = np.roll(self.array, offset)
array.reshape(self.raster_row, self.raster_col)
return hv.Image((np.arange(self.raster_col), np.arange(self.raster_row)[::-1], array)).opts(cmap='cwr')
def __panel__(self):
return self.layout
Any help is appreciated!