supervisely 6.73.246__py3-none-any.whl → 6.73.248__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.

@@ -1418,13 +1418,14 @@ class AnnotationApi(ModuleApi):
1418
1418
  # if so, download them separately and update the annotation
1419
1419
  if len(additonal_geometries) > 0:
1420
1420
  figure_ids = list(additonal_geometries.keys())
1421
- figures = self._api.image.figure.download_geometries_batch(
1421
+ figures = await self._api.image.figure.download_geometries_batch_async(
1422
1422
  figure_ids,
1423
1423
  (
1424
1424
  progress_cb
1425
1425
  if progress_cb is not None and progress_cb_type == "size"
1426
1426
  else None
1427
1427
  ),
1428
+ semaphore=semaphore,
1428
1429
  )
1429
1430
  for figure_id, geometry in zip(figure_ids, figures):
1430
1431
  label_idx = additonal_geometries[figure_id]
@@ -1515,3 +1516,131 @@ class AnnotationApi(ModuleApi):
1515
1516
  tasks.append(task)
1516
1517
  ann_infos = await asyncio.gather(*tasks)
1517
1518
  return ann_infos
1519
+
1520
+ async def download_bulk_async(
1521
+ self,
1522
+ dataset_id: int,
1523
+ image_ids: List[int],
1524
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1525
+ with_custom_data: Optional[bool] = False,
1526
+ force_metadata_for_links: Optional[bool] = True,
1527
+ semaphore: Optional[asyncio.Semaphore] = None,
1528
+ ) -> List[AnnotationInfo]:
1529
+ """
1530
+ Get list of AnnotationInfos for given dataset ID from API.
1531
+ This method is optimized for downloading a large number of small size annotations with a single API call.
1532
+
1533
+ :param dataset_id: Dataset ID in Supervisely.
1534
+ :type dataset_id: int
1535
+ :param image_ids: List of integers.
1536
+ :type image_ids: List[int]
1537
+ :param progress_cb: Function for tracking download progress.
1538
+ :type progress_cb: tqdm
1539
+ :param with_custom_data: Include custom data in the response.
1540
+ :type with_custom_data: bool, optional
1541
+ :param force_metadata_for_links: Force metadata for links.
1542
+ :type force_metadata_for_links: bool, optional
1543
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1544
+ :type semaphore: asyncio.Semaphore, optional
1545
+ :return: Information about Annotations. See :class:`info_sequence<info_sequence>`
1546
+ :rtype: :class:`List[AnnotationInfo]`
1547
+
1548
+ :Usage example:
1549
+
1550
+ .. code-block:: python
1551
+
1552
+ import supervisely as sly
1553
+
1554
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1555
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1556
+ api = sly.Api.from_env()
1557
+
1558
+ dataset_id = 254737
1559
+ image_ids = [121236918, 121236919]
1560
+ p = tqdm(desc="Annotations downloaded: ", total=len(image_ids))
1561
+
1562
+ ann_infos = await api.annotation.download_bulk_async(dataset_id, image_ids, progress_cb=p)
1563
+
1564
+ Optimizing the download process by using the context to avoid redundant API calls.:
1565
+ # 1. Download the project meta
1566
+ project_id = api.dataset.get_info_by_id(dataset_id).project_id
1567
+ project_meta = api.project.get_meta(project_id)
1568
+
1569
+ # 2. Use the context to avoid redundant API calls
1570
+ dataset_id = 254737
1571
+ image_ids = [121236918, 121236919]
1572
+ with sly.ApiContext(api, dataset_id=dataset_id, project_id=project_id, project_meta=project_meta):
1573
+ ann_infos = await api.annotation.download_bulk_async(dataset_id, image_ids)
1574
+ """
1575
+ if semaphore is None:
1576
+ semaphore = self._api.get_default_semaphore()
1577
+
1578
+ # use context to avoid redundant API calls
1579
+ context = self._api.optimization_context
1580
+ context_dataset_id = context.get("dataset_id")
1581
+ project_meta = context.get("project_meta")
1582
+ project_id = context.get("project_id")
1583
+ if dataset_id != context_dataset_id:
1584
+ context["dataset_id"] = dataset_id
1585
+ project_id, project_meta = None, None
1586
+
1587
+ if not isinstance(project_meta, ProjectMeta):
1588
+ if project_id is None:
1589
+ project_id = self._api.dataset.get_info_by_id(dataset_id).project_id
1590
+ context["project_id"] = project_id
1591
+ project_meta = ProjectMeta.from_json(self._api.project.get_meta(project_id))
1592
+ context["project_meta"] = project_meta
1593
+
1594
+ need_download_alpha_masks = False
1595
+ for obj_cls in project_meta.obj_classes:
1596
+ if obj_cls.geometry_type == AlphaMask:
1597
+ need_download_alpha_masks = True
1598
+ break
1599
+
1600
+ id_to_ann = {}
1601
+ for batch in batched(image_ids):
1602
+ json_data = {
1603
+ ApiField.DATASET_ID: dataset_id,
1604
+ ApiField.IMAGE_IDS: batch,
1605
+ ApiField.WITH_CUSTOM_DATA: with_custom_data,
1606
+ ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
1607
+ ApiField.INTEGER_COORDS: False,
1608
+ }
1609
+ async with semaphore:
1610
+ results = await self._api.post_async("annotations.bulk.info", json=json_data)
1611
+ results = results.json()
1612
+ if need_download_alpha_masks is True:
1613
+ additonal_geometries = defaultdict(tuple)
1614
+ for ann_idx, ann_dict in enumerate(results):
1615
+ # check if there are any AlphaMask geometries in the batch
1616
+ for label_idx, label in enumerate(
1617
+ ann_dict[ApiField.ANNOTATION][AnnotationJsonFields.LABELS]
1618
+ ):
1619
+ if label[LabelJsonFields.GEOMETRY_TYPE] == AlphaMask.geometry_name():
1620
+ figure_id = label[LabelJsonFields.ID]
1621
+ additonal_geometries[figure_id] = (ann_idx, label_idx)
1622
+
1623
+ # if there are any AlphaMask geometries, download them separately and update the annotation
1624
+ if len(additonal_geometries) > 0:
1625
+ figure_ids = list(additonal_geometries.keys())
1626
+ figures = await self._api.image.figure.download_geometries_batch_async(
1627
+ figure_ids, semaphore=semaphore
1628
+ )
1629
+ for figure_id, geometry in zip(figure_ids, figures):
1630
+ ann_idx, label_idx = additonal_geometries[figure_id]
1631
+ results[ann_idx][ApiField.ANNOTATION][AnnotationJsonFields.LABELS][
1632
+ label_idx
1633
+ ].update({BITMAP: geometry})
1634
+
1635
+ for ann_dict in results:
1636
+ # Convert annotation to pixel coordinate system
1637
+ ann_dict[ApiField.ANNOTATION] = Annotation._to_pixel_coordinate_system_json(
1638
+ ann_dict[ApiField.ANNOTATION]
1639
+ )
1640
+ ann_info = self._convert_json_info(ann_dict)
1641
+ id_to_ann[ann_info.image_id] = ann_info
1642
+
1643
+ if progress_cb is not None:
1644
+ progress_cb(len(batch))
1645
+ ordered_results = [id_to_ann[image_id] for image_id in image_ids]
1646
+ return ordered_results
supervisely/api/api.py CHANGED
@@ -1437,7 +1437,7 @@ class Api:
1437
1437
  :type range_start: int, optional
1438
1438
  :param range_end: End byte position for streaming.
1439
1439
  :type range_end: int, optional
1440
- :param chunk_size: Size of the chunk to read from the stream.
1440
+ :param chunk_size: Size of the chunk to read from the stream. Default is 8192.
1441
1441
  :type chunk_size: int, optional
1442
1442
  :param use_public_api: Define if public API should be used.
1443
1443
  :type use_public_api: bool, optional
@@ -3,10 +3,21 @@
3
3
  # docs
4
4
  from __future__ import annotations
5
5
 
6
+ import asyncio
6
7
  import json
7
8
  import re
8
9
  from collections import defaultdict
9
- from typing import Callable, Dict, Generator, List, NamedTuple, Optional, Tuple, Union
10
+ from typing import (
11
+ AsyncGenerator,
12
+ Callable,
13
+ Dict,
14
+ Generator,
15
+ List,
16
+ NamedTuple,
17
+ Optional,
18
+ Tuple,
19
+ Union,
20
+ )
10
21
 
11
22
  import numpy as np
12
23
  from requests_toolbelt import MultipartDecoder, MultipartEncoder
@@ -608,3 +619,81 @@ class FigureApi(RemoveableBulkModuleApi):
608
619
  )
