jethrovic commited on
Commit
1007af5
·
1 Parent(s): 06ed63e

new drawing tool

Browse files
Files changed (1) hide show
  1. app.py +321 -34
app.py CHANGED
@@ -1,43 +1,330 @@
 
 
 
 
 
 
 
 
 
 
1
  import pandas as pd
2
- from PIL import Image
3
  import streamlit as st
 
4
  from streamlit_drawable_canvas import st_canvas
 
5
 
6
- # Specify canvas parameters in application
7
- drawing_mode = st.sidebar.selectbox(
8
- "Drawing tool:", ("point", "freedraw", "line", "rect", "circle", "transform")
9
- )
10
 
11
- stroke_width = st.sidebar.slider("Stroke width: ", 1, 25, 3)
12
- if drawing_mode == 'point':
13
- point_display_radius = st.sidebar.slider("Point display radius: ", 1, 25, 3)
14
- stroke_color = st.sidebar.color_picker("Stroke color hex: ")
15
- bg_color = st.sidebar.color_picker("Background color hex: ", "#eee")
16
- bg_image = st.sidebar.file_uploader("Background image:", type=["png", "jpg"])
 
 
 
 
 
 
 
 
 
17
 
18
- realtime_update = st.sidebar.checkbox("Update in realtime", True)
 
 
 
 
 
 
 
 
 
19
 
 
 
 
 
 
 
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- # Create a canvas component
23
- canvas_result = st_canvas(
24
- fill_color="rgba(255, 165, 0, 0.3)", # Fixed fill color with some opacity
25
- stroke_width=stroke_width,
26
- stroke_color=stroke_color,
27
- background_color=bg_color,
28
- background_image=Image.open(bg_image) if bg_image else None,
29
- update_streamlit=realtime_update,
30
- height=150,
31
- drawing_mode=drawing_mode,
32
- point_display_radius=point_display_radius if drawing_mode == 'point' else 0,
33
- key="canvas",
34
- )
35
-
36
- # Do something interesting with the image data and paths
37
- if canvas_result.image_data is not None:
38
- st.image(canvas_result.image_data)
39
- if canvas_result.json_data is not None:
40
- objects = pd.json_normalize(canvas_result.json_data["objects"]) # need to convert obj to str because PyArrow
41
- for col in objects.select_dtypes(include=['object']).columns:
42
- objects[col] = objects[col].astype("str")
43
- st.dataframe(objects)
 
1
+ import base64
2
+ import json
3
+ import os
4
+ import re
5
+ import time
6
+ import uuid
7
+ from io import BytesIO
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
  import pandas as pd
 
12
  import streamlit as st
13
+ from PIL import Image
14
  from streamlit_drawable_canvas import st_canvas
15
+ from svgpathtools import parse_path
16
 
 
 
 
 
17
 
18
+ def main():
19
+ if "button_id" not in st.session_state:
20
+ st.session_state["button_id"] = ""
21
+ if "color_to_label" not in st.session_state:
22
+ st.session_state["color_to_label"] = {}
23
+ PAGES = {
24
+ "About": about,
25
+ "Basic example": full_app,
26
+ "Get center coords of circles": center_circle_app,
27
+ "Color-based image annotation": color_annotation_app,
28
+ "Download Base64 encoded PNG": png_export,
29
+ "Compute the length of drawn arcs": compute_arc_length,
30
+ }
31
+ page = st.sidebar.selectbox("Page:", options=list(PAGES.keys()))
32
+ PAGES[page]()
33
 
34
+ with st.sidebar:
35
+ st.markdown("---")
36
+ st.markdown(
37
+ '<h6>Made in &nbsp<img src="https://streamlit.io/images/brand/streamlit-mark-color.png" alt="Streamlit logo" height="16">&nbsp by <a href="https://twitter.com/andfanilo">@andfanilo</a></h6>',
38
+ unsafe_allow_html=True,
39
+ )
40
+ st.markdown(
41
+ '<div style="margin: 0.75em 0;"><a href="https://www.buymeacoffee.com/andfanilo" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a></div>',
42
+ unsafe_allow_html=True,
43
+ )
44
 
