supervisely 6.73.376__py3-none-any.whl → 6.73.378__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 CHANGED
@@ -133,6 +133,7 @@ from supervisely.cli import _handle_creds_error_to_console
133
133
  from supervisely._utils import (
134
134
  rand_str,
135
135
  batched,
136
+ batched_iter,
136
137
  get_bytes_hash,
137
138
  generate_names,
138
139
  ENTERPRISE,
supervisely/_utils.py CHANGED
@@ -98,6 +98,17 @@ def batched(seq, batch_size=50):
98
98
  yield seq[i : i + batch_size]
99
99
 
100
100
 
101
+ def batched_iter(iterable, batch_size=50):
102
+ batch = []
103
+ for item in iterable:
104
+ batch.append(item)
105
+ if len(batch) == batch_size:
106
+ yield batch
107
+ batch = []
108
+ if batch:
109
+ yield batch
110
+
111
+
101
112
  def get_bytes_hash(bytes):
102
113
  return base64.b64encode(hashlib.sha256(bytes).digest()).decode("utf-8")
103
114
 
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- from typing import List, Optional
3
+ from typing import Any, Dict, List, Optional, Union
4
4
 
5
5
  from supervisely._utils import batched
6
6
  from supervisely.api.module_api import ApiField, ModuleApi
@@ -210,11 +210,13 @@ class TagApi(ModuleApi):
210
210
  raise RuntimeError("SDK error: len(tags_keys) != len(tags_to_add)")
211
211
  if len(tags_keys) == 0:
212
212
  return
213
- ids = self.append_to_objects_json(entity_id, tags_to_add)
213
+ ids = self.append_to_objects_json(entity_id, tags_to_add, project_id)
214
214
  KeyIdMap.add_tags_to(key_id_map, tags_keys, ids)
215
215
  return ids
216
216
 
217
- def append_to_objects_json(self, entity_id: int, tags_json: dict) -> list:
217
+ def append_to_objects_json(
218
+ self, entity_id: int, tags_json: List[Dict], project_id: Optional[int] = None
219
+ ) -> List[int]:
218
220
  """
219
221
  Add Tags to Annotation Objects for specific entity (image etc.).
220
222
 
@@ -224,14 +226,50 @@ class TagApi(ModuleApi):
224
226
  :type tags_json: dict
225
227
  :return: List of tags IDs
226
228
  :rtype: list
229
+
230
+ :Usage example:
231
+
232
+ .. code-block:: python
233
+
234
+ import supervisely as sly
235
+
236
+ api = sly.Api(server_address, token)
237
+
238
+ tags_list = [
239
+ {
240
+ "tagId": 25926,
241
+ "objectId": 652959,
242
+ "value": None
243
+ },
244
+ {
245
+ "tagId": 25927,
246
+ "objectId": 652959,
247
+ "value": "v1"
248
+ },
249
+ {
250
+ "tagId": 25927,
251
+ "objectId": 652958,
252
+ "value": "v2"
253
+ }
254
+ ]
255
+ response = api.video.tag.append_to_objects_json(12345, tags_list)
256
+
257
+ print(response)
258
+ # Output:
259
+ # [
260
+ # 80421101,
261
+ # 80421102,
262
+ # 80421103
263
+ # ]
227
264
  """
228
265
 
229
266
  if len(tags_json) == 0:
230
267
  return []
231
- response = self._api.post(
232
- "annotation-objects.tags.bulk.add",
233
- {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json},
234
- )
268
+ if project_id is not None:
269
+ json_data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: tags_json}
270
+ else:
271
+ json_data = {ApiField.ENTITY_ID: entity_id, ApiField.TAGS: tags_json}
272
+ response = self._api.post("annotation-objects.tags.bulk.add", json_data)
235
273
  ids = [obj[ApiField.ID] for obj in response.json()]
236
274
  return ids
237
275
 
@@ -242,11 +280,21 @@ class TagApi(ModuleApi):
242
280
  batch_size: int = 100,
243
281
  log_progress: bool = False,
244
282
  progress: Optional[tqdm_sly] = None,
