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

@@ -5,12 +5,14 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  import asyncio
8
+ import copy
8
9
  import io
9
10
  import json
10
11
  import re
11
12
  import urllib.parse
12
13
  from collections import defaultdict
13
14
  from concurrent.futures import ThreadPoolExecutor
15
+ from contextlib import contextmanager
14
16
  from datetime import datetime
15
17
  from functools import partial
16
18
  from math import ceil
@@ -103,7 +105,8 @@ class ImageInfo(NamedTuple):
103
105
  meta={},
104
106
  path_original='/h5un6l2bnaz1vj8a9qgms4-public/images/original/7/h/Vo/...jpg',
105
107
  full_storage_url='http://app.supervise.ly/h5un6l2bnaz1vj8a9qgms4-public/images/original/7/h/Vo/...jpg'),
106
- tags=[]
108
+ tags=[],
109
+ created_by='admin'
107
110
  )
108
111
  """
109
112
 
@@ -148,7 +151,13 @@ class ImageInfo(NamedTuple):
148
151
  #: :class:`str`: Time of last image update. e.g. "2019-02-22T14:59:53.381Z".
149
152
  updated_at: str
150
153
 
151
- #: :class:`dict`: Custom additional image info.
154
+ #: :class:`dict`: Custom additional image info that contain image technical and/or user-generated data.
155
+ #: To set custom sort parameter for image, you can do the follwoing:
156
+ #: 1. With the uploading use `add_custom_sort` context manager to set the key name of meta object that will be used for custom sorting.
157
+ #: 2. Before uploading add value to meta dict with method `update_custom_sort`
158
+ #: 3. Before uploading add key-value pair with key `customSort` to meta dict, image info file or meta file.
159
+ #: 4. After uploading `set_custom_sort` method to set custom sort value for image.
160
+ #: e.g. {'my-key':'a', 'my-key: "b", "customSort": "c"}.
152
161
  meta: dict
153
162
 
154
163
  #: :class:`str`: Relative storage URL to image. e.g.
@@ -249,12 +258,62 @@ class ImageApi(RemoveableBulkModuleApi):
249
258
  """
250
259
  return "ImageInfo"
251
260
 