609
620
  encoder = MultipartEncoder(fields=fields)
610
621
  self._api.post("figures.bulk.upload.geometry", encoder)
622
+
623
+ async def _download_geometries_generator_async(
624
+ self, ids: List[int], semaphore: Optional[asyncio.Semaphore] = None
625
+ ) -> AsyncGenerator[Tuple[int, bytes], None, None]:
626
+ """
627
+ Private method. Download figures geometries with given IDs from storage asynchronously.
628
+
629
+ :param ids: List of figure IDs in Supervisely.
630
+ :type ids: List[int]
631
+ :return: Async generator with pairs of figure ID and figure geometry.
632
+ :rtype: AsyncGenerator[Tuple[int, bytes], None, None]
633
+ """
634
+ if semaphore is None:
635
+ semaphore = self._api.get_default_semaphore()
636
+
637
+ for batch_ids in batched(ids):
638
+ async with semaphore:
639
+ response = await self._api.post_async(
640
+ "figures.bulk.download.geometry", {ApiField.IDS: batch_ids}
641
+ )
642
+ decoder = MultipartDecoder.from_response(response)
643
+ for part in decoder.parts:
644
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
645
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
646
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
647
+ figure_id = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
648
+ yield figure_id, part.content
649
+
650
+ async def download_geometries_batch_async(
651
+ self,
652
+ ids: List[int],
653
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
654
+ semaphore: Optional[asyncio.Semaphore] = None,
655
+ ) -> List[dict]:
656
+ """
657
+ Download figure geometries with given IDs from storage asynchronously.
658
+
659
+ :param ids: List of figure IDs in Supervisely.
660
+ :type ids: List[int]
661
+ :param progress_cb: Progress bar to show the download progress. Shows the number of bytes downloaded.
662
+ :type progress_cb: Union[tqdm, Callable], optional
663
+ :param semaphore: Semaphore to limit the number of concurrent downloads.
664
+ :type semaphore: Optional[asyncio.Semaphore], optional
665
+ :return: List of figure geometries in Supervisely JSON format.
666
+ :rtype: List[dict]
667
+
668
+ :Usage example:
669
+
670
+ .. code-block:: python
671
+
672
+ import asyncio
673
+ import supervisely as sly
674
+
675
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
676
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
677
+ api = sly.Api.from_env()
678
+
679
+ figure_ids = [642155547, 642155548, 642155549]
680
+ loop = sly.utils.get_or_create_event_loop()
681
+ geometries = loop.run_until_complete(
682
+ api.figure.download_geometries_batch_async(
683
+ figure_ids,
684
+ progress_cb=tqdm(total=len(figure_ids), desc="Downloading geometries"),
685
+ semaphore=asyncio.Semaphore(15),
686
+ )
687
+ )
688
+ """
689
+ geometries = {}
690
+ async for idx, part in self._download_geometries_generator_async(ids, semaphore):
691
+ if progress_cb is not None:
692
+ progress_cb(len(part))
693
+ geometry_json = json.loads(part)
694
+ geometries[idx] = geometry_json
695
+
696
+ if len(geometries) != len(ids):
697
+ raise RuntimeError("Not all geometries were downloaded")
698
+ ordered_results = [geometries[i] for i in ids]
699
+ return ordered_results
@@ -13,6 +13,7 @@ from collections import defaultdict
13
13
  from concurrent.futures import ThreadPoolExecutor
14
14
  from datetime import datetime
15
15
  from functools import partial
16
+ from math import ceil
16
17
  from pathlib import Path
17
18
  from time import sleep
18
19
  from typing import (
@@ -3705,11 +3706,12 @@ class ImageApi(RemoveableBulkModuleApi):
3705
3706
  range_start: Optional[int] = None,
3706
3707
  range_end: Optional[int] = None,
3707
3708
  headers: Optional[dict] = None,
3709
+ chunk_size: Optional[int] = None,
3708
3710
  ) -> AsyncGenerator:
3709
3711
  """
3710
3712
  Download Image with given ID asynchronously.
3711
3713
  If is_stream is True, returns stream of bytes, otherwise returns response object.
3712
- For streaming, returns tuple of chunk and hash.
3714
+ For streaming, returns tuple of chunk and hash. Chunk size is 8 MB by default.
3713
3715
 
3714
3716
  :param id: Image ID in Supervisely.
3715
3717
  :type id: int
@@ -3721,11 +3723,16 @@ class ImageApi(RemoveableBulkModuleApi):
3721
3723
  :type range_end: int, optional
3722
3724
  :param headers: Headers for request.
3723
3725
  :type headers: dict, optional
3726
+ :param chunk_size: Size of chunk for streaming. Default is 8 MB.
3727
+ :type chunk_size: int, optional
3724
3728
  :return: Stream of bytes or response object.
3725
3729
  :rtype: AsyncGenerator
3726
3730
  """
3727
3731
  api_method_name = "images.download"
3728
3732
 
3733
+ if chunk_size is None:
3734
+ chunk_size = 8 * 1024 * 1024
3735
+
3729
3736
  json_body = {ApiField.ID: id}
3730
3737
 
3731
3738
  if is_stream:
@@ -3736,6 +3743,7 @@ class ImageApi(RemoveableBulkModuleApi):
3736
3743
  headers=headers,
3737
3744
  range_start=range_start,
3738
3745
  range_end=range_end,
3746
+ chunk_size=chunk_size,
3739
3747
  ):
3740
3748
  yield chunk, hhash
3741
3749
  else:
@@ -4157,3 +4165,180 @@ class ImageApi(RemoveableBulkModuleApi):
4157
4165
  tasks.append(task)
4158
4166
  results = await asyncio.gather(*tasks)
4159
4167
  return results