245
- ) -> List[dict]:
283
+ ) -> List[Dict[str, Union[str, int, None]]]:
246
284
  """
247
- Add Tags to existing Annotation Figures.
248
- All figures must belong to entities of the same project.
249
- Applies only to images project.
285
+ For images project:
286
+ Add Tags to existing Annotation Figures (labels).
287
+ The `tags_list` example:
288
+ [{"tagId": 12345, "figureId": 54321, "value": "tag_value"}, ...].
289
+ For video, pointcloud, volume and pointcloud episodes projects:
290
+ Add Tags to existing Annotation Objects.
291
+ The `frameRange` field is optional and is supported only for video and pointcloud episodes projects.
292
+ The `tags_list`` example:
293
+ [{"tagId": 12345, "objectId": 54321, "value": "tag_value"}, ...].
294
+ or with frameRange:
295
+ [{"tagId": 12345, "objectId": 54321, "value": "tag_value", "frameRange": [1, 10]}, ...].
296
+
297
+ All objects must belong to entities of the same project.
250
298
 
251
299
  :param project_id: Project ID in Supervisely.
252
300
  :type project_id: int
@@ -259,7 +307,7 @@ class TagApi(ModuleApi):
259
307
  :param progress: Progress bar object to display progress.
260
308
  :type progress: Optional[tqdm_sly]
261
309
  :return: List of tags infos as dictionaries.
262
- :rtype: List[dict]
310
+ :rtype: List[Dict[str, Union[str, int, None]]]
263
311
 
264
312
  Usage example:
265
313
  .. code-block:: python
@@ -272,7 +320,8 @@ class TagApi(ModuleApi):
272
320
  {
273
321
  "tagId": 25926,
274
322
  "figureId": 652959,
275
- "value": None # value is optional for tag with type 'None'
323
+ "value": None # optional for tag with type 'None'
324
+ "frameRange": [1, 10] # optional (supported only for video and pointcloud episodes projects)
276
325
  },
277
326
  {
278
327
  "tagId": 25927,
@@ -282,7 +331,7 @@ class TagApi(ModuleApi):
282
331
  {
283
332
  "tagId": 25927,
284
333
  "figureId": 652958,
285
- "value": "v2"
334
+ "value": "v2",
286
335
  }
287
336
  ]
288
337
  response = api.image.tag.add_to_figures(12345, tag_list)
@@ -310,16 +359,14 @@ class TagApi(ModuleApi):
310
359
  # }
311
360
  # ]
312
361
  """
313
- if type(self) is not TagApi:
314
- raise NotImplementedError("This method is not available for classes except TagApi")
315
-
316
- if len(tags_list) == 0:
317
- return []
318
362
 
319
363
  if progress is not None:
320
364
  log_progress = False
321
365
 
322
366
  result = []
367
+
368
+ if len(tags_list) == 0:
369
+ return result
323
370
  if log_progress:
324
371
  progress = tqdm_sly(
325
372
  desc="Adding tags to figures",
@@ -327,8 +374,164 @@ class TagApi(ModuleApi):
327
374
  )
328
375
  for batch in batched(tags_list, batch_size):
329
376
  data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
330
- response = self._api.post("figures.tags.bulk.add", data)
377
+ if type(self) is TagApi:
378
+ response = self._api.post("figures.tags.bulk.add", data)
379
+ else:
380
+ response = self._api.post("annotation-objects.tags.bulk.add", data)
331
381
  result.extend(response.json())
332
382
  if progress is not None:
333
383
  progress.update(len(batch))
334
384
  return result
