supervisely 6.73.452__py3-none-any.whl → 6.73.513__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.
- supervisely/__init__.py +25 -1
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +13 -12
- supervisely/api/annotation_api.py +6 -3
- supervisely/api/api.py +2 -0
- supervisely/api/app_api.py +10 -1
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +28 -0
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +4 -0
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +5 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +22 -2
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +795 -199
- supervisely/nn/inference/inference_request.py +42 -9
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
- supervisely/nn/inference/tracking/tracker_interface.py +7 -2
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/prediction_dto.py +12 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/visualize.py +87 -90
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/train_app.py +28 -29
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +40 -11
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import shutil
|
|
2
|
+
import tempfile
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterator, List, Optional, Tuple, Union
|
|
6
|
+
|
|
3
7
|
import cv2
|
|
4
8
|
import ffmpeg
|
|
5
|
-
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
import tempfile
|
|
9
|
-
import shutil
|
|
9
|
+
import numpy as np
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
from supervisely import
|
|
11
|
+
from supervisely import VideoAnnotation, logger
|
|
12
|
+
from supervisely.geometry.geometry import Geometry
|
|
13
13
|
from supervisely.nn.model.prediction import Prediction
|
|
14
|
-
from supervisely import VideoAnnotation
|
|
15
14
|
from supervisely.nn.tracker.utils import predictions_to_video_annotation
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class TrackingVisualizer:
|
|
18
|
+
|
|
19
19
|
def __init__(
|
|
20
20
|
self,
|
|
21
21
|
show_labels: bool = True,
|
|
@@ -29,7 +29,7 @@ class TrackingVisualizer:
|
|
|
29
29
|
codec: str = "mp4",
|
|
30
30
|
output_fps: float = 30.0,
|
|
31
31
|
colorize_tracks: bool = True,
|
|
32
|
-
|
|
32
|
+
trajectory_thickness: int = 2,
|
|
33
33
|
):
|
|
34
34
|
"""
|
|
35
35
|
Initialize the visualizer with configuration.
|
|
@@ -58,6 +58,7 @@ class TrackingVisualizer:
|
|
|
58
58
|
self.text_scale = text_scale
|
|
59
59
|
self.text_thickness = text_thickness
|
|
60
60
|
self.trajectory_length = trajectory_length
|
|
61
|
+
self.trajectory_thickness = trajectory_thickness
|
|
61
62
|
self.colorize_tracks = colorize_tracks
|
|
62
63
|
|
|
63
64
|
# Output settings
|
|
@@ -71,7 +72,7 @@ class TrackingVisualizer:
|
|
|
71
72
|
self.track_colors = {}
|
|
72
73
|
self.color_palette = self._generate_color_palette()
|
|
73
74
|
self._temp_dir = None
|
|
74
|
-
|
|
75
|
+
|
|
75
76
|
def _generate_color_palette(self, num_colors: int = 100) -> List[Tuple[int, int, int]]:
|
|
76
77
|
"""
|
|
77
78
|
Generate bright, distinct color palette for track visualization.
|
|
@@ -88,11 +89,11 @@ class TrackingVisualizer:
|
|
|
88
89
|
bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
|
|
89
90
|
colors.append(tuple(map(int, bgr_color)))
|
|
90
91
|
return colors
|
|
91
|
-
|
|
92
|
+
|
|
92
93
|
def _get_track_color(self, track_id: int) -> Tuple[int, int, int]:
|
|
93
94
|
"""Get consistent color for track ID from palette."""
|
|
94
95
|
return self.color_palette[track_id % len(self.color_palette)]
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
def _get_video_info(self, video_path: Path) -> Tuple[int, int, float, int]:
|
|
97
98
|
"""
|
|
98
99
|
Get video metadata using ffmpeg.
|
|
@@ -104,13 +105,13 @@ class TrackingVisualizer:
|
|
|
104
105
|
probe = ffmpeg.probe(str(video_path))
|
|
105
106
|
video_stream = next((stream for stream in probe['streams']
|
|
106
107
|
if stream['codec_type'] == 'video'), None)
|
|
107
|
-
|
|
108
|
+
|
|
108
109
|
if video_stream is None:
|
|
109
110
|
raise ValueError(f"No video stream found in: {video_path}")
|
|
110
|
-
|
|
111
|
+
|
|
111
112
|
width = int(video_stream['width'])
|
|
112
113
|
height = int(video_stream['height'])
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
# Extract FPS
|
|
115
116
|
fps_str = video_stream.get('r_frame_rate', '30/1')
|
|
116
117
|
if '/' in fps_str:
|
|
@@ -118,19 +119,19 @@ class TrackingVisualizer:
|
|
|
118
119
|
fps = num / den if den != 0 else 30.0
|
|
119
120
|
else:
|
|
120
121
|
fps = float(fps_str)
|
|
121
|
-
|
|
122
|
+
|
|
122
123
|
# Get total frames
|
|
123
124
|
total_frames = int(video_stream.get('nb_frames', 0))
|
|
124
125
|
if total_frames == 0:
|
|
125
126
|
# Fallback: estimate from duration and fps
|
|
126
127
|
duration = float(video_stream.get('duration', 0))
|
|
127
128
|
total_frames = int(duration * fps) if duration > 0 else 0
|
|
128
|
-
|
|
129
|
+
|
|
129
130
|
return width, height, fps, total_frames
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
except Exception as e:
|
|
132
133
|
raise ValueError(f"Could not read video metadata {video_path}: {str(e)}")
|
|
133
|
-
|
|
134
|
+
|
|
134
135
|
def _create_frame_iterator(self, source: Union[str, Path]) -> Iterator[Tuple[int, np.ndarray]]:
|
|
135
136
|
"""
|
|
136
137
|
Create iterator that yields (frame_index, frame) tuples.
|
|
@@ -142,38 +143,38 @@ class TrackingVisualizer:
|
|
|
142
143
|
Tuple of (frame_index, frame_array)
|
|
143
144
|
"""
|
|
144
145
|
source = Path(source)
|
|
145
|
-
|
|
146
|
+
|
|
146
147
|
if source.is_file():
|
|
147
148
|
yield from self._iterate_video_frames(source)
|
|
148
149
|
elif source.is_dir():
|
|
149
150
|
yield from self._iterate_directory_frames(source)
|
|
150
151
|
else:
|
|
151
152
|
raise ValueError(f"Source must be a video file or directory, got: {source}")
|
|
152
|
-
|
|
153
|
+
|
|
153
154
|
def _iterate_video_frames(self, video_path: Path) -> Iterator[Tuple[int, np.ndarray]]:
|
|
154
155
|
"""Iterate through video frames using ffmpeg."""
|
|
155
156
|
width, height, fps, total_frames = self._get_video_info(video_path)
|
|
156
|
-
|
|
157
|
+
|
|
157
158
|
# Store video info for later use
|
|
158
159
|
self.source_fps = fps
|
|
159
160
|
self.frame_size = (width, height)
|
|
160
|
-
|
|
161
|
+
|
|
161
162
|
process = (
|
|
162
163
|
ffmpeg
|
|
163
164
|
.input(str(video_path))
|
|
164
165
|
.output('pipe:', format='rawvideo', pix_fmt='bgr24', loglevel='quiet')
|
|
165
166
|
.run_async(pipe_stdout=True, pipe_stderr=False)
|
|
166
167
|
)
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
try:
|
|
169
170
|
frame_size_bytes = width * height * 3
|
|
170
171
|
frame_idx = 0
|
|
171
|
-
|
|
172
|
+
|
|
172
173
|
while True:
|
|
173
174
|
frame_data = process.stdout.read(frame_size_bytes)
|
|
174
175
|
if len(frame_data) != frame_size_bytes:
|
|
175
176
|
break
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
frame = np.frombuffer(frame_data, np.uint8).reshape([height, width, 3])
|
|
178
179
|
yield frame_idx, frame
|
|
179
180
|
frame_idx += 1
|
|
@@ -186,26 +187,26 @@ class TrackingVisualizer:
|
|
|
186
187
|
if process.stderr:
|
|
187
188
|
process.stderr.close()
|
|
188
189
|
process.wait()
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
def _iterate_directory_frames(self, frames_dir: Path) -> Iterator[Tuple[int, np.ndarray]]:
|
|
191
192
|
"""Iterate through image frames in directory."""
|
|
192
193
|
if not frames_dir.is_dir():
|
|
193
194
|
raise ValueError(f"Directory does not exist: {frames_dir}")
|
|
194
|
-
|
|
195
|
+
|
|
195
196
|
# Support common image extensions
|
|
196
197
|
extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
|
|
197
198
|
image_files = []
|
|
198
199
|
for ext in extensions:
|
|
199
200
|
image_files.extend(frames_dir.glob(f'*{ext}'))
|
|
200
201
|
image_files.extend(frames_dir.glob(f'*{ext.upper()}'))
|
|
201
|
-
|
|
202
|
+
|
|
202
203
|
image_files = sorted(image_files)
|
|
203
204
|
if not image_files:
|
|
204
205
|
raise ValueError(f"No image files found in directory: {frames_dir}")
|
|
205
|
-
|
|
206
|
+
|
|
206
207
|
# Set fps from config for image sequences
|
|
207
208
|
self.source_fps = self.output_fps
|
|
208
|
-
|
|
209
|
+
|
|
209
210
|
for frame_idx, img_path in enumerate(image_files):
|
|
210
211
|
frame = cv2.imread(str(img_path))
|
|
211
212
|
if frame is not None:
|
|
@@ -215,7 +216,7 @@ class TrackingVisualizer:
|
|
|
215
216
|
yield frame_idx, frame
|
|
216
217
|
else:
|
|
217
218
|
logger.warning(f"Could not read image: {img_path}")
|
|
218
|
-
|
|
219
|
+
|
|
219
220
|
def _extract_tracks_from_annotation(self) -> None:
|
|
220
221
|
"""
|
|
221
222
|
Extract tracking data from Supervisely VideoAnnotation.
|
|
@@ -224,29 +225,22 @@ class TrackingVisualizer:
|
|
|
224
225
|
"""
|
|
225
226
|
self.tracks_by_frame = defaultdict(list)
|
|
226
227
|
self.track_colors = {}
|
|
227
|
-
|
|
228
|
+
|
|
228
229
|
# Map object keys to track info
|
|
229
230
|
objects = {}
|
|
230
231
|
for i, obj in enumerate(self.annotation.objects):
|
|
231
232
|
objects[obj.key] = (i, obj.obj_class.name)
|
|
232
|
-
|
|
233
|
+
|
|
233
234
|
# Extract tracks from frames
|
|
234
235
|
for frame in self.annotation.frames:
|
|
235
236
|
frame_idx = frame.index
|
|
236
237
|
for figure in frame.figures:
|
|
237
|
-
if figure.geometry.geometry_name() != 'rectangle':
|
|
238
|
-
continue
|
|
239
|
-
|
|
240
238
|
object_key = figure.parent_object.key
|
|
241
239
|
if object_key not in objects:
|
|
242
240
|
continue
|
|
243
|
-
|
|
241
|
+
|
|
244
242
|
track_id, class_name = objects[object_key]
|
|
245
|
-
|
|
246
|
-
# Extract bbox coordinates
|
|
247
|
-
rect = figure.geometry
|
|
248
|
-
bbox = (rect.left, rect.top, rect.right, rect.bottom)
|
|
249
|
-
|
|
243
|
+
|
|
250
244
|
if track_id not in self.track_colors:
|
|
251
245
|
if self.colorize_tracks:
|
|
252
246
|
# auto-color override everything
|
|
@@ -263,26 +257,30 @@ class TrackingVisualizer:
|
|
|
263
257
|
|
|
264
258
|
self.track_colors[track_id] = color
|
|
265
259
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
self.tracks_by_frame[frame_idx].append((track_id, figure.geometry, class_name))
|
|
261
|
+
|
|
269
262
|
logger.info(f"Extracted tracks from {len(self.tracks_by_frame)} frames")
|
|
270
|
-
|
|
271
|
-
def _draw_detection(
|
|
272
|
-
|
|
263
|
+
|
|
264
|
+
def _draw_detection(
|
|
265
|
+
self, img: np.ndarray, track_id: int, geometry: Geometry, class_name: str
|
|
266
|
+
) -> Optional[Tuple[int, int]]:
|
|
273
267
|
"""
|
|
274
268
|
Draw single detection with track ID and class label.
|
|
275
269
|
Returns the center point of the bbox for trajectory drawing.
|
|
276
270
|
"""
|
|
271
|
+
rect = geometry.to_bbox()
|
|
272
|
+
bbox = (rect.left, rect.top, rect.right, rect.bottom)
|
|
273
|
+
|
|
277
274
|
x1, y1, x2, y2 = map(int, bbox)
|
|
278
275
|
|
|
279
276
|
if x2 <= x1 or y2 <= y1:
|
|
280
277
|
return None
|
|
281
|
-
|
|
278
|
+
|
|
282
279
|
color = self.track_colors[track_id]
|
|
283
280
|
|
|
284
281
|
# Draw bounding box
|
|
285
|
-
|
|
282
|
+
geometry.draw_contour(img, color=color, thickness=self.box_thickness)
|
|
283
|
+
# cv2.rectangle(img, (x1, y1), (x2, y2), color, self.box_thickness)
|
|
286
284
|
|
|
287
285
|
# Draw label if enabled
|
|
288
286
|
if self.show_labels:
|
|
@@ -304,7 +302,6 @@ class TrackingVisualizer:
|
|
|
304
302
|
# Return center point for trajectory
|
|
305
303
|
return (x1 + x2) // 2, (y1 + y2) // 2
|
|
306
304
|
|
|
307
|
-
|
|
308
305
|
def _draw_trajectories(self, img: np.ndarray) -> None:
|
|
309
306
|
"""Draw trajectory lines for all tracks, filtering out big jumps."""
|
|
310
307
|
if not self.show_trajectories:
|
|
@@ -312,24 +309,24 @@ class TrackingVisualizer:
|
|
|
312
309
|
|
|
313
310
|
max_jump = 200
|
|
314
311
|
|
|
315
|
-
for
|
|
316
|
-
|
|
312
|
+
for centers_with_colors in self.track_centers.values():
|
|
313
|
+
|
|
314
|
+
if len(centers_with_colors) < 2:
|
|
317
315
|
continue
|
|
318
316
|
|
|
319
|
-
|
|
320
|
-
points = centers[-self.trajectory_length:]
|
|
317
|
+
points, colors = zip(*centers_with_colors[-self.trajectory_length :])
|
|
321
318
|
|
|
322
319
|
for i in range(1, len(points)):
|
|
320
|
+
color = colors[i]
|
|
323
321
|
p1, p2 = points[i - 1], points[i]
|
|
324
322
|
if p1 is None or p2 is None:
|
|
325
323
|
continue
|
|
326
|
-
|
|
324
|
+
|
|
327
325
|
if np.hypot(p2[0] - p1[0], p2[1] - p1[1]) > max_jump:
|
|
328
326
|
continue
|
|
329
|
-
cv2.line(img, p1, p2, color,
|
|
327
|
+
cv2.line(img, p1, p2, color, self.trajectory_thickness)
|
|
330
328
|
cv2.circle(img, p1, 3, color, -1)
|
|
331
329
|
|
|
332
|
-
|
|
333
330
|
def _process_single_frame(self, frame: np.ndarray, frame_idx: int) -> np.ndarray:
|
|
334
331
|
"""
|
|
335
332
|
Process single frame: add annotations and return processed frame.
|
|
@@ -345,29 +342,30 @@ class TrackingVisualizer:
|
|
|
345
342
|
active_ids = set()
|
|
346
343
|
# Draw detections for current frame
|
|
347
344
|
if frame_idx in self.tracks_by_frame:
|
|
348
|
-
for track_id,
|
|
349
|
-
center = self._draw_detection(img, track_id,
|
|
350
|
-
self.
|
|
345
|
+
for track_id, geometry, class_name in self.tracks_by_frame[frame_idx]:
|
|
346
|
+
center = self._draw_detection(img, track_id, geometry, class_name)
|
|
347
|
+
color = self.track_colors[track_id]
|
|
348
|
+
self.track_centers[track_id].append((center, color))
|
|
351
349
|
if len(self.track_centers[track_id]) > self.trajectory_length:
|
|
352
350
|
self.track_centers[track_id].pop(0)
|
|
353
351
|
active_ids.add(track_id)
|
|
354
|
-
|
|
352
|
+
|
|
355
353
|
for tid in self.track_centers.keys():
|
|
356
354
|
if tid not in active_ids:
|
|
357
|
-
self.track_centers[tid].append(None)
|
|
355
|
+
self.track_centers[tid].append((None, None))
|
|
358
356
|
if len(self.track_centers[tid]) > self.trajectory_length:
|
|
359
357
|
self.track_centers[tid].pop(0)
|
|
360
|
-
|
|
358
|
+
|
|
361
359
|
# Draw trajectories
|
|
362
360
|
self._draw_trajectories(img)
|
|
363
|
-
|
|
361
|
+
|
|
364
362
|
# Add frame number if requested
|
|
365
363
|
if self.show_frame_number:
|
|
366
364
|
cv2.putText(img, f"Frame: {frame_idx + 1}", (10, 30),
|
|
367
365
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA)
|
|
368
|
-
|
|
366
|
+
|
|
369
367
|
return img
|
|
370
|
-
|
|
368
|
+
|
|
371
369
|
def _save_processed_frame(self, frame: np.ndarray, frame_idx: int) -> str:
|
|
372
370
|
"""
|
|
373
371
|
Save processed frame to temporary directory.
|
|
@@ -382,7 +380,7 @@ class TrackingVisualizer:
|
|
|
382
380
|
frame_path = self._temp_dir / f"frame_{frame_idx:08d}.jpg"
|
|
383
381
|
cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
|
|
384
382
|
return str(frame_path)
|
|
385
|
-
|
|
383
|
+
|
|
386
384
|
def _create_video_from_frames(self, output_path: Union[str, Path]) -> None:
|
|
387
385
|
"""
|
|
388
386
|
Create final video from processed frames using ffmpeg.
|
|
@@ -392,10 +390,10 @@ class TrackingVisualizer:
|
|
|
392
390
|
"""
|
|
393
391
|
output_path = Path(output_path)
|
|
394
392
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
395
|
-
|
|
393
|
+
|
|
396
394
|
# Create video from frame sequence
|
|
397
395
|
input_pattern = str(self._temp_dir / "frame_%08d.jpg")
|
|
398
|
-
|
|
396
|
+
|
|
399
397
|
try:
|
|
400
398
|
(
|
|
401
399
|
ffmpeg
|
|
@@ -405,17 +403,17 @@ class TrackingVisualizer:
|
|
|
405
403
|
.run(capture_stdout=True, capture_stderr=True)
|
|
406
404
|
)
|
|
407
405
|
logger.info(f"Video saved to {output_path}")
|
|
408
|
-
|
|
406
|
+
|
|
409
407
|
except ffmpeg.Error as e:
|
|
410
408
|
error_msg = e.stderr.decode() if e.stderr else "Unknown ffmpeg error"
|
|
411
409
|
raise ValueError(f"Failed to create video: {error_msg}")
|
|
412
|
-
|
|
410
|
+
|
|
413
411
|
def _cleanup_temp_directory(self) -> None:
|
|
414
412
|
"""Clean up temporary directory and all its contents."""
|
|
415
413
|
if self._temp_dir and self._temp_dir.exists():
|
|
416
414
|
shutil.rmtree(self._temp_dir)
|
|
417
415
|
self._temp_dir = None
|
|
418
|
-
|
|
416
|
+
|
|
419
417
|
def visualize_video_annotation(self, annotation: VideoAnnotation,
|
|
420
418
|
source: Union[str, Path],
|
|
421
419
|
output_path: Union[str, Path]) -> None:
|
|
@@ -433,43 +431,43 @@ class TrackingVisualizer:
|
|
|
433
431
|
"""
|
|
434
432
|
if not isinstance(annotation, VideoAnnotation):
|
|
435
433
|
raise TypeError(f"Annotation must be VideoAnnotation, got {type(annotation)}")
|
|
436
|
-
|
|
434
|
+
|
|
437
435
|
# Store annotation
|
|
438
436
|
self.annotation = annotation
|
|
439
|
-
|
|
437
|
+
|
|
440
438
|
# Create temporary directory for processed frames
|
|
441
439
|
self._temp_dir = Path(tempfile.mkdtemp(prefix="video_viz_"))
|
|
442
|
-
|
|
440
|
+
|
|
443
441
|
try:
|
|
444
442
|
# Extract tracking data
|
|
445
443
|
self._extract_tracks_from_annotation()
|
|
446
|
-
|
|
444
|
+
|
|
447
445
|
if not self.tracks_by_frame:
|
|
448
446
|
logger.warning("No tracking data found in annotation")
|
|
449
|
-
|
|
447
|
+
|
|
450
448
|
# Reset trajectory tracking
|
|
451
449
|
self.track_centers = defaultdict(list)
|
|
452
|
-
|
|
450
|
+
|
|
453
451
|
# Process frames one by one
|
|
454
452
|
frame_count = 0
|
|
455
453
|
for frame_idx, frame in self._create_frame_iterator(source):
|
|
456
454
|
# Process frame
|
|
457
455
|
processed_frame = self._process_single_frame(frame, frame_idx)
|
|
458
|
-
|
|
456
|
+
|
|
459
457
|
# Save processed frame
|
|
460
458
|
self._save_processed_frame(processed_frame, frame_idx)
|
|
461
|
-
|
|
459
|
+
|
|
462
460
|
frame_count += 1
|
|
463
|
-
|
|
461
|
+
|
|
464
462
|
# Progress logging
|
|
465
463
|
if frame_count % 100 == 0:
|
|
466
464
|
logger.info(f"Processed {frame_count} frames")
|
|
467
|
-
|
|
465
|
+
|
|
468
466
|
logger.info(f"Finished processing {frame_count} frames")
|
|
469
|
-
|
|
467
|
+
|
|
470
468
|
# Create final video from saved frames
|
|
471
469
|
self._create_video_from_frames(output_path)
|
|
472
|
-
|
|
470
|
+
|
|
473
471
|
finally:
|
|
474
472
|
# Always cleanup temporary files
|
|
475
473
|
self._cleanup_temp_directory()
|
|
@@ -477,7 +475,7 @@ class TrackingVisualizer:
|
|
|
477
475
|
def __del__(self):
|
|
478
476
|
"""Cleanup temporary directory on object destruction."""
|
|
479
477
|
self._cleanup_temp_directory()
|
|
480
|
-
|
|
478
|
+
|
|
481
479
|
|
|
482
480
|
def visualize(
|
|
483
481
|
predictions: Union[VideoAnnotation, List[Prediction]],
|
|
@@ -519,4 +517,3 @@ def visualize(
|
|
|
519
517
|
visualizer.visualize_video_annotation(predictions, source, output_path)
|
|
520
518
|
else:
|
|
521
519
|
raise TypeError(f"Predictions must be VideoAnnotation or list of Prediction, got {type(predictions)}")
|
|
522
|
-
|
|
@@ -51,7 +51,19 @@ class ClassesSelector:
|
|
|
51
51
|
text=f"<i class='zmdi zmdi-chart-donut' style='color: #7f858e'></i> <a href='{qa_stats_link}' target='_blank'> <b> QA & Stats </b></a>"
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
models = model_selector.models
|
|
55
|
+
task_types = [model["meta"]["task_type"] for model in models]
|
|
56
|
+
task_types = list(set(task_types))
|
|
57
|
+
allowed_types = []
|
|
58
|
+
for task_type in task_types:
|
|
59
|
+
if task_type.endswith("detection"):
|
|
60
|
+
allowed_types.append(Rectangle)
|
|
61
|
+
elif task_type.endswith("segmentation"):
|
|
62
|
+
allowed_types.extend([Bitmap, Polygon])
|
|
63
|
+
elif task_type == TaskType.POSE_ESTIMATION:
|
|
64
|
+
allowed_types.append(GraphNodes)
|
|
65
|
+
|
|
66
|
+
self.classes_table = ClassesTable(project_id=project_id, allowed_types=allowed_types)
|
|
55
67
|
if len(classes) > 0:
|
|
56
68
|
self.classes_table.select_classes(classes)
|
|
57
69
|
else:
|
|
@@ -107,6 +119,9 @@ class ClassesSelector:
|
|
|
107
119
|
TaskType.INSTANCE_SEGMENTATION: {Bitmap},
|
|
108
120
|
TaskType.SEMANTIC_SEGMENTATION: {Bitmap},
|
|
109
121
|
TaskType.POSE_ESTIMATION: {GraphNodes},
|
|
122
|
+
TaskType.PROMPTABLE_SEGMENTATION: {Bitmap},
|
|
123
|
+
TaskType.INTERACTIVE_SEGMENTATION: {Bitmap},
|
|
124
|
+
TaskType.PROMPT_BASED_OBJECT_DETECTION: {Rectangle},
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
if task_type not in allowed_shapes:
|
|
@@ -43,6 +43,7 @@ from supervisely import (
|
|
|
43
43
|
logger,
|
|
44
44
|
)
|
|
45
45
|
from supervisely._utils import abs_url, get_filename_from_headers
|
|
46
|
+
from supervisely.api.entities_collection_api import EntitiesCollectionInfo
|
|
46
47
|
from supervisely.api.file_api import FileInfo
|
|
47
48
|
from supervisely.app import get_synced_data_dir, show_dialog
|
|
48
49
|
from supervisely.app.widgets import Progress
|
|
@@ -72,7 +73,6 @@ from supervisely.project.download import (
|
|
|
72
73
|
is_cached,
|
|
73
74
|
)
|
|
74
75
|
from supervisely.template.experiment.experiment_generator import ExperimentGenerator
|
|
75
|
-
from supervisely.api.entities_collection_api import EntitiesCollectionInfo
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
class TrainApp:
|
|
@@ -3162,8 +3162,11 @@ class TrainApp:
|
|
|
3162
3162
|
|
|
3163
3163
|
# Case 1: Use existing collections for training. No need to create new collections
|
|
3164
3164
|
split_method = self.gui.train_val_splits_selector.get_split_method()
|
|
3165
|
+
self.gui.train_val_splits_selector._parse_collections()
|
|
3165
3166
|
all_train_collections = self.gui.train_val_splits_selector.all_train_collections
|
|
3166
3167
|
all_val_collections = self.gui.train_val_splits_selector.all_val_collections
|
|
3168
|
+
latest_train_collection = self.gui.train_val_splits_selector.latest_train_collection
|
|
3169
|
+
latest_val_collection = self.gui.train_val_splits_selector.latest_val_collection
|
|
3167
3170
|
if split_method == "Based on collections":
|
|
3168
3171
|
current_selected_train_collection_ids = self.gui.train_val_splits_selector.train_val_splits.get_train_collections_ids()
|
|
3169
3172
|
train_match = _check_match(current_selected_train_collection_ids, all_train_collections)
|
|
@@ -3178,9 +3181,6 @@ class TrainApp:
|
|
|
3178
3181
|
# ------------------------------------------------------------ #
|
|
3179
3182
|
|
|
3180
3183
|
# Case 2: Create new collections for selected train val splits. Need to create new collections
|
|
3181
|
-
item_type = self.project_info.type
|
|
3182
|
-
experiment_name = self.gui.training_process.get_experiment_name()
|
|
3183
|
-
|
|
3184
3184
|
train_collection_idx = 1
|
|
3185
3185
|
val_collection_idx = 1
|
|
3186
3186
|
|
|
@@ -3191,42 +3191,41 @@ class TrainApp:
|
|
|
3191
3191
|
return None
|
|
3192
3192
|
|
|
3193
3193
|
# Get train collection with max idx
|
|
3194
|
-
if
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
train_collection_idx = max(train_indices) + 1
|
|
3194
|
+
if latest_train_collection:
|
|
3195
|
+
train_collection_idx = (
|
|
3196
|
+
_extract_index_from_col_name(latest_train_collection.name, "train") + 1
|
|
3197
|
+
)
|
|
3199
3198
|
|
|
3200
3199
|
# Get val collection with max idx
|
|
3201
|
-
if
|
|
3202
|
-
|
|
3203
|
-
val_indices = [idx for idx in val_indices if idx is not None]
|
|
3204
|
-
if len(val_indices) > 0:
|
|
3205
|
-
val_collection_idx = max(val_indices) + 1
|
|
3200
|
+
if latest_val_collection:
|
|
3201
|
+
val_collection_idx = _extract_index_from_col_name(latest_val_collection.name, "val") + 1
|
|
3206
3202
|
# -------------------------------- #
|
|
3207
3203
|
|
|
3208
3204
|
# Create Train Collection
|
|
3209
3205
|
train_img_ids = list(self._train_split_item_ids)
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
train_collection_id = getattr(train_collection, "id", None)
|
|
3213
|
-
if train_collection_id is None:
|
|
3214
|
-
raise AttributeError("Train EntitiesCollectionInfo object does not have 'id' attribute")
|
|
3215
|
-
self._api.entities_collection.add_items(train_collection_id, train_img_ids)
|
|
3216
|
-
self._train_collection_id = train_collection_id
|
|
3206
|
+
self._train_collection_id = self._create_collection("train", train_collection_idx)
|
|
3207
|
+
self._api.entities_collection.add_items(self._train_collection_id, train_img_ids)
|
|
3217
3208
|
|
|
3218
3209
|
# Create Val Collection
|
|
3219
3210
|
val_img_ids = list(self._val_split_item_ids)
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
val_collection_id = getattr(val_collection, "id", None)
|
|
3223
|
-
if val_collection_id is None:
|
|
3224
|
-
raise AttributeError("Val EntitiesCollectionInfo object does not have 'id' attribute")
|
|
3225
|
-
self._api.entities_collection.add_items(val_collection_id, val_img_ids)
|
|
3226
|
-
self._val_collection_id = val_collection_id
|
|
3211
|
+
self._val_collection_id = self._create_collection("val", val_collection_idx)
|
|
3212
|
+
self._api.entities_collection.add_items(self._val_collection_id, val_img_ids)
|
|
3227
3213
|
|
|
3228
3214
|
# Update Project Custom Data
|
|
3229
|
-
self._update_project_custom_data(
|
|
3215
|
+
self._update_project_custom_data(self._train_collection_id, self._val_collection_id)
|
|
3216
|
+
|
|
3217
|
+
def _create_collection(self, split_type: str, suffix: int) -> int:
|
|
3218
|
+
experiment_name = self.gui.training_process.get_experiment_name()
|
|
3219
|
+
description = f"Collection with {split_type} {self.project_info.type} for experiment: {experiment_name}"
|
|
3220
|
+
collection = self._api.entities_collection.create(
|
|
3221
|
+
project_id=self.project_id,
|
|
3222
|
+
name=f"{split_type}_{suffix:03d}",
|
|
3223
|
+
description=description,
|
|
3224
|
+
change_name_if_conflict=True,
|
|
3225
|
+
)
|
|
3226
|
+
if collection is None or collection.id is None: # pylint: disable=no-member
|
|
3227
|
+
raise RuntimeError(f"Failed to create {split_type} collection")
|
|
3228
|
+
return collection.id # pylint: disable=no-member
|
|
3230
3229
|
|
|
3231
3230
|
def _update_project_custom_data(self, train_collection_id: int, val_collection_id: int):
|
|
3232
3231
|
train_info = {
|