4168
+
4169
+ async def download_bytes_generator_async(
4170
+ self,
4171
+ dataset_id: int,
4172
+ img_ids: List[int],
4173
+ semaphore: Optional[asyncio.Semaphore] = None,
4174
+ headers: Optional[dict] = None,
4175
+ check_hash: bool = False,
4176
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
4177
+ progress_cb_type: Literal["number", "size"] = "number",
4178
+ ) -> AsyncGenerator[Tuple[int, bytes]]:
4179
+ """
4180
+ Downloads Image bytes with given ID in batch asynchronously.
4181
+ Yields tuple of Image ID and bytes of downloaded image.
4182
+ Uses bulk download API method.
4183
+
4184
+ :param dataset_id: Dataset ID in Supervisely.
4185
+ :type dataset_id: int
4186
+ :param img_ids: List of Image IDs in Supervisely.
4187
+ :type img_ids: :class:`List[int]`
4188
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
4189
+ :type semaphore: :class:`asyncio.Semaphore`, optional
4190
+ :param headers: Headers for request.
4191
+ :type headers: dict, optional
4192
+ :param check_hash: If True, checks hash of downloaded bytes. Default is False.
4193
+ :type check_hash: bool, optional
4194
+ :param progress_cb: Function for tracking download progress.
4195
+ :type progress_cb: Optional[Union[tqdm, Callable]]
4196
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
4197
+ :type progress_cb_type: Literal["number", "size"], optional
4198
+ :return: Tuple of Image ID and bytes of downloaded image.
4199
+ :rtype: :class:`Tuple[int, bytes]`
4200
+
4201
+ :Usage example:
4202
+
4203
+ .. code-block:: python
4204
+
4205
+ import supervisely as sly
4206
+ import asyncio
4207
+
4208
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
4209
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
4210
+ api = sly.Api.from_env()
4211
+
4212
+ dataset_id = 123456
4213
+ img_ids = [770918, 770919, 770920, 770921, ... , 770992]
4214
+ tasks = []
4215
+ for batch in batched(img_ids, 50):
4216
+ task = api.image.download_bytes_batch_async(dataset_id, batch)
4217
+ tasks.append(task)
4218
+ results = await asyncio.gather(*tasks)
4219
+ """
4220
+ api_method_name = "images.bulk.download"
4221
+ json_body = {
4222
+ ApiField.DATASET_ID: dataset_id,
4223
+ ApiField.IMAGE_IDS: img_ids,
4224
+ }
4225
+
4226
+ if semaphore is None:
4227
+ semaphore = self._api.get_default_semaphore()
4228
+ async with semaphore:
4229
+ response = await self._api.post_async(
4230
+ api_method_name,
4231
+ json=json_body,
4232
+ headers=headers,
4233
+ )
4234
+ decoder = MultipartDecoder.from_response(response)
4235
+ for part in decoder.parts:
4236
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
4237
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
4238
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
4239
+ img_id = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
4240
+ if check_hash:
4241
+ hhash = part.headers.get("x-content-checksum-sha256", None)
4242
+ if hhash is not None:
4243
+ downloaded_bytes_hash = get_bytes_hash(part)
4244
+ if hhash != downloaded_bytes_hash:
4245
+ raise RuntimeError(
4246
+ f"Downloaded hash of image with ID:{img_id} does not match the expected hash: {downloaded_bytes_hash} != {hhash}"
4247
+ )
4248
+ if progress_cb is not None and progress_cb_type == "number":
4249
+ progress_cb(1)
4250
+ elif progress_cb is not None and progress_cb_type == "size":
4251
+ progress_cb(len(part.content))
4252
+
4253
+ yield img_id, part.content
4254
+
4255
+ async def get_list_generator_async(
4256
+ self,
4257
+ dataset_id: int = None,
4258
+ filters: Optional[List[Dict[str, str]]] = None,
4259
+ sort: Optional[str] = "id",
4260
+ sort_order: Optional[str] = "asc",
4261
+ force_metadata_for_links: Optional[bool] = True,
4262
+ only_labelled: Optional[bool] = False,
4263
+ fields: Optional[List[str]] = None,
4264
+ per_page: Optional[int] = 500,
4265
+ semaphore: Optional[List[asyncio.Semaphore]] = None,
4266
+ **kwargs,
4267
+ ) -> AsyncGenerator[List[ImageInfo]]:
4268
+ """
4269
+ Yields list of images in dataset asynchronously page by page.
4270
+
4271
+ :param dataset_id: Dataset ID in Supervisely.
4272
+ :type dataset_id: int
4273
+ :param filters: Filters for images.
4274
+ :type filters: List[Dict[str, str]], optional
4275
+ :param sort: Sort images by field.
4276
+ :type sort: str, optional
4277
+ :param sort_order: Sort order for images.
4278
+ :type sort_order: str, optional
4279
+ :param force_metadata_for_links: If True, forces metadata for links.
4280
+ :type force_metadata_for_links: bool, optional
4281
+ :param only_labelled: If True, returns only labelled images.
4282
+ :type only_labelled: bool, optional
4283
+ :param fields: List of fields to return.
4284
+ :type fields: List[str], optional
4285
+ :param per_page: Number of images to return per page.
4286
+ :type per_page: int, optional
4287
+ :param semaphore: Semaphore for limiting the number of simultaneous requests.
4288
+ :type semaphore: :class:`asyncio.Semaphore`, optional
4289
+ :param kwargs: Additional arguments.
4290
+ :return: List of images in dataset.
4291
+ :rtype: AsyncGenerator[List[ImageInfo]]
4292
+
4293
+ :Usage example:
4294
+
4295
+ .. code-block:: python
4296
+
4297
+ import supervisely as sly
4298
+ import asyncio
4299
+
4300
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
4301
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
4302
+ api = sly.Api.from_env()
4303
+
4304
+ loop = sly.utils.get_or_create_event_loop()
4305
+ images = loop.run_until_complete(api.image.get_list_async(123456, per_page=600))
4306
+ """
4307
+
4308
+ method = "images.list"
4309
+ dataset_info = kwargs.get("dataset_info", None)
4310
+
4311
+ if dataset_info is None:
4312
+ dataset_info = self._api.dataset.get_info_by_id(dataset_id, raise_error=True)
4313
+
4314
+ total_pages = ceil(dataset_info.items_count / per_page)
4315
+
4316
+ data = {
4317
+ ApiField.DATASET_ID: dataset_info.id,
4318
+ ApiField.PROJECT_ID: dataset_info.project_id,
4319
+ ApiField.SORT: sort,
4320
+ ApiField.SORT_ORDER: sort_order,
4321
+ ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
4322
+ ApiField.FILTER: filters or [],
4323
+ ApiField.PER_PAGE: per_page,
4324
+ }
4325
+ if fields is not None:
4326
+ data[ApiField.FIELDS] = fields
4327
+ if only_labelled:
4328
+ data[ApiField.FILTERS] = [
4329
+ {
4330
+ "type": "objects_class",
4331
+ "data": {
4332
+ "from": 1,
4333
+ "to": 9999,
4334
+ "include": True,
4335
+ "classId": None,
4336
+ },
4337
+ }
4338
+ ]
4339
+
4340
+ if semaphore is None:
4341
+ semaphore = self._api.get_default_semaphore()
4342
+
4343
+ async for page in self.get_list_page_generator_async(method, data, total_pages, semaphore):
4344
+ yield page
@@ -1,7 +1,9 @@
1
1
  # coding: utf-8
2
+ import asyncio
2
3
  from collections import namedtuple
3
4
  from copy import deepcopy
4
- from typing import TYPE_CHECKING, List
5
+ from math import ceil
6
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, List, NamedTuple, Optional, Tuple
5
7
 
6
8
  import requests
7
9
 
@@ -867,6 +869,98 @@ class ModuleApiBase(_JsonConvertibleModule):
867
869
  response = self._get_response_by_id(id, method, id_field=ApiField.ID, fields=fields)
868
870
  return self._convert_json_info(response.json()) if (response is not None) else None
869
871
 
