3

With Plotly, I'd like to display two imshow on the same page, at the same place, with opacity.

This nearly works:

import plotly.express as px, numpy as np
from skimage import io
img = io.imread('https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Crab_Nebula.jpg/240px-Crab_Nebula.jpg')
fig = px.imshow(img)
x = np.random.random((100, 200))
fig2 = px.imshow(x)
fig.show()
fig2.show()

but it displays the two imshow images in two different tabs.

How to display the two "imshow" on the same plot, with an opacity slider for both layers?

For reference, here is the matplotlib equivalent:

import numpy as np, matplotlib.pyplot as plt, matplotlib.widgets as mpwidgets, scipy.misc
x = scipy.misc.face(gray=False)     # shape (768, 1024, 3)
y = np.random.random((100, 133))    # shape (100, 133)
fig, (ax0, ax1) = plt.subplots(2, 1, gridspec_kw={'height_ratios': [5, 1]})
OPACITY = 0.5
img0 = ax0.imshow(x, cmap="jet")
img1 = ax0.imshow(y, cmap="jet", alpha=OPACITY, extent=img0.get_extent())
slider0 = mpwidgets.Slider(ax=ax1, label='opacity', valmin=0, valmax=1, valinit=OPACITY)
slider0.on_changed(lambda value: img1.set_alpha(value))
plt.show()
Basj
  • 41,386
  • 99
  • 383
  • 673
  • I am not sure I understand the requirements. Say, the slider value is `v`. Would you like both images to have the opacity `v`, or maybe one will have the opacity `v`, the other `1-v`? Also, in your snippet `y` will appear on top of `x` - should it remain like this for all values of `v`? – Yulia V Oct 31 '22 at 16:34
  • @YuliaV `v` for one and `1-v` for the other would be perfect (in my case I did slightly differently - one was always displayed with opacity `1` - but here it doesn't matter). – Basj Oct 31 '22 at 16:59
  • @YuliaV in my example `y` was on top, but when opacity is near 0, it vanishes. Any combination that allows the user to go from one imshow to the other smoothly (with opacity) would be perfect. – Basj Oct 31 '22 at 17:02
  • Which IDE are you using? – Hamzah Nov 03 '22 at 17:32
  • @Hamzah the script is not run inside an IDE (I'm not using Jupyter for this precise application, it is a standalone app), it is run with `python myscript.py`. – Basj Nov 05 '22 at 11:27
  • Even with . py file you can run ipywidgets. Please look at this https://github.com/jupyter-widgets/ipywidgets/issues/2622 – Hamzah Nov 05 '22 at 18:22

3 Answers3

3

The main problem is that pre-computing all traces is an essential step to create the layer.Slider in Plotly as documented here. Therefore, we want to compute the only required trace on the fly rather than pre-computing all traces upfront. Moreover, I totally agree with you that it is ineffective solution to build all traces in advanced on the user machine because this will take long time and consume huge amount of memory, especially if the image has large size.

From my perspective, I see the best option is to use both Plotly and ipywidgets and doing this will save much more time and memory in compared to the first option. In the solution below, I update the only trace with the selected opacity by the user interactively.

Full Code

import plotly.express as px
import plotly.graph_objects as go
from skimage import io 
import numpy as np
from PIL import Image
from ipywidgets import FloatSlider, VBox

img = io.imread("https://images.unsplash.com/photo-1543349689-9a4d426bee8e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=801&q=80")
noise = np.random.random(img.shape[:2]) 


fig = go.FigureWidget(px.imshow(img))

fig.add_trace(go.Heatmap(z=noise, opacity=0.0, 
                         showscale=False, colorscale ="Greys",
                         name = "noise"))

def update(value):
    fig.update_traces(opacity=value["new"],selector=({'name':'noise'}))

Opacity_slider = FloatSlider(value=0.0,min=0.0,max=1.0,
                             step=0.1, description='Opacity:')

Opacity_slider.observe(update, names="value")

vb = VBox((fig, Opacity_slider))
vb.layout.align_items = 'center'
display(vb)

Output

enter image description here

Hamzah
  • 8,175
  • 3
  • 19
  • 43
  • Thank you for your answer! For my application, I don't use Jupyter notebooks but only Plotly. Or maybe can we use Jupyter Widget in the middle of a Plotly dashboard, without using a Jupyter notebook? Would you have an example code to update the opacity in realtime with `layout.slider`? – Basj Oct 25 '22 at 10:25
  • @Basj try to use `animation_frame` as in https://plotly.com/python/imshow/ – Hamzah Oct 25 '22 at 10:55
  • @Basj but you should create the noisy image with different opacities as frames and feed it to the px.imshow to get the slider. – Hamzah Oct 25 '22 at 11:08
  • I guess we can compute it in realtime (=when the slider moves), because if we should send to the web client *all* variations of the noisy image, that's a lot of unnecessary data (255x the normal amount of data). Would you have an example of code with slider, without JupyterLab? – Basj Oct 25 '22 at 12:04
  • Would you have an example code with a slider, without precomputing everything? Usually with a slider, we compute new data on the fly. – Basj Oct 30 '22 at 15:04
  • Only 2 frames are needed. The opacity of the images can be directly controlled with a slider with steps like: `steps = [dict(method='restyle', args=['opacity', [1.0, i and i/10]]) for i in range(11)]` – user7138814 Nov 03 '22 at 11:13
  • Hi @Basj , if you run my code with single value of opacity, you will find that it takes long time to show the image. Therefore, you cannot build your image with a slider without any delay. You will find that during tuning the value of opacity through the slider, it will take long time to show the noisy image and that undesirable behavior from the user perspective. Therefore, I recommend pre-computing the traces upfront, however, you can build your trace “noisy images” on the fly. – Hamzah Nov 03 '22 at 14:31
  • Thanks @Hamzah. Your current code doesn't run in a script `python app.py`: `NameError: name 'display' is not defined`. How to run it outside Jupyter? `display` is only available in IPython, is that right? (`from IPython.display import display`), how to use it with browser renderer? – Basj Nov 06 '22 at 21:23
2

As pointed out by the OP, since opacity is a style attribute applied by the client regardless of the trace (data) which is associated to a given image, there is no need to precompute one trace for each image variation, nor to redraw anything when the slider moves.

Using simple slider controls, we should be able to apply such (minor) changes in real-time.


Option 1 :

The first option is to let Plotly.js handle the changes by specifying the proper method and args in the slider' steps configuration (ie. using the restyle method, Plotly is smart enough to not redraw the whole plot) :

from PIL import Image
import plotly.graph_objects as go
import numpy as np
import scipy.misc

imgA = scipy.misc.face()
imgB = Image.fromarray(np.random.random(imgA.shape[:2])*255).convert('RGB')

fig = go.Figure([
    go.Image(name='raccoon', z=imgA, opacity=1), # trace 0
    go.Image(name='noise', z=imgB, opacity=0.5)  # trace 1
])

slider = {
    'active': 50,
    'currentvalue': {'prefix': 'Noise: '},
    'steps': [{
        'value': step/100,
        'label': f'{step}%',
        'visible': True,
        'execute': True,
        'method': 'restyle',
        'args': [{'opacity': step/100}, [1]]     # apply to trace [1] only
    } for step in range(101)]
}

fig.update_layout(sliders=[slider])
fig.show(renderer='browser')

Option 2 :

The second option demonstrates a more general case where one wants to bypass Plotly API on slider events and trigger his own code instead. For example, what if the restyle method were not efficient enough for this task ?

In this situation, one can hook into the plotly_sliderchange event and apply the changes manually in order to obtain smooth transitions.

The browser (default) renderer makes it possible by using the post_script parameter, which you can pass to the show() method. It allows to add javascript snippets which are executed just after plot creation. Once one knows that (documentation should emphasize this part!), it is just a matter of binding the appropriate handler. For example (JS syntax highlighting) :

// {plot_id} is a placeholder for the graphDiv id.
const gd = document.getElementById('{plot_id}');

// Retrieve the image (easier with d3, cf. 1st revision, see comments)
const trace = gd.calcdata.find(data => data[0].trace.name === 'noise')[0];
const group = Object.keys(trace).find(prop => (/node[0-4]/).test(prop));
const img = trace[group][0][0].firstChild;

// Listen for slider events and apply changes on the fly
gd.on('plotly_sliderchange', event => img.style.opacity = event.step.value);

Now for the slider configuration, the important thing is to set execute=False and method='skip' to bypass API calls and prevent redrawing the plot when the slider changes :

slider = {
    'active': 50,
    'currentvalue': {'prefix': 'Noise: '},
    'steps': [{
        'value': step/100,
        'label': f'{step}%',
        'visible': True,
        'execute': False,
        'method': 'skip',
    } for step in range(101)]
}

js = '''
    const gd = document.getElementById('{plot_id}');
    const trace = gd.calcdata.find(data => data[0].trace.name === 'noise')[0];
    const group = Object.keys(trace).find(prop => (/node[0-4]/).test(prop));
    const img = trace[group][0][0].firstChild;
    gd.on('plotly_sliderchange', event => img.style.opacity = event.step.value);
'''

fig.update_layout(sliders=[slider])
fig.show(renderer='browser', post_script=[js])
EricLavault
  • 12,130
  • 3
  • 23
  • 45
  • Your code does not work. I think you missed something. – Hamzah Nov 04 '22 at 13:56
  • When I play with the slider, the noise is unchanged. – Hamzah Nov 04 '22 at 14:19
  • You're right, I was running an outdated version of plotly and didn't notice that [d3 is not exported anymore with the API](https://github.com/plotly/plotly.js/commit/c71d68966db2fd25e0f923f59ac8377a2d751c23#diff-8ab41fe13597e1554b5d6b4c227b5f123ff2d6726a7f3688a8b8d1224fe1d4f3) :/ so retrieving the proper image element among several images having no id and without making assumption is a bit less easy (ie. which image belongs to which trace, as we can't assign DOM id to them from Python). I'm fixing the code so that d3 is not required. Thanks for pointing this out ! – EricLavault Nov 04 '22 at 18:46
  • Have you updated your code? – Hamzah Nov 04 '22 at 18:49
  • @Hamzah I just did. – EricLavault Nov 04 '22 at 18:51
  • But why did you select the render to be a browser instead of the default renderer? – Hamzah Nov 04 '22 at 18:55
  • 1
    Because "_explicit is better than implicit_". Any renderer could be set as default but not all of them allow for interactions driven by javascript code, and the OP mentioned he doesn't use notebooks. – EricLavault Nov 04 '22 at 19:43
  • @EricLavault Would you know how to convert your first code block into full Plotly.JS solution? Here is a beginning but I don't know how to add the two traces: https://stackoverflow.com/questions/75359950/two-heatmaps-on-top-of-each-other-with-opacity-with-plotly-js – Basj Feb 06 '23 at 10:16
1

My solution is uses little memory and works very quickly. Although it does use a heatmap rather than a second imshow, the end result is the same.

import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from skimage import io 

img = io.imread('https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Crab_Nebula.jpg/240px-Crab_Nebula.jpg')
x = np.random.random((len(img), len(img[0])))

fig = go.Figure()

fig.add_trace(go.Image(z=img))

for step in np.arange(0, 1.1, 0.1):
    fig.add_trace(go.Heatmap(z=x,
                 showscale=False,
                 opacity=step))

for n in range(1, len(fig.data)):
    fig.data[n].visible = False

fig.data[6].visible = True

steps = []
for i in range(1, len(fig.data)):
    step = dict(
        method = 'restyle',
        args = ['visible', [False] * len(fig.data)],
        )
    step['args'][1][0] = True
    step['args'][1][i] = True
    steps.append(step)
sliders = [dict(
    active=5,
    steps=steps
    )]

fig.layout.sliders = sliders
fig.show()

Fig

Basj
  • 41,386
  • 99
  • 383
  • 673
amance
  • 883
  • 4
  • 14