385
+
386
+ def add_to_entities_json(
387
+ self,
388
+ project_id: int,
389
+ tags_list: List[Dict[str, Union[str, int, None]]],
390
+ batch_size: int = 100,
391
+ log_progress: bool = False,
392
+ ) -> List[int]:
393
+ """
394
+ Bulk add tags to entities (images, videos, pointclouds, volumes) in a project.
395
+ Not supported for pointcloud episodes projects.
396
+ All entities must belong to the same project.
397
+ The `frameRange` field in a tag object within the tags list is optional and is supported only for video projects.
398
+
399
+ The `tags_list` example:
400
+ [{"tagId": 12345, "entityId": 54321, "value": "tag_value"}, ...].
401
+ or with frameRange:
402
+ [{"tagId": 12345, "entityId": 54321, "value": "tag_value", "frameRange": [1, 10]}, ...].
403
+
404
+ :param project_id: Project ID in Supervisely.
405
+ :type project_id: int
406
+ :param tags_list: List of tag object infos as dictionaries
407
+ (e.g. {"tagId": 12345, "entityId": 54321, "value": "tag_value"}).
408
+ :param batch_size: Number of tags to add in one request.
409
+ :type batch_size: int
410
+ :param log_progress: If True, will display a progress bar.
411
+ :type log_progress: bool
412
+ :return: List of tags IDs.
413
+ :rtype: List[int]
414
+
415
+ Usage example:
416
+ .. code-block:: python
417
+
418
+ import supervisely as sly
419
+
420
+ api = sly.Api(server_address, token)
421
+
422
+ tag_list = [
423
+ {
424
+ "tagId": 25926,
425
+ "entityId": 652959,
426
+ "value": None # optional for tag with type 'None'
427
+ "frameRange": [1, 10] # optional (supported only for video projects)
428
+ },
429
+ {
430
+ "tagId": 25927,
431
+ "entityId": 652959,
432
+ "value": "v1"
433
+ },
434
+ {
435
+ "tagId": 25927,
436
+ "entityId": 652958,
437
+ "value": "v2"
438
+ }
439
+ ]
440
+ api.image.tag.add_to_entities_json(project_id=12345, tag_list=tag_list)
441
+ """
442
+
443
+ result = []
444
+
445
+ if len(tags_list) == 0:
446
+ return result
447
+
448
+ if log_progress:
449
+ ds_progress = tqdm_sly(desc="Adding tags to entities", total=len(tags_list))
450
+
451
+ for batch in batched(tags_list, batch_size):
452
+ data = {ApiField.PROJECT_ID: project_id, ApiField.TAGS: batch}
453
+ response = self._api.post("tags.entities.bulk.add", data)
454
+ result.extend([obj[ApiField.ID] for obj in response.json()])
455
+ if log_progress:
456
+ ds_progress.update(len(batch))
457
+
458
+ return result
459
+
460
+ def add_tags_collection_to_objects(
461
+ self,
462
+ project_id: int,
463
+ tags_map: Dict[int, Any],
464
+ batch_size: int = 100,
465
+ log_progress: bool = False,
466
+ ) -> List[Dict[str, Union[str, int, None]]]:
467
+ """
468
+ For images project:
469
+ Add Tags to existing Annotation Figures (labels).
470
+ The `tags_map` example: {figure_id_1: TagCollection, ...}.
471
+ For video, pointcloud, volume and pointcloud episodes projects:
472
+ Add Tags to existing Annotation Objects.
473
+ The `frameRange` field is optional and is supported only for video and pointcloud episodes projects.
474
+ The `tags_map` example: {object_id_1: TagCollection, ...}.
475
+
476
+ All objects must belong to entities of the same project.
477
+
478
+ :param project_id: Project ID in Supervisely.
479
+ :type project_id: int
480
+ :param tags_map: Dictionary with mapping figure/object ID to tags collection.
481
+ :type tags_map: Dict[int, Any]
482
+ :param batch_size: Number of tags to add in one request.
483
+ :type batch_size: int
484
+ :param log_progress: If True, will display a progress bar.
485
+ :type log_progress: bool
486
+ :return: List of tags infos as dictionaries.
487
+ :rtype: List[Dit[str, Union[str, int, None]]]
488
+
489
+ Usage example:
490
+ .. code-block:: python
491
+
492
+ import supervisely as sly
493
+
494
+ api = sly.Api(server_address, token)
495
+
496
+ project_id = 12345
497
+
498
+ tag_meta = sly.TagMeta("tag_name", sly.TagValueType.ANY_STRING)
499
+ meta = sly.ProjectMeta(tag_metas=[tag_meta])
500
+ meta = sly.ProjectMeta.from_json(api.project.update_meta(project_id, meta))
501
+ tag_meta = meta.get_tag_meta("tag_name")
502
+
503
+ # for images project:
504
+ tag_map = {
505
+ 652959: sly.TagCollection([sly.Tag(tag_meta, value="v1"), sly.Tag(tag_meta, value="v2"), ...]),
506
+ 652958: sly.TagCollection([sly.Tag(tag_meta, value="v3"), sly.Tag(tag_meta, value="v4"), ...]),
507
+ ...
508
+ }
509
+ api.image.tag.add_tags_to_objects(project_id, tag_map)
510
+
511
+ # for videos projects (frameRange is optional):
512
+ tag_map = {
513
+ 652959: sly.VideoTagCollection([sly.VideoTag(tag_meta, value="v1", frameRange=[1, 10]), ...]),
514
+ 652958: sly.VideoTagCollection([sly.VideoTag(tag_meta, value="v2", frameRange=[4, 12]), ...]),
515
+ ...
516
+ }
517
+ api.video.tag.add_to_objects_json_batch(project_id, tag_map)
518
+ """
519
+
520
+ OBJ_ID_FIELD = ApiField.FIGURE_ID if type(self) is TagApi else ApiField.OBJECT_ID
521
+
522
+ data = []
523
+ for obj_id, tags in tags_map.items():
524
+ for tag in tags:
525
+
526
+ if tag.meta.sly_id is None:
527
+ raise ValueError(f"Tag {tag.name} meta has no sly_id")
528
+
529
+ data.append(
530
+ {
531
+ ApiField.TAG_ID: tag.meta.sly_id,
532
+ OBJ_ID_FIELD: obj_id,
533
+ **tag.to_json()
534
+ }
535
+ )
536
+
537
+ return self.add_to_objects(project_id, data, batch_size, log_progress)
@@ -3601,7 +3601,87 @@ class ImageApi(RemoveableBulkModuleApi):
3601
3601
  if progress_cb is not None:
