andfanilo / streamlit-drawable-canvas

Do you like Quick, Draw? Well what if you could train/predict doodles drawn inside Streamlit? Also draws lines, circles and boxes over background images for annotation.
https://drawable-canvas.streamlit.app/
MIT License
544 stars 83 forks source link

canvas resets inside a button and does not work #60

Closed mohbay closed 2 years ago

mohbay commented 2 years ago

The canvas becomes unusable if defined inside a button or such. Here is a simple example code:

import pandas as pd
from PIL import Image
import streamlit as st
from streamlit_drawable_canvas import st_canvas

# Specify canvas parameters in application
stroke_width = st.sidebar.slider("Stroke width: ", 1, 25, 3)
stroke_color = st.sidebar.color_picker("Stroke color hex: ")
bg_color = st.sidebar.color_picker("Background color hex: ", "#eee")
bg_image = st.sidebar.file_uploader("Background image:", type=["png", "jpg"])
drawing_mode = st.sidebar.selectbox(
    "Drawing tool:", ("freedraw", "line", "rect", "circle", "transform")
)
realtime_update = st.sidebar.checkbox("Update in realtime", True)

if st.button("Next"):
# Create a canvas component
    canvas_result = st_canvas(
        fill_color="rgba(255, 165, 0, 0.3)",  # Fixed fill color with some opacity
        stroke_width=stroke_width,
        stroke_color=stroke_color,
        background_color=bg_color,
        background_image=Image.open(bg_image) if bg_image else None,
        update_streamlit=realtime_update,
        height=150,
        drawing_mode=drawing_mode,
        key="canvas",
    )

    # Do something interesting with the image data and paths
    if canvas_result.image_data is not None:
        st.image(canvas_result.image_data)
    if canvas_result.json_data is not None:
        st.dataframe(pd.json_normalize(canvas_result.json_data["objects"]))
andfanilo commented 2 years ago

Hello @mohbay!

The Streamlit model makes it that every time you interact with a widget, the whole script is rerun from start.

  1. The first time you click on the button, st.button("Next") becomes True and you get to the canvas code
  2. When the canvas starts, a rerun actually happens to render it and get back the results. Anytime you interact with the canvas will also rerun the whole script
  3. When the script is rerun, since you did not click on the st.button, it will be back to False, as such you will not go into the canvas loop.

To fix this, you can use the Streamlit Session State to preserve the fact that you clicked the button once between reruns:

import pandas as pd
from PIL import Image
import streamlit as st
from streamlit_drawable_canvas import st_canvas

# INITIALIZE SESSION STATE
if "button_clicked" not in st.session_state:
    st.session_state["button_clicked"] = False

# Specify canvas parameters in application
stroke_width = st.sidebar.slider("Stroke width: ", 1, 25, 3)
stroke_color = st.sidebar.color_picker("Stroke color hex: ")
bg_color = st.sidebar.color_picker("Background color hex: ", "#eee")
bg_image = st.sidebar.file_uploader("Background image:", type=["png", "jpg"])
drawing_mode = st.sidebar.selectbox(
    "Drawing tool:", ("freedraw", "line", "rect", "circle", "transform")
)
realtime_update = st.sidebar.checkbox("Update in realtime", True)

if st.button("Next") or st.session_state["button_clicked"]: # check session state if you had already clicked the button
    st.session_state["button_clicked"] = True # you just clicked the button, preserve that fact in session state
    # Create a canvas component
    canvas_result = st_canvas(
        fill_color="rgba(255, 165, 0, 0.3)",  # Fixed fill color with some opacity
        stroke_width=stroke_width,
        stroke_color=stroke_color,
        background_color=bg_color,
        background_image=Image.open(bg_image) if bg_image else None,
        update_streamlit=realtime_update,
        height=150,
        drawing_mode=drawing_mode,
        key="canvas",
    )

    # Do something interesting with the image data and paths
    if canvas_result.image_data is not None:
        st.image(canvas_result.image_data)
    if canvas_result.json_data is not None:
        objects = pd.json_normalize(canvas_result.json_data["objects"])
        for col in objects.select_dtypes(include=['object']).columns:
            objects[col] = objects[col].astype("str")
        st.dataframe(objects)

Hope this helps! Fanilo

mohbay commented 2 years ago

Yes. Thank you very much. I am closing the issue.