Based on the custom component template, I’ve made a clickable image component. It works as expected; when you click on an image, the image has a boolean state that is inverted. However, I have some questions to ask in regards to optimisation:
-
Is it possible to load images in parallel instead of creating each one sequentially? What is the fastest way to load ~500 images on a page? I am using st.exerimental_memo() for caching requests I make for each image, but the images still take roughly 30 seconds per 100 images.
-
How does streamlit handle local image URLs/URIs? I can pass a local file path to st.image and it loads fine, but when passing the path to my custom component, it refuses to load due to javascript not being able to access local files. My workaround, as you can see below, is serving such images on a local server as base64 files.
import os
import requests
import streamlit.components.v1 as components
_RELEASE = False
if not _RELEASE:
_component_func = components.declare_component("interactive_image", url="http://localhost:3001")
else:
parent_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(parent_dir, "frontend/build")
_component_func = components.declare_component("interactive_image", path=build_dir)
def interactive_image(path, labelling=False, format="JPEG", key=None):
component_value = _component_func(path=path, labelling=labelling, format=format, key=key, default=0)
return component_value
if not _RELEASE:
import streamlit as st
from itertools import cycle
st.set_page_config(layout="wide")
with st.sidebar:
st.write("Example sidebar")
st.subheader("Interactive image component")
path = f"http://{HOST}:{PORT}/image.png"
# Decode base64 into ascii and convert with "const new_path = "data:image/{format};base64," + path"
labelling = False
cols = cycle(st.columns(4))
for idx in range(4):
with next(cols):
labelling = not labelling
st.write(labelling)
r = requests.get(path)
data_uri = r.content.decode('ascii')
interactive_image(data_uri, labelling, "PNG", key=idx)
import {
Streamlit,
StreamlitComponentBase,
withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ReactNode } from "react"
interface State {
selected: boolean
}
class InteractiveImage extends StreamlitComponentBase<State> {
public state = { selected: false }
readonly labelling = this.props.args["labelling"]
public render = (): ReactNode => {
// Variables
const format = this.props.args["format"]
const { theme } = this.props
const style: React.CSSProperties = {}
// Determing correct path
let path
if (format === "PNG" || format === "png") {
path = "data:image/png;base64," + this.props.args["path"]
} else {
path = "data:image/jpeg;base64," + this.props.args["path"]
}
// Styling
if (theme) {
let borderStyling = `3px solid ${
this.state.selected ? theme.primaryColor : theme.backgroundColor
}`
if (this.labelling) {
borderStyling = `3px solid ${theme.primaryColor}`
style.filter = "grayscale(100%)"
style.WebkitFilter = "grayscale(100%)" /* Safari 6.0 - 9.0 */
}
style.border = borderStyling
style.outline = borderStyling
style.borderRadius = "4px"
}
// Image
return (
<img
style={style}
width={this.props.width}
src={path}
srcSet={path}
alt={path}
loading="lazy"
onLoad={this.onLoad}
onClick={this.onClicked}
key={path}
data-testid="stInteractiveImage"
/>
)
}
// Invert current state of image when clicked
private onClicked = (): void => {
if (this.labelling !== true) {
this.setState(
(prevState) => ({ selected: !prevState.selected }),
() => Streamlit.setComponentValue(this.state.selected)
)
}
}
// When the image has finished loading, update it's height for correct rendering
private onLoad = (): void => {
Streamlit.setFrameHeight()
}
}
export default withStreamlitConnection(InteractiveImage)