3602
3602
  progress_cb(len(batch_ids))
3603
3603
 
3604
- def update_tag_value(self, tag_id: int, value: Union[str, float]) -> Dict:
3604
+ def add_tags_batch(
3605
+ self,
3606
+ image_ids: List[int],
3607
+ tag_ids: Union[int, List[int]],
3608
+ values: Optional[Union[str, int, List[Union[str, int, None]]]] = None,
3609
+ log_progress: bool = False,
3610
+ batch_size: Optional[int] = 100,
3611
+ tag_metas: Optional[Union[TagMeta, List[TagMeta]]] = None,
3612
+ ) -> List[int]:
3613
+ """
3614
+ Add tag with given ID to Images by IDs with different values.
3615
+
3616
+ :param image_ids: List of Images IDs in Supervisely.
3617
+ :type image_ids: List[int]
3618
+ :param tag_ids: Tag IDs in Supervisely.
3619
+ :type tag_ids: int or List[int]
3620
+ :param values: List of tag values for each image or single value for all images.
3621
+ :type values: List[str] or List[int] or str or int, optional
3622
+ :param log_progress: If True, will log progress.
3623
+ :type log_progress: bool, optional
3624
+ :param batch_size: Batch size
3625
+ :type batch_size: int, optional
3626
+ :param tag_metas: Tag Metas. Needed for values validation, omit to skip validation
3627
+ :type tag_metas: TagMeta or List[TagMeta], optional
3628
+ :return: List of tags IDs.
3629
+ :rtype: List[int]
3630
+ :Usage example:
3631
+
3632
+ .. code-block:: python
3633
+
3634
+ import supervisely as sly
3635
+
3636
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
3637
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
3638
+ api = sly.Api.from_env()
3639
+ image_ids = [2389126, 2389127]
3640
+ tag_ids = 277083
3641
+ values = ['value1', 'value2']
3642
+ api.image.add_tags_batch(image_ids, tag_ids, values)
3643
+ """
3644
+ if len(image_ids) == 0:
3645
+ return []
3646
+
3647
+ if isinstance(tag_ids, int):
3648
+ tag_ids = [tag_ids] * len(image_ids)
3649
+
3650
+ if isinstance(tag_metas, TagMeta):
3651
+ tag_metas = [tag_metas] * len(image_ids)
3652
+
3653
+ if values is None:
3654
+ values = [None] * len(image_ids)
3655
+ elif isinstance(values, (str, int)):
3656
+ values = [values] * len(image_ids)
3657
+
3658
+ if len(values) != len(image_ids):
3659
+ raise ValueError("Length of image_ids and values should be the same")
3660
+
3661
+ if len(tag_ids) != len(image_ids):
3662
+ raise ValueError("Length of image_ids and tag_ids should be the same")
3663
+
3664
+ if tag_metas and len(tag_metas) != len(image_ids):
3665
+ raise ValueError("Length of image_ids and tag_metas should be the same")
3666
+
3667
+ if tag_metas:
3668
+ for tag_meta, tag_id, value in zip(tag_metas, tag_ids, values):
3669
+ if not (tag_meta.sly_id == tag_id):
3670
+ raise ValueError(f"{tag_meta.name = } and {tag_id = } should be same")
3671
+ if not tag_meta.is_valid_value(value):
3672
+ raise ValueError(f"{tag_meta.name = } can not have value {value = }")
3673
+
3674
+ project_id = self.get_project_id(image_ids[0])
3675
+ data = [
3676
+ {ApiField.ENTITY_ID: image_id, ApiField.TAG_ID: tag_id, ApiField.VALUE: value}
3677
+ for image_id, tag_id, value in zip(image_ids, tag_ids, values)
3678
+ ]
3679
+
3680
+ return self.tag.add_to_entities_json(project_id, data, batch_size, log_progress)
3681
+
3682
+ def update_tag_value(
3683
+ self, tag_id: int, value: Union[str, float]
3684
+ ) -> Dict:
3605
3685
  """
3606
3686
  Update tag value with given ID.
3607
3687
 
@@ -57,6 +57,26 @@
57
57
  border-bottom: 1px solid #dfe2e8;
58
58
  position: relative;
59
59
  }
60
+
61
+ .el-input-number--mini .el-input-number__decrease,
62
+ .el-input-number--mini .el-input-number__increase {
63
+ height: 20px !important;
64
+ width: 20px !important;
65
+ line-height: 18px !important;
66
+ font-size: 12px !important;
67
+ min-width: 20px !important;
68
+ padding: 0 !important;
69
+ }
70
+
71
+ .el-input-number--mini .el-input-number__decrease {
72
+ right: 20px !important;
73
+ }
74
+
75
+ .el-input-number--mini .el-input-number__decrease i,
76
+ .el-input-number--mini .el-input-number__increase i {
77
+ font-size: 10px !important;
78
+ line-height: 18px !important;
79
+ }
60
80
  </style>
61
81
  <title>{{{app_name}}}</title>
62
82
  </head>
@@ -151,3 +151,4 @@ from supervisely.app.widgets.experiment_selector.experiment_selector import Expe
151
151
  from supervisely.app.widgets.bokeh.bokeh import Bokeh
152
152
  from supervisely.app.widgets.run_app_button.run_app_button import RunAppButton
153
153
  from supervisely.app.widgets.select_collection.select_collection import SelectCollection
154
+ from supervisely.app.widgets.sampling.sampling import Sampling
@@ -1,6 +1,6 @@
1
- from supervisely.app.widgets import Widget
2
- from typing import Dict, List
1
+ from typing import Dict, List, Literal
3
2
 
3
+ from supervisely.app.widgets import Widget
4
4
 
5
5
  """
