supervisely 6.73.221__py3-none-any.whl → 6.73.222__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of supervisely might be problematic. Click here for more details.
- supervisely/api/api.py +609 -3
- supervisely/api/file_api.py +522 -9
- supervisely/api/image_api.py +469 -0
- supervisely/api/pointcloud/pointcloud_api.py +390 -1
- supervisely/api/video/video_api.py +231 -1
- supervisely/api/volume/volume_api.py +223 -2
- supervisely/convert/base_converter.py +44 -3
- supervisely/convert/converter.py +6 -5
- supervisely/convert/image/image_converter.py +26 -13
- supervisely/convert/image/sly/fast_sly_image_converter.py +4 -0
- supervisely/convert/image/sly/sly_image_converter.py +9 -4
- supervisely/convert/video/sly/sly_video_converter.py +9 -1
- supervisely/convert/video/video_converter.py +44 -23
- supervisely/io/fs.py +125 -0
- supervisely/io/fs_cache.py +19 -1
- supervisely/io/network_exceptions.py +20 -3
- supervisely/task/progress.py +1 -1
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/METADATA +3 -1
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/RECORD +23 -23
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/LICENSE +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/WHEEL +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import os
|
|
2
|
-
from typing import Callable, List, NamedTuple, Optional, Union
|
|
3
|
+
from typing import AsyncGenerator, Callable, List, NamedTuple, Optional, Union
|
|
3
4
|
|
|
5
|
+
import aiofiles
|
|
4
6
|
from tqdm import tqdm
|
|
5
7
|
from tqdm.contrib.logging import logging_redirect_tqdm
|
|
6
8
|
|
|
@@ -19,6 +21,7 @@ from supervisely.io.fs import (
|
|
|
19
21
|
ensure_base_path,
|
|
20
22
|
get_bytes_hash,
|
|
21
23
|
get_file_ext,
|
|
24
|
+
get_file_hash_async,
|
|
22
25
|
get_file_name,
|
|
23
26
|
get_file_name_with_ext,
|
|
24
27
|
)
|
|
@@ -852,7 +855,7 @@ class VolumeApi(RemoveableBulkModuleApi):
|
|
|
852
855
|
|
|
853
856
|
def _download(self, id: int, is_stream: bool = False):
|
|
854
857
|
"""
|
|
855
|
-
Private method for volume
|
|
858
|
+
Private method for volume downloading.
|
|
856
859
|
|
|
857
860
|
:param id: Volume ID in Supervisely.
|
|
858
861
|
:type id: int
|
|
@@ -1236,3 +1239,221 @@ class VolumeApi(RemoveableBulkModuleApi):
|
|
|
1236
1239
|
)
|
|
1237
1240
|
)
|
|
1238
1241
|
return volume_infos
|
|
1242
|
+
|
|
1243
|
+
async def _download_async(
|
|
1244
|
+
self,
|
|
1245
|
+
id: int,
|
|
1246
|
+
is_stream: bool = False,
|
|
1247
|
+
range_start: Optional[int] = None,
|
|
1248
|
+
range_end: Optional[int] = None,
|
|
1249
|
+
headers: Optional[dict] = None,
|
|
1250
|
+
chunk_size: int = 1024 * 1024,
|
|
1251
|
+
) -> AsyncGenerator:
|
|
1252
|
+
"""
|
|
1253
|
+
Download Volume with given ID asynchronously.
|
|
1254
|
+
If is_stream is True, returns stream of bytes, otherwise returns response object.
|
|
1255
|
+
For streaming, returns tuple of chunk and hash.
|
|
1256
|
+
|
|
1257
|
+
:param id: Volume ID in Supervisely.
|
|
1258
|
+
:type id: int
|
|
1259
|
+
:param is_stream: If True, returns stream of bytes, otherwise returns response object.
|
|
1260
|
+
:type is_stream: bool, optional
|
|
1261
|
+
:param range_start: Start byte of range for partial download.
|
|
1262
|
+
:type range_start: int, optional
|
|
1263
|
+
:param range_end: End byte of range for partial download.
|
|
1264
|
+
:type range_end: int, optional
|
|
1265
|
+
:param headers: Headers for request.
|
|
1266
|
+
:type headers: dict, optional
|
|
1267
|
+
:param chunk_size: Size of chunk for downloading. Default is 1MB.
|
|
1268
|
+
:type chunk_size: int, optional
|
|
1269
|
+
:return: Stream of bytes or response object.
|
|
1270
|
+
:rtype: AsyncGenerator
|
|
1271
|
+
"""
|
|
1272
|
+
api_method_name = "volumes.download"
|
|
1273
|
+
|
|
1274
|
+
json_body = {ApiField.ID: id}
|
|
1275
|
+
|
|
1276
|
+
if is_stream:
|
|
1277
|
+
async for chunk, hhash in self._api.stream_async(
|
|
1278
|
+
api_method_name,
|
|
1279
|
+
"POST",
|
|
1280
|
+
json_body,
|
|
1281
|
+
headers=headers,
|
|
1282
|
+
range_start=range_start,
|
|
1283
|
+
range_end=range_end,
|
|
1284
|
+
chunk_size=chunk_size,
|
|
1285
|
+
):
|
|
1286
|
+
yield chunk, hhash
|
|
1287
|
+
else:
|
|
1288
|
+
response = await self._api.post_async(api_method_name, json_body, headers=headers)
|
|
1289
|
+
yield response
|
|
1290
|
+
|
|
1291
|
+
async def download_path_async(
|
|
1292
|
+
self,
|
|
1293
|
+
id: int,
|
|
1294
|
+
path: str,
|
|
1295
|
+
semaphore: Optional[asyncio.Semaphore] = None,
|
|
1296
|
+
range_start: Optional[int] = None,
|
|
1297
|
+
range_end: Optional[int] = None,
|
|
1298
|
+
headers: Optional[dict] = None,
|
|
1299
|
+
chunk_size: int = 1024 * 1024,
|
|
1300
|
+
check_hash: bool = True,
|
|
1301
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
1302
|
+
progress_cb_type: Literal["number", "size"] = "number",
|
|
1303
|
+
) -> None:
|
|
1304
|
+
"""
|
|
1305
|
+
Downloads Volume with given ID to local path.
|
|
1306
|
+
|
|
1307
|
+
:param id: Volume ID in Supervisely.
|
|
1308
|
+
:type id: int
|
|
1309
|
+
:param path: Local save path for Volume.
|
|
1310
|
+
:type path: str
|
|
1311
|
+
:param semaphore: Semaphore for limiting the number of simultaneous downloads.
|
|
1312
|
+
:type semaphore: :class:`asyncio.Semaphore`, optional
|
|
1313
|
+
:param range_start: Start byte of range for partial download.
|
|
1314
|
+
:type range_start: int, optional
|
|
1315
|
+
:param range_end: End byte of range for partial download.
|
|
1316
|
+
:type range_end: int, optional
|
|
1317
|
+
:param headers: Headers for request.
|
|
1318
|
+
:type headers: dict, optional
|
|
1319
|
+
:param chunk_size: Size of chunk for downloading. Default is 1MB.
|
|
1320
|
+
:type chunk_size: int, optional
|
|
1321
|
+
:param check_hash: If True, checks hash of downloaded file.
|
|
1322
|
+
Check is not supported for partial downloads.
|
|
1323
|
+
When range is set, hash check is disabled.
|
|
1324
|
+
:type check_hash: bool, optional
|
|
1325
|
+
:param progress_cb: Function for tracking download progress.
|
|
1326
|
+
:type progress_cb: tqdm or callable, optional
|
|
1327
|
+
:param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
|
|
1328
|
+
:type progress_cb_type: str, optional
|
|
1329
|
+
:return: None
|
|
1330
|
+
:rtype: :class:`NoneType`
|
|
1331
|
+
:Usage example:
|
|
1332
|
+
|
|
1333
|
+
.. code-block:: python
|
|
1334
|
+
|
|
1335
|
+
import supervisely as sly
|
|
1336
|
+
import asyncio
|
|
1337
|
+
|
|
1338
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
1339
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
1340
|
+
api = sly.Api.from_env()
|
|
1341
|
+
|
|
1342
|
+
volume_info = api.volume.get_info_by_id(770918)
|
|
1343
|
+
save_path = os.path.join("/path/to/save/", volume_info.name)
|
|
1344
|
+
|
|
1345
|
+
semaphore = asyncio.Semaphore(100)
|
|
1346
|
+
loop = asyncio.new_event_loop()
|
|
1347
|
+
asyncio.set_event_loop(loop)
|
|
1348
|
+
loop.run_until_complete(
|
|
1349
|
+
api.volume.download_path_async(volume_info.id, save_path, semaphore)
|
|
1350
|
+
)
|
|
1351
|
+
"""
|
|
1352
|
+
|
|
1353
|
+
if range_start is not None or range_end is not None:
|
|
1354
|
+
check_hash = False # Hash check is not supported for partial downloads
|
|
1355
|
+
headers = headers or {}
|
|
1356
|
+
headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
|
|
1357
|
+
logger.debug(f"Image ID: {id}. Setting Range header: {headers['Range']}")
|
|
1358
|
+
|
|
1359
|
+
writing_method = "ab" if range_start not in [0, None] else "wb"
|
|
1360
|
+
|
|
1361
|
+
ensure_base_path(path)
|
|
1362
|
+
hash_to_check = None
|
|
1363
|
+
if semaphore is None:
|
|
1364
|
+
semaphore = self._api._get_default_semaphore()
|
|
1365
|
+
async with semaphore:
|
|
1366
|
+
async with aiofiles.open(path, writing_method) as fd:
|
|
1367
|
+
async for chunk, hhash in self._download_async(
|
|
1368
|
+
id,
|
|
1369
|
+
is_stream=True,
|
|
1370
|
+
headers=headers,
|
|
1371
|
+
range_start=range_start,
|
|
1372
|
+
range_end=range_end,
|
|
1373
|
+
chunk_size=chunk_size,
|
|
1374
|
+
):
|
|
1375
|
+
await fd.write(chunk)
|
|
1376
|
+
hash_to_check = hhash
|
|
1377
|
+
if progress_cb is not None and progress_cb_type == "size":
|
|
1378
|
+
progress_cb(len(chunk))
|
|
1379
|
+
if check_hash:
|
|
1380
|
+
if hash_to_check is not None:
|
|
1381
|
+
downloaded_file_hash = await get_file_hash_async(path)
|
|
1382
|
+
if hash_to_check != downloaded_file_hash:
|
|
1383
|
+
raise RuntimeError(
|
|
1384
|
+
f"Downloaded hash of volume with ID:{id} does not match the expected hash: {downloaded_file_hash} != {hash_to_check}"
|
|
1385
|
+
)
|
|
1386
|
+
if progress_cb is not None and progress_cb_type == "number":
|
|
1387
|
+
progress_cb(1)
|
|
1388
|
+
|
|
1389
|
+
async def download_paths_async(
|
|
1390
|
+
self,
|
|
1391
|
+
ids: List[int],
|
|
1392
|
+
paths: List[str],
|
|
1393
|
+
semaphore: Optional[asyncio.Semaphore] = None,
|
|
1394
|
+
headers: Optional[dict] = None,
|
|
1395
|
+
chunk_size: int = 1024 * 1024,
|
|
1396
|
+
check_hash: bool = True,
|
|
1397
|
+
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
1398
|
+
progress_cb_type: Literal["number", "size"] = "number",
|
|
1399
|
+
) -> None:
|
|
1400
|
+
"""
|
|
1401
|
+
Download Volumes with given IDs and saves them to given local paths asynchronously.
|
|
1402
|
+
|
|
1403
|
+
:param ids: List of Volume IDs in Supervisely.
|
|
1404
|
+
:type ids: :class:`List[int]`
|
|
1405
|
+
:param paths: Local save paths for Volumes.
|
|
1406
|
+
:type paths: :class:`List[str]`
|
|
1407
|
+
:param semaphore: Semaphore for limiting the number of simultaneous downloads.
|
|
1408
|
+
:type semaphore: :class:`asyncio.Semaphore`, optional
|
|
1409
|
+
:param headers: Headers for request.
|
|
1410
|
+
:type headers: dict, optional
|
|
1411
|
+
:param chunk_size: Size of chunk for downloading. Default is 1MB.
|
|
1412
|
+
:type chunk_size: int, optional
|
|
1413
|
+
:param check_hash: If True, checks hash of downloaded file.
|
|
1414
|
+
:type check_hash: bool, optional
|
|
1415
|
+
:param progress_cb: Function for tracking download progress.
|
|
1416
|
+
:type progress_cb: tqdm or callable, optional
|
|
1417
|
+
:param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
|
|
1418
|
+
:type progress_cb_type: str, optional
|
|
1419
|
+
:raises: :class:`ValueError` if len(ids) != len(paths)
|
|
1420
|
+
:return: None
|
|
1421
|
+
:rtype: :class:`NoneType`
|
|
1422
|
+
|
|
1423
|
+
:Usage example:
|
|
1424
|
+
|
|
1425
|
+
.. code-block:: python
|
|
1426
|
+
|
|
1427
|
+
import supervisely as sly
|
|
1428
|
+
import asyncio
|
|
1429
|
+
|
|
1430
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
1431
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
1432
|
+
api = sly.Api.from_env()
|
|
1433
|
+
|
|
1434
|
+
ids = [770914, 770915]
|
|
1435
|
+
paths = ["/path/to/save/volume1.nrrd", "/path/to/save/volume2.nrrd"]
|
|
1436
|
+
loop = asyncio.new_event_loop()
|
|
1437
|
+
asyncio.set_event_loop(loop)
|
|
1438
|
+
loop.run_until_complete(api.volume.download_paths_async(ids, paths))
|
|
1439
|
+
"""
|
|
1440
|
+
if len(ids) == 0:
|
|
1441
|
+
return
|
|
1442
|
+
if len(ids) != len(paths):
|
|
1443
|
+
raise ValueError(f'Can not match "ids" and "paths" lists, {len(ids)} != {len(paths)}')
|
|
1444
|
+
if semaphore is None:
|
|
1445
|
+
semaphore = self._api._get_default_semaphore()
|
|
1446
|
+
tasks = []
|
|
1447
|
+
for img_id, img_path in zip(ids, paths):
|
|
1448
|
+
task = self.download_path_async(
|
|
1449
|
+
img_id,
|
|
1450
|
+
img_path,
|
|
1451
|
+
semaphore,
|
|
1452
|
+
headers=headers,
|
|
1453
|
+
chunk_size=chunk_size,
|
|
1454
|
+
check_hash=check_hash,
|
|
1455
|
+
progress_cb=progress_cb,
|
|
1456
|
+
progress_cb_type=progress_cb_type,
|
|
1457
|
+
)
|
|
1458
|
+
tasks.append(task)
|
|
1459
|
+
await asyncio.gather(*tasks)
|
|
@@ -5,11 +5,12 @@ from typing import Dict, List, Optional, Tuple, Union
|
|
|
5
5
|
|
|
6
6
|
from tqdm import tqdm
|
|
7
7
|
|
|
8
|
-
from supervisely._utils import is_production
|
|
8
|
+
from supervisely._utils import batched, is_production
|
|
9
9
|
from supervisely.annotation.annotation import Annotation
|
|
10
10
|
from supervisely.annotation.tag_meta import TagValueType
|
|
11
11
|
from supervisely.api.api import Api
|
|
12
|
-
from supervisely.io.
|
|
12
|
+
from supervisely.io.env import team_id
|
|
13
|
+
from supervisely.io.fs import get_file_ext, get_file_name_with_ext, silent_remove
|
|
13
14
|
from supervisely.project.project_meta import ProjectMeta
|
|
14
15
|
from supervisely.project.project_settings import LabelingInterface
|
|
15
16
|
from supervisely.sly_logger import logger
|
|
@@ -145,12 +146,16 @@ class BaseConverter:
|
|
|
145
146
|
remote_files_map: Optional[Dict[str, str]] = None,
|
|
146
147
|
):
|
|
147
148
|
self._input_data: str = input_data
|
|
148
|
-
self._items: List[
|
|
149
|
+
self._items: List[BaseConverter.BaseItem] = []
|
|
149
150
|
self._meta: ProjectMeta = None
|
|
150
151
|
self._labeling_interface = labeling_interface or LabelingInterface.DEFAULT
|
|
152
|
+
|
|
153
|
+
# import as links settings
|
|
151
154
|
self._upload_as_links: bool = upload_as_links
|
|
152
155
|
self._remote_files_map: Optional[Dict[str, str]] = remote_files_map
|
|
153
156
|
self._supports_links = False # if converter supports uploading by links
|
|
157
|
+
self._api = Api.from_env() if self._upload_as_links else None
|
|
158
|
+
self._team_id = team_id() if self._upload_as_links else None
|
|
154
159
|
self._converter = None
|
|
155
160
|
|
|
156
161
|
if self._labeling_interface not in LabelingInterface.values():
|
|
@@ -419,3 +424,39 @@ class BaseConverter:
|
|
|
419
424
|
|
|
420
425
|
return meta1.clone(project_settings=new_settings)
|
|
421
426
|
return meta1
|
|
427
|
+
|
|
428
|
+
def _download_remote_ann_files(self) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Download all annotation files from Cloud Storage to the local storage.
|
|
431
|
+
Needed to detect annotation format if "upload_as_links" is enabled.
|
|
432
|
+
"""
|
|
433
|
+
if not self.upload_as_links:
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
files_to_download = {
|
|
437
|
+
l: r for l, r in self._remote_files_map.items() if get_file_ext(l) == self.ann_ext
|
|
438
|
+
}
|
|
439
|
+
if not files_to_download:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
import asyncio
|
|
443
|
+
|
|
444
|
+
loop = asyncio.get_event_loop()
|
|
445
|
+
_, progress_cb = self.get_progress(
|
|
446
|
+
len(files_to_download),
|
|
447
|
+
"Downloading annotation files from remote storage",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
for local_path in files_to_download.keys():
|
|
451
|
+
silent_remove(local_path)
|
|
452
|
+
|
|
453
|
+
logger.info("Downloading annotation files from remote storage...")
|
|
454
|
+
loop.run_until_complete(
|
|
455
|
+
self._api.storage.download_bulk_async(
|
|
456
|
+
team_id=self._team_id,
|
|
457
|
+
remote_paths=list(files_to_download.values()),
|
|
458
|
+
local_save_paths=list(files_to_download.keys()),
|
|
459
|
+
progress_cb=progress_cb,
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
logger.info("Annotation files downloaded successfully")
|
supervisely/convert/converter.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from tqdm import tqdm
|
|
5
6
|
|
|
6
|
-
from typing import Literal, Optional
|
|
7
|
-
|
|
8
7
|
from supervisely._utils import is_production
|
|
9
8
|
from supervisely.api.api import Api
|
|
10
9
|
from supervisely.app import get_data_dir
|
|
11
10
|
from supervisely.convert.image.csv.csv_converter import CSVConverter
|
|
12
|
-
from supervisely.convert.image.high_color.high_color_depth import
|
|
11
|
+
from supervisely.convert.image.high_color.high_color_depth import (
|
|
12
|
+
HighColorDepthImageConverter,
|
|
13
|
+
)
|
|
13
14
|
from supervisely.convert.image.image_converter import ImageConverter
|
|
14
15
|
from supervisely.convert.pointcloud.pointcloud_converter import PointcloudConverter
|
|
15
16
|
from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import (
|
|
@@ -28,10 +29,10 @@ from supervisely.io.fs import (
|
|
|
28
29
|
touch,
|
|
29
30
|
unpack_archive,
|
|
30
31
|
)
|
|
32
|
+
from supervisely.project.project_settings import LabelingInterface
|
|
31
33
|
from supervisely.project.project_type import ProjectType
|
|
32
34
|
from supervisely.sly_logger import logger
|
|
33
35
|
from supervisely.task.progress import Progress
|
|
34
|
-
from supervisely.project.project_settings import LabelingInterface
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class ImportManager:
|
|
@@ -164,7 +165,7 @@ class ImportManager:
|
|
|
164
165
|
dir_path = remote_path.rstrip("/") if is_dir else os.path.dirname(remote_path)
|
|
165
166
|
dir_name = os.path.basename(dir_path)
|
|
166
167
|
|
|
167
|
-
local_path = os.path.join(get_data_dir(), dir_name)
|
|
168
|
+
local_path = os.path.abspath(os.path.join(get_data_dir(), dir_name))
|
|
168
169
|
mkdir(local_path, remove_content_if_exists=True)
|
|
169
170
|
|
|
170
171
|
if is_dir:
|
|
@@ -137,8 +137,8 @@ class ImageConverter(BaseConverter):
|
|
|
137
137
|
if item.path is None:
|
|
138
138
|
continue # image has failed validation
|
|
139
139
|
item.name = f"{get_file_name(item.path)}{get_file_ext(item.path).lower()}"
|
|
140
|
-
if self.upload_as_links:
|
|
141
|
-
ann = None
|
|
140
|
+
if self.upload_as_links and not self.supports_links:
|
|
141
|
+
ann = None
|
|
142
142
|
else:
|
|
143
143
|
ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
|
|
144
144
|
name = generate_free_name(
|
|
@@ -160,19 +160,29 @@ class ImageConverter(BaseConverter):
|
|
|
160
160
|
with ApiContext(
|
|
161
161
|
api=api, project_id=project_id, dataset_id=dataset_id, project_meta=meta
|
|
162
162
|
):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
if self.upload_as_links:
|
|
164
|
+
img_infos = api.image.upload_links(
|
|
165
|
+
dataset_id,
|
|
166
|
+
item_names,
|
|
167
|
+
item_paths,
|
|
168
|
+
metas=item_metas,
|
|
169
|
+
conflict_resolution="rename",
|
|
170
|
+
force_metadata_for_links=False,
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
img_infos = api.image.upload_paths(
|
|
174
|
+
dataset_id,
|
|
175
|
+
item_names,
|
|
176
|
+
item_paths,
|
|
177
|
+
metas=item_metas,
|
|
178
|
+
conflict_resolution="rename",
|
|
179
|
+
)
|
|
180
|
+
|
|
173
181
|
img_ids = [img_info.id for img_info in img_infos]
|
|
174
182
|
if len(anns) == len(img_ids):
|
|
175
|
-
api.annotation.upload_anns(
|
|
183
|
+
api.annotation.upload_anns(
|
|
184
|
+
img_ids, anns, skip_bounds_validation=self.upload_as_links
|
|
185
|
+
)
|
|
176
186
|
|
|
177
187
|
if log_progress:
|
|
178
188
|
progress_cb(len(batch))
|
|
@@ -188,6 +198,9 @@ class ImageConverter(BaseConverter):
|
|
|
188
198
|
return image_helper.validate_image(path)
|
|
189
199
|
|
|
190
200
|
def is_image(self, path: str) -> bool:
|
|
201
|
+
if self._upload_as_links and self.supports_links:
|
|
202
|
+
ext = get_file_ext(path)
|
|
203
|
+
return ext.lower() in self.allowed_exts
|
|
191
204
|
mimetypes.add_type("image/heic", ".heic") # to extend types_map
|
|
192
205
|
mimetypes.add_type("image/heif", ".heif") # to extend types_map
|
|
193
206
|
mimetypes.add_type("image/jpeg", ".jfif") # to extend types_map
|
|
@@ -20,6 +20,10 @@ from supervisely.convert.image.image_helper import validate_image_bounds
|
|
|
20
20
|
|
|
21
21
|
class FastSlyImageConverter(SLYImageConverter, ImageConverter):
|
|
22
22
|
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self._supports_links = False
|
|
26
|
+
|
|
23
27
|
def validate_format(self) -> bool:
|
|
24
28
|
|
|
25
29
|
detected_ann_cnt = 0
|
|
@@ -2,21 +2,21 @@ import os
|
|
|
2
2
|
from typing import Dict, Optional
|
|
3
3
|
|
|
4
4
|
import supervisely.convert.image.sly.sly_image_helper as sly_image_helper
|
|
5
|
-
from supervisely.convert.image.image_helper import validate_image_bounds
|
|
6
5
|
from supervisely import (
|
|
7
6
|
Annotation,
|
|
8
7
|
Dataset,
|
|
8
|
+
Label,
|
|
9
9
|
OpenMode,
|
|
10
10
|
Project,
|
|
11
11
|
ProjectMeta,
|
|
12
12
|
Rectangle,
|
|
13
|
-
Label,
|
|
14
13
|
logger,
|
|
15
14
|
)
|
|
16
15
|
from supervisely._utils import generate_free_name
|
|
17
16
|
from supervisely.api.api import Api
|
|
18
17
|
from supervisely.convert.base_converter import AvailableImageConverters
|
|
19
18
|
from supervisely.convert.image.image_converter import ImageConverter
|
|
19
|
+
from supervisely.convert.image.image_helper import validate_image_bounds
|
|
20
20
|
from supervisely.io.fs import dirs_filter, file_exists, get_file_ext
|
|
21
21
|
from supervisely.io.json import load_json_file
|
|
22
22
|
from supervisely.project.project import find_project_dirs
|
|
@@ -31,6 +31,7 @@ class SLYImageConverter(ImageConverter):
|
|
|
31
31
|
def __init__(self, *args, **kwargs):
|
|
32
32
|
super().__init__(*args, **kwargs)
|
|
33
33
|
self._project_structure = None
|
|
34
|
+
self._supports_links = True
|
|
34
35
|
|
|
35
36
|
def __str__(self):
|
|
36
37
|
return AvailableImageConverters.SLY
|
|
@@ -74,6 +75,8 @@ class SLYImageConverter(ImageConverter):
|
|
|
74
75
|
return False
|
|
75
76
|
|
|
76
77
|
def validate_format(self) -> bool:
|
|
78
|
+
if self.upload_as_links and self._supports_links:
|
|
79
|
+
self._download_remote_ann_files()
|
|
77
80
|
if self.read_sly_project(self._input_data):
|
|
78
81
|
return True
|
|
79
82
|
|
|
@@ -136,6 +139,8 @@ class SLYImageConverter(ImageConverter):
|
|
|
136
139
|
meta = self._meta
|
|
137
140
|
|
|
138
141
|
if item.ann_data is None:
|
|
142
|
+
if self._upload_as_links:
|
|
143
|
+
item.set_shape([None, None])
|
|
139
144
|
return item.create_empty_annotation()
|
|
140
145
|
|
|
141
146
|
try:
|
|
@@ -151,7 +156,7 @@ class SLYImageConverter(ImageConverter):
|
|
|
151
156
|
)
|
|
152
157
|
return Annotation.from_json(ann_json, meta).clone(labels=labels)
|
|
153
158
|
except Exception as e:
|
|
154
|
-
logger.
|
|
159
|
+
logger.warning(f"Failed to convert annotation: {repr(e)}")
|
|
155
160
|
return item.create_empty_annotation()
|
|
156
161
|
|
|
157
162
|
def read_sly_project(self, input_data: str) -> bool:
|
|
@@ -163,7 +168,7 @@ class SLYImageConverter(ImageConverter):
|
|
|
163
168
|
logger.debug("Trying to find Supervisely project format in the input data")
|
|
164
169
|
project_dirs = [d for d in find_project_dirs(input_data)]
|
|
165
170
|
if len(project_dirs) > 1:
|
|
166
|
-
logger.info("Found multiple Supervisely projects")
|
|
171
|
+
logger.info("Found multiple possible Supervisely projects in the input data")
|
|
167
172
|
meta = None
|
|
168
173
|
for project_dir in project_dirs:
|
|
169
174
|
project_fs = Project(project_dir, mode=OpenMode.READ)
|
|
@@ -12,6 +12,10 @@ from supervisely.video.video import validate_ext as validate_video_ext
|
|
|
12
12
|
|
|
13
13
|
class SLYVideoConverter(VideoConverter):
|
|
14
14
|
|
|
15
|
+
def __init__(self, *args, **kwargs):
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
self._supports_links = True
|
|
18
|
+
|
|
15
19
|
def __str__(self) -> str:
|
|
16
20
|
return AvailableVideoConverters.SLY
|
|
17
21
|
|
|
@@ -45,6 +49,8 @@ class SLYVideoConverter(VideoConverter):
|
|
|
45
49
|
return False
|
|
46
50
|
|
|
47
51
|
def validate_format(self) -> bool:
|
|
52
|
+
if self.upload_as_links and self._supports_links:
|
|
53
|
+
self._download_remote_ann_files()
|
|
48
54
|
detected_ann_cnt = 0
|
|
49
55
|
videos_list, ann_dict = [], {}
|
|
50
56
|
for root, _, files in os.walk(self._input_data):
|
|
@@ -103,6 +109,8 @@ class SLYVideoConverter(VideoConverter):
|
|
|
103
109
|
meta = self._meta
|
|
104
110
|
|
|
105
111
|
if item.ann_data is None:
|
|
112
|
+
if self._upload_as_links:
|
|
113
|
+
return None
|
|
106
114
|
return item.create_empty_annotation()
|
|
107
115
|
|
|
108
116
|
try:
|
|
@@ -113,5 +121,5 @@ class SLYVideoConverter(VideoConverter):
|
|
|
113
121
|
ann_json = sly_video_helper.rename_in_json(ann_json, renamed_classes, renamed_tags)
|
|
114
122
|
return VideoAnnotation.from_json(ann_json, meta)
|
|
115
123
|
except Exception as e:
|
|
116
|
-
logger.
|
|
124
|
+
logger.warning(f"Failed to convert annotation: {repr(e)}")
|
|
117
125
|
return item.create_empty_annotation()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
|
-
from typing import Dict, Optional, Union
|
|
3
|
+
from typing import Dict, Optional, Tuple, Union
|
|
4
4
|
|
|
5
5
|
import cv2
|
|
6
6
|
import magic
|
|
@@ -57,10 +57,22 @@ class VideoConverter(BaseConverter):
|
|
|
57
57
|
self._frame_count = frame_count
|
|
58
58
|
self._custom_data = custom_data if custom_data is not None else {}
|
|
59
59
|
|
|
60
|
+
@property
|
|
61
|
+
def shape(self) -> Tuple[int, int]:
|
|
62
|
+
return self._shape
|
|
63
|
+
|
|
64
|
+
@shape.setter
|
|
65
|
+
def shape(self, shape: Optional[Tuple[int, int]] = None):
|
|
66
|
+
self._shape = shape if shape is not None else [None, None]
|
|
67
|
+
|
|
60
68
|
@property
|
|
61
69
|
def frame_count(self) -> int:
|
|
62
70
|
return self._frame_count
|
|
63
71
|
|
|
72
|
+
@frame_count.setter
|
|
73
|
+
def frame_count(self, frame_count: int):
|
|
74
|
+
self._frame_count = frame_count
|
|
75
|
+
|
|
64
76
|
@property
|
|
65
77
|
def name(self) -> str:
|
|
66
78
|
if self._name is not None:
|
|
@@ -75,11 +87,11 @@ class VideoConverter(BaseConverter):
|
|
|
75
87
|
return VideoAnnotation(self._shape, self._frame_count)
|
|
76
88
|
|
|
77
89
|
def __init__(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
self,
|
|
91
|
+
input_data: str,
|
|
92
|
+
labeling_interface: Optional[Union[LabelingInterface, str]],
|
|
93
|
+
upload_as_links: bool,
|
|
94
|
+
remote_files_map: Optional[Dict[str, str]] = None,
|
|
83
95
|
):
|
|
84
96
|
super().__init__(input_data, labeling_interface, upload_as_links, remote_files_map)
|
|
85
97
|
self._key_id_map: KeyIdMap = None
|
|
@@ -114,7 +126,9 @@ class VideoConverter(BaseConverter):
|
|
|
114
126
|
existing_names = set([vid.name for vid in api.video.get_list(dataset_id)])
|
|
115
127
|
|
|
116
128
|
# check video codecs, mimetypes and convert if needed
|
|
117
|
-
convert_progress, convert_progress_cb = self.get_progress(
|
|
129
|
+
convert_progress, convert_progress_cb = self.get_progress(
|
|
130
|
+
self.items_count, "Preparing videos..."
|
|
131
|
+
)
|
|
118
132
|
for item in self._items:
|
|
119
133
|
item_name, item_path = self.convert_to_mp4_if_needed(item.path)
|
|
120
134
|
item.name = item_name
|
|
@@ -124,11 +138,14 @@ class VideoConverter(BaseConverter):
|
|
|
124
138
|
convert_progress.close()
|
|
125
139
|
|
|
126
140
|
has_large_files = False
|
|
141
|
+
size_progress_cb = None
|
|
127
142
|
progress_cb, progress, ann_progress, ann_progress_cb = None, None, None, None
|
|
128
143
|
if log_progress and not self.upload_as_links:
|
|
129
144
|
progress, progress_cb = self.get_progress(self.items_count, "Uploading videos...")
|
|
130
145
|
file_sizes = [get_file_size(item.path) for item in self._items]
|
|
131
|
-
has_large_files = any(
|
|
146
|
+
has_large_files = any(
|
|
147
|
+
[self._check_video_file_size(file_size) for file_size in file_sizes]
|
|
148
|
+
)
|
|
132
149
|
if has_large_files:
|
|
133
150
|
upload_progress = []
|
|
134
151
|
size_progress_cb = self._get_video_upload_progress(upload_progress)
|
|
@@ -146,17 +163,19 @@ class VideoConverter(BaseConverter):
|
|
|
146
163
|
item_paths.append(item.path)
|
|
147
164
|
item_names.append(item.name)
|
|
148
165
|
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
ann = None
|
|
167
|
+
if not self.upload_as_links or self.supports_links:
|
|
151
168
|
ann = self.to_supervisely(item, meta, renamed_classes, renamed_tags)
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
if ann is not None:
|
|
170
|
+
figures_cnt += len(ann.figures)
|
|
171
|
+
anns.append(ann)
|
|
154
172
|
|
|
155
173
|
if self.upload_as_links:
|
|
156
174
|
vid_infos = api.video.upload_links(
|
|
157
175
|
dataset_id,
|
|
158
176
|
item_paths,
|
|
159
177
|
item_names,
|
|
178
|
+
skip_download=True,
|
|
160
179
|
)
|
|
161
180
|
else:
|
|
162
181
|
vid_infos = api.video.upload_paths(
|
|
@@ -164,22 +183,24 @@ class VideoConverter(BaseConverter):
|
|
|
164
183
|
item_names,
|
|
165
184
|
item_paths,
|
|
166
185
|
progress_cb=progress_cb if log_progress else None,
|
|
167
|
-
item_progress=size_progress_cb if log_progress and has_large_files else None,
|
|
186
|
+
item_progress=(size_progress_cb if log_progress and has_large_files else None),
|
|
168
187
|
)
|
|
169
|
-
|
|
188
|
+
vid_ids = [vid_info.id for vid_info in vid_infos]
|
|
170
189
|
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
if log_progress and has_large_files and figures_cnt > 0:
|
|
191
|
+
ann_progress, ann_progress_cb = self.get_progress(
|
|
192
|
+
figures_cnt, "Uploading annotations..."
|
|
193
|
+
)
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
for vid, ann, item, info in zip(vid_ids, anns, batch, vid_infos):
|
|
196
|
+
if ann is None:
|
|
197
|
+
ann = VideoAnnotation((info.frame_height, info.frame_width), info.frames_count)
|
|
198
|
+
api.video.annotation.append(vid, ann, progress_cb=ann_progress_cb)
|
|
178
199
|
|
|
179
200
|
if log_progress and is_development():
|
|
180
|
-
if progress is not None:
|
|
201
|
+
if progress is not None:
|
|
181
202
|
progress.close()
|
|
182
|
-
if
|
|
203
|
+
if ann_progress is not None:
|
|
183
204
|
ann_progress.close()
|
|
184
205
|
logger.info(f"Dataset ID:{dataset_id} has been successfully uploaded.")
|
|
185
206
|
|
|
@@ -268,7 +289,7 @@ class VideoConverter(BaseConverter):
|
|
|
268
289
|
)
|
|
269
290
|
|
|
270
291
|
def _check_video_file_size(self, file_size):
|
|
271
|
-
return file_size > 20 * 1024 * 1024
|
|
292
|
+
return file_size > 20 * 1024 * 1024 # 20 MB
|
|
272
293
|
|
|
273
294
|
def _get_video_upload_progress(self, upload_progress):
|
|
274
295
|
upload_progress = []
|