Pre-loading images and eliminating flashing in a

TL;DR: Trying to fix issues with flickering animation here (press ‘play’): [widget] Understanding Limited Palette’s Color Control Settings — PyTTI-Tools

background

I’m using sliders that interact with a panel.widgets.Player to provide interactivity to documentation (jupyterbook) for an AI art framework I maintain. Specifically, I’m adding widgets to let users explore how adjusting different settings affects the resulting animation.

A single animation is 20-40 images. I pre-generate animations for every combination of settings supported by a particular widget: e.g. the widget linked above has 6 sliders with 3 values each, meaning I had to pre-compute 3^6 animations, or just under 30k images. To use interaction with jupyterbook, I need to embed the wdiget. Embedding the image objects resulted in a page weighing over a gig, which is obviously untenable. To resolve this, I’m hosting the images externally (in a github repo cause idgaf), and the function that I wrapped with @interact returns an HTML pane instead of an Image pane, i.e. instead of embedding all of the image data, I’m just embedding their URLs. Here’s what that looks like in code:

kargs = {k:pn.widgets.DiscreteSlider(name=k, options=list(v), value=v[0]) for k,v in variant_ranges.items()}
kargs['i'] = pn.widgets.Player(interval=100, name='step', start=1, end=n_imgs_per_group, step=1, value=1, loop_policy='reflect')

@pn.interact(
    **kargs
)
def display_images(
    palettes,
    palette_size,
    gamma,
    hdr_weight,
    smoothing_weight,
    palette_normalization_weight,
    i,
):
    folder = df_meta[
        (palettes == df_meta['palettes']) &
        (palette_size == df_meta['palette_size']) &
        (gamma == df_meta['gamma']) &
        (hdr_weight == df_meta['hdr_weight']) &
        (smoothing_weight == df_meta['smoothing_weight']) &
        (palette_normalization_weight == df_meta['palette_normalization_weight'])
    ]['fpath'].values[0]
    im_path = str(folder / f"{folder.name}_{i}.png")
    im_url = d_image_urls[im_path]
    return pn.pane.HTML(f'<img src="{im_url}" width="700">', width=700, height=350, sizing_mode='fixed')

pn.panel(display_images).embed(max_opts=n_imgs_per_group, max_states=999999999)

Issues

  1. Because an image is not requested by the browser until it is needed, “playing” an animation results in a blank sequence for the first pass through the animation (the player is trying to display images the browser hasn’t downloaded and cached yet).
  2. After the first “empty” animation plays, the images are cached in the browser and I’d expect that they should load pretty much immediately, but instead there is often a noticeable but brief moment between frames where no image is displayed in the player, creating a distracting “flashing” effect.

proposed solutions

  1. I suspect I can pre-fetch the images for a particular animation and load them into a non-visible div. Not entirely sure how to go about this, but I’m pretty sure I can hack something together if no one has relevant demo code.
  2. I think the solution here is probably to have a fixed element in the DOM and have the player just update the src attribute rather than destroying and rebuilding the page element. Conversely, if I can figure out the image pre-fetching thing, maybe I could just have the player set and unset a visibility attribute.

(1) is mainly just annoying, my priority is fixing the flickering described in issue (2).

Thanks!

1 Like

HTML actually has a solution for preloading images. See web.dev - preload.

So for each url you need to add f'<link rel="preload" as="image" href="{url}">'

An example using this is shown below.

import panel as pn

pn.extension(sizing_mode="stretch_width", template="fast")

BASE_URL = "https://raw.githubusercontent.com/MarcSkovMadsen/awesome-panel-assets/master/awesome-panel/resources/thumbnails/"