872
+ async def get_list_idx_page_async(
873
+ self,
874
+ method: str,
875
+ data: dict,
876
+ ) -> Tuple[int, List[NamedTuple]]:
877
+ """
878
+ Get the list of items for a given page number.
879
+ Page number is specified in the data dictionary.
880
+
881
+ :param method: Method to call for listing items.
882
+ :type method: str
883
+ :param data: Data to pass to the API method.
884
+ :type data: dict
885
+ :return: List of items.
886
+ :rtype: Tuple[int, List[NamedTuple]]
887
+ """
888
+
889
+ response = await self._api.post_async(method, data)
890
+ response_json = response.json()
891
+ entities = response_json.get("entities", [])
892
+ # To avoid empty pages when a filter is applied to the data and the `pagesCount` is less than the number calculated based on the items and `per_page` size.
893
+ # Process `pagesCount` in the main function according to the actual number of pages returned.
894
+ pages_count = response_json.get("pagesCount", None)
895
+ if pages_count is None:
896
+ raise ValueError("Can not determine the number of pages to retrieve.")
897
+ return pages_count, [self._convert_json_info(item) for item in entities]
898
+
899
+ async def get_list_page_generator_async(
900
+ self,
901
+ method: str,
902
+ data: dict,
903
+ pages_count: Optional[int] = None,
904
+ semaphore: Optional[List[asyncio.Semaphore]] = None,
905
+ ) -> AsyncGenerator[List[Any], None]:
906
+ """
907
+ Yields list of images in dataset asynchronously page by page.
908
+
909
+ :param method: Method to call for listing items.
910
+ :type method: str
911
+ :param data: Data to pass to the API method.
912
+ :type data: dict
913
+ :param pages_count: Preferred number of pages to retrieve if used with a `per_page` limit.
914
+ Will be automatically adjusted if the `pagesCount` differs from the requested number.
915
+ :type pages_count: int, optional
916
+ :param semaphore: Semaphore for limiting the number of simultaneous requests.
917
+ :type semaphore: :class:`asyncio.Semaphore`, optional
918
+ :param kwargs: Additional arguments.
919
+ :return: List of images in dataset.
920
+ :rtype: AsyncGenerator[List[ImageInfo]]
921
+
922
+ :Usage example:
923
+
924
+ .. code-block:: python
925
+
926
+ import supervisely as sly
927
+ import asyncio
928
+
929
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
930
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
931
+ api = sly.Api.from_env()
932
+
933
+ method = 'images.list'
934
+ data = {
935
+ 'datasetId': 123456
936
+ }
937
+
938
+ loop = sly.utils.get_or_create_event_loop()
939
+ images = loop.run_until_complete(api.image.get_list_generator_async(method, data))
940
+ """
941
+
942
+ if semaphore is None:
943
+ semaphore = self._api.get_default_semaphore()
944
+
945
+ async def sem_task(task):
946
+ async with semaphore:
947
+ return await task
948
+
949
+ if pages_count is None:
950
+ pages_count = 999999 # to avoid range lesser than total pages count
951
+ for page_num in range(1, pages_count + 1):
952
+ if page_num <= pages_count:
953
+ data[ApiField.PAGE] = page_num
954
+ total_pages, items = await sem_task(self.get_list_idx_page_async(method, data))
955
+
956
+ # To correct `total_pages` count in case filter is applied
957
+ if page_num == 1:
958
+ pages_count = total_pages
959
+
960
+ yield items
961
+ else:
962
+ break
963
+
870
964
 
871
965
  class ModuleApi(ModuleApiBase):
872
966
  """Base class for entities that have a parent object in the system."""
@@ -1,6 +1,5 @@
1
1
  import cv2
2
2
  import numpy as np
3
- import scipy
4
3
 
5
4
 
6
5
  def one_hot(segmentation, num_classes):
@@ -18,12 +17,16 @@ def single_one_hot(segmentation, cls):
18
17
 
19
18
 
20
19
  def get_single_contiguous_segment(one_hot_segmentation):
20
+ import scipy # pylint: disable=import-error
21
+
21
22
  kernel = np.ones((3, 3), dtype=one_hot_segmentation.dtype)
22
23
  seg = scipy.ndimage.label(one_hot_segmentation, structure=kernel)[0]
23
24
  return [np.where(seg == l) for l in range(1, seg.max() + 1)]
24
25
 
25
26
 
26
27
  def get_contiguous_segments(one_hot_segmentation):
28
+ import scipy # pylint: disable=import-error
29
+
27
30
  kernel = np.ones((3, 3), dtype=one_hot_segmentation.dtype)
28
31
  segments_tensor = np.stack(
29
32
  [scipy.ndimage.label(seg, structure=kernel)[0] for seg in one_hot_segmentation]
@@ -1,7 +1,5 @@
1
1
  # vim: expandtab:ts=4:sw=4
2
2
  import numpy as np
3
- import scipy.linalg
4
-
5
3
 
6
4
  """
7
5
  Table for the 0.95 quantile of the chi-square distribution with N degrees of
@@ -171,6 +169,8 @@ class KalmanFilter(object):
171
169
  Returns the measurement-corrected state distribution.
172
170
 
173
171
  """
172
+ import scipy.linalg # pylint: disable=import-error
173
+
174
174
  projected_mean, projected_cov = self.project(mean, covariance)
175
175
 
176
176
  chol_factor, lower = scipy.linalg.cho_factor(
@@ -215,6 +215,8 @@ class KalmanFilter(object):
215
215
  `measurements[i]`.
216
216
 
217
217
  """
218
+ import scipy.linalg # pylint: disable=import-error
219
+
218
220
  mean, covariance = self.project(mean, covariance)
219
221
  if only_position:
220
222
  mean, covariance = mean[:2], covariance[:2, :2]
@@ -1,5 +1,4 @@
1
1
  import numpy as np
2
- import scipy.linalg
3
2
 
4
3
  """
5
4
  Table for the 0.95 quantile of the chi-square distribution with N degrees of
@@ -215,6 +214,8 @@ class KalmanFilterXYWH(object):
215
214
  Returns the measurement-corrected state distribution.
216
215
 
217
216
  """
217
+ import scipy.linalg # pylint: disable=import-error
218
+
218
219
  projected_mean, projected_cov = self.project(mean, covariance)
219
220
 
220
221
  chol_factor, lower = scipy.linalg.cho_factor(projected_cov, lower=True, check_finite=False)
@@ -254,6 +255,8 @@ class KalmanFilterXYWH(object):
254
255
  squared Mahalanobis distance between (mean, covariance) and
255
256
  `measurements[i]`.
256
257
  """
258
+ import scipy.linalg # pylint: disable=import-error
259
+
257
260
  mean, covariance = self.project(mean, covariance)
258
261
  if only_position:
259
262
  mean, covariance = mean[:2], covariance[:2, :2]
@@ -428,6 +431,8 @@ class KalmanFilterXYAH(object):
428
431
  Returns the measurement-corrected state distribution.
429
432
 
430
433
  """
434
+ import scipy.linalg # pylint: disable=import-error
435
+
431
436
  projected_mean, projected_cov = self.project(mean, covariance)
432
437
 
433
438
  chol_factor, lower = scipy.linalg.cho_factor(projected_cov, lower=True, check_finite=False)
@@ -471,6 +476,8 @@ class KalmanFilterXYAH(object):
471
476
  `measurements[i]`.
472
477
 
473
478
  """
479
+ import scipy.linalg # pylint: disable=import-error
480
+
474
481
  mean, covariance = self.project(mean, covariance)
475
482
  if only_position:
476
483
  mean, covariance = mean[:2], covariance[:2, :2]
@@ -44,9 +44,6 @@ class TrainingProcess:
44
44
  self.model_benchmark_report_thumbnail = ReportThumbnail()
45
45
  self.model_benchmark_report_thumbnail.hide()
46
46
 
47
- self.model_benchmark_report_text = Text(status="info", text="Creating report on model...")
48
- self.model_benchmark_report_text.hide()
49
-
50
47
  self.validator_text = Text("")
51
48
  self.validator_text.hide()
52
49
  self.start_button = Button("Start")
@@ -67,7 +64,6 @@ class TrainingProcess:
67
64
  self.validator_text,
68
65
  self.artifacts_thumbnail,
69
66
  self.model_benchmark_report_thumbnail,
70
- self.model_benchmark_report_text,
71
67
  ]
72
68
 
73
69
  if self.app_options.get("device_selector", False):
@@ -1208,7 +1208,7 @@ class TrainApp:
1208
1208
  # Prepare logs
1209
1209
  if sly_fs.dir_exists(self.log_dir):
1210
1210
  logs_dir = join(self.output_dir, "logs")
1211
- shutil.move(self.log_dir, logs_dir)
1211
+ shutil.copytree(self.log_dir, logs_dir)
1212
1212
 
1213
1213
  # Generate experiment_info.json and app_state.json
1214
1214
  def _upload_file_to_team_files(self, local_path: str, remote_path: str, message: str) -> None:
@@ -1568,7 +1568,9 @@ class TrainApp:
1568
1568
  remote_config_path = None
1569
1569
 
1570
1570
  logger.info(f"Creating the report for the best model: {best_filename!r}")
1571
- self.gui.training_process.model_benchmark_report_text.show()
1571
+ self.gui.training_process.validator_text.set(
1572
+ f"Creating evaluation report for the best model: {best_filename!r}", "info"
1573
+ )
1572
1574
  self.progress_bar_main(message="Starting Model Benchmark evaluation", total=1)
1573
1575
  self.progress_bar_main.show()
1574
1576
 
@@ -1697,7 +1699,6 @@ class TrainApp:
1697
1699
  self._team_id, remote_dir + "template.vue"
1698
1700
  )