261
+ def _add_custom_sort(self, meta: dict, name: Optional[str] = None) -> dict:
262
+ """
263
+ Add `customSort` key with value to meta dict based on the `sort_by` attribute of `ImageApi` object:
264
+ - `sort_by` attribute is set by `add_custom_sort` context manager and available for the duration of the context.
265
+ - `sort_by` attribute is used to set the key name of meta object that will "link" its value to the custom sorting.
266
+
267
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
268
+ :type meta: dict
269
+ :param name: Name of the image. Used for improved debug logging.
270
+ :type name: str, optional
271
+ :return: Updated meta.
272
+ :rtype: dict
273
+ """
274
+ sort_value = meta.get(self.sort_by, None)
275
+ if sort_value:
276
+ meta[ApiField.CUSTOM_SORT] = str(sort_value)
277
+ message = f"Custom sorting applied with key '{self.sort_by}' and value '{sort_value}'."
278
+ else:
279
+ message = f"Custom sorting will not be applied. Key '{self.sort_by}' not found in meta."
280
+ if name:
281
+ message = f"Image '{name}': {message}"
282
+ logger.debug(message)
283
+ return meta
284
+
285
+ @contextmanager
286
+ def add_custom_sort(self, key: str):
287
+ """
288
+ Use this context manager to set the key name of meta object that will be used for custom sorting.
289
+ This context manager allows you to set the `sort_by` attribute of ImageApi object for the duration of the context, then delete it.
290
+ If nested functions support this functionality, each image they process will automatically receive a custom sorting parameter based on the available meta object.
291
+
292
+ :param key: It is a key name of meta object that will be used for sorting.
293
+ :type key: str
294
+ """
295
+ # pylint: disable=access-member-before-definition
296
+ if hasattr(self, "sort_by") and self.sort_by != key:
297
+ raise AttributeError(
298
+ f"Attribute 'sort_by' already exists and has different value: {self.sort_by}"
299
+ )
300
+ # pylint: enable=access-member-before-definition
301
+ self.sort_by = key
302
+ self.sort_by_context_counter = getattr(self, "sort_by_context_counter", 0) + 1
303
+ try:
304
+ yield
305
+ finally:
306
+ self.sort_by_context_counter -= 1
307
+ if self.sort_by_context_counter == 0:
308
+ del self.sort_by
309
+ del self.sort_by_context_counter
310
+
252
311
  def get_list_generator(
253
312
  self,
254
313
  dataset_id: int = None,
255
314
  filters: Optional[List[Dict[str, str]]] = None,
256
- sort: Optional[str] = "id",
257
- sort_order: Optional[str] = "asc",
315
+ sort: Optional[str] = "id", #! Does not work with pagination mode 'token'
316
+ sort_order: Optional[str] = "asc", #! Does not work with pagination mode 'token'
258
317
  limit: Optional[int] = None,
259
318
  force_metadata_for_links: Optional[bool] = False,
260
319
  batch_size: Optional[int] = None,
@@ -267,7 +326,7 @@ class ImageApi(RemoveableBulkModuleApi):
267
326
  :type dataset_id: :class:`int`
268
327
  :param filters: List of params to sort output Images.
269
328
  :type filters: :class:`List[Dict]`, optional
270
- :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt'}
329
+ :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt', `customSort`}
271
330
  :type sort: :class:`str`, optional
272
331
  :param sort_order: Sort order. One of {'asc' (default), 'desc'}
273
332
  :type sort_order: :class:`str`, optional
@@ -339,7 +398,7 @@ class ImageApi(RemoveableBulkModuleApi):
339
398
  :type dataset_id: :class:`int`
340
399
  :param filters: List of params to sort output Images.
341
400
  :type filters: :class:`List[Dict]`, optional
342
- :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt'}
401
+ :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt', `customSort`}
343
402
  :type sort: :class:`str`, optional
344
403
  :param sort_order: Sort order. One of {'asc' (default), 'desc'}
345
404
  :type sort_order: :class:`str`, optional
@@ -455,7 +514,7 @@ class ImageApi(RemoveableBulkModuleApi):
455
514
  :type dataset_id: :class:`int`
456
515
  :param filters: List of params to sort output Images.
457
516
  :type filters: :class:`List[Dict]`, optional
458
- :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt'}.
517
+ :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt', 'customSort'}.
459
518
  :type sort: :class:`str`, optional
460
519
  :param sort_order: Sort order. One of {'asc' (default), 'desc'}
461
520
  :type sort_order: :class:`str`, optional
@@ -1157,13 +1216,17 @@ class ImageApi(RemoveableBulkModuleApi):
1157
1216
  """
1158
1217
  Uploads Image with given name from given local path to Dataset.
1159
1218
 
1219
+ If you include `meta` during the upload, you can add a custom sort parameter for image.
1220
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1221
+ Refer to the example section for more details.
1222
+
1160
1223
  :param dataset_id: Dataset ID in Supervisely.
1161
1224
  :type dataset_id: int
1162
1225
  :param name: Image name with extension.
1163
1226
  :type name: str
1164
1227
  :param path: Local Image path.
1165
1228
  :type path: str
1166
- :param meta: Image metadata.
1229
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
1167
1230
  :type meta: dict, optional
1168
1231
  :param validate_meta: If True, validates provided meta with saved JSON schema.
1169
1232
  :type validate_meta: bool, optional
@@ -1184,6 +1247,12 @@ class ImageApi(RemoveableBulkModuleApi):
1184
1247
  api = sly.Api.from_env()
1185
1248
 
1186
1249
  img_info = api.image.upload_path(dataset_id, name="7777.jpeg", path="/home/admin/Downloads/7777.jpeg")
1250
+
1251
+ # Add custom sort parameter for image
1252
+ img_meta = {'my-key':'a'}
1253
+ with api.image.add_custom_sort(key="my-key"):
1254
+ img_info = api.image.upload_path(dataset_id, name="7777.jpeg", path="/home/admin/Downloads/7777.jpeg", meta=img_meta)
1255
+
1187
1256
  """
1188
1257
  metas = None if meta is None else [meta]
1189
1258
  return self.upload_paths(
@@ -1211,6 +1280,10 @@ class ImageApi(RemoveableBulkModuleApi):
1211
1280
  """
1212
1281
  Uploads Images with given names from given local path to Dataset.
1213
1282
 
1283
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
1284
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1285
+ Refer to the example section for more details.
1286
+
1214
1287
  :param dataset_id: Dataset ID in Supervisely.
1215
1288
  :type dataset_id: int
1216
1289
  :param names: List of Images names with extension.
@@ -1219,7 +1292,7 @@ class ImageApi(RemoveableBulkModuleApi):
1219
1292
  :type paths: List[str]
1220
1293
  :param progress_cb: Function for tracking the progress of uploading.
1221
1294
  :type progress_cb: tqdm or callable, optional
1222
- :param metas: Images metadata.
1295
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
1223
1296
  :type metas: List[dict], optional
1224
1297
  :param conflict_resolution: The strategy to resolve upload conflicts. 'Replace' option will replace the existing images in the dataset with the new images. The images that are being deleted are logged. 'Skip' option will ignore the upload of new images that would result in a conflict. An original image's ImageInfo list will be returned instead. 'Rename' option will rename the new images to prevent any conflict.
1225
1298
  :type conflict_resolution: Optional[Literal["rename", "skip", "replace"]]
@@ -1243,7 +1316,12 @@ class ImageApi(RemoveableBulkModuleApi):
1243
1316
  img_names = ["7777.jpeg", "8888.jpeg", "9999.jpeg"]
1244
1317
  image_paths = ["/home/admin/Downloads/img/770918.jpeg", "/home/admin/Downloads/img/770919.jpeg", "/home/admin/Downloads/img/770920.jpeg"]
1245
1318
 
1246
- img_infos = api.image.upload_path(dataset_id, names=img_names, paths=img_paths)
1319
+ img_infos = api.image.upload_paths(dataset_id, names=img_names, paths=img_paths)
1320
+
1321
+ # Add custom sort parameter for images
1322
+ img_metas = [{'my-key':'a'}, {'my-key':'b'}, {'my-key':'c'}]
1323
+ with api.image.add_custom_sort(key="my-key"):
1324
+ img_infos = api.image.upload_paths(dataset_id, names=img_names, paths=img_paths, metas=img_metas)
1247
1325
  """
1248
1326
 
1249
1327
  def path_to_bytes_stream(path):
@@ -1270,13 +1348,17 @@ class ImageApi(RemoveableBulkModuleApi):
1270
1348
  """
1271
1349
  Upload given Image in numpy format with given name to Dataset.
1272
1350
 
1351
+ If you include `meta` during the upload, you can add a custom sort parameter for image.
1352
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1353
+ Refer to the example section for more details.
1354
+
1273
1355
  :param dataset_id: Dataset ID in Supervisely.
1274
1356
  :type dataset_id: int
1275
1357
  :param name: Image name with extension.
1276
1358
  :type name: str
1277
1359
  :param img: image in RGB format(numpy matrix)
1278
1360
  :type img: np.ndarray
1279
- :param meta: Image metadata.
1361
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
1280
1362
  :type meta: dict, optional
1281
1363
  :return: Information about Image. See :class:`info_sequence<info_sequence>`
1282
1364
  :rtype: :class:`ImageInfo`
@@ -1292,6 +1374,11 @@ class ImageApi(RemoveableBulkModuleApi):
1292
1374
 
1293
1375
  img_np = sly.image.read("/home/admin/Downloads/7777.jpeg")
1294
1376
  img_info = api.image.upload_np(dataset_id, name="7777.jpeg", img=img_np)
1377
+
1378
+ # Add custom sort parameter for image
1379
+ img_meta = {'my-key':'a'}
1380
+ with api.image.add_custom_sort(key="my-key"):
1381
+ img_info = api.image.upload_np(dataset_id, name="7777.jpeg", img=img_np, meta=img_meta)
1295
1382
  """
1296
1383
  metas = None if meta is None else [meta]
1297
1384
  return self.upload_nps(dataset_id, [name], [img], metas=metas)[0]
@@ -1308,6 +1395,10 @@ class ImageApi(RemoveableBulkModuleApi):
1308
1395
  """
1309
1396
  Upload given Images in numpy format with given names to Dataset.
1310
1397
 
1398
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
1399
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1400
+ Refer to the example section for more details.
1401
+
1311
1402
  :param dataset_id: Dataset ID in Supervisely.
1312
1403
  :type dataset_id: int
1313
1404
  :param names: Images names with extension.
@@ -1316,7 +1407,7 @@ class ImageApi(RemoveableBulkModuleApi):
1316
1407
  :type imgs: List[np.ndarray]
1317
1408
  :param progress_cb: Function for tracking the progress of uploading.
1318
1409
  :type progress_cb: tqdm or callable, optional
1319
- :param metas: Images metadata.
1410
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
1320
1411
  :type metas: List[dict], optional
1321
1412
  :param conflict_resolution: The strategy to resolve upload conflicts. 'Replace' option will replace the existing images in the dataset with the new images. The images that are being deleted are logged. 'Skip' option will ignore the upload of new images that would result in a conflict. An original image's ImageInfo list will be returned instead. 'Rename' option will rename the new images to prevent any conflict.
1322
1413
  :type conflict_resolution: Optional[Literal["rename", "skip", "replace"]]
@@ -1340,6 +1431,11 @@ class ImageApi(RemoveableBulkModuleApi):
1340
1431
  img_nps = [img_np_1, img_np_2, img_np_3]
1341
1432
 
1342
1433
  img_infos = api.image.upload_nps(dataset_id, names=img_names, imgs=img_nps)
1434
+
1435
+ # Add custom sort parameter for images
1436
+ img_metas = [{'my-key':'a'}, {'my-key':'b'}, {'my-key':'c'}]
1437
+ with api.image.add_custom_sort(key="my-key"):
1438
+ img_infos = api.image.upload_nps(dataset_id, names=img_names, imgs=img_nps, metas=img_metas)
1343
1439
  """
1344
1440
 
1345
1441
  def img_to_bytes_stream(item):
@@ -1372,13 +1468,17 @@ class ImageApi(RemoveableBulkModuleApi):
1372
1468
  """
1373
1469
  Uploads Image from given link to Dataset.
1374
1470
 
1471
+ If you include `meta` during the upload, you can add a custom sort parameter for image.
1472
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1473
+ Refer to the example section for more details.
1474
+
1375
1475
  :param dataset_id: Dataset ID in Supervisely.
1376
1476
  :type dataset_id: int
1377
1477
  :param name: Image name with extension.
1378
1478
  :type name: str
1379
1479
  :param link: Link to Image.
1380
1480
  :type link: str
1381
- :param meta: Image metadata.
1481
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
1382
1482
  :type meta: dict, optional
1383
1483
  :param force_metadata_for_links: Calculate metadata for link. If False, metadata will be empty.
1384
1484
  :type force_metadata_for_links: bool, optional
@@ -1398,6 +1498,11 @@ class ImageApi(RemoveableBulkModuleApi):
1398
1498
  img_link = 'https://m.media-amazon.com/images/M/MV5BMTYwOTEwNjAzMl5BMl5BanBnXkFtZTcwODc5MTUwMw@@._V1_.jpg'
1399
1499
 
1400
1500
  img_info = api.image.upload_link(dataset_id, img_name, img_link)
1501
+
1502
+ # Add custom sort parameter for image
1503
+ img_meta = {"my-key": "a"}
1504
+ with api.image.add_custom_sort(key="my-key"):
1505
+ img_info = api.image.upload_link(dataset_id, img_name, img_link, meta=img_meta)
1401
1506
  """
1402
1507
  metas = None if meta is None else [meta]
1403
1508
  return self.upload_links(
@@ -1423,6 +1528,10 @@ class ImageApi(RemoveableBulkModuleApi):
1423
1528
  """
1424
1529
  Uploads Images from given links to Dataset.
1425
1530
 
1531
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
1532
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1533
+ Refer to the example section for more details.
1534
+
1426
1535
  :param dataset_id: Dataset ID in Supervisely.
1427
1536
  :type dataset_id: int
1428
1537
  :param names: Images names with extension.
@@ -1431,7 +1540,7 @@ class ImageApi(RemoveableBulkModuleApi):
1431
1540
  :type links: List[str]
1432
1541
  :param progress_cb: Function for tracking the progress of uploading.
1433
1542
  :type progress_cb: tqdm or callable, optional
1434
- :param metas: Images metadata.
1543
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
1435
1544
  :type metas: List[dict], optional
1436
1545
  :param force_metadata_for_links: Calculate metadata for links. If False, metadata will be empty.
1437
1546
  :type force_metadata_for_links: bool, optional
@@ -1457,6 +1566,11 @@ class ImageApi(RemoveableBulkModuleApi):
1457
1566
  'https://m.media-amazon.com/images/M/MV5BNjQ3NWNlNmQtMTE5ZS00MDdmLTlkZjUtZTBlM2UxMGFiMTU3XkEyXkFqcGdeQXVyNjUwNzk3NDc@._V1_.jpg']
1458
1567
 
1459
1568
  img_infos = api.image.upload_links(dataset_id, img_names, img_links)
1569
+
1570
+ # Add custom sort parameter for images
1571
+ img_metas = [{'my-key':'a'}, {'my-key':'b'}, {'my-key':'c'}]
1572
+ with api.image.add_custom_sort(key="my-key"):
1573
+ img_infos = api.image.upload_links(dataset_id, names=img_names, links=img_links, metas=img_metas)
1460
1574
  """
1461
1575
  return self._upload_bulk_add(
1462
1576
  lambda item: (ApiField.LINK, item),
@@ -1477,13 +1591,17 @@ class ImageApi(RemoveableBulkModuleApi):
1477
1591
  """
1478
1592
  Upload Image from given hash to Dataset.
1479
1593
 
1594
+ If you include `meta` during the upload, you can add a custom sort parameter for image.
1595
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1596
+ Refer to the example section for more details.
1597
+
1480
1598
  :param dataset_id: Dataset ID in Supervisely.
1481
1599
  :type dataset_id: int
1482
1600
  :param name: Image name with extension.
1483
1601
  :type name: str
1484
1602
  :param hash: Image hash.
1485
1603
  :type hash: str
1486
- :param meta: Image metadata.
1604
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
1487
1605
  :type meta: dict, optional
1488
1606
  :return: Information about Image. See :class:`info_sequence<info_sequence>`
1489
1607
  :rtype: :class:`ImageInfo`
@@ -1525,6 +1643,14 @@ class ImageApi(RemoveableBulkModuleApi):
1525
1643
  # "/h5un6l2bnaz1vj8a9qgms4-public/images/original/P/a/kn/W2mzMQg435d6wG0.jpg",
1526
1644
  # "https://app.supervise.ly/h5un6l2bnaz1vj8a9qgms4-public/images/original/P/a/kn/W2mzMQg435hiHJAPgMU.jpg"
1527
1645
  # ]
1646
+
1647
+ # Add custom sort parameter for image
1648
+ new_dataset_id = 452985
1649
+ im_info = api.image.get_info_by_id(193940090)
1650
+ print(im_info.meta)
1651
+ # Output: {'my-key':'a'}
1652
+ with api.image.add_custom_sort(key="my-key"):
1653
+ img_info = api.image.upload_hash(new_dataset_id, name=im_info.name, hash=im_info.hash, meta=im_info.meta)
1528
1654
  """
1529
1655
  metas = None if meta is None else [meta]
1530
1656
  return self.upload_hashes(dataset_id, [name], [hash], metas=metas)[0]
@@ -1546,6 +1672,10 @@ class ImageApi(RemoveableBulkModuleApi):
1546
1672
  """
1547
1673
  Upload images from given hashes to Dataset.
1548
1674
 
1675
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
1676
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1677
+ Refer to the example section for more details.
1678
+
1549
1679
  :param dataset_id: Dataset ID in Supervisely.
1550
1680
  :type dataset_id: int
1551
1681
  :param names: Images names with extension.
@@ -1554,7 +1684,7 @@ class ImageApi(RemoveableBulkModuleApi):
1554
1684
  :type hashes: List[str]
1555
1685
  :param progress_cb: Function for tracking the progress of uploading.
1556
1686
  :type progress_cb: tqdm or callable, optional
1557
- :param metas: Images metadata.
1687
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
1558
1688
  :type metas: List[dict], optional
1559
1689
  :param batch_size: Number of images to upload in one batch.
1560
1690
  :type batch_size: int, optional
@@ -1598,6 +1728,12 @@ class ImageApi(RemoveableBulkModuleApi):
1598
1728
  # Output:
1599
1729
  # {"message": "progress", "event_type": "EventType.PROGRESS", "subtask": "Images downloaded: ", "current": 0, "total": 10, "timestamp": "2021-03-16T11:59:07.444Z", "level": "info"}
1600
1730
  # {"message": "progress", "event_type": "EventType.PROGRESS", "subtask": "Images downloaded: ", "current": 10, "total": 10, "timestamp": "2021-03-16T11:59:07.644Z", "level": "info"}
1731
+
1732
+ # Add custom sort parameter for images
1733
+ new_dataset_id = 452985
1734
+ new_metas = [{'my-key':'a'}, {'my-key':'b'}, {'my-key':'c'}]
1735
+ with api.image.add_custom_sort(key="my-key"):
1736
+ img_infos = api.image.upload_hashes(new_dataset_id, names=names, hashes=hashes, metas=new_metas)
1601
1737
  """
1602
1738
  return self._upload_bulk_add(
1603
1739
  lambda item: (ApiField.HASH, item),
@@ -1620,13 +1756,17 @@ class ImageApi(RemoveableBulkModuleApi):
1620
1756
  """
1621
1757
  Upload Image by ID to Dataset.
1622
1758
 
1759
+ If you include `meta` during the upload, you can add a custom sort parameter for image.
1760
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1761
+ Refer to the example section for more details.
1762
+
1623
1763
  :param dataset_id: Destination Dataset ID in Supervisely.
1624
1764
  :type dataset_id: int
1625
1765
  :param name: Image name with extension.
1626
1766
  :type name: str
1627
1767
  :param id: Source image ID in Supervisely.
1628
1768
  :type id: int
1629
- :param meta: Image metadata.
1769
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
1630
1770
  :type meta: dict, optional
1631
1771
  :return: Information about Image. See :class:`info_sequence<info_sequence>`
1632
1772
  :rtype: :class:`ImageInfo`
@@ -1668,6 +1808,14 @@ class ImageApi(RemoveableBulkModuleApi):
1668
1808
  # "/h5un6l2bnaz1vj8a9qgms4-public/images/original/P/a/kn/W2mzMQg435d6wG0AJGJTOsL1FqMUNOPqu4VdzFAN36LqtGwBIE4AmLOQ1BAxuIyB0bHJAPgMU.jpg",
1669
1809
  # "https://app.supervise.ly/h5un6l2bnaz1vj8a9qgms4-public/images/original/P/a/kn/iEaDEkejnfnb1Tz56ka0hiHJAPgMU.jpg"
1670
1810
  # ]
1811
+
1812
+ # Add custom sort parameter for image
1813
+ new_dataset_id = 452985
1814
+ im_info = api.image.get_info_by_id(193940090)
1815
+ print(im_info.meta)
1816
+ # Output: {"my-key": "a"}
1817
+ with api.image.add_custom_sort(key="my-key"):
1818
+ img_info = api.image.upload_id(new_dataset_id, name=im_info.name, id=im_info.id, meta=im_info.meta)
1671
1819
  """
1672
1820
  metas = None if meta is None else [meta]
1673
1821
  return self.upload_ids(dataset_id, [name], [id], metas=metas)[0]
@@ -1688,6 +1836,10 @@ class ImageApi(RemoveableBulkModuleApi):
1688
1836
  """
1689
1837
  Upload Images by IDs to Dataset.
1690
1838
 
1839
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
1840
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
1841
+ Refer to the example section for more details.
1842
+
1691
1843
  :param dataset_id: Destination Dataset ID in Supervisely.
1692
1844
  :type dataset_id: int
1693
1845
  :param names: Source images names with extension.
@@ -1696,7 +1848,7 @@ class ImageApi(RemoveableBulkModuleApi):
1696
1848
  :type ids: List[int]
1697
1849
  :param progress_cb: Function for tracking the progress of uploading.
1698
1850
  :type progress_cb: tqdm or callable, optional
1699
- :param metas: Images metadata.
1851
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
1700
1852
  :type metas: List[dict], optional
1701
1853
  :param batch_size: Number of images to upload in one batch.
1702
1854
  :type batch_size: int, optional
@@ -1739,6 +1891,12 @@ class ImageApi(RemoveableBulkModuleApi):
1739
1891
  # Output:
1740
1892
  # {"message": "progress", "event_type": "EventType.PROGRESS", "subtask": "Images downloaded: ", "current": 0, "total": 10, "timestamp": "2021-03-16T12:31:36.550Z", "level": "info"}
1741
1893
  # {"message": "progress", "event_type": "EventType.PROGRESS", "subtask": "Images downloaded: ", "current": 10, "total": 10, "timestamp": "2021-03-16T12:31:37.119Z", "level": "info"}
1894
+
1895
+ # Add custom sort parameter for images
1896
+ new_dataset_id = 452985
1897
+ new_metas = [{'my-key':'a'}, {'my-key':'b'}, {'my-key':'c'}]
1898
+ with api.image.add_custom_sort(key="my-key"):
1899
+ img_infos = api.image.upload_ids(new_dataset_id, names=names, ids=ids, metas=new_metas)
1742
1900
  """
1743
1901
  if metas is None:
1744
1902
  metas = [{}] * len(names)
@@ -1868,6 +2026,8 @@ class ImageApi(RemoveableBulkModuleApi):
1868
2026
  for name, item, meta in zip(names, items, metas):
1869
2027
  item_tuple = func_item_to_kv(item)
1870
2028
  image_data = {ApiField.TITLE: name, item_tuple[0]: item_tuple[1]}
2029
+ if hasattr(self, "sort_by") and self.sort_by is not None:
2030
+ meta = self._add_custom_sort(meta, name)
1871
2031
  if len(meta) != 0 and type(meta) == dict:
1872
2032
  image_data[ApiField.META] = meta
1873
2033
  images.append(image_data)
@@ -2743,7 +2903,7 @@ class ImageApi(RemoveableBulkModuleApi):
2743
2903
 
2744
2904
  :param id: Image ID in Supervisely.
2745
2905
  :type id: int
2746
- :param meta: Image metadata.
2906
+ :param meta: Custom additional image info that contain image technical and/or user-generated data.
2747
2907
  :type meta: dict
2748
2908
  :raises: :class:`TypeError` if meta type is not dict
2749
2909
  :return: Image information in dict format with new meta
@@ -2794,7 +2954,7 @@ class ImageApi(RemoveableBulkModuleApi):
2794
2954
  :type name: str, optional
2795
2955
  :param description: New Image description.
2796
2956
  :type description: str, optional
2797
- :param meta: New Image metadata.
2957
+ :param meta: New Image metadata. Custom additional image info that contain image technical and/or user-generated data.
2798
2958
  :type meta: dict, optional
2799
2959
  :return_json: If True, return response in JSON format, otherwise convert it ImageInfo object.
2800
2960
  This parameter is only added for backward compatibility for update_meta method.
@@ -3137,6 +3297,10 @@ class ImageApi(RemoveableBulkModuleApi):
3137
3297
  Uploads images to Supervisely and adds a tag to them.
3138
3298
  At least one of `paths` or `links` must be provided.
3139
3299
 
3300
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
3301
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
3302
+ Refer to the example section for more details.
3303
+
3140
3304
  :param dataset_id: Dataset ID in Supervisely.
3141
3305
  :type dataset_id: int
3142
3306
  :param tag_name: Tag name in Supervisely.
@@ -3147,7 +3311,7 @@ class ImageApi(RemoveableBulkModuleApi):
3147
3311
  :type group_name: str
3148
3312
  :param paths: List of paths to images.
3149
3313
  :type paths: List[str]
3150
- :param metas: List of dictionaries which adds a customizable meta for every image provided in `paths` parameter.
3314
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
3151
3315
  :type metas: Optional[List[Dict]]
3152
3316
  :param progress_cb: Function for tracking upload progress.
3153
3317
  :type progress_cb: Optional[Union[tqdm, Callable]]
@@ -3190,6 +3354,10 @@ class ImageApi(RemoveableBulkModuleApi):
3190
3354
 
3191
3355
  image_infos = api.image.upload_multiview_images(dataset_id, group_name, paths)
3192
3356
 
3357
+ # Add custom sort parameter for images
3358
+ metas = [{'my-key':'a'}, {'my-key':'b'}]
3359
+ with api.image.add_custom_sort(key="my-key"):
3360
+ image_infos = api.image.upload_multiview_images(dataset_id, group_name, paths, metas)
3193
3361
  """
3194
3362
 
3195
3363
  if paths is None and links is None:
@@ -3389,13 +3557,17 @@ class ImageApi(RemoveableBulkModuleApi):
3389
3557
  """
3390
3558
  Upload medical 2D images (DICOM) to Supervisely and group them by specified or default tag.
3391
3559
 
3560
+ If you include `metas` during the upload, you can add a custom sort parameter for images.
3561
+ To achieve this, use the context manager :func:`api.image.add_custom_sort` with the desired key name from the meta dictionary to be used for sorting.
3562
+ Refer to the example section for more details.
3563
+
3392
3564
  :param dataset_id: Dataset ID in Supervisely.
3393
3565
  :type dataset_id: int
3394
3566
  :param paths: List of paths to images.
3395
3567
  :type paths: List[str]
3396
3568
  :param group_tag_name: Group name. All images will be assigned by tag with this group name. If `group_tag_name` is None, the images will be grouped by one of the default tags.
3397
3569
  :type group_tag_name: str, optional
3398
- :param metas: List of dictionaries which adds a customizable meta for every image provided in `paths` parameter.
3570
+ :param metas: Custom additional image infos that contain images technical and/or user-generated data as list of separate dicts.
3399
3571
  :type metas: List[Dict], optional
3400
3572
  :param progress_cb: Function for tracking upload progress.
3401
3573
  :type progress_cb: tqdm or callable, optional
@@ -3431,6 +3603,10 @@ class ImageApi(RemoveableBulkModuleApi):
3431
3603
  pbar = tqdm(desc="Uploading images", total=len(paths))
3432
3604
  image_infos = api.image.upload_medical_images(dataset_id, paths, group_tag_name, metas)
3433
3605
 
3606
+ # Add custom sort parameter for images
3607
+ metas = [{'my-key':'a'}, {'my-key':'b'}]
3608
+ with api.image.add_custom_sort(key="my-key"):
3609
+ image_infos = api.image.upload_medical_images(dataset_id, paths, group_tag_name, metas)
3434
3610
  """
3435
3611
 
3436
3612
  if metas is None:
@@ -3703,6 +3879,51 @@ class ImageApi(RemoveableBulkModuleApi):
3703
3879
  r = self._api.post("images.update.links", data)
3704
3880
  return r.json()
3705
3881
 
3882
+ def set_custom_sort(
3883
+ self,
3884
+ id: int,
3885
+ sort_value: str,
3886
+ ) -> Dict[str, Any]:
3887
+ """
3888
+ Sets custom sort value for image with given ID.
3889
+
3890
+ :param id: Image ID in Supervisely.
3891
+ :type id: int
3892
+ :param sort_value: Sort value.
3893
+ :type sort_value: str
3894
+ :return: json-encoded content of a response.
3895
+ :rtype: Dict[str, Any]
3896
+ """
3897
+ return self.set_custom_sort_bulk([id], [sort_value])
3898
+
3899
+ def set_custom_sort_bulk(
3900
+ self,
3901
+ ids: List[int],
3902
+ sort_values: List[str],
3903
+ ) -> Dict[str, Any]:
3904
+ """
3905
+ Sets custom sort values for images with given IDs.
3906
+
3907
+ :param ids: Image IDs in Supervisely.
3908
+ :type ids: List[int]
3909
+ :param sort_values: List of custom sort values that will be set for images. It is stored as a key `customSort` value in the image `meta`.
3910
+ :type sort_values: List[str]
3911
+ :return: json-encoded content of a response.
3912
+ :rtype: Dict[str, Any]
3913
+ """
3914
+ if len(ids) != len(sort_values):
3915
+ raise ValueError(
3916
+ f"Length of 'ids' and 'sort_values' is not equal, {len(ids)} != {len(sort_values)}."
3917
+ )
3918
+ data = {
3919
+ ApiField.IMAGES: [
3920
+ {ApiField.ID: id, ApiField.CUSTOM_SORT: sort_value}
3921
+ for id, sort_value in zip(ids, sort_values)
3922
+ ]
3923
+ }
3924
+ response = self._api.post("images.bulk.set-custom-sort", data)
3925
+ return response.json()
3926
+
3706
3927
  async def _download_async(
3707
3928
  self,
3708
3929
  id: int,
@@ -4276,9 +4497,9 @@ class ImageApi(RemoveableBulkModuleApi):
4276
4497
  :type dataset_id: int
4277
4498
  :param filters: Filters for images.
4278
4499
  :type filters: List[Dict[str, str]], optional
4279
- :param sort: Sort images by field.
4500
+ :param sort: Field name to sort. One of {'id' (default), 'name', 'description', 'labelsCount', 'createdAt', 'updatedAt', 'customSort'}.
4280
4501
  :type sort: str, optional
4281
- :param sort_order: Sort order for images.
4502
+ :param sort_order: Sort order for images. One of {'asc' (default), 'desc'}
4282
4503
  :type sort_order: str, optional
4283
4504
  :param force_metadata_for_links: If True, forces metadata for links.
4284
4505
  :type force_metadata_for_links: bool, optional
@@ -4346,3 +4567,19 @@ class ImageApi(RemoveableBulkModuleApi):
4346
4567
 
4347
4568
  async for page in self.get_list_page_generator_async(method, data, total_pages, semaphore):
4348
4569
  yield page
4570
+
4571
+ @staticmethod
4572
+ def update_custom_sort(meta: Dict[str, Any], custom_sort: str) -> Dict[str, Any]:
4573
+ """
4574
+ Updates a copy of the meta dictionary with a new custom sort value.
4575
+
4576
+ :param meta: Image meta dictionary.
4577
+ :type meta: Dict[str, Any]
4578
+ :param custom_sort: Custom sort value.
4579
+ :type custom_sort: str
4580
+ :return: Updated meta dictionary.
4581
+ :rtype: Dict[str, Any]
4582
+ """
4583
+ meta_copy = copy.deepcopy(meta)
4584
+ meta_copy[ApiField.CUSTOM_SORT] = custom_sort
4585
+ return meta_copy
@@ -602,6 +602,10 @@ class ApiField:
602
602
  """"""
603
603
  WITH_SHARED = "withShared"
604
604
  """"""
605
+ EXTRA_FIELDS = "extraFields"
606
+ """"""
607
+ CUSTOM_SORT = "customSort"
608
+ """"""
605
609
 
606
610
 
607
611
  def _get_single_item(items):
@@ -64,6 +64,7 @@ class TrainGUI:
64
64
  self.hyperparameters = hyperparameters
65
65
  self.app_options = app_options
66
66
  self.collapsable = app_options.get("collapsable", False)
67
+ self.need_convert_shapes_for_bm = False
67
68
 
68
69
  self.team_id = sly_env.team_id(raise_not_found=False)
69
70
  self.workspace_id = sly_env.workspace_id(raise_not_found=False)
@@ -141,24 +142,33 @@ class TrainGUI:
141
142
  self.training_process.set_experiment_name(experiment_name)
142
143
 
143
144
  def need_convert_class_shapes() -> bool:
144
- task_type = self.model_selector.get_selected_task_type()
145
-
146
- def _need_convert(shape):
147
- if task_type == TaskType.OBJECT_DETECTION:
148
- return shape != Rectangle.geometry_name()
149
- elif task_type in [TaskType.INSTANCE_SEGMENTATION, TaskType.SEMANTIC_SEGMENTATION]:
150
- return shape == Polygon.geometry_name()
151
- return
152
-
153
- data = self.classes_selector.classes_table._table_data
154
- selected_classes = set(self.classes_selector.classes_table.get_selected_classes())
155
- empty = set(r[0]["data"] for r in data if r[2]["data"] == 0 and r[3]["data"] == 0)
156
- need_convert = set(r[0]["data"] for r in data if _need_convert(r[1]["data"]))
157
-
158
- if need_convert.intersection(selected_classes - empty):
159
- self.hyperparameters_selector.model_benchmark_auto_convert_warning.show()
160
- else:
145
+ if not self.hyperparameters_selector.run_model_benchmark_checkbox.is_checked():
161
146
  self.hyperparameters_selector.model_benchmark_auto_convert_warning.hide()
147
+ self.need_convert_shapes_for_bm = False
148
+ else:
149
+ task_type = self.model_selector.get_selected_task_type()
150
+
151
+ def _need_convert(shape):
152
+ if task_type == TaskType.OBJECT_DETECTION:
153
+ return shape != Rectangle.geometry_name()
154
+ elif task_type in [
155
+ TaskType.INSTANCE_SEGMENTATION,
156
+ TaskType.SEMANTIC_SEGMENTATION,
157
+ ]:
158
+ return shape == Polygon.geometry_name()
159
+ return
160
+
161
+ data = self.classes_selector.classes_table._table_data
162
+ selected_classes = set(self.classes_selector.classes_table.get_selected_classes())
163
+ empty = set(r[0]["data"] for r in data if r[2]["data"] == 0 and r[3]["data"] == 0)
164
+ need_convert = set(r[0]["data"] for r in data if _need_convert(r[1]["data"]))
165
+
166
+ if need_convert.intersection(selected_classes - empty):
167
+ self.hyperparameters_selector.model_benchmark_auto_convert_warning.show()
168
+ self.need_convert_shapes_for_bm = True
169
+ else:
170
+ self.hyperparameters_selector.model_benchmark_auto_convert_warning.hide()
171
+ self.need_convert_shapes_for_bm = False
162
172
 
163
173
  # ------------------------------------------------- #
164
174
 
@@ -482,7 +482,6 @@ class TrainApp:
482
482
  downloading project and model data.
483
483
  """
484
484
  logger.info("Preparing for training")
485
- self.gui.disable_select_buttons()
486
485
 
487
486
  # Step 1. Workflow Input
488
487
  if is_production():
@@ -503,40 +502,40 @@ class TrainApp:
503
502
  :type experiment_info: dict
504
503
  """
505
504
  logger.info("Finalizing training")
505
+ # Step 1. Validate experiment TaskType
506
+ experiment_info = self._validate_experiment_task_type(experiment_info)
506
507
 
507
- # Step 1. Validate experiment_info
508
+ # Step 2. Validate experiment_info
508
509
  success, reason = self._validate_experiment_info(experiment_info)
509
510
  if not success:
510
511
  raise ValueError(f"{reason}. Failed to upload artifacts")
511
512
 
512
- # Step 2. Preprocess artifacts
513
+ # Step 3. Preprocess artifacts
513
514
  experiment_info = self._preprocess_artifacts(experiment_info)
514
515
 
515
- # Step3. Postprocess splits
516
+ # Step 4. Postprocess splits
516
517
  train_splits_data = self._postprocess_splits()
517
518
 
518
- # Step 3. Upload artifacts
519
+ # Step 5. Upload artifacts
519
520
  self._set_text_status("uploading")
520
521
  remote_dir, file_info = self._upload_artifacts()
521
522
 
522
- # Step 4. Run Model Benchmark
523
+ # Step 6. Create model meta according to model CV task type
523
524
  model_meta = self.create_model_meta(experiment_info["task_type"])
524
- mb_eval_lnk_file_info, mb_eval_report, mb_eval_report_id, eval_metrics = (
525
- None,
526
- None,
527
- None,
528
- {},
529
- )
525
+
526
+ # Step 7. [Optional] Run Model Benchmark
527
+ mb_eval_lnk_file_info, mb_eval_report = None, None
528
+ mb_eval_report_id, eval_metrics = None, {}
530
529
  if self.is_model_benchmark_enabled:
531
530
  try:
532
531
  # Convert GT project
532
+ gt_project_id, bm_splits_data = None, train_splits_data
533
533
  if self._app_options.get("auto_convert_classes", True):
534
- self._set_text_status("convert_gt_project")
535
- gt_project_id, bm_splits_data = self._convert_and_split_gt_project(
536
- experiment_info["task_type"]
537
- )
538
- else:
539
- gt_project_id, bm_splits_data = None, train_splits_data
534
+ if self.gui.need_convert_shapes_for_bm:
535
+ self._set_text_status("convert_gt_project")
536
+ gt_project_id, bm_splits_data = self._convert_and_split_gt_project(
537
+ experiment_info["task_type"]
538
+ )
540
539
 
541
540
  self._set_text_status("benchmark")
542
541
  (
@@ -555,7 +554,7 @@ class TrainApp:
555
554
  except Exception as e:
556
555
  logger.error(f"Model benchmark failed: {e}")
557
556
 
558
- # Step 5. [Optional] Convert weights
557
+ # Step 8. [Optional] Convert weights
559
558
  export_weights = {}
560
559
  if self.gui.hyperparameters_selector.is_export_required():
561
560
  try:
@@ -564,7 +563,7 @@ class TrainApp:
564
563
  except Exception as e:
565
564
  logger.error(f"Export weights failed: {e}")
566
565
 
567
- # Step 6. Generate and upload additional files
566
+ # Step 9. Generate and upload additional files
568
567
  self._set_text_status("metadata")
569
568
  self._generate_experiment_info(
570
569
  remote_dir, experiment_info, eval_metrics, mb_eval_report_id, export_weights
@@ -575,12 +574,12 @@ class TrainApp:
575
574
  self._generate_model_meta(remote_dir, model_meta)
576
575
  self._upload_demo_files(remote_dir)
577
576
 
578
- # Step 7. Set output widgets
577
+ # Step 10. Set output widgets
579
578
  self._set_text_status("reset")
580
579
  self._set_training_output(remote_dir, file_info, mb_eval_report)
581
580
  self._set_ws_progress_status("completed")
582
581
 
583
- # Step 8. Workflow output
582
+ # Step 11. Workflow output
584
583
  if is_production():
585
584
  self._workflow_output(remote_dir, file_info, mb_eval_lnk_file_info, mb_eval_report_id)
586
585
 
@@ -1120,6 +1119,24 @@ class TrainApp:
1120
1119
  # ----------------------------------------- #
1121
1120
 
1122
1121
  # Postprocess
1122
+ def _validate_experiment_task_type(self, experiment_info: dict) -> dict:
1123
+ """
1124
+ Checks if the task_type key if returned from the user's training function.
1125
+ If not, it will be set to the task type of the model selected in the model selector.
1126
+
1127
+ :param experiment_info: Information about the experiment results.
1128
+ :type experiment_info: dict
1129
+ :return: Experiment info with task_type key.
1130
+ :rtype: dict
1131
+ """
1132
+ task_type = experiment_info.get("task_type", None)
1133
+ if task_type is None:
1134
+ logger.debug(
1135
+ "Task type not found in experiment_info. Task type from model config will be used."
1136
+ )
1137
+ task_type = self.gui.model_selector.get_selected_task_type()
1138
+ experiment_info["task_type"] = task_type
1139
+ return experiment_info
1123
1140
 
1124
1141
  def _validate_experiment_info(self, experiment_info: dict) -> tuple:
1125
1142
  """
@@ -2218,6 +2235,7 @@ class TrainApp:
2218
2235
  Wrapper function to wrap the training process.
2219
2236
  """
2220
2237
  experiment_info = None
2238
+ check_logs_text = "Please check the logs for more details."
2221
2239
 
2222
2240
  try:
2223
2241
  self._set_train_widgets_state_on_start()
@@ -2226,7 +2244,7 @@ class TrainApp:
2226
2244
  self._prepare_working_dir()
2227
2245
  self._init_logger()
2228
2246
  except Exception as e:
2229
- message = "Error occurred during training initialization. Please check the logs for more details."
2247
+ message = f"Error occurred during training initialization. {check_logs_text}"
2230
2248
  self._show_error(message, e)
2231
2249
  self._restore_train_widgets_state_on_error()
2232
2250
  self._set_ws_progress_status("reset")
@@ -2237,9 +2255,7 @@ class TrainApp:
2237
2255
  self._set_ws_progress_status("preparing")
2238
2256
  self._prepare()
2239
2257
  except Exception as e:
2240
- message = (
2241
- "Error occurred during data preparation. Please check the logs for more details."
2242
- )
2258
+ message = f"Error occurred during data preparation. {check_logs_text}"
2243
2259
  self._show_error(message, e)
2244
2260
  self._restore_train_widgets_state_on_error()
2245
2261
  self._set_ws_progress_status("reset")
@@ -2250,8 +2266,18 @@ class TrainApp:
2250
2266
  if self._app_options.get("train_logger", None) is None:
2251
2267
  self._set_ws_progress_status("training")
2252
2268
  experiment_info = self._train_func()
2269
+ except ZeroDivisionError as e:
2270
+ message = (
2271
+ "'ZeroDivisionError' occurred during training. "
2272
+ "The error was caused by an insufficient dataset size relative to the specified batch size in hyperparameters. "
2273
+ "Please check input data and hyperparameters."
2274
+ )
2275
+ self._show_error(message, e)
2276
+ self._restore_train_widgets_state_on_error()
2277
+ self._set_ws_progress_status("reset")
2278
+ return
2253
2279
  except Exception as e:
2254
- message = "Error occurred during training. Please check the logs for more details."
2280
+ message = f"Error occurred during training. {check_logs_text}"
2255
2281
  self._show_error(message, e)
2256
2282
  self._restore_train_widgets_state_on_error()
2257
2283
  self._set_ws_progress_status("reset")
@@ -2263,7 +2289,7 @@ class TrainApp:
2263
2289
  self._finalize(experiment_info)
2264
2290
  self.gui.training_process.start_button.loading = False
2265
2291
  except Exception as e:
2266
- message = "Error occurred during finalizing and uploading training artifacts . Please check the logs for more details."
2292
+ message = f"Error occurred during finalizing and uploading training artifacts. {check_logs_text}"
2267
2293
  self._show_error(message, e)
2268
2294
  self._restore_train_widgets_state_on_error()
2269
2295
  self._set_ws_progress_status("reset")
@@ -2280,6 +2306,7 @@ class TrainApp:
2280
2306
  self._restore_train_widgets_state_on_error()
2281
2307
 
2282
2308
  def _set_train_widgets_state_on_start(self):
2309
+ self.gui.disable_select_buttons()
2283
2310
  self.gui.training_artifacts.validator_text.hide()
2284
2311
  self._validate_experiment_name()
2285
2312
  self.gui.training_process.experiment_name_input.disable()
@@ -2305,6 +2332,7 @@ class TrainApp:
2305
2332
  if self._app_options.get("device_selector", False):
2306
2333
  self.gui.training_process.select_device._select.enable()
2307
2334
  self.gui.training_process.select_device.enable()
2335
+ self.gui.enable_select_buttons()
2308
2336
 
2309
2337
  def _validate_experiment_name(self) -> bool:
2310
2338
  experiment_name = self.gui.training_process.get_experiment_name()
@@ -2475,7 +2503,7 @@ class TrainApp:
2475
2503
  change_name_if_conflict=True,
2476
2504
  )
2477
2505
 
2478
- # 3. Upload gt project to benchmark workspace
2506
+ # 3. Upload converted gt project
2479
2507
  project = Project("tmp_project", OpenMode.READ)
2480
2508
  self._api.project.update_meta(gt_project_info.id, project.meta)
2481
2509
  for dataset in project.datasets:
@@ -2491,6 +2519,7 @@ class TrainApp:
2491
2519
  img_infos = self._api.image.copy_batch(ds_info.id, img_ids)
2492
2520
  img_ids = [img_info.id for img_info in img_infos]
2493
2521
  self._api.annotation.upload_anns(img_ids, anns)
2522
+ sly_fs.remove_dir(project.directory)
2494
2523
 
2495
2524
  # 4. Match splits with original project
2496
2525
  gt_split_data = self._postprocess_splits(gt_project_info.id)
@@ -3544,6 +3544,11 @@ class Project:
3544
3544
  """
3545
3545
  Uploads project to Supervisely from the given directory.
3546
3546
 
3547
+ If you have a metadata.json files in the project directory for images, you will be able to upload images with added custom sort parameter.
3548
+ To do this, use context manager :func:`api.image.add_custom_sort` with the desired key name from the metadata.json file which will be used for sorting.
3549
+ More about project struture: https://developer.supervisely.com/getting-started/supervisely-annotation-format/project-structure#project-structure-example
3550
+ Refer to the example section for usage details.
3551
+
3547
3552
  :param dir: Path to project directory.
3548
3553
  :type dir: :class:`str`
3549
3554
  :param api: Supervisely API address and token.
@@ -3582,6 +3587,18 @@ class Project:
3582
3587
  workspace_id=45,
3583
3588
  project_name="My Project"
3584
3589
  )
3590
+
3591
+ # Upload project with added custom sort order
3592
+ # This context manager processes every image and adds a custom sort order
3593
+ # if `meta` is present in the image info file or image meta file.
3594
+ # Otherwise, it will be uploaded without a custom sort order.
3595
+ with api.image.add_custom_sort(key="key_name"):
3596
+ project_id, project_name = sly.Project.upload(
3597
+ project_directory,
3598
+ api,
3599
+ workspace_id=45,
3600
+ project_name="My Project"
3601
+ )
3585
3602
  """
3586
3603
  return upload_project(
3587
3604
  dir=dir,
@@ -4136,12 +4153,16 @@ def upload_project(
4136
4153
 
4137
4154
  if os.path.isfile(img_info_path):
4138
4155
  img_infos.append(ds_fs.get_image_info(item_name=item_name))
4156
+ else:
4157
+ img_infos.append(None)
4139
4158
 
4140
4159
  img_paths = list(filter(lambda x: os.path.isfile(x), img_paths))
4141
4160
  ann_paths = list(filter(lambda x: os.path.isfile(x), ann_paths))
4142
4161
  metas = [{} for _ in names]
4143
4162
 
4144
- if img_paths == []:
4163
+ img_infos_count = sum(1 for item in img_infos if item is not None)
4164
+
4165
+ if len(img_paths) == 0 and img_infos_count == 0:
4145
4166
  # Dataset is empty
4146
4167
  continue
4147
4168
 
@@ -4162,17 +4183,31 @@ def upload_project(
4162
4183
  total=len(names),
4163
4184
  )
4164
4185
 
4186
+ if img_infos_count != 0:
4187
+ merged_metas = []
4188
+ for img_info, meta in zip(img_infos, metas):
4189
+ if img_info is None:
4190
+ merged_metas.append(meta)
4191
+ continue
4192
+ merged_meta = {**(img_info.meta or {}), **meta}
4193
+ merged_metas.append(merged_meta)
4194
+ metas = merged_metas
4195
+
4165
4196
  if len(img_paths) != 0:
4166
4197
  uploaded_img_infos = api.image.upload_paths(
4167
4198
  dataset.id, names, img_paths, ds_progress, metas=metas
4168
4199
  )
4169
- elif len(img_paths) == 0 and len(img_infos) != 0:
4200
+ elif img_infos_count != 0:
4201
+ if img_infos_count != len(names):
4202
+ raise ValueError(
4203
+ f"Cannot upload Project: image info files count ({img_infos_count}) doesn't match with images count ({len(names)}) that are going to be uploaded. "
4204
+ "Check the directory structure, all annotation files should have corresponding image info files."
4205
+ )
4170
4206
  # uploading links and hashes (the code from api.image.upload_ids)
4171
- img_metas = [{}] * len(names)
4172
4207
  links, links_names, links_order, links_metas = [], [], [], []
4173
4208
  hashes, hashes_names, hashes_order, hashes_metas = [], [], [], []
4174
4209
  dataset_id = dataset.id
4175
- for idx, (name, info, meta) in enumerate(zip(names, img_infos, img_metas)):
4210
+ for idx, (name, info, meta) in enumerate(zip(names, img_infos, metas)):
4176
4211
  if info.link is not None:
4177
4212
  links.append(info.link)
4178
4213
  links_names.append(name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.279
3
+ Version: 6.73.281
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -28,11 +28,11 @@ supervisely/api/dataset_api.py,sha256=GH7prDRJKyJlTv_7_Y-RkTwJN7ED4EkXNqqmi3iIdI
28
28
  supervisely/api/file_api.py,sha256=v2FsD3oljwNPqcDgEJRe8Bu5k0PYKzVhqmRb5QFaHAQ,83422
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=lLt8z_OE7cXwb94_UKxWiSKxe28a4meMrVM7dhHIWZY,176956
31
+ supervisely/api/image_api.py,sha256=qZwTjeCo6bkEuXDuB8RhhP0g6PzlRuCXJkUfN9rsUZ4,190985
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=8z7K6K77fa9oijnix4vnCADJwe5nZtsDiWKZTWc_yuI,43273
35
+ supervisely/api/module_api.py,sha256=Jc_nf5ZgQdzcLYVbt6Vx7IT7KvXOKNqVIETXuewhnpg,43359
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
@@ -968,10 +968,10 @@ supervisely/nn/tracker/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
968
968
  supervisely/nn/tracker/utils/gmc.py,sha256=3JX8979H3NA-YHNaRQyj9Z-xb9qtyMittPEjGw8y2Jo,11557
969
969
  supervisely/nn/tracker/utils/kalman_filter.py,sha256=eSFmCjM0mikHCAFvj-KCVzw-0Jxpoc3Cfc2NWEjJC1Q,17268
970
970
  supervisely/nn/training/__init__.py,sha256=gY4PCykJ-42MWKsqb9kl-skemKa8yB6t_fb5kzqR66U,111
971
- supervisely/nn/training/train_app.py,sha256=mxoD8sgSuIc3B-LcieP9m1lEaUawuOuLeceRqDU6l6U,100168
971
+ supervisely/nn/training/train_app.py,sha256=PZ4zWMYRvOFj97vy2rOofCBYqnpkDtmouzFTjs9UyN4,101747
972
972
  supervisely/nn/training/gui/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
973
973
  supervisely/nn/training/gui/classes_selector.py,sha256=8UgzA4aogOAr1s42smwEcDbgaBj_i0JLhjwlZ9bFdIA,3772
974
- supervisely/nn/training/gui/gui.py,sha256=uXpNWU6q741PimYGZ5aFWpr0boewa0yDrpIz8JGcqXE,25004
974
+ supervisely/nn/training/gui/gui.py,sha256=CnT_QhihrxdSHKybpI0pXhPLwCaXEana_qdn0DhXByg,25558
975
975
  supervisely/nn/training/gui/hyperparameters_selector.py,sha256=UAXZYyhuUOY7d2ZKAx4R5Kz-KQaiFZ7AnY8BDoj3_30,7071
976
976
  supervisely/nn/training/gui/input_selector.py,sha256=Jp9PnVVADv1fhndPuZdMlKuzWTOBQZogrOks5dwATlc,2179
977
977
  supervisely/nn/training/gui/model_selector.py,sha256=n2Xn6as60bNPtSlImJtyrVEo0gjKnvHLT3yq_m39TXk,4334
@@ -1008,7 +1008,7 @@ supervisely/project/data_version.py,sha256=nknaWJSUCwoDyNG9_d1KA-GjzidhV9zd9Cn8c
1008
1008
  supervisely/project/download.py,sha256=zb8sb4XZ6Qi3CP7fmtLRUAYzaxs_W0WnOfe2x3ZVRMs,24639
1009
1009
  supervisely/project/pointcloud_episode_project.py,sha256=yiWdNBQiI6f1O9sr1pg8JHW6O-w3XUB1rikJNn3Oung,41866
1010
1010
  supervisely/project/pointcloud_project.py,sha256=Kx1Vaes-krwG3BiRRtHRLQxb9G5m5bTHPN9IzRqmNWo,49399
1011
- supervisely/project/project.py,sha256=69EDmG1Q39xssa9w3SVlbQFOa8b_RmcKpel43H6KWdY,199973
1011
+ supervisely/project/project.py,sha256=tvNPGyIZVs4p3iMz2eDU1tmtsPZWZOhQ9vBJCqCMxbs,202003
1012
1012
  supervisely/project/project_meta.py,sha256=26s8IiHC5Pg8B1AQi6_CrsWteioJP2in00cRNe8QlW0,51423
1013
1013
  supervisely/project/project_settings.py,sha256=NLThzU_DCynOK6hkHhVdFyezwprn9UqlnrLDe_3qhkY,9347
1014
1014
  supervisely/project/project_type.py,sha256=_3RqW2CnDBKFOvSIrQT1RJQaiHirs34_jiQS8CkwCpo,530
@@ -1070,9 +1070,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1070
1070
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1071
1071
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1072
1072
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1073
- supervisely-6.73.279.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1074
- supervisely-6.73.279.dist-info/METADATA,sha256=LtI71cTqLVkLsxFnPgUIWaWidSe5J_2h2Pt7DzEVWXA,33573
1075
- supervisely-6.73.279.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1076
- supervisely-6.73.279.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1077
- supervisely-6.73.279.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1078
- supervisely-6.73.279.dist-info/RECORD,,
1073
+ supervisely-6.73.281.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1074
+ supervisely-6.73.281.dist-info/METADATA,sha256=24JmJE-D0v4zGZJO-XnHJ3yiFXVK69F_UwSGz0rItEQ,33573
1075
+ supervisely-6.73.281.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1076
+ supervisely-6.73.281.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1077
+ supervisely-6.73.281.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1078
+ supervisely-6.73.281.dist-info/RECORD,,