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,121 @@
|
|
|
1
|
+
"""Inference — Data tab."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from flashstudio.constants import (
|
|
8
|
+
INFER_MAX_FRAMES, INFER_STREAM_DURATION,
|
|
9
|
+
INFER_FRAME_SKIP, MAX_PREVIEW_IMAGES,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _tab_data():
|
|
14
|
+
input_type = st.radio("Input", ["Images", "Video", "RTSP"], horizontal=True, key="infer_input_type")
|
|
15
|
+
|
|
16
|
+
if input_type == "Images":
|
|
17
|
+
uploaded = st.file_uploader("Images", type=["jpg", "jpeg", "png", "webp", "bmp"],
|
|
18
|
+
accept_multiple_files=True, key="infer_images")
|
|
19
|
+
if uploaded:
|
|
20
|
+
cols = st.columns(min(len(uploaded), MAX_PREVIEW_IMAGES))
|
|
21
|
+
for i, f in enumerate(uploaded[:MAX_PREVIEW_IMAGES]):
|
|
22
|
+
with cols[i]:
|
|
23
|
+
st.image(f, caption=f.name[:10], use_container_width=True)
|
|
24
|
+
if len(uploaded) > MAX_PREVIEW_IMAGES:
|
|
25
|
+
st.caption(f"+{len(uploaded) - MAX_PREVIEW_IMAGES} more")
|
|
26
|
+
|
|
27
|
+
elif input_type == "Video":
|
|
28
|
+
vc1, vc2 = st.columns([3, 1])
|
|
29
|
+
with vc1:
|
|
30
|
+
st.file_uploader("Video", type=["mp4", "avi", "mov", "mkv"], key="infer_video")
|
|
31
|
+
vid = st.session_state.get("infer_video")
|
|
32
|
+
if vid:
|
|
33
|
+
st.caption(f"{vid.name} ({vid.size / 1e6:.1f}MB)")
|
|
34
|
+
meta = _get_video_metadata(vid)
|
|
35
|
+
if meta:
|
|
36
|
+
mc = st.columns(4)
|
|
37
|
+
with mc[0]: st.metric("Res", f"{meta['width']}x{meta['height']}")
|
|
38
|
+
with mc[1]: st.metric("FPS", f"{meta['fps']:.0f}")
|
|
39
|
+
with mc[2]: st.metric("Frames", meta["total_frames"])
|
|
40
|
+
with mc[3]: st.metric("Dur", f"{meta['total_frames'] / max(meta['fps'], 1):.0f}s")
|
|
41
|
+
with vc2:
|
|
42
|
+
st.number_input("Max frames (0=all)", 0, 100000, INFER_MAX_FRAMES, key="infer_max_frames")
|
|
43
|
+
st.select_slider("Skip N", [1, 2, 3, 5, 10], value=INFER_FRAME_SKIP, key="infer_frame_skip")
|
|
44
|
+
st.checkbox("Save output", True, key="infer_save_video")
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
sc1, sc2 = st.columns([4, 1])
|
|
48
|
+
with sc1:
|
|
49
|
+
st.text_input("RTSP URL", placeholder="rtsp://192.168.1.100:554/stream", key="infer_stream_url")
|
|
50
|
+
with sc2:
|
|
51
|
+
st.number_input("Duration (s)", 0, 3600, INFER_STREAM_DURATION, key="infer_stream_duration")
|
|
52
|
+
if st.session_state.get("infer_stream_url") and st.button("Test", key="test_stream"):
|
|
53
|
+
_test_stream()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_video_metadata(video_file):
|
|
57
|
+
tmp_path = None
|
|
58
|
+
try:
|
|
59
|
+
import cv2
|
|
60
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
|
61
|
+
tmp_path = tmp.name
|
|
62
|
+
tmp.write(video_file.read()); tmp.flush(); tmp.close()
|
|
63
|
+
video_file.seek(0)
|
|
64
|
+
cap = cv2.VideoCapture(tmp_path)
|
|
65
|
+
if cap.isOpened():
|
|
66
|
+
meta = {"width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
|
|
67
|
+
"fps": cap.get(cv2.CAP_PROP_FPS) or 30, "total_frames": int(cap.get(cv2.CAP_PROP_FRAME_COUNT))}
|
|
68
|
+
cap.release()
|
|
69
|
+
return meta
|
|
70
|
+
cap.release()
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
finally:
|
|
74
|
+
if tmp_path and os.path.exists(tmp_path):
|
|
75
|
+
try:
|
|
76
|
+
os.unlink(tmp_path)
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_first_frame():
|
|
83
|
+
video = st.session_state.get("infer_video")
|
|
84
|
+
if video:
|
|
85
|
+
try:
|
|
86
|
+
import cv2
|
|
87
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
|
88
|
+
tmp.write(video.read()); tmp.flush(); tmp.close()
|
|
89
|
+
video.seek(0)
|
|
90
|
+
cap = cv2.VideoCapture(tmp.name)
|
|
91
|
+
ret, frame = cap.read(); cap.release()
|
|
92
|
+
os.unlink(tmp.name)
|
|
93
|
+
if ret:
|
|
94
|
+
return Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
images = st.session_state.get("infer_images")
|
|
98
|
+
if images:
|
|
99
|
+
try:
|
|
100
|
+
img = Image.open(images[0]).convert("RGB"); images[0].seek(0)
|
|
101
|
+
return img
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _test_stream():
|
|
108
|
+
try:
|
|
109
|
+
import cv2
|
|
110
|
+
cap = cv2.VideoCapture(st.session_state["infer_stream_url"])
|
|
111
|
+
if cap.isOpened():
|
|
112
|
+
ret, frame = cap.read(); cap.release()
|
|
113
|
+
if ret:
|
|
114
|
+
st.success("Connected")
|
|
115
|
+
st.image(Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)), use_container_width=True)
|
|
116
|
+
else:
|
|
117
|
+
st.error("No frame")
|
|
118
|
+
else:
|
|
119
|
+
st.error("Failed to connect")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
st.error(str(e)[:60])
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Inference — Model tab."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import streamlit as st
|
|
5
|
+
from flashstudio.constants import (
|
|
6
|
+
INFER_CONF_THRESHOLD, INFER_NMS_THRESHOLD, INFER_IMG_SIZE,
|
|
7
|
+
INFER_NUM_CLASSES, DEFAULT_SAVE_DIR, BEST_WEIGHT_PRIORITY,
|
|
8
|
+
CKPT_BEST_INFERENCE, FLASHDET_MODELS,
|
|
9
|
+
)
|
|
10
|
+
from flashstudio.pages.inference._common import _get_class_names, _get_device_options
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _tab_model():
|
|
14
|
+
col1, col2 = st.columns(2)
|
|
15
|
+
with col1:
|
|
16
|
+
with st.container(border=True):
|
|
17
|
+
st.markdown("#### Architecture & Params")
|
|
18
|
+
st.selectbox("Model", list(FLASHDET_MODELS.keys()),
|
|
19
|
+
key="infer_model_arch")
|
|
20
|
+
pc1, pc2 = st.columns(2)
|
|
21
|
+
with pc1:
|
|
22
|
+
st.slider("Confidence", 0.0, 1.0, INFER_CONF_THRESHOLD, 0.05, key="infer_conf")
|
|
23
|
+
st.number_input("Img Size", 320, 1920, INFER_IMG_SIZE, 32, key="infer_img_size")
|
|
24
|
+
with pc2:
|
|
25
|
+
st.slider("NMS IoU", 0.0, 1.0, INFER_NMS_THRESHOLD, 0.05, key="infer_nms")
|
|
26
|
+
st.selectbox("Device", _get_device_options(), key="infer_device")
|
|
27
|
+
dataset_nc = st.session_state.get("num_classes", 0)
|
|
28
|
+
default_nc = dataset_nc if dataset_nc > 0 else INFER_NUM_CLASSES
|
|
29
|
+
st.number_input("Classes", 1, 1000, default_nc, key="infer_num_classes")
|
|
30
|
+
filter_classes = _get_class_names()
|
|
31
|
+
st.multiselect("Filter (empty=all)", filter_classes, default=[], key="infer_class_filter")
|
|
32
|
+
|
|
33
|
+
with col2:
|
|
34
|
+
with st.container(border=True):
|
|
35
|
+
st.markdown("#### Weights")
|
|
36
|
+
ws = st.radio("Source", ["Upload", "Path", "Training output"], key="weight_source", horizontal=True)
|
|
37
|
+
if ws == "Upload":
|
|
38
|
+
up = st.file_uploader("File", type=["pt", "pth", "onnx", "engine"], key="infer_weights_file")
|
|
39
|
+
if up:
|
|
40
|
+
st.success(f"{up.name} ({up.size / 1e6:.1f}MB)")
|
|
41
|
+
elif ws == "Path":
|
|
42
|
+
st.text_input("Path", placeholder="/path/best.pth", key="infer_weights_path")
|
|
43
|
+
else:
|
|
44
|
+
sd = st.session_state.get("save_dir", DEFAULT_SAVE_DIR)
|
|
45
|
+
best_path = ""
|
|
46
|
+
for candidate in BEST_WEIGHT_PRIORITY:
|
|
47
|
+
if os.path.isdir(sd):
|
|
48
|
+
for root, _d, files in os.walk(sd):
|
|
49
|
+
if candidate in files:
|
|
50
|
+
best_path = os.path.join(root, candidate)
|
|
51
|
+
break
|
|
52
|
+
if best_path:
|
|
53
|
+
break
|
|
54
|
+
if not best_path:
|
|
55
|
+
best_path = os.path.join(sd, CKPT_BEST_INFERENCE)
|
|
56
|
+
st.session_state["infer_weights_path"] = best_path
|
|
57
|
+
exists = os.path.isfile(best_path)
|
|
58
|
+
if exists:
|
|
59
|
+
sz = os.path.getsize(best_path) / (1024 * 1024)
|
|
60
|
+
st.success(f"`{os.path.basename(best_path)}` ({sz:.1f}MB)")
|
|
61
|
+
else:
|
|
62
|
+
st.caption(f"Not found: `{best_path}`")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""FlashStudio — Inference Page (ultra-compact, no-scroll)."""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
from flashstudio.components.styles import render_page_header
|
|
5
|
+
from flashstudio.pages.inference._common import SOLUTIONS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_inference_page():
|
|
9
|
+
from flashstudio.utils import show_flashes
|
|
10
|
+
from flashstudio.pages.inference.model.tab import _tab_model
|
|
11
|
+
from flashstudio.pages.inference.data.tab import _tab_data
|
|
12
|
+
from flashstudio.pages.inference.solution.tab import _tab_solution
|
|
13
|
+
from flashstudio.pages.inference.run.tab import _tab_run
|
|
14
|
+
|
|
15
|
+
render_page_header("", "Inference")
|
|
16
|
+
show_flashes()
|
|
17
|
+
|
|
18
|
+
has_model = bool(st.session_state.get("infer_weights_file") or st.session_state.get("infer_weights_path"))
|
|
19
|
+
has_data = bool(st.session_state.get("infer_images") or st.session_state.get("infer_video") or st.session_state.get("infer_stream_url"))
|
|
20
|
+
sol = SOLUTIONS.get(st.session_state.get("selected_solution", "None (Detection Only)"), {})
|
|
21
|
+
has_zone = not sol.get("needs_zone") or bool(st.session_state.get("zone_line_points") or st.session_state.get("zone_polygons"))
|
|
22
|
+
def dot(ok):
|
|
23
|
+
return '<span style="color:#22C55E;">●</span>' if ok else '<span style="color:#EF4444;">●</span>'
|
|
24
|
+
st.markdown(
|
|
25
|
+
f'<div class="ds-card-stats" style="padding:0.2rem 0;">'
|
|
26
|
+
f'<span>{dot(has_model)} Model</span>'
|
|
27
|
+
f'<span>{dot(has_data)} Data</span>'
|
|
28
|
+
f'<span>{dot(has_zone)} Zone</span>'
|
|
29
|
+
f'<span style="margin-left:auto;color:#9CA3AF;">Ready: {sum([has_model, has_data, has_zone])}/3</span>'
|
|
30
|
+
f'</div>',
|
|
31
|
+
unsafe_allow_html=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
tab_model, tab_data, tab_solution, tab_run = st.tabs(["Model", "Data", "Solution", "Run"])
|
|
35
|
+
with tab_model:
|
|
36
|
+
_tab_model()
|
|
37
|
+
with tab_data:
|
|
38
|
+
_tab_data()
|
|
39
|
+
with tab_solution:
|
|
40
|
+
_tab_solution()
|
|
41
|
+
with tab_run:
|
|
42
|
+
_tab_run()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Inference — Detection utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import tempfile
|
|
6
|
+
import streamlit as st
|
|
7
|
+
import numpy as np
|
|
8
|
+
from PIL import ImageDraw, ImageFont
|
|
9
|
+
from flashstudio.constants import (
|
|
10
|
+
COCO_CLASSES, BBOX_COLORS_HEX, BBOX_COLORS_RGB,
|
|
11
|
+
INFER_CONF_THRESHOLD, INFER_NMS_THRESHOLD, INFER_IMG_SIZE,
|
|
12
|
+
INFER_DEFAULT_RESOLUTION, FONT_PATH_LINUX, FONT_SIZE_DEFAULT,
|
|
13
|
+
)
|
|
14
|
+
from flashstudio.pages.inference._common import _get_class_names, _HAS_PREDICTOR
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from flashdet import Predictor
|
|
18
|
+
except ImportError:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _detect(image):
|
|
23
|
+
weights = st.session_state.get("infer_weights_path", "")
|
|
24
|
+
if not weights:
|
|
25
|
+
cached = st.session_state.get("_infer_weights_tmp_path", "")
|
|
26
|
+
if cached and os.path.isfile(cached):
|
|
27
|
+
weights = cached
|
|
28
|
+
else:
|
|
29
|
+
up = st.session_state.get("infer_weights_file")
|
|
30
|
+
if up:
|
|
31
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".pth", delete=False)
|
|
32
|
+
tmp.write(up.read()); tmp.flush(); up.seek(0)
|
|
33
|
+
weights = tmp.name
|
|
34
|
+
st.session_state["_infer_weights_tmp_path"] = weights
|
|
35
|
+
if weights:
|
|
36
|
+
try:
|
|
37
|
+
return _detect_real(image, weights)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
from flashstudio.utils import flash
|
|
40
|
+
flash(f"Model error: {e} — showing demo detections", "warning")
|
|
41
|
+
return _detect_demo(image)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _detect_real(image, weights_path):
|
|
45
|
+
if not _HAS_PREDICTOR:
|
|
46
|
+
raise ImportError("Predictor unavailable")
|
|
47
|
+
custom_cls = _get_class_names()
|
|
48
|
+
predictor = Predictor(model_path=weights_path, device=st.session_state.get("infer_device", "cpu").split(" ")[0],
|
|
49
|
+
conf_thresh=st.session_state.get("infer_conf", INFER_CONF_THRESHOLD),
|
|
50
|
+
nms_thresh=st.session_state.get("infer_nms", INFER_NMS_THRESHOLD),
|
|
51
|
+
input_size=st.session_state.get("infer_img_size", INFER_IMG_SIZE),
|
|
52
|
+
class_names=custom_cls if custom_cls != COCO_CLASSES else None)
|
|
53
|
+
raw = predictor(np.array(image))
|
|
54
|
+
class_filter = st.session_state.get("infer_class_filter", [])
|
|
55
|
+
label_list = custom_cls or COCO_CLASSES
|
|
56
|
+
dets = []
|
|
57
|
+
for bbox, score, cls_id in raw:
|
|
58
|
+
name = label_list[int(cls_id)] if int(cls_id) < len(label_list) else f"class_{cls_id}"
|
|
59
|
+
if class_filter and name not in class_filter:
|
|
60
|
+
continue
|
|
61
|
+
dets.append([name, f"{float(score):.2f}", str([int(x) for x in (bbox.tolist() if hasattr(bbox, 'tolist') else list(bbox))])])
|
|
62
|
+
return dets
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _detect_demo(image):
|
|
66
|
+
w, h = image.size
|
|
67
|
+
rng = np.random.default_rng(hash(image.tobytes()[:100]) % (2 ** 31))
|
|
68
|
+
conf_thr = st.session_state.get("infer_conf", INFER_CONF_THRESHOLD)
|
|
69
|
+
class_filter = st.session_state.get("infer_class_filter", [])
|
|
70
|
+
dets = []
|
|
71
|
+
for _ in range(rng.integers(3, 8)):
|
|
72
|
+
cid = rng.integers(0, 10)
|
|
73
|
+
name = COCO_CLASSES[cid]
|
|
74
|
+
if class_filter and name not in class_filter:
|
|
75
|
+
continue
|
|
76
|
+
conf = rng.uniform(0.3, 0.98)
|
|
77
|
+
if conf < conf_thr:
|
|
78
|
+
continue
|
|
79
|
+
x1 = int(rng.integers(0, max(w - 100, 1)))
|
|
80
|
+
y1 = int(rng.integers(0, max(h - 100, 1)))
|
|
81
|
+
dets.append([name, f"{conf:.2f}", str([x1, y1, min(x1 + int(rng.integers(60, 200)), w), min(y1 + int(rng.integers(60, 200)), h)])])
|
|
82
|
+
return sorted(dets, key=lambda x: float(x[1]), reverse=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _draw_boxes_cv2(frame, raw_results):
|
|
86
|
+
"""Draw bounding boxes on a BGR cv2 frame from Predictor output [(bbox, score, cls_id), ...]."""
|
|
87
|
+
import cv2
|
|
88
|
+
label_list = _get_class_names() or COCO_CLASSES
|
|
89
|
+
for bbox, score, cls_id in raw_results:
|
|
90
|
+
x1, y1, x2, y2 = [int(v) for v in (bbox.tolist() if hasattr(bbox, 'tolist') else list(bbox))]
|
|
91
|
+
cid = int(cls_id)
|
|
92
|
+
name = label_list[cid] if cid < len(label_list) else f"cls_{cid}"
|
|
93
|
+
label = f"{name} {float(score):.2f}"
|
|
94
|
+
cv2.rectangle(frame, (x1, y1), (x2, y2), BBOX_COLORS_RGB[cid % len(BBOX_COLORS_RGB)], 2)
|
|
95
|
+
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
|
|
96
|
+
cv2.rectangle(frame, (x1, y1 - th - 6), (x1 + tw + 4, y1), BBOX_COLORS_RGB[cid % len(BBOX_COLORS_RGB)], -1)
|
|
97
|
+
cv2.putText(frame, label, (x1 + 2, y1 - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
|
98
|
+
return frame
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _draw_boxes(image, dets):
|
|
102
|
+
draw = ImageDraw.Draw(image)
|
|
103
|
+
try:
|
|
104
|
+
font = ImageFont.truetype(FONT_PATH_LINUX, FONT_SIZE_DEFAULT)
|
|
105
|
+
except (OSError, IOError):
|
|
106
|
+
font = ImageFont.load_default()
|
|
107
|
+
for i, (cls, conf, bbox_str) in enumerate(dets):
|
|
108
|
+
bbox = json.loads(bbox_str.replace("'", '"'))
|
|
109
|
+
color = BBOX_COLORS_HEX[i % len(BBOX_COLORS_HEX)]
|
|
110
|
+
draw.rectangle(bbox, outline=color, width=3)
|
|
111
|
+
label = f"{cls} {conf}"
|
|
112
|
+
tb = draw.textbbox((0, 0), label, font=font)
|
|
113
|
+
tw, th = tb[2] - tb[0], tb[3] - tb[1]
|
|
114
|
+
ly = max(bbox[1] - th - 6, 0)
|
|
115
|
+
draw.rectangle([bbox[0], ly, bbox[0] + tw + 8, ly + th + 6], fill=color)
|
|
116
|
+
draw.text((bbox[0] + 4, ly + 2), label, fill="white", font=font)
|
|
117
|
+
return image
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _to_coco_format(results):
|
|
121
|
+
coco = {"images": [], "annotations": [],
|
|
122
|
+
"categories": [{"id": i, "name": n} for i, n in enumerate(COCO_CLASSES)]}
|
|
123
|
+
aid = 1
|
|
124
|
+
for iid, r in enumerate(results, 1):
|
|
125
|
+
coco["images"].append({"id": iid, "file_name": r["name"], "width": r.get("width", INFER_DEFAULT_RESOLUTION[0]), "height": r.get("height", INFER_DEFAULT_RESOLUTION[1])})
|
|
126
|
+
for cls, conf, bbox_str in r["dets"]:
|
|
127
|
+
bbox = json.loads(bbox_str.replace("'", '"'))
|
|
128
|
+
coco["annotations"].append({"id": aid, "image_id": iid,
|
|
129
|
+
"category_id": COCO_CLASSES.index(cls) if cls in COCO_CLASSES else 0,
|
|
130
|
+
"bbox": [bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1]],
|
|
131
|
+
"area": (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), "score": float(conf), "iscrowd": 0})
|
|
132
|
+
aid += 1
|
|
133
|
+
return coco
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Inference — Run tab."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
import streamlit as st
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from flashstudio.constants import DEFAULT_MODEL_ARCH
|
|
9
|
+
from flashstudio.pages.inference.run.detection import (
|
|
10
|
+
_detect, _draw_boxes, _to_coco_format,
|
|
11
|
+
)
|
|
12
|
+
from flashstudio.pages.inference.run.video import _run_video
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _tab_run():
|
|
16
|
+
m1, m2, m3, m4 = st.columns(4)
|
|
17
|
+
with m1:
|
|
18
|
+
st.metric("Model", st.session_state.get("infer_model_arch", DEFAULT_MODEL_ARCH).replace("FlashDet-", ""))
|
|
19
|
+
with m2:
|
|
20
|
+
st.metric("Input", st.session_state.get("infer_input_type", "Images"))
|
|
21
|
+
with m3:
|
|
22
|
+
sol_name = st.session_state.get("selected_solution", "Detection")
|
|
23
|
+
sol_display = "Detect" if "Detection Only" in sol_name else sol_name[:12]
|
|
24
|
+
st.metric("Solution", sol_display)
|
|
25
|
+
with m4:
|
|
26
|
+
st.metric("Device", st.session_state.get("infer_device", "cpu").split(" ")[0])
|
|
27
|
+
|
|
28
|
+
input_type = st.session_state.get("infer_input_type", "Images")
|
|
29
|
+
has_data = st.session_state.get("infer_images") or st.session_state.get("infer_video") or st.session_state.get("infer_stream_url")
|
|
30
|
+
if not has_data:
|
|
31
|
+
st.warning("No data. Upload in Data tab.")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
rc1, rc2 = st.columns([1, 3])
|
|
35
|
+
with rc1:
|
|
36
|
+
run = st.button("Run Inference", type="primary", key="run_infer_btn", use_container_width=True)
|
|
37
|
+
with rc2:
|
|
38
|
+
st.checkbox("Per-frame details", False, key="show_per_frame")
|
|
39
|
+
|
|
40
|
+
if run:
|
|
41
|
+
if input_type == "Images":
|
|
42
|
+
_run_images()
|
|
43
|
+
elif input_type == "Video":
|
|
44
|
+
_run_video()
|
|
45
|
+
else:
|
|
46
|
+
st.info("Stream — coming soon")
|
|
47
|
+
|
|
48
|
+
if input_type == "Images":
|
|
49
|
+
_show_image_results()
|
|
50
|
+
else:
|
|
51
|
+
_show_video_results()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_images():
|
|
55
|
+
images = st.session_state.get("infer_images", [])
|
|
56
|
+
if not images:
|
|
57
|
+
return
|
|
58
|
+
results = []
|
|
59
|
+
total_time = 0
|
|
60
|
+
progress = st.progress(0)
|
|
61
|
+
for i, f in enumerate(images):
|
|
62
|
+
img = Image.open(f).convert("RGB")
|
|
63
|
+
t0 = time.perf_counter()
|
|
64
|
+
dets = _detect(img)
|
|
65
|
+
elapsed = time.perf_counter() - t0
|
|
66
|
+
total_time += elapsed
|
|
67
|
+
results.append({"name": f.name, "dets": dets, "annotated": _draw_boxes(img.copy(), dets),
|
|
68
|
+
"inference_time_ms": elapsed * 1000, "width": img.width, "height": img.height})
|
|
69
|
+
progress.progress((i + 1) / len(images))
|
|
70
|
+
progress.empty()
|
|
71
|
+
st.session_state["infer_img_results"] = results
|
|
72
|
+
st.session_state["infer_total_time"] = total_time
|
|
73
|
+
st.rerun()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _show_image_results():
|
|
77
|
+
if "infer_img_results" not in st.session_state:
|
|
78
|
+
return
|
|
79
|
+
results = st.session_state["infer_img_results"]
|
|
80
|
+
total_dets = sum(len(r["dets"]) for r in results)
|
|
81
|
+
total_time = st.session_state.get("infer_total_time", 1)
|
|
82
|
+
|
|
83
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
84
|
+
with c1:
|
|
85
|
+
st.metric("FPS", f"{len(results) / max(total_time, 0.001):.1f}")
|
|
86
|
+
with c2:
|
|
87
|
+
st.metric("Latency", f"{total_time / max(len(results), 1) * 1000:.1f}ms")
|
|
88
|
+
with c3:
|
|
89
|
+
st.metric("Detections", total_dets)
|
|
90
|
+
with c4:
|
|
91
|
+
st.metric("Images", len(results))
|
|
92
|
+
|
|
93
|
+
for r in results:
|
|
94
|
+
ci, ct = st.columns([1, 1])
|
|
95
|
+
with ci:
|
|
96
|
+
st.image(r["annotated"], caption=f"{r['name'][:15]} ({r['inference_time_ms']:.0f}ms)", use_container_width=True)
|
|
97
|
+
with ct:
|
|
98
|
+
if r["dets"]:
|
|
99
|
+
import pandas as pd
|
|
100
|
+
st.dataframe(pd.DataFrame(r["dets"], columns=["Class", "Conf", "BBox"]),
|
|
101
|
+
use_container_width=True, hide_index=True, height=180)
|
|
102
|
+
|
|
103
|
+
with st.expander("Export"):
|
|
104
|
+
ec1, ec2, ec3 = st.columns(3)
|
|
105
|
+
with ec1:
|
|
106
|
+
import pandas as pd
|
|
107
|
+
all_d = [[r["name"]] + d for r in results for d in r["dets"]]
|
|
108
|
+
if all_d:
|
|
109
|
+
st.download_button("CSV", pd.DataFrame(all_d, columns=["File", "Class", "Conf", "BBox"]).to_csv(index=False),
|
|
110
|
+
"detections.csv", "text/csv", use_container_width=True)
|
|
111
|
+
with ec2:
|
|
112
|
+
st.download_button("JSON", json.dumps(_to_coco_format(results), indent=2),
|
|
113
|
+
"coco.json", "application/json", use_container_width=True)
|
|
114
|
+
with ec3:
|
|
115
|
+
if results:
|
|
116
|
+
buf = io.BytesIO()
|
|
117
|
+
results[0]["annotated"].save(buf, format="PNG")
|
|
118
|
+
st.download_button("Image", buf.getvalue(), "annotated.png", "image/png", use_container_width=True)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _show_video_results():
|
|
122
|
+
if "video_results" not in st.session_state:
|
|
123
|
+
return
|
|
124
|
+
res = st.session_state["video_results"]
|
|
125
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
126
|
+
with c1:
|
|
127
|
+
st.metric("Frames", res.get("frames", 0))
|
|
128
|
+
with c2:
|
|
129
|
+
st.metric("Detections", res.get("detections", 0))
|
|
130
|
+
with c3:
|
|
131
|
+
st.metric("FPS", f"{res.get('processing_fps', 0):.1f}")
|
|
132
|
+
with c4:
|
|
133
|
+
st.metric("Time", f"{res.get('elapsed_time', 0):.1f}s")
|
|
134
|
+
|
|
135
|
+
sol = res.get("solution_output", {})
|
|
136
|
+
in_count = sol.get("in_count") or sol.get("in", 0)
|
|
137
|
+
out_count = sol.get("out_count") or sol.get("out", 0)
|
|
138
|
+
if in_count or out_count:
|
|
139
|
+
sc1, sc2, sc3 = st.columns(3)
|
|
140
|
+
with sc1: st.metric("In", in_count)
|
|
141
|
+
with sc2: st.metric("Out", out_count)
|
|
142
|
+
with sc3: st.metric("Net", in_count - out_count)
|
|
143
|
+
region_data = sol.get("region_counts") or {k: v for k, v in sol.items()
|
|
144
|
+
if k not in ("in", "out", "in_count", "out_count", "total", "speeds") and isinstance(v, (int, float))}
|
|
145
|
+
if region_data:
|
|
146
|
+
cols = st.columns(min(len(region_data), 6))
|
|
147
|
+
for i, (r, c) in enumerate(list(region_data.items())[:6]):
|
|
148
|
+
with cols[i]:
|
|
149
|
+
st.metric(r, c)
|
|
150
|
+
if "speeds" in sol:
|
|
151
|
+
import pandas as pd
|
|
152
|
+
st.dataframe(pd.DataFrame(sol["speeds"], columns=["ID", "km/h"]), hide_index=True, height=150)
|
|
153
|
+
|
|
154
|
+
if res.get("frames_preview"):
|
|
155
|
+
cols = st.columns(min(len(res["frames_preview"]), 4))
|
|
156
|
+
for i, frame in enumerate(res["frames_preview"][:4]):
|
|
157
|
+
with cols[i]:
|
|
158
|
+
st.image(frame, use_container_width=True)
|
|
159
|
+
|
|
160
|
+
if res.get("output_path"):
|
|
161
|
+
st.caption(f"Output: `{res['output_path']}`")
|