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.
- flashstudio/__init__.py +5 -0
- flashstudio/app.py +194 -0
- flashstudio/cli.py +18 -0
- flashstudio/components/__init__.py +0 -0
- flashstudio/components/project_manager.py +490 -0
- flashstudio/components/sidebar.py +87 -0
- flashstudio/components/styles.py +346 -0
- flashstudio/components/wizard.py +45 -0
- flashstudio/components/zone_drawer/__init__.py +177 -0
- flashstudio/constants.py +245 -0
- flashstudio/core/__init__.py +3 -0
- flashstudio/core/constants.py +3 -0
- flashstudio/launcher.py +288 -0
- flashstudio/pages/__init__.py +8 -0
- flashstudio/pages/dashboard/__init__.py +3 -0
- flashstudio/pages/dashboard/_common.py +10 -0
- flashstudio/pages/dashboard/overview/__init__.py +3 -0
- flashstudio/pages/dashboard/overview/metrics.py +33 -0
- flashstudio/pages/dashboard/overview/pipeline_status.py +35 -0
- flashstudio/pages/dashboard/overview/project_banner.py +23 -0
- flashstudio/pages/dashboard/overview/quick_actions.py +28 -0
- flashstudio/pages/dashboard/overview/recent_runs.py +99 -0
- flashstudio/pages/dashboard/overview/tab.py +35 -0
- flashstudio/pages/dashboard/page.py +17 -0
- flashstudio/pages/data/__init__.py +3 -0
- flashstudio/pages/data/_common.py +107 -0
- flashstudio/pages/data/download/__init__.py +3 -0
- flashstudio/pages/data/download/tab.py +75 -0
- flashstudio/pages/data/helpers.py +338 -0
- flashstudio/pages/data/page.py +50 -0
- flashstudio/pages/data/preview/__init__.py +3 -0
- flashstudio/pages/data/preview/tab.py +139 -0
- flashstudio/pages/data/upload/__init__.py +3 -0
- flashstudio/pages/data/upload/tab.py +156 -0
- flashstudio/pages/data/verify/__init__.py +3 -0
- flashstudio/pages/data/verify/tab.py +108 -0
- flashstudio/pages/export/__init__.py +3 -0
- flashstudio/pages/export/page.py +54 -0
- flashstudio/pages/export/weights/__init__.py +4 -0
- flashstudio/pages/export/weights/exporter.py +60 -0
- flashstudio/pages/export/weights/settings.py +86 -0
- flashstudio/pages/inference/__init__.py +3 -0
- flashstudio/pages/inference/_common.py +76 -0
- flashstudio/pages/inference/data/__init__.py +3 -0
- flashstudio/pages/inference/data/tab.py +121 -0
- flashstudio/pages/inference/model/__init__.py +3 -0
- flashstudio/pages/inference/model/tab.py +62 -0
- flashstudio/pages/inference/page.py +42 -0
- flashstudio/pages/inference/run/__init__.py +3 -0
- flashstudio/pages/inference/run/detection.py +133 -0
- flashstudio/pages/inference/run/tab.py +161 -0
- flashstudio/pages/inference/run/video.py +195 -0
- flashstudio/pages/inference/solution/__init__.py +3 -0
- flashstudio/pages/inference/solution/tab.py +121 -0
- flashstudio/pages/model/__init__.py +3 -0
- flashstudio/pages/model/_common.py +3 -0
- flashstudio/pages/model/advanced/__init__.py +3 -0
- flashstudio/pages/model/advanced/tab.py +54 -0
- flashstudio/pages/model/architecture/__init__.py +3 -0
- flashstudio/pages/model/architecture/tab.py +90 -0
- flashstudio/pages/model/augment/__init__.py +3 -0
- flashstudio/pages/model/augment/tab.py +18 -0
- flashstudio/pages/model/hyperparams/__init__.py +4 -0
- flashstudio/pages/model/hyperparams/tab.py +89 -0
- flashstudio/pages/model/page.py +30 -0
- flashstudio/pages/model/utils/__init__.py +3 -0
- flashstudio/pages/model/utils/config.py +27 -0
- flashstudio/pages/training/__init__.py +3 -0
- flashstudio/pages/training/_common.py +24 -0
- flashstudio/pages/training/launch/__init__.py +3 -0
- flashstudio/pages/training/launch/controls.py +84 -0
- flashstudio/pages/training/launch/dialogs.py +247 -0
- flashstudio/pages/training/launch/preflight.py +77 -0
- flashstudio/pages/training/launch/runner.py +440 -0
- flashstudio/pages/training/launch/tab.py +134 -0
- flashstudio/pages/training/monitor/__init__.py +3 -0
- flashstudio/pages/training/monitor/checkpoints.py +96 -0
- flashstudio/pages/training/monitor/curves.py +113 -0
- flashstudio/pages/training/monitor/dashboard.py +129 -0
- flashstudio/pages/training/monitor/gt_verification.py +122 -0
- flashstudio/pages/training/monitor/log_viewer.py +43 -0
- flashstudio/pages/training/monitor/parsers.py +253 -0
- flashstudio/pages/training/monitor/run_meta.py +165 -0
- flashstudio/pages/training/monitor/tab.py +142 -0
- flashstudio/pages/training/monitor/visualizations.py +52 -0
- flashstudio/pages/training/page.py +134 -0
- flashstudio/utils/__init__.py +152 -0
- flashstudio/utils/config.py +104 -0
- flashstudio/utils/device.py +67 -0
- flashstudio/utils/filesystem.py +67 -0
- flashstudio/utils/jobs.py +95 -0
- flashstudio/utils/training_hooks.py +266 -0
- flashstudio-0.0.1.dist-info/METADATA +107 -0
- flashstudio-0.0.1.dist-info/RECORD +97 -0
- flashstudio-0.0.1.dist-info/WHEEL +5 -0
- flashstudio-0.0.1.dist-info/entry_points.txt +2 -0
- flashstudio-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Ingest helper functions — extraction, conversion, download runners."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from flashstudio.constants import IMG_EXTENSIONS, DEFAULT_DATA_DIR, INFER_NUM_CLASSES
|
|
7
|
+
from flashstudio.pages.data._common import (
|
|
8
|
+
_HAS_FLASHDET, _FLASHDET_INSTALL_MSG,
|
|
9
|
+
_auto_detect_classes, _check_network, _dataset_already_downloaded,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from flashdet import download_dataset
|
|
14
|
+
from flashdet.data import convert_dataset, detect_dataset_format
|
|
15
|
+
except ImportError:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_upload(uploaded_file, split):
|
|
20
|
+
import zipfile
|
|
21
|
+
import tarfile
|
|
22
|
+
import tempfile
|
|
23
|
+
import shutil
|
|
24
|
+
name = uploaded_file.name
|
|
25
|
+
out = os.path.join(DEFAULT_DATA_DIR, "uploaded", split)
|
|
26
|
+
os.makedirs(out, exist_ok=True)
|
|
27
|
+
try:
|
|
28
|
+
if name.endswith(".zip"):
|
|
29
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
|
|
30
|
+
tmp.write(uploaded_file.read()); tmp.flush(); tmp.close()
|
|
31
|
+
uploaded_file.seek(0)
|
|
32
|
+
if not zipfile.is_zipfile(tmp.name):
|
|
33
|
+
os.unlink(tmp.name)
|
|
34
|
+
st.error(f"❌ **Invalid ZIP file:** `{name}` is not a valid zip archive. Please upload a correct file.")
|
|
35
|
+
return
|
|
36
|
+
with zipfile.ZipFile(tmp.name, "r") as zf:
|
|
37
|
+
members = zf.namelist()
|
|
38
|
+
total = len(members)
|
|
39
|
+
if total == 0:
|
|
40
|
+
os.unlink(tmp.name)
|
|
41
|
+
st.error(f"❌ **Empty archive:** `{name}` contains no files.")
|
|
42
|
+
return
|
|
43
|
+
progress = st.progress(0, text=f"Extracting {name} — 0 / {total} files")
|
|
44
|
+
img_count = 0
|
|
45
|
+
for i, member in enumerate(members, 1):
|
|
46
|
+
zf.extract(member, out)
|
|
47
|
+
if member.lower().endswith(IMG_EXTENSIONS):
|
|
48
|
+
img_count += 1
|
|
49
|
+
progress.progress(i / total, text=f"Extracting {name} — {i} / {total} files · {img_count} images")
|
|
50
|
+
progress.progress(1.0, text=f"✓ Extracted {total} files · {img_count} images")
|
|
51
|
+
os.unlink(tmp.name)
|
|
52
|
+
elif name.endswith((".tar", ".tar.gz", ".tgz")) or (name.endswith(".gz") and ".tar." in name):
|
|
53
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False)
|
|
54
|
+
tmp.write(uploaded_file.read()); tmp.flush(); tmp.close()
|
|
55
|
+
uploaded_file.seek(0)
|
|
56
|
+
try:
|
|
57
|
+
with tarfile.open(tmp.name, "r:*") as tf:
|
|
58
|
+
members = tf.getmembers()
|
|
59
|
+
total = len(members)
|
|
60
|
+
if total == 0:
|
|
61
|
+
os.unlink(tmp.name)
|
|
62
|
+
st.error(f"❌ **Empty archive:** `{name}` contains no files.")
|
|
63
|
+
return
|
|
64
|
+
progress = st.progress(0, text=f"Extracting {name} — 0 / {total} files")
|
|
65
|
+
img_count = 0
|
|
66
|
+
for i, member in enumerate(members, 1):
|
|
67
|
+
tf.extract(member, out)
|
|
68
|
+
if member.name.lower().endswith(IMG_EXTENSIONS):
|
|
69
|
+
img_count += 1
|
|
70
|
+
progress.progress(i / total, text=f"Extracting {name} — {i} / {total} files · {img_count} images")
|
|
71
|
+
progress.progress(1.0, text=f"✓ Extracted {total} files · {img_count} images")
|
|
72
|
+
except tarfile.TarError:
|
|
73
|
+
os.unlink(tmp.name)
|
|
74
|
+
st.error(f"❌ **Invalid TAR file:** `{name}` is not a valid tar archive. Please upload a correct file.")
|
|
75
|
+
return
|
|
76
|
+
os.unlink(tmp.name)
|
|
77
|
+
else:
|
|
78
|
+
st.error(f"Unsupported: {name}")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# --- Post-extraction: validate, detect format, find images ---
|
|
82
|
+
if not os.listdir(out):
|
|
83
|
+
shutil.rmtree(out, ignore_errors=True)
|
|
84
|
+
st.error(f"❌ **Empty archive:** `{name}` contains no files. Please upload a valid dataset zip.")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
root = _find_root(out)
|
|
88
|
+
st.session_state["dataset_output_path"] = root
|
|
89
|
+
|
|
90
|
+
fmt = "unknown"
|
|
91
|
+
if _HAS_FLASHDET:
|
|
92
|
+
fmt = detect_dataset_format(root)
|
|
93
|
+
st.session_state["detected_format"] = fmt
|
|
94
|
+
|
|
95
|
+
if fmt in ("txt", "voc"):
|
|
96
|
+
_convert_to_coco(root, split, fmt)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
img_dir = _find_images(root)
|
|
100
|
+
if img_dir:
|
|
101
|
+
st.session_state[f"{split}_img_path"] = img_dir
|
|
102
|
+
ann = _find_ann(root)
|
|
103
|
+
if ann:
|
|
104
|
+
st.session_state[f"{split}_ann_path"] = ann
|
|
105
|
+
label_dir = _find_labels(root)
|
|
106
|
+
if label_dir:
|
|
107
|
+
st.session_state[f"{split}_label_path"] = label_dir
|
|
108
|
+
st.session_state["dataset_name"] = name.rsplit(".", 1)[0]
|
|
109
|
+
|
|
110
|
+
parent = os.path.dirname(img_dir)
|
|
111
|
+
for sibling, key in [("valid", "val"), ("val", "val"), ("test", "test"), ("train", "train")]:
|
|
112
|
+
sib_path = os.path.join(parent, sibling)
|
|
113
|
+
if os.path.isdir(sib_path) and key != split:
|
|
114
|
+
sk = "val_img_path" if key in ("val",) else f"{key}_img_path"
|
|
115
|
+
if not st.session_state.get(sk):
|
|
116
|
+
st.session_state[sk] = sib_path
|
|
117
|
+
|
|
118
|
+
detected_cls = _auto_detect_classes()
|
|
119
|
+
_existing_cls = st.session_state.get("class_names", "")
|
|
120
|
+
if isinstance(_existing_cls, list):
|
|
121
|
+
_existing_cls = "\n".join(_existing_cls)
|
|
122
|
+
if detected_cls and not _existing_cls.strip():
|
|
123
|
+
st.session_state["class_names"] = "\n".join(detected_cls)
|
|
124
|
+
st.session_state["num_classes"] = len(detected_cls)
|
|
125
|
+
|
|
126
|
+
from flashstudio.utils import flash
|
|
127
|
+
flash(f"Extracted `{name}` to {img_dir}", "success")
|
|
128
|
+
st.rerun()
|
|
129
|
+
else:
|
|
130
|
+
st.session_state[f"{split}_img_path"] = root
|
|
131
|
+
st.session_state["dataset_name"] = name.rsplit(".", 1)[0]
|
|
132
|
+
from flashstudio.utils import flash
|
|
133
|
+
flash(f"Extracted `{name}` (no images dir found, using root)", "warning")
|
|
134
|
+
st.rerun()
|
|
135
|
+
except Exception as e:
|
|
136
|
+
st.error(f"Extract failed: {e}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _find_root(base):
|
|
140
|
+
entries = os.listdir(base)
|
|
141
|
+
if len(entries) == 1:
|
|
142
|
+
s = os.path.join(base, entries[0])
|
|
143
|
+
if os.path.isdir(s):
|
|
144
|
+
return s
|
|
145
|
+
for e in entries:
|
|
146
|
+
full = os.path.join(base, e)
|
|
147
|
+
if os.path.isdir(full) and _HAS_FLASHDET and detect_dataset_format(full) != "unknown":
|
|
148
|
+
return full
|
|
149
|
+
return base
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _convert_to_coco(src, split, src_fmt):
|
|
153
|
+
if not _HAS_FLASHDET:
|
|
154
|
+
st.error(_FLASHDET_INSTALL_MSG)
|
|
155
|
+
return
|
|
156
|
+
out = os.path.join(DEFAULT_DATA_DIR, "uploaded", f"{split}_coco")
|
|
157
|
+
cls_names = None
|
|
158
|
+
raw = st.session_state.get("class_names", "")
|
|
159
|
+
if isinstance(raw, list):
|
|
160
|
+
raw = "\n".join(raw)
|
|
161
|
+
if raw.strip():
|
|
162
|
+
cls_names = [c.strip() for c in raw.strip().replace(",", "\n").split("\n") if c.strip()]
|
|
163
|
+
with st.spinner("Converting to COCO..."):
|
|
164
|
+
try:
|
|
165
|
+
stats = convert_dataset(source_dir=src, output_dir=out, target_format="coco", class_names=cls_names)
|
|
166
|
+
if stats.get("status") == "already_in_target_format":
|
|
167
|
+
out = src
|
|
168
|
+
st.session_state["dataset_output_path"] = out
|
|
169
|
+
st.session_state["ann_format"] = "COCO JSON"
|
|
170
|
+
t = os.path.join(out, "train")
|
|
171
|
+
v = os.path.join(out, "valid")
|
|
172
|
+
if not os.path.isdir(v):
|
|
173
|
+
v = os.path.join(out, "val")
|
|
174
|
+
if os.path.isdir(t):
|
|
175
|
+
st.session_state[f"{split}_img_path"] = t
|
|
176
|
+
if os.path.isdir(v):
|
|
177
|
+
st.session_state["val_img_path"] = v
|
|
178
|
+
from flashstudio.utils import flash
|
|
179
|
+
flash("Converted to COCO format", "success")
|
|
180
|
+
st.rerun()
|
|
181
|
+
except Exception as e:
|
|
182
|
+
st.error(f"Conversion failed: {e}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _find_images(base):
|
|
186
|
+
best = None
|
|
187
|
+
best_count = 0
|
|
188
|
+
for root, dirs, files in os.walk(base):
|
|
189
|
+
img_count = len([f for f in files if f.lower().endswith(IMG_EXTENSIONS)])
|
|
190
|
+
if img_count > best_count:
|
|
191
|
+
best = root
|
|
192
|
+
best_count = img_count
|
|
193
|
+
for d in dirs:
|
|
194
|
+
if d.lower() in ("images", "imgs"):
|
|
195
|
+
c = os.path.join(root, d)
|
|
196
|
+
for sub_root, _, sub_files in os.walk(c):
|
|
197
|
+
sc = len([f for f in sub_files if f.lower().endswith(IMG_EXTENSIONS)])
|
|
198
|
+
if sc > best_count:
|
|
199
|
+
best = sub_root
|
|
200
|
+
best_count = sc
|
|
201
|
+
return best if best_count >= 1 else None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _find_ann(base):
|
|
205
|
+
for root, _, files in os.walk(base):
|
|
206
|
+
for f in files:
|
|
207
|
+
if f.endswith(".json") and ("annotation" in f.lower() or "coco" in f.lower()):
|
|
208
|
+
return os.path.join(root, f)
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _find_labels(base):
|
|
213
|
+
"""Find YOLO-format label directory (folder named 'labels' with .txt files)."""
|
|
214
|
+
for root, dirs, files in os.walk(base):
|
|
215
|
+
for d in dirs:
|
|
216
|
+
if d.lower() == "labels":
|
|
217
|
+
lbl_dir = os.path.join(root, d)
|
|
218
|
+
for sub_root, _, sub_files in os.walk(lbl_dir):
|
|
219
|
+
if any(f.endswith(".txt") for f in sub_files):
|
|
220
|
+
return sub_root
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _use_existing(path, ds_info):
|
|
225
|
+
from flashstudio.utils import flash
|
|
226
|
+
st.session_state["dataset_output_path"] = path
|
|
227
|
+
name = ds_info.get("name", os.path.basename(path))
|
|
228
|
+
st.session_state["dataset_name"] = name
|
|
229
|
+
st.session_state["dataset_classes"] = ds_info.get("classes", ds_info.get("cls", INFER_NUM_CLASSES))
|
|
230
|
+
if ds_info.get("id"):
|
|
231
|
+
st.session_state["dataset_id"] = ds_info["id"]
|
|
232
|
+
t = os.path.join(path, "train")
|
|
233
|
+
v = os.path.join(path, "valid")
|
|
234
|
+
if not os.path.isdir(v):
|
|
235
|
+
v = os.path.join(path, "val")
|
|
236
|
+
if os.path.isdir(t):
|
|
237
|
+
st.session_state["train_img_path"] = t
|
|
238
|
+
if os.path.isdir(v):
|
|
239
|
+
st.session_state["val_img_path"] = v
|
|
240
|
+
|
|
241
|
+
detected_cls = _auto_detect_classes()
|
|
242
|
+
if detected_cls:
|
|
243
|
+
st.session_state["class_names"] = "\n".join(detected_cls)
|
|
244
|
+
st.session_state["num_classes"] = len(detected_cls)
|
|
245
|
+
|
|
246
|
+
flash(f"Dataset `{name}` loaded", "success")
|
|
247
|
+
st.rerun()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _run_flashdet_download(dataset_id):
|
|
251
|
+
if not _HAS_FLASHDET:
|
|
252
|
+
st.error(_FLASHDET_INSTALL_MSG)
|
|
253
|
+
return
|
|
254
|
+
out = st.session_state.get("download_output_dir") or os.path.join(DEFAULT_DATA_DIR, dataset_id)
|
|
255
|
+
existing = _dataset_already_downloaded(dataset_id, out)
|
|
256
|
+
if existing:
|
|
257
|
+
st.session_state["dataset_output_path"] = existing
|
|
258
|
+
for d, k in [("train", "train_img_path"), ("valid", "val_img_path")]:
|
|
259
|
+
p = os.path.join(existing, d)
|
|
260
|
+
if os.path.isdir(p):
|
|
261
|
+
st.session_state[k] = p
|
|
262
|
+
detected_cls = _auto_detect_classes()
|
|
263
|
+
if detected_cls:
|
|
264
|
+
st.session_state["class_names"] = "\n".join(detected_cls)
|
|
265
|
+
st.session_state["num_classes"] = len(detected_cls)
|
|
266
|
+
st.rerun()
|
|
267
|
+
return
|
|
268
|
+
if not _check_network():
|
|
269
|
+
st.error("Network unavailable.")
|
|
270
|
+
return
|
|
271
|
+
with st.spinner(f"Downloading {dataset_id}..."):
|
|
272
|
+
try:
|
|
273
|
+
result = download_dataset(dataset_id=dataset_id, output_dir=out)
|
|
274
|
+
st.session_state["dataset_output_path"] = result
|
|
275
|
+
for d, k in [("train", "train_img_path"), ("valid", "val_img_path")]:
|
|
276
|
+
p = os.path.join(result, d)
|
|
277
|
+
if os.path.isdir(p):
|
|
278
|
+
st.session_state[k] = p
|
|
279
|
+
detected_cls = _auto_detect_classes()
|
|
280
|
+
if detected_cls:
|
|
281
|
+
st.session_state["class_names"] = "\n".join(detected_cls)
|
|
282
|
+
st.session_state["num_classes"] = len(detected_cls)
|
|
283
|
+
from flashstudio.utils import flash
|
|
284
|
+
flash(f"Downloaded `{dataset_id}` successfully", "success")
|
|
285
|
+
st.rerun()
|
|
286
|
+
except Exception as e:
|
|
287
|
+
st.error(f"Download failed: {e}")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _run_external_download(ds):
|
|
291
|
+
import urllib.request
|
|
292
|
+
import urllib.error
|
|
293
|
+
import zipfile
|
|
294
|
+
import tempfile
|
|
295
|
+
url, name = ds.get("url", ""), ds.get("name", "dataset")
|
|
296
|
+
if not url or "VisDrone" in url:
|
|
297
|
+
st.info(f"Manual: [{url}]({url})")
|
|
298
|
+
return
|
|
299
|
+
out = os.path.join(DEFAULT_DATA_DIR, name.lower().replace(" ", "_").replace("(", "").replace(")", ""))
|
|
300
|
+
if os.path.isdir(out) and os.listdir(out):
|
|
301
|
+
st.session_state.update({"dataset_output_path": out, "dataset_name": name})
|
|
302
|
+
st.rerun()
|
|
303
|
+
return
|
|
304
|
+
with st.spinner(f"Downloading {name}..."):
|
|
305
|
+
tmp_path = None
|
|
306
|
+
try:
|
|
307
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
|
|
308
|
+
tmp_path = tmp.name; tmp.close()
|
|
309
|
+
urllib.request.urlretrieve(url, tmp_path)
|
|
310
|
+
if os.path.getsize(tmp_path) < 1000 or not zipfile.is_zipfile(tmp_path):
|
|
311
|
+
st.error(f"Invalid download. Manual: [{url}]({url})")
|
|
312
|
+
return
|
|
313
|
+
os.makedirs(out, exist_ok=True)
|
|
314
|
+
with zipfile.ZipFile(tmp_path, "r") as zf:
|
|
315
|
+
members = zf.namelist()
|
|
316
|
+
total = len(members)
|
|
317
|
+
progress = st.progress(0, text=f"Extracting {name} — 0 / {total}")
|
|
318
|
+
img_count = 0
|
|
319
|
+
for i, member in enumerate(members, 1):
|
|
320
|
+
zf.extract(member, out)
|
|
321
|
+
if member.lower().endswith(IMG_EXTENSIONS):
|
|
322
|
+
img_count += 1
|
|
323
|
+
progress.progress(i / total, text=f"Extracting {name} — {i} / {total} files · {img_count} images")
|
|
324
|
+
progress.progress(1.0, text=f"Extracted {total} files · {img_count} images")
|
|
325
|
+
st.session_state.update({"dataset_output_path": out, "dataset_name": name})
|
|
326
|
+
for d, k in [("train", "train_img_path"), ("valid", "val_img_path")]:
|
|
327
|
+
p = os.path.join(out, d)
|
|
328
|
+
if os.path.isdir(p):
|
|
329
|
+
st.session_state[k] = p
|
|
330
|
+
st.rerun()
|
|
331
|
+
except Exception as e:
|
|
332
|
+
st.error(f"Error: {e}")
|
|
333
|
+
finally:
|
|
334
|
+
if tmp_path and os.path.exists(tmp_path):
|
|
335
|
+
try:
|
|
336
|
+
os.unlink(tmp_path)
|
|
337
|
+
except OSError:
|
|
338
|
+
pass
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""FlashStudio — Data Page (ultra-compact, no-scroll)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from flashstudio.constants import IMG_EXTENSIONS
|
|
7
|
+
from flashstudio.pages.data.upload.tab import _render_upload
|
|
8
|
+
from flashstudio.pages.data.download.tab import _render_download
|
|
9
|
+
from flashstudio.pages.data.preview.tab import _render_preview
|
|
10
|
+
from flashstudio.pages.data.verify.tab import _render_verify
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_data_page():
|
|
14
|
+
from flashstudio.components.styles import render_page_header
|
|
15
|
+
from flashstudio.utils import show_flashes
|
|
16
|
+
render_page_header("", "Data")
|
|
17
|
+
show_flashes()
|
|
18
|
+
|
|
19
|
+
# Status banner
|
|
20
|
+
dataset = st.session_state.get("dataset_name")
|
|
21
|
+
if dataset:
|
|
22
|
+
train_path = st.session_state.get("train_img_path", "")
|
|
23
|
+
val_path = st.session_state.get("val_img_path", "")
|
|
24
|
+
tc = len([f for f in os.listdir(train_path) if f.lower().endswith(IMG_EXTENSIONS)]) if train_path and os.path.isdir(train_path) else 0
|
|
25
|
+
vc = len([f for f in os.listdir(val_path) if f.lower().endswith(IMG_EXTENSIONS)]) if val_path and os.path.isdir(val_path) else 0
|
|
26
|
+
dc1, dc2 = st.columns([8, 1])
|
|
27
|
+
with dc1:
|
|
28
|
+
st.success(f"**{dataset}** — {tc} train / {vc} val images")
|
|
29
|
+
with dc2:
|
|
30
|
+
if st.button("Clear", key="clear_dataset_btn", use_container_width=True):
|
|
31
|
+
from flashstudio.utils import flash
|
|
32
|
+
for k in ["dataset_name", "train_img_path", "val_img_path",
|
|
33
|
+
"train_label_path", "val_label_path",
|
|
34
|
+
"dataset_output_path", "dataset_id", "dataset_classes",
|
|
35
|
+
"class_names", "num_classes",
|
|
36
|
+
"detected_format", "_last_verify_ok"]:
|
|
37
|
+
st.session_state.pop(k, None)
|
|
38
|
+
flash("Dataset cleared", "info")
|
|
39
|
+
st.rerun()
|
|
40
|
+
|
|
41
|
+
tab_upload, tab_download, tab_preview, tab_verify = st.tabs(["Upload", "Download", "Preview", "Verify"])
|
|
42
|
+
|
|
43
|
+
with tab_upload:
|
|
44
|
+
_render_upload()
|
|
45
|
+
with tab_download:
|
|
46
|
+
_render_download()
|
|
47
|
+
with tab_preview:
|
|
48
|
+
_render_preview()
|
|
49
|
+
with tab_verify:
|
|
50
|
+
_render_verify()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Preview tab — image display with COCO/YOLO bbox overlay."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from flashstudio.constants import IMG_EXTENSIONS, BBOX_COLORS_RGB, MAX_PREVIEW_COLS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _render_preview():
|
|
11
|
+
train_path = st.session_state.get("train_img_path", "")
|
|
12
|
+
val_path = st.session_state.get("val_img_path", "")
|
|
13
|
+
if not train_path or not os.path.isdir(train_path):
|
|
14
|
+
st.info("No dataset loaded yet.")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
c1, c2, c3 = st.columns([2, 1, 1])
|
|
18
|
+
with c1:
|
|
19
|
+
split = st.radio("Split", ["Train", "Val"], horizontal=True, key="prev_split")
|
|
20
|
+
preview_path = train_path if split == "Train" else val_path
|
|
21
|
+
if not preview_path or not os.path.isdir(preview_path):
|
|
22
|
+
st.warning("Path not set.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
all_imgs = sorted([f for f in os.listdir(preview_path) if f.lower().endswith(IMG_EXTENSIONS)])
|
|
26
|
+
if not all_imgs:
|
|
27
|
+
st.warning("No images.")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
with c2:
|
|
31
|
+
n = st.slider("Count", 1, min(12, len(all_imgs)), min(4, len(all_imgs)), key="prev_n")
|
|
32
|
+
with c3:
|
|
33
|
+
bbox = st.checkbox("Boxes", value=True, key="prev_bb")
|
|
34
|
+
|
|
35
|
+
if "prev_start" not in st.session_state:
|
|
36
|
+
st.session_state["prev_start"] = 0
|
|
37
|
+
if st.session_state["prev_start"] >= len(all_imgs):
|
|
38
|
+
st.session_state["prev_start"] = 0
|
|
39
|
+
start = st.session_state["prev_start"]
|
|
40
|
+
|
|
41
|
+
nav_prev, nav_info, nav_next = st.columns([1, 3, 1])
|
|
42
|
+
with nav_prev:
|
|
43
|
+
if st.button("Prev", disabled=(start <= 0), key="prev_btn"):
|
|
44
|
+
st.session_state["prev_start"] = max(0, start - n)
|
|
45
|
+
st.rerun()
|
|
46
|
+
with nav_info:
|
|
47
|
+
end_idx = min(start + n, len(all_imgs))
|
|
48
|
+
st.markdown(f'<p style="text-align:center;margin:0;padding:0.3rem 0;color:#6B7280;">'
|
|
49
|
+
f'{start + 1}–{end_idx} of {len(all_imgs)}</p>', unsafe_allow_html=True)
|
|
50
|
+
with nav_next:
|
|
51
|
+
if st.button("Next", disabled=(start + n >= len(all_imgs)), key="next_btn"):
|
|
52
|
+
st.session_state["prev_start"] = min(start + n, max(len(all_imgs) - n, 0))
|
|
53
|
+
st.rerun()
|
|
54
|
+
|
|
55
|
+
# Load COCO annotations if available
|
|
56
|
+
ann_file = os.path.join(preview_path, "_annotations.coco.json")
|
|
57
|
+
ann_data = None
|
|
58
|
+
if os.path.isfile(ann_file):
|
|
59
|
+
try:
|
|
60
|
+
with open(ann_file) as f:
|
|
61
|
+
ann_data = json.load(f)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
img_id_map, ann_by_img, cat_map = {}, {}, {}
|
|
66
|
+
if ann_data:
|
|
67
|
+
for c in ann_data.get("categories", []):
|
|
68
|
+
cat_map[c["id"]] = c["name"]
|
|
69
|
+
for im in ann_data.get("images", []):
|
|
70
|
+
img_id_map[im["file_name"]] = im["id"]
|
|
71
|
+
for a in ann_data.get("annotations", []):
|
|
72
|
+
ann_by_img.setdefault(a["image_id"], []).append(a)
|
|
73
|
+
|
|
74
|
+
# YOLO label directory — check sibling 'labels' folder or session state
|
|
75
|
+
yolo_label_dir = st.session_state.get(f"{split.lower()}_label_path", "")
|
|
76
|
+
if not yolo_label_dir or not os.path.isdir(yolo_label_dir):
|
|
77
|
+
candidate = preview_path.replace("/images/", "/labels/").replace("\\images\\", "\\labels\\")
|
|
78
|
+
if os.path.isdir(candidate):
|
|
79
|
+
yolo_label_dir = candidate
|
|
80
|
+
else:
|
|
81
|
+
yolo_label_dir = ""
|
|
82
|
+
|
|
83
|
+
from PIL import Image, ImageDraw
|
|
84
|
+
selected = all_imgs[start:start + n]
|
|
85
|
+
|
|
86
|
+
total_boxes = 0
|
|
87
|
+
for row in range(0, len(selected), MAX_PREVIEW_COLS):
|
|
88
|
+
cols = st.columns(MAX_PREVIEW_COLS)
|
|
89
|
+
for j, col in enumerate(cols):
|
|
90
|
+
idx = row + j
|
|
91
|
+
if idx >= len(selected):
|
|
92
|
+
break
|
|
93
|
+
img_name = selected[idx]
|
|
94
|
+
with col:
|
|
95
|
+
try:
|
|
96
|
+
img = Image.open(os.path.join(preview_path, img_name)).convert("RGB")
|
|
97
|
+
w_img, h_img = img.size
|
|
98
|
+
if bbox:
|
|
99
|
+
draw = ImageDraw.Draw(img)
|
|
100
|
+
drawn = False
|
|
101
|
+
if ann_data and img_name in img_id_map:
|
|
102
|
+
for a in ann_by_img.get(img_id_map[img_name], []):
|
|
103
|
+
b = a.get("bbox", [])
|
|
104
|
+
if len(b) == 4:
|
|
105
|
+
x, y, w, h = b
|
|
106
|
+
color = BBOX_COLORS_RGB[a.get("category_id", 0) % len(BBOX_COLORS_RGB)]
|
|
107
|
+
draw.rectangle([x, y, x+w, y+h], outline=color, width=2)
|
|
108
|
+
total_boxes += 1
|
|
109
|
+
drawn = True
|
|
110
|
+
if not drawn and yolo_label_dir:
|
|
111
|
+
lbl_name = os.path.splitext(img_name)[0] + ".txt"
|
|
112
|
+
lbl_path = os.path.join(yolo_label_dir, lbl_name)
|
|
113
|
+
if os.path.isfile(lbl_path):
|
|
114
|
+
with open(lbl_path) as lf:
|
|
115
|
+
for line in lf:
|
|
116
|
+
parts = line.strip().split()
|
|
117
|
+
if len(parts) >= 5:
|
|
118
|
+
cls_id = int(parts[0])
|
|
119
|
+
cx, cy, bw, bh = float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
|
|
120
|
+
x1 = int((cx - bw / 2) * w_img)
|
|
121
|
+
y1 = int((cy - bh / 2) * h_img)
|
|
122
|
+
x2 = int((cx + bw / 2) * w_img)
|
|
123
|
+
y2 = int((cy + bh / 2) * h_img)
|
|
124
|
+
color = BBOX_COLORS_RGB[cls_id % len(BBOX_COLORS_RGB)]
|
|
125
|
+
draw.rectangle([x1, y1, x2, y2], outline=color, width=2)
|
|
126
|
+
total_boxes += 1
|
|
127
|
+
st.image(img, caption=f"{img_name[:15]} {w_img}x{h_img}", use_container_width=True)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
st.error(str(e)[:40])
|
|
130
|
+
|
|
131
|
+
m1, m2, m3 = st.columns(3)
|
|
132
|
+
with m1:
|
|
133
|
+
st.metric("Images", len(all_imgs))
|
|
134
|
+
with m2:
|
|
135
|
+
ann_count = len(ann_data.get("annotations", [])) if ann_data else total_boxes
|
|
136
|
+
st.metric("Annotations", ann_count)
|
|
137
|
+
with m3:
|
|
138
|
+
cat_count = len(cat_map) if cat_map else "—"
|
|
139
|
+
st.metric("Categories", cat_count)
|