1

I have a set of grayscale images (2D numpy arrays) and want to display several of them simultaneously according to the state of a holoviews.MultiSelect widget but can't get the images to update when a selection is made in the MultiSelect widget. Here is a minimum working example:

pn.extension()

import numpy as np
import holoviews as hv
from holoviews import opts
hv.extension('bokeh')

# Make grayscale test images
images = []
xmax, ymax = 2560, 1600
for i in range(5):
    # Gray background
    image = np.ones(shape=(ymax, xmax), dtype=np.uint8) * 200
    # Make each image unique with different position for a black square
    # with interior white square
    image[200*i:200*i + 400,200*i:200*i + 400] = 0
    image[200*i+175:200*i+175 + 50,200*i+175:200*i+175 + 50] = 255
    images.append(image)

# Make hv.Images objects
scale_factor = 8
bounds=(0, 0, xmax, ymax)   # Coordinate system: (left, bottom, right, top)
img_0 = hv.Image(images[0], bounds=bounds)
img_1 = hv.Image(images[1], bounds=bounds)
options = [
    opts.Image(cmap='gray',
               aspect='equal',
               frame_width=int(xmax/scale_factor),
               frame_height=int(ymax/scale_factor),
              )
]
img_0.opts(*options)
img_1.opts(*options)

# Set up selector object
image_selector = pn.widgets.MultiSelect(name="Choose image index", 
                                 options=[i for i in range(len(images))],
                                 size=6,
                                 width=120
                                )

# Define interactivity
@pn.depends(image_selector, watch=True)
def change_image_shown(image_selector):
    index = image_selector[0]
    img_0.data = images[index]
    if index != len(images) - 1:
        img_1.data = images[index + 1]
    else:
        img_1.data = images[index]

# Create panel layout
layout = pn.Row(image_selector, pn.Column(img_0, img_1))
layout

The initial panel layout is as expected, but when I select an index in MultiSelect, the images do not update. Checking the value of the MultiSelect object,

image_selector.value[0]

I get the selected index so the problem has to be in the decorated change_image_shown function, but nothing I have tried works. What am I missing?

alwaysCurious
  • 523
  • 5
  • 14

1 Answers1

2

So you seem to have misunderstood the model for updating HoloViews plots a little bit. HoloViews elements should not be modified inplace, instead you set up a callback that returns the updated plot given a set of dependencies. You then wrap that in a DynamicMap which will dynamically rerender when an event is triggered.

import numpy as np
import holoviews as hv

from holoviews import opts

hv.extension('bokeh')

# Make grayscale test images
images = []
xmax, ymax = 2560, 1600
for i in range(5):
    # Gray background
    image = np.ones(shape=(ymax, xmax), dtype=np.uint8) * 200
    # Make each image unique with different position for a black square
    # with interior white square
    image[200*i:200*i + 400,200*i:200*i + 400] = 0
    image[200*i+175:200*i+175 + 50,200*i+175:200*i+175 + 50] = 255
    images.append(image)

# Make hv.Images objects
scale_factor = 8
bounds=(0, 0, xmax, ymax)   # Coordinate system: (left, bottom, right, top)
options = [
    opts.Image(cmap='gray',
               aspect='equal',
               frame_width=int(xmax/scale_factor),
               frame_height=int(ymax/scale_factor),
              )
]

# Set up selector object
image_selector = pn.widgets.MultiSelect(
    name="Choose image index", 
    options=[i for i in range(len(images))],
    size=6,
    width=120,
    value=[0, 1]
)

# Define interactivity
@pn.depends(indexes=image_selector)
def img0(indexes):
    index = indexes[0]
    return hv.Image(images[index], bounds=bounds).opts(*options)

@pn.depends(indexes=image_selector)
def img1(indexes):
    index = indexes[0]
    if index != len(images) - 1:
        data = images[index + 1]
    else:
        data = images[index]
    return hv.Image(images[index], bounds=bounds).opts(*options)


# Create panel layout
layout = pn.Row(
    image_selector,
    pn.Column(
        hv.DynamicMap(img0),
        hv.DynamicMap(img1)
    )
)
layout

Above is a rewrite of your code using the model. That said I'm still a bit confused about your callback. Why have a MultiSelect widget if you only use the first selected value? Do you want simple Select widget instead?

philippjfr
  • 3,997
  • 14
  • 15
  • You are right--I didn't understand the model for updating HoloViews plots. After going through nearly all of the documentation I missed that. I also thought that DynamicMap was just for when you want to continuously vary one or more parameters, which is what I saw in the documentation. I really appreciate your help in getting my misconceptions sorted out. The above code is extremely helpful. – alwaysCurious Jun 17 '20 at 18:14
  • The reason for the MultiSelect element is as follows: I will have 200-400 images to choose from and my understanding of a Select widget is that creates a dropdown menu, which would be unwieldy with so many items to select from. I thought it would be easier to scroll through the possibilities, which is what MultiSelect enables. It's ability to allow multiple simultaneous selections is extraneous to what I need so I just take the first item in what it returns. – alwaysCurious Jun 17 '20 at 18:17
  • I've gone through the code some more to make sure I understand how it works. I can't thank you enough for your help with this! – alwaysCurious Jun 17 '20 at 19:40
  • Glad you learned something. We're clearly not doing that well on the documentation if you missed this so any suggestions to improve that would be welcome. – philippjfr Jun 17 '20 at 22:50
  • You also make a good point, it would be nice to have a widget like MultiSelect that allowed just selecting one item and iirc there is an issue about that in Panel, so hopefully I or someone else will have time to address that soon. – philippjfr Jun 17 '20 at 22:51