6
6
  <div
@@ -21,10 +21,12 @@ class Flexbox(Widget):
21
21
  gap: int = 10,
22
22
  center_content: bool = False,
23
23
  widget_id: str = None,
24
+ vertical_alignment: Literal["start", "end", "center", "stretch", "baseline"] = None,
24
25
  ):
25
26
  self._widgets = widgets
26
27
  self._gap = gap
27
28
  self._center_content = center_content
29
+ self._vertical_alignment = vertical_alignment
28
30
  super().__init__(widget_id=widget_id, file_path=__file__)
29
31
 
30
32
  def get_json_data(self) -> Dict:
@@ -1,11 +1,7 @@
1
- <div
2
- {% if widget._center_content == true %}
3
- style="display: flex; gap: {{{widget._gap}}}px; justify-content: center;"
4
- {% else %}
5
- style="display: flex; gap: {{{widget._gap}}}px;"
6
- {% endif %}
7
- >
1
+ <div {% if widget._center_content==true %}
2
+ style="display: flex; gap: {{{widget._gap}}}px; align-items: {{{widget._vertical_alignment}}}; justify-content: center;"
3
+ {% else %} style="display: flex; gap: {{{widget._gap}}}px; align-items: {{{widget._vertical_alignment}}};" {% endif%}>
8
4
  {% for w in widget._widgets %}
9
- {{{w}}}
5
+ {{{w}}}
10
6
  {% endfor %}
11
- </div>
7
+ </div>
@@ -20,6 +20,7 @@ class InputNumber(Widget):
20
20
  debounce: int = 300,
21
21
  precision: int = 0,
22
22
  widget_id: str = None,
23
+ width: int = None,
23
24
  ):
24
25
  self._value = value
25
26
  self._min = min
@@ -29,6 +30,7 @@ class InputNumber(Widget):
29
30
  self._controls = controls
30
31
  self._debounce = debounce
31
32
  self._precision = precision
33
+ self._width = width
32
34
  self._changes_handled = False
33
35
 
34
36
  super().__init__(widget_id=widget_id, file_path=__file__)
@@ -16,5 +16,8 @@
16
16
  :controls="data.{{{widget.widget_id}}}.controls"
17
17
  :debounce="data.{{{widget.widget_id}}}.debounce"
18
18
  :precision="data.{{{widget.widget_id}}}.precision"
19
+ {% if widget._width %}
20
+ :style="{ width: '{{{widget._width}}}px' }"
21
+ {% endif %}
19
22
  >
20
23
  </el-input-number>
File without changes