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.

Files changed (68) hide show
  1. supervisely/__init__.py +24 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/api/dataset_api.py +74 -12
  5. supervisely/api/entity_annotation/figure_api.py +8 -5
  6. supervisely/api/image_api.py +4 -0
  7. supervisely/api/video/video_annotation_api.py +4 -2
  8. supervisely/api/video/video_api.py +41 -1
  9. supervisely/app/__init__.py +1 -1
  10. supervisely/app/content.py +14 -6
  11. supervisely/app/fastapi/__init__.py +1 -0
  12. supervisely/app/fastapi/custom_static_files.py +1 -1
  13. supervisely/app/fastapi/multi_user.py +88 -0
  14. supervisely/app/fastapi/subapp.py +88 -42
  15. supervisely/app/fastapi/websocket.py +77 -9
  16. supervisely/app/singleton.py +21 -0
  17. supervisely/app/v1/app_service.py +18 -2
  18. supervisely/app/v1/constants.py +7 -1
  19. supervisely/app/widgets/card/card.py +20 -0
  20. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  21. supervisely/app/widgets/dialog/dialog.py +12 -0
  22. supervisely/app/widgets/dialog/template.html +2 -1
  23. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  24. supervisely/app/widgets/fast_table/fast_table.py +121 -31
  25. supervisely/app/widgets/fast_table/template.html +1 -1
  26. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  27. supervisely/app/widgets/radio_tabs/template.html +1 -0
  28. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  29. supervisely/app/widgets/table/table.py +68 -13
  30. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  31. supervisely/convert/image/csv/csv_converter.py +24 -15
  32. supervisely/convert/video/video_converter.py +2 -2
  33. supervisely/geometry/polyline_3d.py +110 -0
  34. supervisely/io/env.py +76 -1
  35. supervisely/nn/inference/cache.py +37 -17
  36. supervisely/nn/inference/inference.py +667 -114
  37. supervisely/nn/inference/inference_request.py +15 -8
  38. supervisely/nn/inference/predict_app/gui/classes_selector.py +81 -12
  39. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  40. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  41. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  42. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  43. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  44. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  45. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  46. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  47. supervisely/nn/inference/session.py +43 -35
  48. supervisely/nn/model/model_api.py +9 -0
  49. supervisely/nn/model/prediction_session.py +8 -7
  50. supervisely/nn/prediction_dto.py +7 -0
  51. supervisely/nn/tracker/base_tracker.py +11 -1
  52. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  53. supervisely/nn/tracker/botsort_tracker.py +14 -7
  54. supervisely/nn/tracker/visualize.py +70 -72
  55. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  56. supervisely/nn/training/train_app.py +10 -5
  57. supervisely/project/project.py +9 -1
  58. supervisely/video/sampling.py +39 -20
  59. supervisely/video/video.py +41 -12
  60. supervisely/volume/stl_converter.py +2 -0
  61. supervisely/worker_api/agent_rpc.py +24 -1
  62. supervisely/worker_api/rpc_servicer.py +31 -7
  63. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/METADATA +14 -11
  64. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/RECORD +68 -66
  65. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/LICENSE +0 -0
  66. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/WHEEL +0 -0
  67. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/entry_points.txt +0 -0
  68. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,22 @@
1
- from typing import Union, Dict, List, Tuple, Iterator, Optional
2
- import numpy as np
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 tempfile
9
- import shutil
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, 2)
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 = [{ ApiField.FIELD: ApiField.ID, ApiField.OPERATOR: "in", ApiField.VALUE: train_dataset_id + val_dataset_id}]
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
- for collection in all_collections:
361
- if collection.name.lower().startswith("train_"):
362
- train_collections.append(collection)
363
- elif collection.name.lower().startswith("val_"):
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(train_val_dataset_ids["train"], train_val_dataset_ids["val"])
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
- if dataset.parent_id is not None:
1604
- parent_ds = self._api.dataset.get_info_by_id(dataset.parent_id)
1605
- dataset_name = f"{parent_ds.name}/{dataset.name}"
1606
- else:
1607
- dataset_name = dataset.name
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):
@@ -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
- else:
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
@@ -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
- with VideoFrameReader(video_path, frame_indices) as reader:
230
- for batch in batched_iter(zip(reader, frame_indices), 10):
231
- frames, indices = zip(*batch)
232
- for frame in frames:
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
- cv2.resize(frame, [*resize, frame.shape[2]], interpolation=cv2.INTER_LINEAR)
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
- image_ids = _upload_frames(
237
- api=api,
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
- if progress is not None:
250
- progress.update(len(image_ids))
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(