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,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,3 @@
1
+ """Inference — Model tab sub-package."""
2
+
3
+ from flashstudio.pages.inference.model.tab import _tab_model # noqa: F401
@@ -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;">&#9679;</span>' if ok else '<span style="color:#EF4444;">&#9679;</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,3 @@
1
+ """Inference — Run tab sub-package."""
2
+
3
+ from flashstudio.pages.inference.run.tab import _tab_run # noqa: F401
@@ -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']}`")