IMAGES = {
  'astronomical': 'astronomical.png',
  'awesome-panel': 'awesome-panel.png',
  'benjamin-cooley': 'benjamin-cooley.png',
  'bokeh': 'bokeh.png',
  'cea-monitoring-data-viewer': 'cea-monitoring-data-viewer.png',
  'color-dropper': 'color-dropper.png',
  'color-map-distortions': 'color-map-distortions.png',
  'cuxfilter': 'cuxfilter.png',
  'dashboards-battle': 'dashboards-battle.png',
  'dashboards-bioinformatics': 'dashboards-bioinformatics.png',
  'dashboards-for-iot': 'dashboards-for-iot.png',
  'deploy-bokeh-on-google-cloud-run': 'deploy-bokeh-on-google-cloud-run.png',
  'deploy-panel-to-google-app-engine': 'deploy-panel-to-google-app-engine.png',
  'deploy-panel-to-google-cloud-run': 'deploy-panel-to-google-cloud-run.png',
  'deploying-using-heroku': 'deploying-using-heroku.png',
  'digital-vulnerabilities-map': 'digital-vulnerabilities-map.png',
  'distribution-explorer': 'distribution-explorer.png',
  'elvis': 'elvis.png',
  'error-analysis-dashboard': 'error-analysis-dashboard.png',
  'game-of-dashboards': 'game-of-dashboards.png',
  'getting-started-hvplot-interactive': 'getting-started-hvplot-interactive.png',
  'getting-started-panel': 'getting-started-panel.png',
  'gradient-descent': 'gradient-descent.png',
  'heroku-deployment': 'heroku-deployment.png',
  'holo-grid-generator': 'holo-grid-generator.png',
  'hologridgen': 'hologridgen.png',
  'holoviz-pytorch': 'holoviz-pytorch.png',
  'holoviz': 'holoviz.png',
  'intake': 'intake.png',
  'interactive-pytorch-layers': 'interactive-pytorch-layers.png',
  'intro-to-dataanalysis': 'intro-to-dataanalysis.png',
  'introduction-to-panel-ryan-noonan': 'introduction-to-panel-ryan-noonan.png',
  'jupyter-and-back': 'jupyter-and-back.png',
  'lumen': 'lumen.png',
  'merit-order': 'merit-order.png',
  'network-based-infection-model': 'network-based-infection-model.png',
  'neural-rock-viewer': 'neural-rock-viewer.png',
  'added neural-rock-viewer': 'added neural-rock-viewer.png',
  'nic-fox-projects': 'nic-fox-projects.png',
  'nic-fox-tutorial': 'nic-fox-tutorial.png',
  'ocean-glider-data': 'ocean-glider-data.png',
  'open-source-directions-29': 'open-source-directions-29.png',
  'paithon': 'paithon.png',
  'panel-and-ipywidgets': 'panel-and-ipywidgets.png',
  'panel-announcement': 'panel-announcement.png',
  'panel-business-dashboard': 'panel-business-dashboard.png',
  'panel-discourse': 'panel-discourse.png',
  'panel-highcharts': 'panel-highcharts.png',
  'panel-learning-aid': 'panel-learning-aid.png',
  'panel-pydata-austin-2019': 'panel-pydata-austin-2019.png',
  'panel-vda-tutorial': 'panel-vda-tutorial.png',
  'panel': 'panel.png',
  'pydata-berlin-2019': 'pydata-berlin-2019.png',
  'pydata-global-2020': 'pydata-global-2020.png',
  'pydata2021-interactive': 'pydata2021-interactive.png',
  'quick-panel-dashboard': 'quick-panel-dashboard.png',
  'scipy-2019-tutorial': 'scipy-2019-tutorial.png',
  'scipy-2019': 'scipy-2019.png',
  'statseuro2020': 'statseuro2020.png',
  'streaming-data-with-crossbario': 'streaming-data-with-crossbario.png',
  'sympy-plotting-backends': 'sympy-plotting-backends.png',
  'talk-python-holoviz': 'talk-python-holoviz.png',
  'thalassa': 'thalassa.png',
  'thor-dataset': 'thor-dataset.png',
  'thu_vu_beautiful_python_visualization_dashboard_with_panel': 'thu_vu_beautiful_python_visualization_dashboard_with_panel.png',
  'vtk-examples': 'vtk-examples.png',
  'world-glacier': 'world-glacier.png',
  'xrviz': 'xrviz.png',
}

NAMES = list(IMAGES.keys())
DEFAULT_NAME = NAMES[0]

def to_url(name):
  return BASE_URL + IMAGES[name]

def to_preload_link(name):
  """Returns a HTML Preload link.
  
  See https://web.dev/preload-responsive-images/#preload-overview
  """
  url = to_url(name)
  return f'<link rel="preload" as="image" href="{url}">'

def get_preloader(names):
    links = [to_preload_link(name) for name in names]
    
    return pn.pane.HTML("".join(links), height=0, width=0, margin=0, sizing_mode="fixed")

preloader = get_preloader(NAMES)

def get_image(name):
  url = to_url(name)
  return pn.pane.HTML(f'<img src="{url}" width="700">', height=700, sizing_mode='scale_height')

slider = pn.widgets.DiscreteSlider(value=DEFAULT_NAME, options=NAMES, name="Select Image")

iget_image = pn.bind(get_image, name=slider)
pn.Column(preloader, slider, iget_image).servable()

pn.state.template.param.update(site="Awesome Panel", title="Preloading Images")
1 Like