1699
1701
 
1700
- self.gui.training_process.model_benchmark_report_text.hide()
1701
1702
  self.gui.training_process.model_benchmark_report_thumbnail.set(
1702
1703
  benchmark_report_template
1703
1704
  )
@@ -1713,7 +1714,9 @@ class TrainApp:
1713
1714
  )
1714
1715
  except Exception as e:
1715
1716
  logger.error(f"Model benchmark failed. {repr(e)}", exc_info=True)
1716
- self.gui.training_process.model_benchmark_report_text.hide()
1717
+ self.gui.training_process.validator_text.set(
1718
+ "Finalizing and uploading training artifacts...", "info"
1719
+ )
1717
1720
  self.progress_bar_main.hide()
1718
1721
  self.progress_bar_secondary.hide()
1719
1722
  try:
@@ -225,31 +225,51 @@ def download_async_or_sync(
225
225
  semaphore: Optional[asyncio.Semaphore] = None,
226
226
  **kwargs,
227
227
  ):
228
- project_info = api.project.get_info_by_id(project_id)
228
+ """
229
+ Download project asynchronously if possible, otherwise download synchronously.
230
+ Automatically detects project type.
231
+ You can pass :class:`ProjectInfo` as `project_info` kwarg to avoid additional API requests.
232
+
233
+ In case of error during asynchronous download, the function will switch to synchronous download.
234
+ """
235
+ project_info = kwargs.pop("project_info", None)
236
+ if not isinstance(project_info, ProjectInfo) or project_info.id != project_id:
237
+ project_info = api.project.get_info_by_id(project_id)
229
238
 
230
239
  if progress_cb is not None:
231
240
  log_progress = False
232
241
 
233
242
  project_class = get_project_class(project_info.type)
234
- if hasattr(project_class, "download_async"):
235
- download_coro = project_class.download_async(
236
- api=api,
237
- project_id=project_id,
238
- dest_dir=dest_dir,
239
- semaphore=semaphore,
240
- dataset_ids=dataset_ids,
241
- log_progress=log_progress,
242
- progress_cb=progress_cb,
243
- **kwargs,
244
- )
245
- loop = get_or_create_event_loop()
246
- if loop.is_running():
247
- future = asyncio.run_coroutine_threadsafe(download_coro, loop=loop)
248
- future.result()
249
- else:
250
- loop.run_until_complete(download_coro)
251
243
 
244
+ switch_to_sync = False
245
+ if hasattr(project_class, "download_async"):
246
+ try:
247
+ download_coro = project_class.download_async(
248
+ api=api,
249
+ project_id=project_id,
250
+ dest_dir=dest_dir,
251
+ semaphore=semaphore,
252
+ dataset_ids=dataset_ids,
253
+ log_progress=log_progress,
254
+ progress_cb=progress_cb,
255
+ **kwargs,
256
+ )
257
+ loop = get_or_create_event_loop()
258
+ if loop.is_running():
259
+ future = asyncio.run_coroutine_threadsafe(download_coro, loop=loop)
260
+ future.result()
261
+ else:
262
+ loop.run_until_complete(download_coro)
263
+ except Exception as e:
264
+ if kwargs.get("resume_download", False) is False:
265
+ remove_dir(dest_dir)
266
+ logger.error(f"Failed to download project {project_id} asynchronously: {e}")
267
+ logger.warning("Switching to synchronous download")
268
+ switch_to_sync = True
252
269
  else:
270
+ switch_to_sync = True
271
+
272
+ if switch_to_sync:
253
273
  project_class.download(
254
274
  api=api,
255
275
  project_id=project_id,
@@ -338,7 +358,7 @@ def _validate_dataset(
338
358
  project_meta_changed = _project_meta_changed(project_meta, project.meta)
339
359
  for dataset in project.datasets:
340
360
  dataset: Dataset
341
- if dataset.name.endswith(dataset_info.name): # TODO: fix it later
361
+ if dataset.name.endswith(dataset_info.name): # TODO: fix it later
342
362
  diff = set(items_infos_dict.keys()).difference(set(dataset.get_items_names()))
343
363
  if diff:
344
364
  logger.debug(
@@ -40,9 +40,9 @@ from supervisely.io.fs import (
40
40
  copy_file,
41
41
  copy_file_async,
42
42
  dir_empty,
43
- file_exists,
44
43
  dir_exists,
45
44
  ensure_base_path,
45
+ file_exists,
46
46
  get_file_name_with_ext,
47
47
  list_dir_recursively,
48
48
  list_files,
@@ -3434,6 +3434,7 @@ class Project:
3434
3434
  save_image_meta: bool = False,
3435
3435
  images_ids: Optional[List[int]] = None,
3436
3436
  resume_download: Optional[bool] = False,
3437
+ **kwargs,
3437
3438
  ) -> None:
3438
3439
  """
3439
3440
  Download project from Supervisely to the given directory in asynchronous mode.
@@ -3488,6 +3489,12 @@ class Project:
3488
3489
  else:
3489
3490
  loop.run_until_complete(coroutine)
3490
3491
  """
3492
+ if kwargs.pop("cache", None) is not None:
3493
+ logger.warning(
3494
+ "Cache is not supported in async mode and will be ignored. "
3495
+ "Use resume_download parameter instead to optimize download process."
3496
+ )
3497
+
3491
3498
  await _download_project_async(
3492
3499
  api=api,
3493
3500
  project_id=project_id,
@@ -3733,7 +3740,8 @@ def _download_project(
3733
3740
  batch, image_names, batch_imgs_bytes, ann_jsons
3734
3741
  ):
3735
3742
  dataset_fs: Dataset
3736
-
3743
+ # to fix already downloaded images that doesn't have info files
3744
+ dataset_fs.delete_item(name)
3737
3745
  dataset_fs.add_item_raw_bytes(
3738
3746
  item_name=name,
3739
3747
  item_raw_bytes=img_bytes if save_images is True else None,
@@ -4401,14 +4409,42 @@ async def _download_project_async(
4401
4409
  only_image_tags: Optional[bool] = False,
4402
4410
  save_image_info: Optional[bool] = False,
4403
4411
  save_images: Optional[bool] = True,
4404
- progress_cb: Optional[Callable] = None,
4412
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
4405
4413
  save_image_meta: Optional[bool] = False,
4406
4414
  images_ids: Optional[List[int]] = None,
4407
4415
  resume_download: Optional[bool] = False,
4416
+ **kwargs,
4408
4417
  ):
4418
+ """
4419
+ Download image project to the local directory asynchronously.
4420
+ Uses queue and semaphore to control the number of parallel downloads.
4421
+ Every image goes through size check to decide if it should be downloaded in bulk or one by one.
4422
+ Checked images are split into two lists: small and large. Small images are downloaded in bulk, large images are downloaded one by one.
4423
+ As soon as the task is created, it is put into the queue. Workers take tasks from the queue and execute them.
4424
+
4425
+ """
4426
+ # to switch between single and bulk download
4427
+ switch_size = kwargs.get("switch_size", 1.28 * 1024 * 1024)
4428
+ # batch size for bulk download
4429
+ batch_size = kwargs.get("batch_size", 100)
4430
+ # number of workers
4431
+ num_workers = kwargs.get("num_workers", 5)
4432
+
4409
4433
  if semaphore is None:
4410
4434
  semaphore = api.get_default_semaphore()
4411
4435
 
4436
+ async def worker(queue: asyncio.Queue, semaphore: asyncio.Semaphore):
4437
+ while True:
4438
+ task = await queue.get()
4439
+ if task is None:
4440
+ break
4441
+ async with semaphore:
4442
+ await task
4443
+ queue.task_done()
4444
+
4445
+ queue = asyncio.Queue()
4446
+ workers = [asyncio.create_task(worker(queue, semaphore)) for _ in range(num_workers)]
4447
+
4412
4448
  dataset_ids = set(dataset_ids) if (dataset_ids is not None) else None
4413
4449
  project_fs = None
4414
4450
  meta = ProjectMeta.from_json(api.project.get_meta(project_id, with_settings=True))
@@ -4448,16 +4484,25 @@ async def _download_project_async(
4448
4484
  force_metadata_for_links = False
4449
4485
  if save_images is False and only_image_tags is True:
4450
4486
  force_metadata_for_links = True
4451
- all_images = api.image.get_list(
4452
- dataset_id, force_metadata_for_links=force_metadata_for_links
4487
+ all_images = api.image.get_list_generator_async(
4488
+ dataset_id, force_metadata_for_links=force_metadata_for_links, dataset_info=dataset
4453
4489
  )
4454
- images = [image for image in all_images if images_ids is None or image.id in images_ids]
4490
+ small_images = []
4491
+ large_images = []
4492
+ async for image_batch in all_images:
4493
+ for image in image_batch:
4494
+ if images_ids is None or image.id in images_ids:
4495
+ if image.size < switch_size:
4496
+ small_images.append(image)
4497
+ else:
4498
+ large_images.append(image)
4455
4499
 
4456
4500
  ds_progress = progress_cb
4457
4501
  if log_progress is True:
4458
4502
  ds_progress = tqdm_sly(
4459
4503
  desc="Downloading images from {!r}".format(dataset.name),
4460
- total=len(images),
4504
+ total=len(small_images) + len(large_images),
4505
+ leave=False,
4461
4506
  )
4462
4507
 
4463
4508
  with ApiContext(
@@ -4466,18 +4511,42 @@ async def _download_project_async(
4466
4511
  dataset_id=dataset_id,
4467
4512
  project_meta=meta,
4468
4513
  ):
4469
- tasks = []
4470
- for image in images:
4471
- try:
4472
- existing = dataset_fs.get_item_info(image.name)
4473
- except:
4474
- existing = None
4475
- else:
4476
- if existing.updated_at == image.updated_at:
4477
- if ds_progress is not None:
4478
- ds_progress(1)
4479
- continue
4480
4514
 
4515
+ async def check_items(check_list: List[sly.ImageInfo]):
4516
+ to_download = []
4517
+ for image in check_list:
4518
+ try:
4519
+ existing = dataset_fs.get_item_info(image.name)
4520
+ except:
4521
+ pass
4522
+ else:
4523
+ if existing.updated_at == image.updated_at:
4524
+ if ds_progress is not None:
4525
+ ds_progress(1)
4526
+ continue
4527
+ to_download.append(image)
4528
+ return to_download
4529
+
4530
+ small_images = await check_items(small_images)
4531
+ large_images = await check_items(large_images)
4532
+ if len(small_images) == 1:
4533
+ large_images.append(small_images.pop())
4534
+ for images_batch in batched(small_images, batch_size=batch_size):
4535
+ task = _download_project_items_batch_async(
4536
+ api=api,
4537
+ dataset_id=dataset_id,
4538
+ img_infos=images_batch,
4539
+ meta=meta,
4540
+ dataset_fs=dataset_fs,
4541
+ id_to_tagmeta=id_to_tagmeta,
4542
+ semaphore=semaphore,
4543
+ save_images=save_images,
4544
+ save_image_info=save_image_info,
4545
+ only_image_tags=only_image_tags,
4546
+ progress_cb=ds_progress,
4547
+ )
4548
+ await queue.put(task)
4549
+ for image in large_images:
4481
4550
  task = _download_project_item_async(
4482
4551
  api=api,
4483
4552
  img_info=image,
@@ -4490,11 +4559,15 @@ async def _download_project_async(
4490
4559
  only_image_tags=only_image_tags,
4491
4560
  progress_cb=ds_progress,
4492
4561
  )
4493
- tasks.append(task)
4494
- await asyncio.gather(*tasks)
4562
+ await queue.put(task)
4563
+
4564
+ await queue.join()
4565
+
4566
+ all_images = small_images + large_images
4567
+
4495
4568
  if save_image_meta:
4496
4569
  meta_dir = dataset_fs.meta_dir
4497
- for image_info in images:
4570
+ for image_info in all_images:
4498
4571
  if image_info.meta:
4499
4572
  sly.fs.mkdir(meta_dir)
4500
4573
  sly.json.dump_json_file(
@@ -4506,6 +4579,10 @@ async def _download_project_async(
4506
4579
  for item_name in dataset_fs.get_items_names():
4507
4580
  if item_name not in items_names_set:
4508
4581
  dataset_fs.delete_item(item_name)
4582
+
4583
+ for _ in range(num_workers):
4584
+ await queue.put(None)
4585
+ await asyncio.gather(*workers)
4509
4586
  try:
4510
4587
  create_readme(dest_dir, project_id, api)
4511
4588
  except Exception as e:
@@ -4557,8 +4634,7 @@ async def _download_project_item_async(
4557
4634
  tmp_ann = Annotation(img_size=(img_info.height, img_info.width), img_tags=tags)
4558
4635
  ann_json = tmp_ann.to_json()
4559
4636
 
4560
- if dataset_fs.item_exists(img_info.name):
4561
- dataset_fs.delete_item(img_info.name)
4637
+ dataset_fs.delete_item(img_info.name)
4562
4638
  await dataset_fs.add_item_raw_bytes_async(
4563
4639
  item_name=img_info.name,
4564
4640
  item_raw_bytes=img_bytes if save_images is True else None,
@@ -4569,4 +4645,78 @@ async def _download_project_item_async(
4569
4645
  progress_cb(1)
4570
4646
 
4571
4647
 
4648
+ async def _download_project_items_batch_async(
4649
+ api: sly.Api,
4650
+ dataset_id: int,
4651
+ img_infos: List[sly.ImageInfo],
4652
+ meta: ProjectMeta,
4653
+ dataset_fs: Dataset,
4654
+ id_to_tagmeta: Dict[int, sly.TagMeta],
4655
+ semaphore: asyncio.Semaphore,
4656
+ save_images: bool,
4657
+ save_image_info: bool,
4658
+ only_image_tags: bool,
4659
+ progress_cb: Optional[Callable],
4660
+ ):
4661
+ """
4662
+ Download images and annotations from Supervisely API and save them to the local filesystem.
4663
+ Uses parameters from the parent function _download_project_async.
4664
+ It is used for batch download of images and annotations with the bulk download API methods.
4665
+ """
4666
+ if save_images:
4667
+ img_ids = [img_info.id for img_info in img_infos]
4668
+ imgs_bytes = [None] * len(img_ids)
4669
+ temp_dict = {}
4670
+ async for img_id, img_bytes in api.image.download_bytes_generator_async(
4671
+ dataset_id,
4672
+ img_ids,
4673
+ semaphore=semaphore,
4674
+ check_hash=True,
4675
+ ):
4676
+ temp_dict[img_id] = img_bytes
4677
+ # to be sure that the order is correct
4678
+ for idx, img_id in enumerate(img_ids):
4679
+ imgs_bytes[idx] = temp_dict[img_id]
4680
+ for img_info, img_bytes in zip(img_infos, imgs_bytes):
4681
+ if None in [img_info.height, img_info.width]:
4682
+ width, height = sly.image.get_size_from_bytes(img_bytes)
4683
+ img_info = img_info._replace(height=height, width=width)
4684
+ else:
4685
+ imgs_bytes = [None] * len(img_infos)
4686
+
4687
+ if only_image_tags is False:
4688
+ ann_infos = await api.annotation.download_bulk_async(
4689
+ dataset_id,
4690
+ img_ids,
4691
+ semaphore=semaphore,
4692
+ force_metadata_for_links=not save_images,
4693
+ )
4694
+ tmps_anns = [Annotation.from_json(ann_info.annotation, meta) for ann_info in ann_infos]
4695
+ ann_jsons = []
4696
+ for tmp_ann in tmps_anns:
4697
+ if None in tmp_ann.img_size:
4698
+ tmp_ann = tmp_ann.clone(img_size=(img_info.height, img_info.width))
4699
+ ann_jsons.append(tmp_ann.to_json())
4700
+ else:
4701
+ ann_jsons = []
4702
+ for img_info in img_infos:
4703
+ tags = TagCollection.from_api_response(
4704
+ img_info.tags,
4705
+ meta.tag_metas,
4706
+ id_to_tagmeta,
4707
+ )
4708
+ tmp_ann = Annotation(img_size=(img_info.height, img_info.width), img_tags=tags)
4709
+ ann_jsons.append(tmp_ann.to_json())
4710
+ for img_info, ann_json, img_bytes in zip(img_infos, ann_jsons, imgs_bytes):
4711
+ dataset_fs.delete_item(img_info.name)
4712
+ await dataset_fs.add_item_raw_bytes_async(
4713
+ item_name=img_info.name,
4714
+ item_raw_bytes=img_bytes,
4715
+ ann=dataset_fs.get_ann(img_info.name, meta) if ann_json is None else ann_json,
4716
+ img_info=img_info if save_image_info is True else None,
4717
+ )
4718
+ if progress_cb is not None:
4719
+ progress_cb(1)
4720
+
4721
+
4572
4722
  DatasetDict = Project.DatasetDict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.246
3
+ Version: 6.73.248
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -21,18 +21,18 @@ supervisely/annotation/tag_meta_mapper.py,sha256=RWeTrxJ64syodyhXIRSH007bX6Hr3B4
21
21
  supervisely/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  supervisely/api/advanced_api.py,sha256=Nd5cCnHFWc3PSUrCtENxTGtDjS37_lCHXsgXvUI3Ti8,2054
23
23
  supervisely/api/agent_api.py,sha256=ShWAIlXcWXcyI9fqVuP5GZVCigCMJmjnvdGUfLspD6Y,8890
24
- supervisely/api/annotation_api.py,sha256=s9kd1G759R8YSvbDcNXrPa9xv95j1cWE1RgD6XeJe2A,58826
25
- supervisely/api/api.py,sha256=SI7DuSi2Jnj5NFS_V9aQ9Sg3CrS97Y5p1pqeeb-4Jb4,64729
24
+ supervisely/api/annotation_api.py,sha256=kB9l0NhQEkunGDC9fWjNzf5DdhqRF1tv-RRnIbkV2k0,64941
25
+ supervisely/api/api.py,sha256=aSFD2Q05q0CK9nmMyFTEv5pHKyTm_ysu9cagwt4GDhs,64746
26
26
  supervisely/api/app_api.py,sha256=-T4sISQ7POyR2yirf1kEWj4JaJFpJxCyRWqbf_99Jak,67036
27
27
  supervisely/api/dataset_api.py,sha256=2-SQBlgEnIN-0uvDbtPlSXr6ztBeZ3WPryhkOtpBmk4,40786
28
28
  supervisely/api/file_api.py,sha256=c4iIzH2BF8-GLFLk_wc9Qz225AbHhbzH22wv5HdsGg4,83128
29
29
  supervisely/api/github_api.py,sha256=NIexNjEer9H5rf5sw2LEZd7C1WR-tK4t6IZzsgeAAwQ,623
30
30
  supervisely/api/image_annotation_tool_api.py,sha256=YcUo78jRDBJYvIjrd-Y6FJAasLta54nnxhyaGyanovA,5237
31
- supervisely/api/image_api.py,sha256=_wXn10tKKCcfvTiQax0O2X9lnE54nXucwQHTjA1WbRM,169172
31
+ supervisely/api/image_api.py,sha256=2cki-IzA5jnN3QqqdSIbIbHJhDWxFGYxXY94WqBOoio,176836
32
32
  supervisely/api/import_storage_api.py,sha256=BDCgmR0Hv6OoiRHLCVPKt3iDxSVlQp1WrnKhAK_Zl84,460
33
33
  supervisely/api/issues_api.py,sha256=BqDJXmNoTzwc3xe6_-mA7FDFC5QQ-ahGbXk_HmpkSeQ,17925
34
34
  supervisely/api/labeling_job_api.py,sha256=odnzZjp29yM16Gq-FYkv-OA4WFMNJCLFo4qSikW2A7c,56280
35
- supervisely/api/module_api.py,sha256=nFMvROP7XB6_BVSsP9W_eEKiTlcGYxezyMCV4pdqk8k,39410
35
+ supervisely/api/module_api.py,sha256=ajVPU5bH1sB-l2PwWq_xtFxSmZIBikflousOxiAYmW4,43141
36
36
  supervisely/api/neural_network_api.py,sha256=ktPVRO4Jeulougio8F0mioJJHwRJcX250Djp1wBoQ9c,7620
37
37
  supervisely/api/object_class_api.py,sha256=-rQcKwhBw3iL9KNH9c1ROgoimgWM1ls6Wi_tb1R-MzY,7683
38
38
  supervisely/api/plugin_api.py,sha256=TlfrosdRuYG4NUxk92QiQoVaOdztFspPpygyVa3M3zk,5283
@@ -49,7 +49,7 @@ supervisely/api/video_annotation_tool_api.py,sha256=Uy1MvT-M7vjC6y-0-V4wFCO-fZt8
49
49
  supervisely/api/workspace_api.py,sha256=5KAxpI9DKBmgF_pyQaXHpGT30HZ9wRtR6DP3FoYFZtY,9228
50
50
  supervisely/api/entity_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  supervisely/api/entity_annotation/entity_annotation_api.py,sha256=K79KdDyepQv4FiNQHBj9V4-zLIemxK9WG1ig1bfBKb8,3083
52
- supervisely/api/entity_annotation/figure_api.py,sha256=WgeB6h8ZQsgeORXnEAq2LCCezLIMeVibetFTC1PxQM8,20896
52
+ supervisely/api/entity_annotation/figure_api.py,sha256=_JS1x0jn5neoCnZCBKHUBwspoHg92N-sJ9Ty9h07Mvg,24427
53
53
  supervisely/api/entity_annotation/object_api.py,sha256=gbcNvN_KY6G80Me8fHKQgryc2Co7VU_kfFd1GYILZ4E,8875
54
54
  supervisely/api/entity_annotation/tag_api.py,sha256=M-28m9h8R4k9Eqo6P1S0UH8_D5kqCwAvQLYY6_Yz4oM,11161
55
55
  supervisely/api/pointcloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -814,7 +814,7 @@ supervisely/nn/benchmark/utils/semantic_segmentation/__init__.py,sha256=47DEQpj8
814
814
  supervisely/nn/benchmark/utils/semantic_segmentation/calculate_metrics.py,sha256=4ifC5r_Q880yIr8gWnjEzwKbS0vizMWqSF4XeyaMvh0,924
815
815
  supervisely/nn/benchmark/utils/semantic_segmentation/evaluator.py,sha256=R1U_mnOiUUH6P87xiKalYqMpj1uGCJKuOBcsTmVHawY,32894
816
816
  supervisely/nn/benchmark/utils/semantic_segmentation/loader.py,sha256=_5ZZ7Nkd8WWYJnKwc1Dx3bEPS_1R84gG_hQc0w0TXWw,1957
817
- supervisely/nn/benchmark/utils/semantic_segmentation/utils.py,sha256=nV9T7PCUxOdipFIBPrpdwLJXs0GJNp_Eft2MJmrKJRM,3787
817
+ supervisely/nn/benchmark/utils/semantic_segmentation/utils.py,sha256=X5NiR02R-0To2_SuSGHZZccl_-Bupg5F9d7nziIMRMc,3874
818
818
  supervisely/nn/benchmark/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
819
819
  supervisely/nn/benchmark/visualization/evaluation_result.py,sha256=733HJL4rJa5XqCJydW9vSyaepvpHzym9wQsw1wFEgeI,10251
820
820
  supervisely/nn/benchmark/visualization/renderer.py,sha256=qxWUYu2glTqksxI5UG08nwDmZA4A2bfGb1wk9DaDct8,3340
@@ -931,16 +931,16 @@ supervisely/nn/tracker/deep_sort/sly_tracker.py,sha256=uhOiwJMt7kh53X_zeWecSm6f3
931
931
  supervisely/nn/tracker/deep_sort/deep_sort/__init__.py,sha256=RleTF3kMKdZteclPHRm7E49pw1I1GlooqToAv3YmBd0,27
932
932
  supervisely/nn/tracker/deep_sort/deep_sort/detection.py,sha256=cS7YqK8RPbr7C3l6myKTHGBvQbYoKxnEyVa4ZGVbsLY,1430
933
933
  supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py,sha256=HSzn9FjOSHrqH9h6qJlYe1i4o0ZJhjERxCcuIYnLmPE,2830
934
- supervisely/nn/tracker/deep_sort/deep_sort/kalman_filter.py,sha256=-pPoynyPcJd8p6D-C1WTZczxpz4-bokAzmNM-HtdLcA,7787
934
+ supervisely/nn/tracker/deep_sort/deep_sort/kalman_filter.py,sha256=wDSbs2QA3cnnWB-WAzjNcKKqGiNZmfpN0_Cm9dTxU38,7888
935
935
  supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py,sha256=hAqMNk2CFPDJvmGJCfNbGVngUgKGM2oYldaGaU2zWC4,7871
936
936
  supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py,sha256=U0NYReianEohMc9eA_eAuXjqMkqkNjWlq40S7VvYKHs,5469
937
937
  supervisely/nn/tracker/deep_sort/deep_sort/track.py,sha256=eNvBlhhBcTzyTWOX_TKyT3sWJM6fSz3vPi30kfz8wE4,4976
938
938
  supervisely/nn/tracker/deep_sort/deep_sort/tracker.py,sha256=cz--uel4ROLHPem8dOofitCoQj_bTd8srWFp2FpaBR0,5357
939
939
  supervisely/nn/tracker/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
940
940
  supervisely/nn/tracker/utils/gmc.py,sha256=3JX8979H3NA-YHNaRQyj9Z-xb9qtyMittPEjGw8y2Jo,11557
941
- supervisely/nn/tracker/utils/kalman_filter.py,sha256=XquwLRFVfZdJMHPqPUtq-kdtuKLY6zgGHbnPGTXL8qU,17044
941
+ supervisely/nn/tracker/utils/kalman_filter.py,sha256=eSFmCjM0mikHCAFvj-KCVzw-0Jxpoc3Cfc2NWEjJC1Q,17268
942
942
  supervisely/nn/training/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
943
- supervisely/nn/training/train_app.py,sha256=i53gk26n1iqFvyGjB2eXBQKMgYsBE2e2Z77DM6gf4Yw,80370
943
+ supervisely/nn/training/train_app.py,sha256=E4Qpr9KSEuPLAUJCVUHyt_-tpPQrPRnmpNBGCjnURyw,80464
944
944
  supervisely/nn/training/gui/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
945
945
  supervisely/nn/training/gui/classes_selector.py,sha256=laF29RllCsGTYFGntwqs2rwn1WHX9QwzbjhC6dUJvlE,3717
946
946
  supervisely/nn/training/gui/gui.py,sha256=o1ryQJjzSewskgxxoFyuQ5oZUvYlzg70paxeCleRmJY,21765
@@ -949,7 +949,7 @@ supervisely/nn/training/gui/input_selector.py,sha256=ygkvgaT9iCetrsqHzCaRAGaTdcP
949
949
  supervisely/nn/training/gui/model_selector.py,sha256=-XXxudFRJBIFGsVU7DWEzweS9h-T6dl13ZrHdo8673w,3449
950
950
  supervisely/nn/training/gui/train_val_splits_selector.py,sha256=nh3nX-XTjz_8p8hAedaGdrBmb0wQFZtnWd_X07kBDuE,8461
951
951
  supervisely/nn/training/gui/training_logs.py,sha256=hd_xE5ljV8bfbcqVeTvcbIOafAsXo0dSGIP7ZrcgSJc,3012
952
- supervisely/nn/training/gui/training_process.py,sha256=_ZyHQdpsaOwDmTtBh4CnQ0gDQHKPBZvXG7bnkeyu8y4,3779
952
+ supervisely/nn/training/gui/training_process.py,sha256=291hH3h76uUluJ4isoJgwj_xt1fHnmFVhOAB9daJp_s,3585
953
953
  supervisely/nn/training/gui/utils.py,sha256=Bi7-BRsAqN7fUkhd7rXVEAqsxhBdIZ2MrrJtrNqVf8I,3905
954
954
  supervisely/nn/training/loggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
955
955
  supervisely/nn/training/loggers/base_train_logger.py,sha256=RqnXx7PLS8Clo3jgCxrP2cqzj1ZFr9Yv7DczFKiZJII,1837
@@ -976,10 +976,10 @@ supervisely/pointcloud_episodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
976
976
  supervisely/pointcloud_episodes/pointcloud_episodes.py,sha256=cRXdtw7bMsbsdVQjxfWxFSESrO-LGiqqsZyyExl2Mbg,3430
977
977
  supervisely/project/__init__.py,sha256=hlzdj9Pgy53Q3qdP8LMtGTChvZHQuuShdtui2eRUQeE,2601
978
978
  supervisely/project/data_version.py,sha256=nknaWJSUCwoDyNG9_d1KA-GjzidhV9zd9Cn8cg15DOU,19270
979
- supervisely/project/download.py,sha256=qonvHBiKX-leHW9qWJdyBqFNmpI2_t9s54e68h9orq0,23687
979
+ supervisely/project/download.py,sha256=zb8sb4XZ6Qi3CP7fmtLRUAYzaxs_W0WnOfe2x3ZVRMs,24639
980
980
  supervisely/project/pointcloud_episode_project.py,sha256=fcaFAaHVn_VvdiIfHl4IyEFE5-Q3VFGfo7_YoxEma0I,41341
981
981
  supervisely/project/pointcloud_project.py,sha256=Y8Xhi6Hg-KyztwFncezuDfKTt2FILss96EU_LdXzmrA,49172
982
- supervisely/project/project.py,sha256=QRFLDYij18lEOMmcfbpqxeaOHfaz1E5qs5skLEKPQws,182031
982
+ supervisely/project/project.py,sha256=UY7nLGRNcUO6nDvBlirsUg0mbX6XW7_tXoqGaofojqo,188125
983
983
  supervisely/project/project_meta.py,sha256=26s8IiHC5Pg8B1AQi6_CrsWteioJP2in00cRNe8QlW0,51423
984
984
  supervisely/project/project_settings.py,sha256=NLThzU_DCynOK6hkHhVdFyezwprn9UqlnrLDe_3qhkY,9347
985
985
  supervisely/project/project_type.py,sha256=_3RqW2CnDBKFOvSIrQT1RJQaiHirs34_jiQS8CkwCpo,530
@@ -1041,9 +1041,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1041
1041
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1042
1042
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1043
1043
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1044
- supervisely-6.73.246.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1045
- supervisely-6.73.246.dist-info/METADATA,sha256=jgk6Q0Nu0ySOl8v0MXtq9cxMgkDye4HdyWMd7QEmZmQ,33351
1046
- supervisely-6.73.246.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1047
- supervisely-6.73.246.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1048
- supervisely-6.73.246.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1049
- supervisely-6.73.246.dist-info/RECORD,,
1044
+ supervisely-6.73.248.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1045
+ supervisely-6.73.248.dist-info/METADATA,sha256=0R4I5m8-EAFFV8m29WqXyaj3kbtRi5O8AbRa3OeyCpI,33351
1046
+ supervisely-6.73.248.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1047
+ supervisely-6.73.248.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1048
+ supervisely-6.73.248.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1049
+ supervisely-6.73.248.dist-info/RECORD,,