supervisely 6.73.444__py3-none-any.whl → 6.73.468__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 supervisely might be problematic. Click here for more details.
- supervisely/__init__.py +24 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/json_geometries_map.py +2 -0
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entity_annotation/figure_api.py +8 -5
- supervisely/api/image_api.py +4 -0
- supervisely/api/video/video_annotation_api.py +4 -2
- supervisely/api/video/video_api.py +41 -1
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +14 -6
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +88 -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/card/card.py +20 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +121 -31
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/video/video_converter.py +2 -2
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/io/env.py +76 -1
- supervisely/nn/inference/cache.py +37 -17
- supervisely/nn/inference/inference.py +667 -114
- supervisely/nn/inference/inference_request.py +15 -8
- supervisely/nn/inference/predict_app/gui/classes_selector.py +81 -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/model/model_api.py +9 -0
- supervisely/nn/model/prediction_session.py +8 -7
- supervisely/nn/prediction_dto.py +7 -0
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort_tracker.py +14 -7
- supervisely/nn/tracker/visualize.py +70 -72
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +10 -5
- supervisely/project/project.py +9 -1
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- 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.444.dist-info → supervisely-6.73.468.dist-info}/METADATA +14 -11
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/RECORD +68 -66
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/LICENSE +0 -0
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/WHEEL +0 -0
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import cv2
|
|
4
|
-
import ffmpeg
|
|
5
|
-
from pathlib import Path
|
|
1
|
+
import shutil
|
|
2
|
+
import tempfile
|
|
6
3
|
from collections import defaultdict
|
|
7
4
|
from dataclasses import dataclass
|
|
8
|
-
import
|
|
9
|
-
import
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Iterator, List, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
import cv2
|
|
9
|
+
import ffmpeg
|
|
10
|
+
import numpy as np
|
|
10
11
|
|
|
11
12
|
import supervisely as sly
|
|
12
|
-
from supervisely import logger
|
|
13
|
+
from supervisely import VideoAnnotation, logger
|
|
13
14
|
from supervisely.nn.model.prediction import Prediction
|
|
14
|
-
from supervisely import VideoAnnotation
|
|
15
15
|
from supervisely.nn.tracker.utils import predictions_to_video_annotation
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class TrackingVisualizer:
|
|
19
|
+
|
|
19
20
|
def __init__(
|
|
20
21
|
self,
|
|
21
22
|
show_labels: bool = True,
|
|
@@ -29,7 +30,7 @@ class TrackingVisualizer:
|
|
|
29
30
|
codec: str = "mp4",
|
|
30
31
|
output_fps: float = 30.0,
|
|
31
32
|
colorize_tracks: bool = True,
|
|
32
|
-
|
|
33
|
+
trajectory_thickness: int = 2,
|
|
33
34
|
):
|
|
34
35
|
"""
|
|
35
36
|
Initialize the visualizer with configuration.
|
|
@@ -58,6 +59,7 @@ class TrackingVisualizer:
|
|
|
58
59
|
self.text_scale = text_scale
|
|
59
60
|
self.text_thickness = text_thickness
|
|
60
61
|
self.trajectory_length = trajectory_length
|
|
62
|
+
self.trajectory_thickness = trajectory_thickness
|
|
61
63
|
self.colorize_tracks = colorize_tracks
|
|
62
64
|
|
|
63
65
|
# Output settings
|
|
@@ -71,7 +73,7 @@ class TrackingVisualizer:
|
|
|
71
73
|
self.track_colors = {}
|
|
72
74
|
self.color_palette = self._generate_color_palette()
|
|
73
75
|
self._temp_dir = None
|
|
74
|
-
|
|
76
|
+
|
|
75
77
|
def _generate_color_palette(self, num_colors: int = 100) -> List[Tuple[int, int, int]]:
|
|
76
78
|
"""
|
|
77
79
|
Generate bright, distinct color palette for track visualization.
|
|
@@ -88,11 +90,11 @@ class TrackingVisualizer:
|
|
|
88
90
|
bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
|
|
89
91
|
colors.append(tuple(map(int, bgr_color)))
|
|
90
92
|
return colors
|
|
91
|
-
|
|
93
|
+
|
|
92
94
|
def _get_track_color(self, track_id: int) -> Tuple[int, int, int]:
|
|
93
95
|
"""Get consistent color for track ID from palette."""
|
|
94
96
|
return self.color_palette[track_id % len(self.color_palette)]
|
|
95
|
-
|
|
97
|
+
|
|
96
98
|
def _get_video_info(self, video_path: Path) -> Tuple[int, int, float, int]:
|
|
97
99
|
"""
|
|
98
100
|
Get video metadata using ffmpeg.
|
|
@@ -104,13 +106,13 @@ class TrackingVisualizer:
|
|
|
104
106
|
probe = ffmpeg.probe(str(video_path))
|
|
105
107
|
video_stream = next((stream for stream in probe['streams']
|
|
106
108
|
if stream['codec_type'] == 'video'), None)
|
|
107
|
-
|
|
109
|
+
|
|
108
110
|
if video_stream is None:
|
|
109
111
|
raise ValueError(f"No video stream found in: {video_path}")
|
|
110
|
-
|
|
112
|
+
|
|
111
113
|
width = int(video_stream['width'])
|
|
112
114
|
height = int(video_stream['height'])
|
|
113
|
-
|
|
115
|
+
|
|
114
116
|
# Extract FPS
|
|
115
117
|
fps_str = video_stream.get('r_frame_rate', '30/1')
|
|
116
118
|
if '/' in fps_str:
|
|
@@ -118,19 +120,19 @@ class TrackingVisualizer:
|
|
|
118
120
|
fps = num / den if den != 0 else 30.0
|
|
119
121
|
else:
|
|
120
122
|
fps = float(fps_str)
|
|
121
|
-
|
|
123
|
+
|
|
122
124
|
# Get total frames
|
|
123
125
|
total_frames = int(video_stream.get('nb_frames', 0))
|
|
124
126
|
if total_frames == 0:
|
|
125
127
|
# Fallback: estimate from duration and fps
|
|
126
128
|
duration = float(video_stream.get('duration', 0))
|
|
127
129
|
total_frames = int(duration * fps) if duration > 0 else 0
|
|
128
|
-
|
|
130
|
+
|
|
129
131
|
return width, height, fps, total_frames
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
except Exception as e:
|
|
132
134
|
raise ValueError(f"Could not read video metadata {video_path}: {str(e)}")
|
|
133
|
-
|
|
135
|
+
|
|
134
136
|
def _create_frame_iterator(self, source: Union[str, Path]) -> Iterator[Tuple[int, np.ndarray]]:
|
|
135
137
|
"""
|
|
136
138
|
Create iterator that yields (frame_index, frame) tuples.
|
|
@@ -142,38 +144,38 @@ class TrackingVisualizer:
|
|
|
142
144
|
Tuple of (frame_index, frame_array)
|
|
143
145
|
"""
|
|
144
146
|
source = Path(source)
|
|
145
|
-
|
|
147
|
+
|
|
146
148
|
if source.is_file():
|
|
147
149
|
yield from self._iterate_video_frames(source)
|
|
148
150
|
elif source.is_dir():
|
|
149
151
|
yield from self._iterate_directory_frames(source)
|
|
150
152
|
else:
|
|
151
153
|
raise ValueError(f"Source must be a video file or directory, got: {source}")
|
|
152
|
-
|
|
154
|
+
|
|
153
155
|
def _iterate_video_frames(self, video_path: Path) -> Iterator[Tuple[int, np.ndarray]]:
|
|
154
156
|
"""Iterate through video frames using ffmpeg."""
|
|
155
157
|
width, height, fps, total_frames = self._get_video_info(video_path)
|
|
156
|
-
|
|
158
|
+
|
|
157
159
|
# Store video info for later use
|
|
158
160
|
self.source_fps = fps
|
|
159
161
|
self.frame_size = (width, height)
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
process = (
|
|
162
164
|
ffmpeg
|
|
163
165
|
.input(str(video_path))
|
|
164
166
|
.output('pipe:', format='rawvideo', pix_fmt='bgr24', loglevel='quiet')
|
|
165
167
|
.run_async(pipe_stdout=True, pipe_stderr=False)
|
|
166
168
|
)
|
|
167
|
-
|
|
169
|
+
|
|
168
170
|
try:
|
|
169
171
|
frame_size_bytes = width * height * 3
|
|
170
172
|
frame_idx = 0
|
|
171
|
-
|
|
173
|
+
|
|
172
174
|
while True:
|
|
173
175
|
frame_data = process.stdout.read(frame_size_bytes)
|
|
174
176
|
if len(frame_data) != frame_size_bytes:
|
|
175
177
|
break
|
|
176
|
-
|
|
178
|
+
|
|
177
179
|
frame = np.frombuffer(frame_data, np.uint8).reshape([height, width, 3])
|
|
178
180
|
yield frame_idx, frame
|
|
179
181
|
frame_idx += 1
|
|
@@ -186,26 +188,26 @@ class TrackingVisualizer:
|
|
|
186
188
|
if process.stderr:
|
|
187
189
|
process.stderr.close()
|
|
188
190
|
process.wait()
|
|
189
|
-
|
|
191
|
+
|
|
190
192
|
def _iterate_directory_frames(self, frames_dir: Path) -> Iterator[Tuple[int, np.ndarray]]:
|
|
191
193
|
"""Iterate through image frames in directory."""
|
|
192
194
|
if not frames_dir.is_dir():
|
|
193
195
|
raise ValueError(f"Directory does not exist: {frames_dir}")
|
|
194
|
-
|
|
196
|
+
|
|
195
197
|
# Support common image extensions
|
|
196
198
|
extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
|
|
197
199
|
image_files = []
|
|
198
200
|
for ext in extensions:
|
|
199
201
|
image_files.extend(frames_dir.glob(f'*{ext}'))
|
|
200
202
|
image_files.extend(frames_dir.glob(f'*{ext.upper()}'))
|
|
201
|
-
|
|
203
|
+
|
|
202
204
|
image_files = sorted(image_files)
|
|
203
205
|
if not image_files:
|
|
204
206
|
raise ValueError(f"No image files found in directory: {frames_dir}")
|
|
205
|
-
|
|
207
|
+
|
|
206
208
|
# Set fps from config for image sequences
|
|
207
209
|
self.source_fps = self.output_fps
|
|
208
|
-
|
|
210
|
+
|
|
209
211
|
for frame_idx, img_path in enumerate(image_files):
|
|
210
212
|
frame = cv2.imread(str(img_path))
|
|
211
213
|
if frame is not None:
|
|
@@ -215,7 +217,7 @@ class TrackingVisualizer:
|
|
|
215
217
|
yield frame_idx, frame
|
|
216
218
|
else:
|
|
217
219
|
logger.warning(f"Could not read image: {img_path}")
|
|
218
|
-
|
|
220
|
+
|
|
219
221
|
def _extract_tracks_from_annotation(self) -> None:
|
|
220
222
|
"""
|
|
221
223
|
Extract tracking data from Supervisely VideoAnnotation.
|
|
@@ -224,29 +226,29 @@ class TrackingVisualizer:
|
|
|
224
226
|
"""
|
|
225
227
|
self.tracks_by_frame = defaultdict(list)
|
|
226
228
|
self.track_colors = {}
|
|
227
|
-
|
|
229
|
+
|
|
228
230
|
# Map object keys to track info
|
|
229
231
|
objects = {}
|
|
230
232
|
for i, obj in enumerate(self.annotation.objects):
|
|
231
233
|
objects[obj.key] = (i, obj.obj_class.name)
|
|
232
|
-
|
|
234
|
+
|
|
233
235
|
# Extract tracks from frames
|
|
234
236
|
for frame in self.annotation.frames:
|
|
235
237
|
frame_idx = frame.index
|
|
236
238
|
for figure in frame.figures:
|
|
237
239
|
if figure.geometry.geometry_name() != 'rectangle':
|
|
238
240
|
continue
|
|
239
|
-
|
|
241
|
+
|
|
240
242
|
object_key = figure.parent_object.key
|
|
241
243
|
if object_key not in objects:
|
|
242
244
|
continue
|
|
243
|
-
|
|
245
|
+
|
|
244
246
|
track_id, class_name = objects[object_key]
|
|
245
|
-
|
|
247
|
+
|
|
246
248
|
# Extract bbox coordinates
|
|
247
249
|
rect = figure.geometry
|
|
248
250
|
bbox = (rect.left, rect.top, rect.right, rect.bottom)
|
|
249
|
-
|
|
251
|
+
|
|
250
252
|
if track_id not in self.track_colors:
|
|
251
253
|
if self.colorize_tracks:
|
|
252
254
|
# auto-color override everything
|
|
@@ -263,11 +265,10 @@ class TrackingVisualizer:
|
|
|
263
265
|
|
|
264
266
|
self.track_colors[track_id] = color
|
|
265
267
|
|
|
266
|
-
|
|
267
268
|
self.tracks_by_frame[frame_idx].append((track_id, bbox, class_name))
|
|
268
|
-
|
|
269
|
+
|
|
269
270
|
logger.info(f"Extracted tracks from {len(self.tracks_by_frame)} frames")
|
|
270
|
-
|
|
271
|
+
|
|
271
272
|
def _draw_detection(self, img: np.ndarray, track_id: int, bbox: Tuple[int, int, int, int],
|
|
272
273
|
class_name: str) -> Optional[Tuple[int, int]]:
|
|
273
274
|
"""
|
|
@@ -278,7 +279,7 @@ class TrackingVisualizer:
|
|
|
278
279
|
|
|
279
280
|
if x2 <= x1 or y2 <= y1:
|
|
280
281
|
return None
|
|
281
|
-
|
|
282
|
+
|
|
282
283
|
color = self.track_colors[track_id]
|
|
283
284
|
|
|
284
285
|
# Draw bounding box
|
|
@@ -304,7 +305,6 @@ class TrackingVisualizer:
|
|
|
304
305
|
# Return center point for trajectory
|
|
305
306
|
return (x1 + x2) // 2, (y1 + y2) // 2
|
|
306
307
|
|
|
307
|
-
|
|
308
308
|
def _draw_trajectories(self, img: np.ndarray) -> None:
|
|
309
309
|
"""Draw trajectory lines for all tracks, filtering out big jumps."""
|
|
310
310
|
if not self.show_trajectories:
|
|
@@ -323,13 +323,12 @@ class TrackingVisualizer:
|
|
|
323
323
|
p1, p2 = points[i - 1], points[i]
|
|
324
324
|
if p1 is None or p2 is None:
|
|
325
325
|
continue
|
|
326
|
-
|
|
326
|
+
|
|
327
327
|
if np.hypot(p2[0] - p1[0], p2[1] - p1[1]) > max_jump:
|
|
328
328
|
continue
|
|
329
|
-
cv2.line(img, p1, p2, color,
|
|
329
|
+
cv2.line(img, p1, p2, color, self.trajectory_thickness)
|
|
330
330
|
cv2.circle(img, p1, 3, color, -1)
|
|
331
331
|
|
|
332
|
-
|
|
333
332
|
def _process_single_frame(self, frame: np.ndarray, frame_idx: int) -> np.ndarray:
|
|
334
333
|
"""
|
|
335
334
|
Process single frame: add annotations and return processed frame.
|
|
@@ -351,23 +350,23 @@ class TrackingVisualizer:
|
|
|
351
350
|
if len(self.track_centers[track_id]) > self.trajectory_length:
|
|
352
351
|
self.track_centers[track_id].pop(0)
|
|
353
352
|
active_ids.add(track_id)
|
|
354
|
-
|
|
353
|
+
|
|
355
354
|
for tid in self.track_centers.keys():
|
|
356
355
|
if tid not in active_ids:
|
|
357
356
|
self.track_centers[tid].append(None)
|
|
358
357
|
if len(self.track_centers[tid]) > self.trajectory_length:
|
|
359
358
|
self.track_centers[tid].pop(0)
|
|
360
|
-
|
|
359
|
+
|
|
361
360
|
# Draw trajectories
|
|
362
361
|
self._draw_trajectories(img)
|
|
363
|
-
|
|
362
|
+
|
|
364
363
|
# Add frame number if requested
|
|
365
364
|
if self.show_frame_number:
|
|
366
365
|
cv2.putText(img, f"Frame: {frame_idx + 1}", (10, 30),
|
|
367
366
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA)
|
|
368
|
-
|
|
367
|
+
|
|
369
368
|
return img
|
|
370
|
-
|
|
369
|
+
|
|
371
370
|
def _save_processed_frame(self, frame: np.ndarray, frame_idx: int) -> str:
|
|
372
371
|
"""
|
|
373
372
|
Save processed frame to temporary directory.
|
|
@@ -382,7 +381,7 @@ class TrackingVisualizer:
|
|
|
382
381
|
frame_path = self._temp_dir / f"frame_{frame_idx:08d}.jpg"
|
|
383
382
|
cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
|
|
384
383
|
return str(frame_path)
|
|
385
|
-
|
|
384
|
+
|
|
386
385
|
def _create_video_from_frames(self, output_path: Union[str, Path]) -> None:
|
|
387
386
|
"""
|
|
388
387
|
Create final video from processed frames using ffmpeg.
|
|
@@ -392,10 +391,10 @@ class TrackingVisualizer:
|
|
|
392
391
|
"""
|
|
393
392
|
output_path = Path(output_path)
|
|
394
393
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
395
|
-
|
|
394
|
+
|
|
396
395
|
# Create video from frame sequence
|
|
397
396
|
input_pattern = str(self._temp_dir / "frame_%08d.jpg")
|
|
398
|
-
|
|
397
|
+
|
|
399
398
|
try:
|
|
400
399
|
(
|
|
401
400
|
ffmpeg
|
|
@@ -405,17 +404,17 @@ class TrackingVisualizer:
|
|
|
405
404
|
.run(capture_stdout=True, capture_stderr=True)
|
|
406
405
|
)
|
|
407
406
|
logger.info(f"Video saved to {output_path}")
|
|
408
|
-
|
|
407
|
+
|
|
409
408
|
except ffmpeg.Error as e:
|
|
410
409
|
error_msg = e.stderr.decode() if e.stderr else "Unknown ffmpeg error"
|
|
411
410
|
raise ValueError(f"Failed to create video: {error_msg}")
|
|
412
|
-
|
|
411
|
+
|
|
413
412
|
def _cleanup_temp_directory(self) -> None:
|
|
414
413
|
"""Clean up temporary directory and all its contents."""
|
|
415
414
|
if self._temp_dir and self._temp_dir.exists():
|
|
416
415
|
shutil.rmtree(self._temp_dir)
|
|
417
416
|
self._temp_dir = None
|
|
418
|
-
|
|
417
|
+
|
|
419
418
|
def visualize_video_annotation(self, annotation: VideoAnnotation,
|
|
420
419
|
source: Union[str, Path],
|
|
421
420
|
output_path: Union[str, Path]) -> None:
|
|
@@ -433,43 +432,43 @@ class TrackingVisualizer:
|
|
|
433
432
|
"""
|
|
434
433
|
if not isinstance(annotation, VideoAnnotation):
|
|
435
434
|
raise TypeError(f"Annotation must be VideoAnnotation, got {type(annotation)}")
|
|
436
|
-
|
|
435
|
+
|
|
437
436
|
# Store annotation
|
|
438
437
|
self.annotation = annotation
|
|
439
|
-
|
|
438
|
+
|
|
440
439
|
# Create temporary directory for processed frames
|
|
441
440
|
self._temp_dir = Path(tempfile.mkdtemp(prefix="video_viz_"))
|
|
442
|
-
|
|
441
|
+
|
|
443
442
|
try:
|
|
444
443
|
# Extract tracking data
|
|
445
444
|
self._extract_tracks_from_annotation()
|
|
446
|
-
|
|
445
|
+
|
|
447
446
|
if not self.tracks_by_frame:
|
|
448
447
|
logger.warning("No tracking data found in annotation")
|
|
449
|
-
|
|
448
|
+
|
|
450
449
|
# Reset trajectory tracking
|
|
451
450
|
self.track_centers = defaultdict(list)
|
|
452
|
-
|
|
451
|
+
|
|
453
452
|
# Process frames one by one
|
|
454
453
|
frame_count = 0
|
|
455
454
|
for frame_idx, frame in self._create_frame_iterator(source):
|
|
456
455
|
# Process frame
|
|
457
456
|
processed_frame = self._process_single_frame(frame, frame_idx)
|
|
458
|
-
|
|
457
|
+
|
|
459
458
|
# Save processed frame
|
|
460
459
|
self._save_processed_frame(processed_frame, frame_idx)
|
|
461
|
-
|
|
460
|
+
|
|
462
461
|
frame_count += 1
|
|
463
|
-
|
|
462
|
+
|
|
464
463
|
# Progress logging
|
|
465
464
|
if frame_count % 100 == 0:
|
|
466
465
|
logger.info(f"Processed {frame_count} frames")
|
|
467
|
-
|
|
466
|
+
|
|
468
467
|
logger.info(f"Finished processing {frame_count} frames")
|
|
469
|
-
|
|
468
|
+
|
|
470
469
|
# Create final video from saved frames
|
|
471
470
|
self._create_video_from_frames(output_path)
|
|
472
|
-
|
|
471
|
+
|
|
473
472
|
finally:
|
|
474
473
|
# Always cleanup temporary files
|
|
475
474
|
self._cleanup_temp_directory()
|
|
@@ -477,7 +476,7 @@ class TrackingVisualizer:
|
|
|
477
476
|
def __del__(self):
|
|
478
477
|
"""Cleanup temporary directory on object destruction."""
|
|
479
478
|
self._cleanup_temp_directory()
|
|
480
|
-
|
|
479
|
+
|
|
481
480
|
|
|
482
481
|
def visualize(
|
|
483
482
|
predictions: Union[VideoAnnotation, List[Prediction]],
|
|
@@ -519,4 +518,3 @@ def visualize(
|
|
|
519
518
|
visualizer.visualize_video_annotation(predictions, source, output_path)
|
|
520
519
|
else:
|
|
521
520
|
raise TypeError(f"Predictions must be VideoAnnotation or list of Prediction, got {type(predictions)}")
|
|
522
|
-
|
|
@@ -180,7 +180,13 @@ class TrainValSplitsSelector:
|
|
|
180
180
|
return False
|
|
181
181
|
|
|
182
182
|
# Check if datasets are not empty
|
|
183
|
-
filters = [
|
|
183
|
+
filters = [
|
|
184
|
+
{
|
|
185
|
+
ApiField.FIELD: ApiField.ID,
|
|
186
|
+
ApiField.OPERATOR: "in",
|
|
187
|
+
ApiField.VALUE: train_dataset_id + val_dataset_id,
|
|
188
|
+
}
|
|
189
|
+
]
|
|
184
190
|
selected_datasets = self.api.dataset.get_list(self.project_id, filters, recursive=True)
|
|
185
191
|
datasets_count = {}
|
|
186
192
|
for dataset in selected_datasets:
|
|
@@ -334,6 +340,7 @@ class TrainValSplitsSelector:
|
|
|
334
340
|
|
|
335
341
|
def _detect_splits(self, collections_split: bool, datasets_split: bool) -> bool:
|
|
336
342
|
"""Detect splits based on the selected method"""
|
|
343
|
+
self._parse_collections()
|
|
337
344
|
splits_found = False
|
|
338
345
|
if collections_split:
|
|
339
346
|
splits_found = self._detect_collections()
|
|
@@ -341,47 +348,59 @@ class TrainValSplitsSelector:
|
|
|
341
348
|
splits_found = self._detect_datasets()
|
|
342
349
|
return splits_found
|
|
343
350
|
|
|
351
|
+
def _parse_collections(self) -> None:
|
|
352
|
+
"""Parse collections with train and val prefixes and set them to train_val_splits variables"""
|
|
353
|
+
all_collections = self.api.entities_collection.get_list(self.project_id)
|
|
354
|
+
existing_train_collections = [
|
|
355
|
+
collection for collection in all_collections if collection.name.startswith("train_")
|
|
356
|
+
]
|
|
357
|
+
existing_val_collections = [
|
|
358
|
+
collection for collection in all_collections if collection.name.startswith("val_")
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
self._all_train_collections = existing_train_collections
|
|
362
|
+
self._all_val_collections = existing_val_collections
|
|
363
|
+
self._latest_train_collection = self._get_latest_collection(existing_train_collections, "train")
|
|
364
|
+
self._latest_val_collection = self._get_latest_collection(existing_val_collections, "val")
|
|
365
|
+
|
|
366
|
+
def _get_latest_collection(
|
|
367
|
+
self, collections: List[EntitiesCollectionInfo], expected_prefix: str
|
|
368
|
+
) -> EntitiesCollectionInfo:
|
|
369
|
+
curr_collection = None
|
|
370
|
+
curr_idx = 0
|
|
371
|
+
for collection in collections:
|
|
372
|
+
parts = collection.name.split("_")
|
|
373
|
+
if len(parts) == 2:
|
|
374
|
+
prefix = parts[0].lower()
|
|
375
|
+
if prefix == expected_prefix:
|
|
376
|
+
if parts[1].isdigit():
|
|
377
|
+
collection_idx = int(parts[1])
|
|
378
|
+
if collection_idx > curr_idx:
|
|
379
|
+
curr_idx = collection_idx
|
|
380
|
+
curr_collection = collection
|
|
381
|
+
return curr_collection
|
|
382
|
+
|
|
383
|
+
|
|
344
384
|
def _detect_collections(self) -> bool:
|
|
345
385
|
"""Find collections with train and val prefixes and set them to train_val_splits"""
|
|
346
|
-
def _get_latest_collection(collections: List[EntitiesCollectionInfo]) -> EntitiesCollectionInfo:
|
|
347
|
-
curr_collection = None
|
|
348
|
-
curr_idx = 0
|
|
349
|
-
for collection in collections:
|
|
350
|
-
collection_idx = int(collection.name.rsplit('_', 1)[-1])
|
|
351
|
-
if collection_idx > curr_idx:
|
|
352
|
-
curr_idx = collection_idx
|
|
353
|
-
curr_collection = collection
|
|
354
|
-
return curr_collection
|
|
355
386
|
|
|
356
|
-
all_collections = self.api.entities_collection.get_list(self.project_id)
|
|
357
|
-
train_collections = []
|
|
358
|
-
val_collections = []
|
|
359
387
|
collections_found = False
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
val_collections.append(collection)
|
|
365
|
-
|
|
366
|
-
train_collection = _get_latest_collection(train_collections)
|
|
367
|
-
val_collection = _get_latest_collection(val_collections)
|
|
368
|
-
if train_collection is not None and val_collection is not None:
|
|
369
|
-
self.train_val_splits.set_collections_splits([train_collection.id], [val_collection.id])
|
|
388
|
+
if self._latest_train_collection is not None and self._latest_val_collection is not None:
|
|
389
|
+
self.train_val_splits.set_collections_splits(
|
|
390
|
+
[self._latest_train_collection.id], [self._latest_val_collection.id]
|
|
391
|
+
)
|
|
370
392
|
self.validator_text = Text("Train and val collections are detected", status="info")
|
|
371
393
|
self.validator_text.show()
|
|
372
394
|
collections_found = True
|
|
373
|
-
self._all_train_collections = train_collections
|
|
374
|
-
self._all_val_collections = val_collections
|
|
375
|
-
self._latest_train_collection = train_collection
|
|
376
|
-
self._latest_val_collection = val_collection
|
|
377
395
|
else:
|
|
378
396
|
self.validator_text = Text("")
|
|
379
397
|
self.validator_text.hide()
|
|
380
398
|
collections_found = False
|
|
381
399
|
return collections_found
|
|
382
|
-
|
|
400
|
+
|
|
383
401
|
def _detect_datasets(self) -> bool:
|
|
384
402
|
"""Find datasets with train and val prefixes and set them to train_val_splits"""
|
|
403
|
+
|
|
385
404
|
def _extend_with_nested(root_ds):
|
|
386
405
|
nested = self.api.dataset.get_nested(self.project_id, root_ds.id)
|
|
387
406
|
nested_ids = [ds.id for ds in nested]
|
|
@@ -407,7 +426,9 @@ class TrainValSplitsSelector:
|
|
|
407
426
|
val_count = len(train_val_dataset_ids["val"])
|
|
408
427
|
|
|
409
428
|
if train_count > 0 and val_count > 0:
|
|
410
|
-
self.train_val_splits.set_datasets_splits(
|
|
429
|
+
self.train_val_splits.set_datasets_splits(
|
|
430
|
+
train_val_dataset_ids["train"], train_val_dataset_ids["val"]
|
|
431
|
+
)
|
|
411
432
|
datasets_found = True
|
|
412
433
|
|
|
413
434
|
if train_count > 0 and val_count > 0:
|
|
@@ -415,7 +436,7 @@ class TrainValSplitsSelector:
|
|
|
415
436
|
message = "train and val datasets are detected"
|
|
416
437
|
else:
|
|
417
438
|
message = "Multiple train and val datasets are detected. Check manually if selection is correct"
|
|
418
|
-
|
|
439
|
+
|
|
419
440
|
self.validator_text = Text(message, status="info")
|
|
420
441
|
self.validator_text.show()
|
|
421
442
|
datasets_found = True
|
|
@@ -423,4 +444,4 @@ class TrainValSplitsSelector:
|
|
|
423
444
|
self.validator_text = Text("")
|
|
424
445
|
self.validator_text.hide()
|
|
425
446
|
datasets_found = False
|
|
426
|
-
return datasets_found
|
|
447
|
+
return datasets_found
|
|
@@ -1598,13 +1598,18 @@ class TrainApp:
|
|
|
1598
1598
|
project_id = self.project_id
|
|
1599
1599
|
|
|
1600
1600
|
dataset_infos = [dataset for _, dataset in self._api.dataset.tree(project_id)]
|
|
1601
|
+
id_to_info = {ds.id: ds for ds in dataset_infos}
|
|
1601
1602
|
ds_infos_dict = {}
|
|
1602
1603
|
for dataset in dataset_infos:
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1604
|
+
name_parts = [dataset.name]
|
|
1605
|
+
parent_id = dataset.parent_id
|
|
1606
|
+
while parent_id is not None:
|
|
1607
|
+
parent_ds = id_to_info.get(parent_id)
|
|
1608
|
+
if parent_ds is None:
|
|
1609
|
+
parent_ds = self._api.dataset.get_info_by_id(parent_id)
|
|
1610
|
+
name_parts.append(parent_ds.name)
|
|
1611
|
+
parent_id = parent_ds.parent_id
|
|
1612
|
+
dataset_name = "/".join(reversed(name_parts))
|
|
1608
1613
|
ds_infos_dict[dataset_name] = dataset
|
|
1609
1614
|
|
|
1610
1615
|
def get_image_infos_by_split(ds_infos_dict: dict, split: list):
|
supervisely/project/project.py
CHANGED
|
@@ -4584,6 +4584,7 @@ def upload_project(
|
|
|
4584
4584
|
blob_file_infos = []
|
|
4585
4585
|
|
|
4586
4586
|
for ds_fs in project_fs.datasets:
|
|
4587
|
+
logger.debug(f"Processing dataset: {ds_fs.name}")
|
|
4587
4588
|
if len(ds_fs.parents) > 0:
|
|
4588
4589
|
parent = f"{os.path.sep}".join(ds_fs.parents)
|
|
4589
4590
|
parent_id = dataset_map.get(parent)
|
|
@@ -4624,8 +4625,15 @@ def upload_project(
|
|
|
4624
4625
|
if os.path.isfile(path):
|
|
4625
4626
|
valid_indices.append(i)
|
|
4626
4627
|
valid_paths.append(path)
|
|
4627
|
-
|
|
4628
|
+
elif len(project_fs.blob_files) > 0:
|
|
4628
4629
|
offset_indices.append(i)
|
|
4630
|
+
else:
|
|
4631
|
+
if img_infos[i] is not None:
|
|
4632
|
+
logger.debug(f"Image will be uploaded by image_info: {names[i]}")
|
|
4633
|
+
else:
|
|
4634
|
+
logger.warning(
|
|
4635
|
+
f"Image and image info file not found, image will be skipped: {names[i]}"
|
|
4636
|
+
)
|
|
4629
4637
|
img_paths = valid_paths
|
|
4630
4638
|
ann_paths = list(filter(lambda x: os.path.isfile(x), ann_paths))
|
|
4631
4639
|
# Create a mapping from name to index position for quick lookups
|
supervisely/video/sampling.py
CHANGED
|
@@ -103,7 +103,6 @@ def _upload_annotations(api: Api, image_ids, frame_indices, video_annotation: Vi
|
|
|
103
103
|
api.annotation.upload_anns(image_ids, anns=anns)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
|
|
107
106
|
def _upload_frames(
|
|
108
107
|
api: Api,
|
|
109
108
|
frames: List[np.ndarray],
|
|
@@ -226,28 +225,48 @@ def sample_video(
|
|
|
226
225
|
progress.miniters = 1
|
|
227
226
|
progress.refresh()
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
for
|
|
228
|
+
batch_size = 50
|
|
229
|
+
try:
|
|
230
|
+
with VideoFrameReader(video_path, frame_indices) as reader:
|
|
231
|
+
for batch_indices in batched_iter(frame_indices, batch_size):
|
|
232
|
+
batch_indices_list = list(batch_indices)
|
|
233
|
+
frames = reader.read_batch(batch_indices_list)
|
|
234
|
+
|
|
233
235
|
if resize:
|
|
234
|
-
|
|
236
|
+
resized_frames = []
|
|
237
|
+
for frame in frames:
|
|
238
|
+
resized_frame = cv2.resize(
|
|
239
|
+
frame,
|
|
240
|
+
(resize[1], resize[0]), # (width, height)
|
|
241
|
+
interpolation=cv2.INTER_LINEAR,
|
|
242
|
+
)
|
|
243
|
+
resized_frames.append(resized_frame)
|
|
244
|
+
frames = resized_frames
|
|
245
|
+
|
|
246
|
+
image_ids = _upload_frames(
|
|
247
|
+
api=api,
|
|
248
|
+
frames=frames,
|
|
249
|
+
video_name=video_info.name,
|
|
250
|
+
video_frames_count=video_info.frames_count,
|
|
251
|
+
indices=batch_indices_list,
|
|
252
|
+
dataset_id=dst_dataset_info.id,
|
|
253
|
+
sample_info=sample_info,
|
|
254
|
+
context=context,
|
|
255
|
+
copy_annotations=copy_annotations,
|
|
256
|
+
video_annotation=video_annotation,
|
|
257
|
+
)
|
|
235
258
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
frames=frames,
|
|
239
|
-
video_name=video_info.name,
|
|
240
|
-
video_frames_count=video_info.frames_count,
|
|
241
|
-
indices=indices,
|
|
242
|
-
dataset_id=dst_dataset_info.id,
|
|
243
|
-
sample_info=sample_info,
|
|
244
|
-
context=context,
|
|
245
|
-
copy_annotations=copy_annotations,
|
|
246
|
-
video_annotation=video_annotation,
|
|
247
|
-
)
|
|
259
|
+
if progress is not None:
|
|
260
|
+
progress.update(len(image_ids))
|
|
248
261
|
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
# Free memory after each batch
|
|
263
|
+
del frames
|
|
264
|
+
if resize:
|
|
265
|
+
del resized_frames
|
|
266
|
+
finally:
|
|
267
|
+
import os
|
|
268
|
+
if os.path.exists(video_path):
|
|
269
|
+
os.remove(video_path)
|
|
251
270
|
|
|
252
271
|
|
|
253
272
|
def _get_or_create_dst_dataset(
|