flashstudio 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flashstudio might be problematic. Click here for more details.

Files changed (97) hide show
  1. flashstudio/__init__.py +5 -0
  2. flashstudio/app.py +194 -0
  3. flashstudio/cli.py +18 -0
  4. flashstudio/components/__init__.py +0 -0
  5. flashstudio/components/project_manager.py +490 -0
  6. flashstudio/components/sidebar.py +87 -0
  7. flashstudio/components/styles.py +346 -0
  8. flashstudio/components/wizard.py +45 -0
  9. flashstudio/components/zone_drawer/__init__.py +177 -0
  10. flashstudio/constants.py +245 -0
  11. flashstudio/core/__init__.py +3 -0
  12. flashstudio/core/constants.py +3 -0
  13. flashstudio/launcher.py +288 -0
  14. flashstudio/pages/__init__.py +8 -0
  15. flashstudio/pages/dashboard/__init__.py +3 -0
  16. flashstudio/pages/dashboard/_common.py +10 -0
  17. flashstudio/pages/dashboard/overview/__init__.py +3 -0
  18. flashstudio/pages/dashboard/overview/metrics.py +33 -0
  19. flashstudio/pages/dashboard/overview/pipeline_status.py +35 -0
  20. flashstudio/pages/dashboard/overview/project_banner.py +23 -0
  21. flashstudio/pages/dashboard/overview/quick_actions.py +28 -0
  22. flashstudio/pages/dashboard/overview/recent_runs.py +99 -0
  23. flashstudio/pages/dashboard/overview/tab.py +35 -0
  24. flashstudio/pages/dashboard/page.py +17 -0
  25. flashstudio/pages/data/__init__.py +3 -0
  26. flashstudio/pages/data/_common.py +107 -0
  27. flashstudio/pages/data/download/__init__.py +3 -0
  28. flashstudio/pages/data/download/tab.py +75 -0
  29. flashstudio/pages/data/helpers.py +338 -0
  30. flashstudio/pages/data/page.py +50 -0
  31. flashstudio/pages/data/preview/__init__.py +3 -0
  32. flashstudio/pages/data/preview/tab.py +139 -0
  33. flashstudio/pages/data/upload/__init__.py +3 -0
  34. flashstudio/pages/data/upload/tab.py +156 -0
  35. flashstudio/pages/data/verify/__init__.py +3 -0
  36. flashstudio/pages/data/verify/tab.py +108 -0
  37. flashstudio/pages/export/__init__.py +3 -0
  38. flashstudio/pages/export/page.py +54 -0
  39. flashstudio/pages/export/weights/__init__.py +4 -0
  40. flashstudio/pages/export/weights/exporter.py +60 -0
  41. flashstudio/pages/export/weights/settings.py +86 -0
  42. flashstudio/pages/inference/__init__.py +3 -0
  43. flashstudio/pages/inference/_common.py +76 -0
  44. flashstudio/pages/inference/data/__init__.py +3 -0
  45. flashstudio/pages/inference/data/tab.py +121 -0
  46. flashstudio/pages/inference/model/__init__.py +3 -0
  47. flashstudio/pages/inference/model/tab.py +62 -0
  48. flashstudio/pages/inference/page.py +42 -0
  49. flashstudio/pages/inference/run/__init__.py +3 -0
  50. flashstudio/pages/inference/run/detection.py +133 -0
  51. flashstudio/pages/inference/run/tab.py +161 -0
  52. flashstudio/pages/inference/run/video.py +195 -0
  53. flashstudio/pages/inference/solution/__init__.py +3 -0
  54. flashstudio/pages/inference/solution/tab.py +121 -0
  55. flashstudio/pages/model/__init__.py +3 -0
  56. flashstudio/pages/model/_common.py +3 -0
  57. flashstudio/pages/model/advanced/__init__.py +3 -0
  58. flashstudio/pages/model/advanced/tab.py +54 -0
  59. flashstudio/pages/model/architecture/__init__.py +3 -0
  60. flashstudio/pages/model/architecture/tab.py +90 -0
  61. flashstudio/pages/model/augment/__init__.py +3 -0
  62. flashstudio/pages/model/augment/tab.py +18 -0
  63. flashstudio/pages/model/hyperparams/__init__.py +4 -0
  64. flashstudio/pages/model/hyperparams/tab.py +89 -0
  65. flashstudio/pages/model/page.py +30 -0
  66. flashstudio/pages/model/utils/__init__.py +3 -0
  67. flashstudio/pages/model/utils/config.py +27 -0
  68. flashstudio/pages/training/__init__.py +3 -0
  69. flashstudio/pages/training/_common.py +24 -0
  70. flashstudio/pages/training/launch/__init__.py +3 -0
  71. flashstudio/pages/training/launch/controls.py +84 -0
  72. flashstudio/pages/training/launch/dialogs.py +247 -0
  73. flashstudio/pages/training/launch/preflight.py +77 -0
  74. flashstudio/pages/training/launch/runner.py +440 -0
  75. flashstudio/pages/training/launch/tab.py +134 -0
  76. flashstudio/pages/training/monitor/__init__.py +3 -0
  77. flashstudio/pages/training/monitor/checkpoints.py +96 -0
  78. flashstudio/pages/training/monitor/curves.py +113 -0
  79. flashstudio/pages/training/monitor/dashboard.py +129 -0
  80. flashstudio/pages/training/monitor/gt_verification.py +122 -0
  81. flashstudio/pages/training/monitor/log_viewer.py +43 -0
  82. flashstudio/pages/training/monitor/parsers.py +253 -0
  83. flashstudio/pages/training/monitor/run_meta.py +165 -0
  84. flashstudio/pages/training/monitor/tab.py +142 -0
  85. flashstudio/pages/training/monitor/visualizations.py +52 -0
  86. flashstudio/pages/training/page.py +134 -0
  87. flashstudio/utils/__init__.py +152 -0
  88. flashstudio/utils/config.py +104 -0
  89. flashstudio/utils/device.py +67 -0
  90. flashstudio/utils/filesystem.py +67 -0
  91. flashstudio/utils/jobs.py +95 -0
  92. flashstudio/utils/training_hooks.py +266 -0
  93. flashstudio-0.0.1.dist-info/METADATA +107 -0
  94. flashstudio-0.0.1.dist-info/RECORD +97 -0
  95. flashstudio-0.0.1.dist-info/WHEEL +5 -0
  96. flashstudio-0.0.1.dist-info/entry_points.txt +2 -0
  97. flashstudio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,346 @@