45
+
46
+ def about():
47
+ st.markdown(
48
+ """
49
+ Welcome to the demo of [Streamlit Drawable Canvas](https://github.com/andfanilo/streamlit-drawable-canvas).
50
+
51
+ On this site, you will find a full use case for this Streamlit component, and answers to some frequently asked questions.
52
 
53
+ :pencil: [Demo source code](https://github.com/andfanilo/streamlit-drawable-canvas-demo/)
54
+ """
55
+ )
56
+ st.image("img/demo.gif")
57
+ st.markdown(
58
+ """
59
+ What you can do with Drawable Canvas:
60
+
61
+ * Draw freely, lines, circles and boxes on the canvas, with options on stroke & fill
62
+ * Rotate, skew, scale, move any object of the canvas on demand
63
+ * Select a background color or image to draw on
64
+ * Get image data and every drawn object properties back to Streamlit !
65
+ * Choose to fetch back data in realtime or on demand with a button
66
+ * Undo, Redo or Drop canvas
67
+ * Save canvas data as JSON to reuse for another session
68
+ """
69
+ )
70
+
71
+
72
+ def full_app():
73
+ st.sidebar.header("Configuration")
74
+ st.markdown(
75
+ """
76
+ Draw on the canvas, get the drawings back to Streamlit!
77
+ * Configure canvas in the sidebar
78
+ * In transform mode, double-click an object to remove it
79
+ * In polygon mode, left-click to add a point, right-click to close the polygon, double-click to remove the latest point
80
+ """
81
+ )
82
+
83
+ with st.echo("below"):
84
+ # Specify canvas parameters in application
85
+ drawing_mode = st.sidebar.selectbox(
86
+ "Drawing tool:",
87
+ ("freedraw", "line", "rect", "circle", "transform", "polygon", "point"),
88
+ )
89
+ stroke_width = st.sidebar.slider("Stroke width: ", 1, 25, 3)
90
+ if drawing_mode == "point":
91
+ point_display_radius = st.sidebar.slider("Point display radius: ", 1, 25, 3)
92
+ stroke_color = st.sidebar.color_picker("Stroke color hex: ")
93
+ bg_color = st.sidebar.color_picker("Background color hex: ", "#eee")
94
+ bg_image = st.sidebar.file_uploader("Background image:", type=["png", "jpg"])
95
+ realtime_update = st.sidebar.checkbox("Update in realtime", True)
96
+
97
+ # Create a canvas component
98
+ canvas_result = st_canvas(
99
+ fill_color="rgba(255, 165, 0, 0.3)", # Fixed fill color with some opacity
100
+ stroke_width=stroke_width,
101
+ stroke_color=stroke_color,
102
+ background_color=bg_color,
103
+ background_image=Image.open(bg_image) if bg_image else None,
104
+ update_streamlit=realtime_update,
105
+ height=150,
106
+ drawing_mode=drawing_mode,
107
+ point_display_radius=point_display_radius if drawing_mode == "point" else 0,
108
+ display_toolbar=st.sidebar.checkbox("Display toolbar", True),
109
+ key="full_app",
110
+ )
111
+
112
+ # Do something interesting with the image data and paths
113
+ if canvas_result.image_data is not None:
114
+ st.image(canvas_result.image_data)
115
+ if canvas_result.json_data is not None:
116
+ objects = pd.json_normalize(canvas_result.json_data["objects"])
117
+ for col in objects.select_dtypes(include=["object"]).columns:
118
+ objects[col] = objects[col].astype("str")
119
+ st.dataframe(objects)
120
+
121
+
122
+ def center_circle_app():
123
+ st.markdown(
124
+ """
125
+ Computation of center coordinates for circle drawings some understanding of Fabric.js coordinate system
126
+ and play with some trigonometry.
127
+
128
+ Coordinates are canvas-related to top-left of image, increasing x going down and y going right.
129
+
130
+ ```
131
+ center_x = left + radius * cos(angle * pi / 180)
132
+ center_y = top + radius * sin(angle * pi / 180)
133
+ ```
134
+ """
135
+ )
136
+ bg_image = Image.open("img/tennis-balls.jpg")
137
+
138
+ with open("saved_state.json", "r") as f:
139
+ saved_state = json.load(f)
140
+
141
+ canvas_result = st_canvas(
142
+ fill_color="rgba(255, 165, 0, 0.2)", # Fixed fill color with some opacity
143
+ stroke_width=5,
144
+ stroke_color="black",
145
+ background_image=bg_image,
146
+ initial_drawing=saved_state
147
+ if st.sidebar.checkbox("Initialize with saved state", False)
148
+ else None,
149
+ height=400,
150
+ width=600,
151
+ drawing_mode="circle",
152
+ key="center_circle_app",
153
+ )
154
+ with st.echo("below"):
155
+ if canvas_result.json_data is not None:
156
+ df = pd.json_normalize(canvas_result.json_data["objects"])
157
+ if len(df) == 0:
158
+ return
159
+ df["center_x"] = df["left"] + df["radius"] * np.cos(
160
+ df["angle"] * np.pi / 180
161
+ )
162
+ df["center_y"] = df["top"] + df["radius"] * np.sin(
163
+ df["angle"] * np.pi / 180
164
+ )
165
+
166
+ st.subheader("List of circle drawings")
167
+ for _, row in df.iterrows():
168
+ st.markdown(
169
+ f'Center coords: ({row["center_x"]:.2f}, {row["center_y"]:.2f}). Radius: {row["radius"]:.2f}'
170
+ )
171
+
172
+
173
+ def color_annotation_app():
174
+ st.markdown(
175
+ """
176
+ Drawable Canvas doesn't provided out-of-the-box image annotation capabilities, but we can hack something with session state,
177
+ by mapping a drawing fill color to a label.
178
+
179
+ Annotate pedestrians, cars and traffic lights with this one, with any color/label you want
180
+ (though in a real app you should rather provide your own label and fills :smile:).
181
+
182
+ If you really want advanced image annotation capabilities, you'd better check [Streamlit Label Studio](https://discuss.streamlit.io/t/new-component-streamlit-labelstudio-allows-you-to-embed-the-label-studio-annotation-frontend-into-your-application/9524)
183
+ """
184
+ )
185
+ with st.echo("below"):
186
+ bg_image = Image.open("img/annotation.jpeg")
187
+ label_color = (
188
+ st.sidebar.color_picker("Annotation color: ", "#EA1010") + "77"
189
+ ) # for alpha from 00 to FF
190
+ label = st.sidebar.text_input("Label", "Default")
191
+ mode = "transform" if st.sidebar.checkbox("Move ROIs", False) else "rect"
192
+
193
+ canvas_result = st_canvas(
194
+ fill_color=label_color,
195
+ stroke_width=3,
196
+ background_image=bg_image,
197
+ height=320,
198
+ width=512,
199
+ drawing_mode=mode,
200
+ key="color_annotation_app",
201
+ )
202
+ if canvas_result.json_data is not None:
203
+ df = pd.json_normalize(canvas_result.json_data["objects"])
204
+ if len(df) == 0:
205
+ return
206
+ st.session_state["color_to_label"][label_color] = label
207
+ df["label"] = df["fill"].map(st.session_state["color_to_label"])
208
+ st.dataframe(df[["top", "left", "width", "height", "fill", "label"]])
209
+
210
+ with st.expander("Color to label mapping"):
211
+ st.json(st.session_state["color_to_label"])
212
+
213
+
214
+ def png_export():
215
+ st.markdown(
216
+ """
217
+ Realtime update is disabled for this demo.
218
+ Press the 'Download' button at the bottom of canvas to update exported image.
219
+ """
220
+ )
221
+ try:
222
+ Path("tmp/").mkdir()
223
+ except FileExistsError:
224
+ pass
225
+
226
+ # Regular deletion of tmp files
227
+ # Hopefully callback makes this better
228
+ now = time.time()
229
+ N_HOURS_BEFORE_DELETION = 1
230
+ for f in Path("tmp/").glob("*.png"):
231
+ st.write(f, os.stat(f).st_mtime, now)
232
+ if os.stat(f).st_mtime < now - N_HOURS_BEFORE_DELETION * 3600:
233
+ Path.unlink(f)
234
+
235
+ if st.session_state["button_id"] == "":
236
+ st.session_state["button_id"] = re.sub(
237
+ "\d+", "", str(uuid.uuid4()).replace("-", "")
238
+ )
239
+
240
+ button_id = st.session_state["button_id"]
241
+ file_path = f"tmp/{button_id}.png"
242
+
243
+ custom_css = f"""
244
+ <style>
245
+ #{button_id} {{
246
+ display: inline-flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ background-color: rgb(255, 255, 255);
250
+ color: rgb(38, 39, 48);
251
+ padding: .25rem .75rem;
252
+ position: relative;
253
+ text-decoration: none;
254
+ border-radius: 4px;
255
+ border-width: 1px;
256
+ border-style: solid;
257
+ border-color: rgb(230, 234, 241);
258
+ border-image: initial;
259
+ }}
260
+ #{button_id}:hover {{
261
+ border-color: rgb(246, 51, 102);
262
+ color: rgb(246, 51, 102);
263
+ }}
264
+ #{button_id}:active {{
265
+ box-shadow: none;
266
+ background-color: rgb(246, 51, 102);
267
+ color: white;
268
+ }}
269
+ </style> """
270
+
271
+ data = st_canvas(update_streamlit=False, key="png_export")
272
+ if data is not None and data.image_data is not None:
273
+ img_data = data.image_data
274
+ im = Image.fromarray(img_data.astype("uint8"), mode="RGBA")
275
+ im.save(file_path, "PNG")
276
+
277
+ buffered = BytesIO()
278
+ im.save(buffered, format="PNG")
279
+ img_data = buffered.getvalue()
280
+ try:
281
+ # some strings <-> bytes conversions necessary here
282
+ b64 = base64.b64encode(img_data.encode()).decode()
283
+ except AttributeError:
284
+ b64 = base64.b64encode(img_data).decode()
285
+
286
+ dl_link = (
287
+ custom_css
288
+ + f'<a download="{file_path}" id="{button_id}" href="data:file/txt;base64,{b64}">Export PNG</a><br></br>'
289
+ )
290
+ st.markdown(dl_link, unsafe_allow_html=True)
291
+
292
+
293
+ def compute_arc_length():
294
+ st.markdown(
295
+ """
296
+ Using an external SVG manipulation library like [svgpathtools](https://github.com/mathandy/svgpathtools)
297
+ You can do some interesting things on drawn paths.
298
+ In this example we compute the length of any drawn path.
299
+ """
300
+ )
301
+ with st.echo("below"):
302
+ bg_image = Image.open("img/annotation.jpeg")
303
+
304
+ canvas_result = st_canvas(
305
+ stroke_color="yellow",
306
+ stroke_width=3,
307
+ background_image=bg_image,
308
+ height=320,
309
+ width=512,
310
+ drawing_mode="freedraw",
311
+ key="compute_arc_length",
312
+ )
313
+ if (
314
+ canvas_result.json_data is not None
315
+ and len(canvas_result.json_data["objects"]) != 0
316
+ ):
317
+ df = pd.json_normalize(canvas_result.json_data["objects"])
318
+ paths = df["path"].tolist()
319
+ for ind, path in enumerate(paths):
320
+ path = parse_path(" ".join([str(e) for line in path for e in line]))
321
+ st.write(f"Path {ind} has length {path.length():.3f} pixels")
322
+
323
 
324
+ if __name__ == "__main__":
325
+ st.set_page_config(
326
+ page_title="Streamlit Drawable Canvas Demo", page_icon=":pencil2:"
327
+ )
328
+ st.title("Drawable Canvas Demo")
329
+ st.sidebar.subheader("Configuration")
330
+ main()