1
+ """FlashStudio — Professional CSS (readable, clean layout)."""
2
+
3
+ import streamlit as st
4
+
5
+
6
+ def inject_custom_css():
7
+ """Professional-grade CSS — clean, readable, well-spaced."""
8
+ st.markdown("""
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
11
+
12
+ /* ═══════ GLOBAL — NO SCROLL ═══════ */
13
+ .stApp {
14
+ font-family: 'Inter', -apple-system, sans-serif;
15
+ overflow: hidden !important;
16
+ height: 100vh !important;
17
+ }
18
+
19
+ .main {
20
+ overflow: hidden !important;
21
+ height: 100vh !important;
22
+ }
23
+
24
+ .main .block-container {
25
+ padding: 0.8rem 1.5rem 0.5rem !important;
26
+ max-width: 1500px;
27
+ height: calc(100vh - 2rem) !important;
28
+ overflow-y: auto !important;
29
+ overflow-x: hidden !important;
30
+ scrollbar-width: thin;
31
+ scrollbar-color: #D1D5DB transparent;
32
+ }
33
+ .main .block-container::-webkit-scrollbar { width: 4px; }
34
+ .main .block-container::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 2px; }
35
+
36
+ /* Allow internal scroll on specific containers */
37
+ .scrollable-panel {
38
+ overflow-y: auto !important;
39
+ scrollbar-width: thin;
40
+ scrollbar-color: #D1D5DB transparent;
41
+ }
42
+ .scrollable-panel::-webkit-scrollbar { width: 4px; }
43
+ .scrollable-panel::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 2px; }
44
+
45
+ .main .stMarkdown { margin-bottom: 0 !important; }
46
+ .main .element-container { margin-bottom: 0.15rem !important; }
47
+ .main div[data-testid="stVerticalBlock"] > div { gap: 0.35rem !important; }
48
+ .main hr { margin: 0.3rem 0 !important; border-color: #E5E7EB !important; }
49
+
50
+ /* ═══════ SIDEBAR ═══════ */
51
+ section[data-testid="stSidebar"] {
52
+ background: #FAFBFC !important;
53
+ border-right: 1px solid #E5E7EB;
54
+ width: 210px !important;
55
+ }
56
+
57
+ section[data-testid="stSidebar"] > div {
58
+ width: 210px !important;
59
+ padding: 0.5rem 0.5rem !important;
60
+ }
61
+
62
+ section[data-testid="stSidebar"] hr {
63
+ border-color: #E5E7EB !important;
64
+ margin: 0.4rem 0 !important;
65
+ }
66
+
67
+ section[data-testid="stSidebar"] .stButton > button {
68
+ font-size: 0.92rem !important;
69
+ padding: 0.42rem 0.7rem !important;
70
+ border-radius: 6px !important;
71
+ text-align: left !important;
72
+ justify-content: flex-start !important;
73
+ min-height: 0 !important;
74
+ height: auto !important;
75
+ transition: all 0.15s ease;
76
+ }
77
+
78
+ section[data-testid="stSidebar"] .stButton > button[kind="secondary"] {
79
+ background: transparent !important;
80
+ border: none !important;
81
+ color: #4B5563 !important;
82
+ }
83
+
84
+ section[data-testid="stSidebar"] .stButton > button[kind="secondary"]:hover {
85
+ background: #EEF2FF !important;
86
+ color: #7C3AED !important;
87
+ }
88
+
89
+ section[data-testid="stSidebar"] .stButton > button[kind="primary"] {
90
+ background: #EEF2FF !important;
91
+ border: none !important;
92
+ color: #7C3AED !important;
93
+ font-weight: 600 !important;
94
+ border-left: 3px solid #7C3AED !important;
95
+ border-radius: 0 6px 6px 0 !important;
96
+ }
97
+
98
+ [data-testid="stSidebarNav"] { display: none !important; }
99
+
100
+ /* ═══════ METRIC CARDS ═══════ */
101
+ div[data-testid="stMetric"] {
102
+ background: #FFF;
103
+ border: 1px solid #E5E7EB;
104
+ border-radius: 8px;
105
+ padding: 0.6rem 0.8rem !important;
106
+ }
107
+
108
+ div[data-testid="stMetric"] label {
109
+ font-size: 0.82rem !important;
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.04em;
112
+ color: #6B7280 !important;
113
+ font-weight: 600;
114
+ margin-bottom: 0.1rem !important;
115
+ }
116
+
117
+ div[data-testid="stMetric"] div[data-testid="stMetricValue"] {
118
+ font-size: 1.25rem !important;
119
+ font-weight: 700;
120
+ color: #1A1A2E !important;
121
+ line-height: 1.3;
122
+ }
123
+
124
+ /* ═══════ BUTTONS ═══════ */
125
+ .main .stButton > button {
126
+ font-size: 0.92rem !important;
127
+ padding: 0.35rem 0.7rem !important;
128
+ min-height: 0 !important;
129
+ border-radius: 6px;
130
+ transition: all 0.15s ease;
131
+ }
132
+
133
+ .main .stButton > button[kind="primary"] {
134
+ background: #7C3AED !important;
135
+ border: none;
136
+ font-weight: 600;
137
+ color: #FFF !important;
138
+ }
139
+
140
+ .main .stButton > button[kind="primary"]:hover {
141
+ background: #6D28D9 !important;
142
+ box-shadow: 0 2px 8px rgba(124, 58, 237, 0.2);
143
+ }
144
+
145
+ .main .stButton > button[kind="secondary"] {
146
+ border: 1px solid #E5E7EB !important;
147
+ color: #374151 !important;
148
+ }
149
+
150
+ /* ═══════ CONTAINERS ═══════ */
151
+ div[data-testid="stVerticalBlockBorderWrapper"] {
152
+ border-radius: 8px !important;
153
+ border-color: #E5E7EB !important;
154
+ }
155
+
156
+ div[data-testid="stExpander"] {
157
+ border-radius: 8px !important;
158
+ border: 1px solid #E5E7EB !important;
159
+ }
160
+
161
+ /* ═══════ TABS ═══════ */
162
+ .stTabs [data-baseweb="tab-list"] {
163
+ gap: 0.4rem;
164
+ border-bottom: 1px solid #E5E7EB;
165
+ }
166
+
167
+ .stTabs [data-baseweb="tab"] {
168
+ font-weight: 500;
169
+ font-size: 0.95rem;
170
+ color: #6B7280;
171
+ padding: 0.45rem 0.8rem;
172
+ }
173
+
174
+ .stTabs [aria-selected="true"] {
175
+ color: #7C3AED !important;
176
+ font-weight: 600;
177
+ border-bottom: 2px solid #7C3AED !important;
178
+ }
179
+
180
+ /* Tab content area: constrain height to prevent page scroll */
181
+ .stTabs [data-baseweb="tab-panel"] {
182
+ max-height: calc(100vh - 12rem) !important;
183
+ overflow-y: auto !important;
184
+ overflow-x: hidden !important;
185
+ scrollbar-width: thin;
186
+ scrollbar-color: #D1D5DB transparent;
187
+ }
188
+ .stTabs [data-baseweb="tab-panel"]::-webkit-scrollbar { width: 4px; }
189
+ .stTabs [data-baseweb="tab-panel"]::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 2px; }
190
+
191
+ /* ═══════ FORM INPUTS ═══════ */
192
+ .main .stSelectbox > div > div,
193
+ .main .stTextInput > div > div > input,
194
+ .main .stNumberInput > div > div > input {
195
+ font-size: 0.92rem !important;
196
+ padding: 0.32rem 0.5rem !important;
197
+ border-radius: 6px;
198
+ }
199
+
200
+ .main .stSelectbox label,
201
+ .main .stTextInput label,
202
+ .main .stNumberInput label,
203
+ .main .stSlider label,
204
+ .main .stCheckbox label,
205
+ .main .stRadio label,
206
+ .main .stFileUploader label,
207
+ .main .stTextArea label {
208
+ font-size: 0.92rem !important;
209
+ margin-bottom: 0 !important;
210
+ }
211
+
212
+ .main .stCheckbox { margin-bottom: 0 !important; }
213
+ .main .stSlider > div { padding-top: 0 !important; }
214
+ .stSlider > div > div > div > div { background: #7C3AED !important; }
215
+
216
+ /* ═══════ HEADINGS ═══════ */
217
+ .main h1 { font-size: 1.5rem !important; margin: 0.15rem 0 !important; font-weight: 700; }
218
+ .main h2 { font-size: 1.25rem !important; margin: 0.15rem 0 !important; font-weight: 600; }
219
+ .main h3 { font-size: 1.1rem !important; margin: 0.1rem 0 !important; font-weight: 600; color: #1A1A2E; }
220
+ .main h4 { font-size: 1.0rem !important; margin: 0.1rem 0 !important; font-weight: 600; color: #374151; }
221
+
222
+ /* ═══════ EXPANDER ═══════ */
223
+ .streamlit-expanderHeader {
224
+ font-weight: 600;
225
+ font-size: 0.92rem;
226
+ padding: 0.3rem 0.5rem !important;
227
+ }
228
+
229
+ /* ═══════ STEP INDICATOR ═══════ */
230
+ .step-indicator {
231
+ display: flex;
232
+ justify-content: center;
233
+ gap: 0.1rem;
234
+ padding: 0.25rem 0;
235
+ margin-bottom: 0.3rem;
236
+ }
237
+
238
+ .step-item { text-align: center; flex: 1; }
239
+ .step-icon { font-size: 1.15rem; display: block; }
240
+ .step-label { font-size: 0.85rem; font-weight: 500; color: #6B7280; }
241
+ .step-bar { height: 2px; border-radius: 1px; margin-top: 0.15rem; }
242
+ .step-active .step-label { color: #7C3AED; font-weight: 700; }
243
+ .step-active .step-bar { background: #7C3AED; }
244
+ .step-done .step-bar { background: #10B981; }
245
+ .step-pending .step-bar { background: #E5E7EB; }
246
+ .step-pending .step-label { opacity: 0.65; }
247
+
248
+ /* ═══════ PIPELINE ═══════ */
249
+ .pipeline-steps {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ gap: 0.15rem;
254
+ padding: 0.25rem 0;
255
+ margin-bottom: 0.2rem;
256
+ }
257
+
258
+ .pipeline-step {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ gap: 0.2rem;
262
+ padding: 0.15rem 0.5rem;
263
+ border-radius: 4px;
264
+ font-size: 0.85rem;
265
+ font-weight: 500;
266
+ }
267
+
268
+ .pipeline-arrow { color: #D1D5DB; font-size: 0.75rem; }
269
+ .ps-done { background: #ECFDF5; color: #065F46; }
270
+ .ps-active { background: #FEF3C7; color: #92400E; }
271
+ .ps-pending { background: #F9FAFB; color: #9CA3AF; }
272
+
273
+ /* ═══════ INFO BAR ═══════ */
274
+ .info-bar {
275
+ background: #F8F7FF;
276
+ border: 1px solid #EDE9FE;
277
+ border-radius: 6px;
278
+ padding: 0.35rem 0.8rem;
279
+ text-align: center;
280
+ font-size: 0.88rem;
281
+ color: #6B21A8;
282
+ }
283
+ .info-bar b { color: #1A1A2E; }
284
+
285
+ /* ═══════ STAT ROW ═══════ */
286
+ .ds-card-stats {
287
+ display: flex;
288
+ gap: 0.6rem;
289
+ font-size: 0.88rem;
290
+ color: #6B7280;
291
+ flex-wrap: wrap;
292
+ line-height: 1.4;
293
+ }
294
+ .ds-card-stats span { white-space: nowrap; }
295
+
296
+ /* ═══════ ALERTS ═══════ */
297
+ .stAlert { padding: 0.35rem 0.6rem !important; font-size: 0.92rem !important; }
298
+
299
+ /* ═══════ CAPTION ═══════ */
300
+ .main .stCaption, .main small { font-size: 0.82rem !important; }
301
+
302
+ /* ═══════ PROGRESS ═══════ */
303
+ .stProgress > div > div {
304
+ background: linear-gradient(90deg, #7C3AED, #A78BFA) !important;
305
+ border-radius: 3px;
306
+ }
307
+
308
+ /* ═══════ FILE UPLOADER ═══════ */
309
+ [data-testid="stFileUploader"] section {
310
+ border-radius: 6px;
311
+ border: 2px dashed #D1D5DB !important;
312
+ padding: 0.6rem !important;
313
+ }
314
+ [data-testid="stFileUploader"] section:hover { border-color: #7C3AED !important; }
315
+
316
+ /* ═══════ DATAFRAME ═══════ */
317
+ .stDataFrame { border-radius: 6px; border: 1px solid #E5E7EB; }
318
+
319
+ /* ═══════ DOWNLOAD ═══════ */
320
+ .stDownloadButton > button {
321
+ border-radius: 6px;
322
+ font-size: 0.92rem !important;
323
+ padding: 0.35rem 0.7rem !important;
324
+ }
325
+ </style>
326
+ """, unsafe_allow_html=True)
327
+
328
+
329
+ def render_info_bar(items: dict):
330
+ inner = " · ".join(f"{k}: <b>{v}</b>" for k, v in items.items())
331
+ st.markdown(f'<div class="info-bar">{inner}</div>', unsafe_allow_html=True)
332
+
333
+
334
+ def render_page_header(icon: str, title: str, subtitle: str = ""):
335
+ icon_html = f'<span style="margin-right:0.4rem;">{icon}</span>' if icon else ""
336
+ subtitle_html = (
337
+ f'<div style="font-size:0.88rem;color:#6B7280;margin-top:0.1rem;">{subtitle}</div>'
338
+ if subtitle else ""
339
+ )
340
+ st.markdown(
341
+ f'<div style="margin-bottom:0.4rem;">'
342
+ f'<h1 style="display:inline;color:#1A1A2E;">{icon_html}{title}</h1>'
343
+ f'{subtitle_html}'
344
+ f'</div>',
345
+ unsafe_allow_html=True,
346
+ )
@@ -0,0 +1,45 @@
1
+ """Step indicator and page navigation buttons."""
2
+
3
+ import streamlit as st
4
+
5
+
6
+ def render_step_indicator(steps: list, current_step: int):
7
+ """Thin horizontal step indicator bar."""
8
+ items = ""
9
+ for i, step in enumerate(steps):
10
+ cls = "step-done" if i < current_step else ("step-active" if i == current_step else "step-pending")
11
+ icon = step["icon"]
12
+ items += (
13
+ f'<div class="step-item {cls}">'
14
+ f'<span class="step-icon">{icon}</span>'
15
+ f'<div class="step-label">{step["label"]}</div>'
16
+ f'<div class="step-bar"></div></div>'
17
+ )
18
+ st.markdown(f'<div class="step-indicator">{items}</div>', unsafe_allow_html=True)
19
+
20
+
21
+ def render_navigation(steps: list, current_step: int):
22
+ """Previous / Next navigation buttons at the bottom of each page."""
23
+ total = len(steps)
24
+ if total <= 1:
25
+ return
26
+
27
+ st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
28
+
29
+ is_first = current_step <= 0
30
+ is_last = current_step >= total - 1
31
+
32
+ prev_label = f"Previous: {steps[current_step - 1]['label']}" if not is_first else ""
33
+ next_label = f"Next: {steps[current_step + 1]['label']}" if not is_last else ""
34
+
35
+ col_prev, col_spacer, col_next = st.columns([1, 3, 1])
36
+ with col_prev:
37
+ if not is_first:
38
+ if st.button(prev_label, key="nav_prev", use_container_width=True):
39
+ st.session_state["current_step"] = current_step - 1
40
+ st.rerun()
41
+ with col_next:
42
+ if not is_last:
43
+ if st.button(next_label, key="nav_next", type="primary", use_container_width=True):
44
+ st.session_state["current_step"] = current_step + 1
45
+ st.rerun()
@@ -0,0 +1,177 @@
1
+ """Zone drawing component using streamlit-drawable-canvas for bi-directional data."""
2
+
3
+ import json
4
+ import io
5
+ import numpy as np
6
+ import streamlit as st
7
+ from PIL import Image
8
+
9
+ try:
10
+ # Patch for streamlit-drawable-canvas 0.9.x + Streamlit 1.30+
11
+ # st.elements.image.image_to_url was removed; provide a shim
12
+ import streamlit.elements.image as _img_mod
13
+ if not hasattr(_img_mod, "image_to_url"):
14
+ def _image_to_url(image, width, clamp, channels, output_format, image_id, allow_emoji=True):
15
+ """Compatibility shim for removed image_to_url."""
16
+ import base64
17
+ buf = io.BytesIO()
18
+ if isinstance(image, Image.Image):
19
+ image.save(buf, format="PNG")
20
+ elif isinstance(image, np.ndarray):
21
+ Image.fromarray(image).save(buf, format="PNG")
22
+ else:
23
+ return ""
24
+ b64 = base64.b64encode(buf.getvalue()).decode()
25
+ return f"data:image/png;base64,{b64}"
26
+ _img_mod.image_to_url = _image_to_url
27
+
28
+ from streamlit_drawable_canvas import st_canvas
29
+ _HAS_CANVAS = True
30
+ except (ImportError, Exception):
31
+ _HAS_CANVAS = False
32
+
33
+
34
+ def zone_drawer(
35
+ image: Image.Image | None = None,
36
+ mode: str = "polygon",
37
+ points: list | None = None,
38
+ closed: bool = False,
39
+ display_width: int = 700,
40
+ key: str | None = None,
41
+ ) -> dict | None:
42
+ if not _HAS_CANVAS:
43
+ st.warning("Install `streamlit-drawable-canvas` for interactive drawing.")
44
+ return None
45
+
46
+ _MODE_MAP = {"polygon": "polygon", "rect": "rect", "line": "line"}
47
+ canvas_mode = _MODE_MAP.get(mode, "polygon")
48
+
49
+ if image is not None:
50
+ aspect = image.height / image.width
51
+ display_height = int(display_width * aspect)
52
+ bg = image.resize((display_width, display_height))
53
+ else:
54
+ display_height = int(display_width * 0.6)
55
+ bg = Image.new("RGB", (display_width, display_height), (26, 26, 46))
56
+
57
+ initial_drawing = None
58
+ if points and len(points) >= 2:
59
+ if image:
60
+ sx = display_width / image.width
61
+ sy = display_height / image.height
62
+ else:
63
+ sx = sy = 1.0
64
+ scaled = [[p[0] * sx, p[1] * sy] for p in points]
65
+ initial_drawing = _build_initial_drawing(scaled, mode, closed, display_width, display_height)
66
+
67
+ canvas_result = st_canvas(
68
+ fill_color="rgba(124, 58, 237, 0.15)",
69
+ stroke_width=3,
70
+ stroke_color="#7C3AED",
71
+ background_image=bg,
72
+ drawing_mode=canvas_mode,
73
+ point_display_radius=0,
74
+ width=display_width,
75
+ height=display_height,
76
+ key=key or "zone_canvas",
77
+ initial_drawing=initial_drawing,
78
+ )
79
+
80
+ if canvas_result is None or canvas_result.json_data is None:
81
+ return None
82
+
83
+ objects = canvas_result.json_data.get("objects", [])
84
+ if not objects:
85
+ return None
86
+
87
+ extracted_points = []
88
+ if image:
89
+ sx = image.width / display_width
90
+ sy = image.height / display_height
91
+ else:
92
+ sx = sy = 1.0
93
+
94
+ for obj in objects:
95
+ obj_type = obj.get("type", "")
96
+ left = obj.get("left", 0)
97
+ top = obj.get("top", 0)
98
+
99
+ if obj_type == "rect":
100
+ w = obj.get("width", 0) * obj.get("scaleX", 1)
101
+ h = obj.get("height", 0) * obj.get("scaleY", 1)
102
+ extracted_points = [
103
+ [int((left) * sx), int((top) * sy)],
104
+ [int((left + w) * sx), int((top) * sy)],
105
+ [int((left + w) * sx), int((top + h) * sy)],
106
+ [int((left) * sx), int((top + h) * sy)],
107
+ ]
108
+ elif obj_type == "line":
109
+ x1 = obj.get("x1", 0) + left
110
+ y1 = obj.get("y1", 0) + top
111
+ x2 = obj.get("x2", 0) + left
112
+ y2 = obj.get("y2", 0) + top
113
+ extracted_points = [
114
+ [int(x1 * sx), int(y1 * sy)],
115
+ [int(x2 * sx), int(y2 * sy)],
116
+ ]
117
+ elif obj_type == "polygon" or obj_type == "path":
118
+ path = obj.get("path", [])
119
+ for cmd in path:
120
+ if len(cmd) >= 3 and cmd[0] in ("M", "L"):
121
+ extracted_points.append([int((cmd[1] + left) * sx), int((cmd[2] + top) * sy)])
122
+
123
+ if not extracted_points:
124
+ return None
125
+
126
+ return {
127
+ "points": extracted_points,
128
+ "closed": closed or mode in ("rect", "polygon"),
129
+ "mode": mode,
130
+ "displayWidth": display_width,
131
+ "displayHeight": display_height,
132
+ }
133
+
134
+
135
+ def _build_initial_drawing(points, mode, closed, dw, dh):
136
+ objects = []
137
+ if mode == "rect" and len(points) >= 2:
138
+ x1, y1 = points[0]
139
+ x2, y2 = points[-1]
140
+ objects.append({
141
+ "type": "rect",
142
+ "left": min(x1, x2),
143
+ "top": min(y1, y2),
144
+ "width": abs(x2 - x1),
145
+ "height": abs(y2 - y1),
146
+ "fill": "rgba(124, 58, 237, 0.15)",
147
+ "stroke": "#7C3AED",
148
+ "strokeWidth": 3,
149
+ })
150
+ elif mode == "line" and len(points) >= 2:
151
+ objects.append({
152
+ "type": "line",
153
+ "x1": points[0][0],
154
+ "y1": points[0][1],
155
+ "x2": points[1][0],
156
+ "y2": points[1][1],
157
+ "left": 0,
158
+ "top": 0,
159
+ "stroke": "#7C3AED",
160
+ "strokeWidth": 3,
161
+ })
162
+ elif len(points) >= 3:
163
+ path = [["M", points[0][0], points[0][1]]]
164
+ for p in points[1:]:
165
+ path.append(["L", p[0], p[1]])
166
+ if closed:
167
+ path.append(["L", points[0][0], points[0][1]])
168
+ objects.append({
169
+ "type": "path",
170
+ "path": path,
171
+ "left": 0,
172
+ "top": 0,
173
+ "fill": "rgba(124, 58, 237, 0.15)" if closed else "transparent",
174
+ "stroke": "#7C3AED",
175
+ "strokeWidth": 3,
176
+ })
177
+ return {"version": "5.3.0", "objects": objects}