supervisely 6.73.320__py3-none-any.whl → 6.73.322__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supervisely/_utils.py +33 -0
- supervisely/api/api.py +17 -13
- supervisely/api/file_api.py +158 -25
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/pointcloud_episodes/__init__.py +1 -0
- supervisely/convert/pointcloud_episodes/kitti_360/__init__.py +0 -0
- supervisely/convert/pointcloud_episodes/kitti_360/kitti_360_converter.py +242 -0
- supervisely/convert/pointcloud_episodes/kitti_360/kitti_360_helper.py +386 -0
- supervisely/io/fs.py +81 -4
- supervisely/nn/inference/inference.py +155 -1
- supervisely/nn/training/train_app.py +48 -36
- {supervisely-6.73.320.dist-info → supervisely-6.73.322.dist-info}/METADATA +1 -1
- {supervisely-6.73.320.dist-info → supervisely-6.73.322.dist-info}/RECORD +17 -14
- {supervisely-6.73.320.dist-info → supervisely-6.73.322.dist-info}/LICENSE +0 -0
- {supervisely-6.73.320.dist-info → supervisely-6.73.322.dist-info}/WHEEL +0 -0
- {supervisely-6.73.320.dist-info → supervisely-6.73.322.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.320.dist-info → supervisely-6.73.322.dist-info}/top_level.txt +0 -0
supervisely/_utils.py
CHANGED
|
@@ -471,6 +471,39 @@ def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
|
|
|
471
471
|
return loop
|
|
472
472
|
|
|
473
473
|
|
|
474
|
+
def sync_call(coro):
|
|
475
|
+
"""
|
|
476
|
+
This function is used to run asynchronous functions in synchronous context.
|
|
477
|
+
|
|
478
|
+
:param coro: Asynchronous function.
|
|
479
|
+
:type coro: Coroutine
|
|
480
|
+
:return: Result of the asynchronous function.
|
|
481
|
+
:rtype: Any
|
|
482
|
+
|
|
483
|
+
:Usage example:
|
|
484
|
+
|
|
485
|
+
.. code-block:: python
|
|
486
|
+
|
|
487
|
+
from supervisely.utils import sync_call
|
|
488
|
+
|
|
489
|
+
async def async_function():
|
|
490
|
+
await asyncio.sleep(1)
|
|
491
|
+
return "Hello, World!"
|
|
492
|
+
coro = async_function()
|
|
493
|
+
result = sync_call(coro)
|
|
494
|
+
print(result)
|
|
495
|
+
# Output: Hello, World!
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
loop = get_or_create_event_loop()
|
|
499
|
+
|
|
500
|
+
if loop.is_running():
|
|
501
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop=loop)
|
|
502
|
+
return future.result()
|
|
503
|
+
else:
|
|
504
|
+
return loop.run_until_complete(coro)
|
|
505
|
+
|
|
506
|
+
|
|
474
507
|
def get_filename_from_headers(url):
|
|
475
508
|
try:
|
|
476
509
|
response = requests.head(url, allow_redirects=True)
|
supervisely/api/api.py
CHANGED
|
@@ -1443,6 +1443,7 @@ class Api:
|
|
|
1443
1443
|
chunk_size: int = 8192,
|
|
1444
1444
|
use_public_api: Optional[bool] = True,
|
|
1445
1445
|
timeout: httpx._types.TimeoutTypes = 60,
|
|
1446
|
+
**kwargs,
|
|
1446
1447
|
) -> AsyncGenerator:
|
|
1447
1448
|
"""
|
|
1448
1449
|
Performs asynchronous streaming GET or POST request to server with given parameters.
|
|
@@ -1486,18 +1487,19 @@ class Api:
|
|
|
1486
1487
|
else:
|
|
1487
1488
|
headers = {**self.headers, **headers}
|
|
1488
1489
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
elif isinstance(data, Dict):
|
|
1494
|
-
json_body = {**data, **self.additional_fields}
|
|
1495
|
-
content = None
|
|
1496
|
-
params = None
|
|
1490
|
+
params = kwargs.get("params", None)
|
|
1491
|
+
if "content" in kwargs or "json_body" in kwargs:
|
|
1492
|
+
content = kwargs.get("content", None)
|
|
1493
|
+
json_body = kwargs.get("json_body", None)
|
|
1497
1494
|
else:
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1495
|
+
if isinstance(data, (bytes, Generator)):
|
|
1496
|
+
content = data
|
|
1497
|
+
json_body = None
|
|
1498
|
+
elif isinstance(data, Dict):
|
|
1499
|
+
json_body = {**data, **self.additional_fields}
|
|
1500
|
+
content = None
|
|
1501
|
+
else:
|
|
1502
|
+
raise ValueError("Data should be either bytes or dict")
|
|
1501
1503
|
|
|
1502
1504
|
if range_start is not None or range_end is not None:
|
|
1503
1505
|
headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
|
|
@@ -1512,17 +1514,19 @@ class Api:
|
|
|
1512
1514
|
url,
|
|
1513
1515
|
content=content,
|
|
1514
1516
|
json=json_body,
|
|
1515
|
-
params=params,
|
|
1516
1517
|
headers=headers,
|
|
1517
1518
|
timeout=timeout,
|
|
1519
|
+
params=params,
|
|
1518
1520
|
)
|
|
1519
1521
|
elif method_type == "GET":
|
|
1520
1522
|
response = self.async_httpx_client.stream(
|
|
1521
1523
|
method_type,
|
|
1522
1524
|
url,
|
|
1523
|
-
|
|
1525
|
+
content=content,
|
|
1526
|
+
json=json_body,
|
|
1524
1527
|
headers=headers,
|
|
1525
1528
|
timeout=timeout,
|
|
1529
|
+
params=params,
|
|
1526
1530
|
)
|
|
1527
1531
|
else:
|
|
1528
1532
|
raise NotImplementedError(
|
supervisely/api/file_api.py
CHANGED
|
@@ -33,7 +33,9 @@ from supervisely.io.fs import (
|
|
|
33
33
|
get_file_name,
|
|
34
34
|
get_file_name_with_ext,
|
|
35
35
|
get_file_size,
|
|
36
|
+
get_or_create_event_loop,
|
|
36
37
|
list_files_recursively,
|
|
38
|
+
list_files_recursively_async,
|
|
37
39
|
silent_remove,
|
|
38
40
|
)
|
|
39
41
|
from supervisely.io.fs_cache import FileCache
|
|
@@ -2041,7 +2043,7 @@ class FileApi(ModuleApiBase):
|
|
|
2041
2043
|
# check_hash: bool = True, #TODO add with resumaple api
|
|
2042
2044
|
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
2043
2045
|
progress_cb_type: Literal["number", "size"] = "size",
|
|
2044
|
-
) ->
|
|
2046
|
+
) -> None:
|
|
2045
2047
|
"""
|
|
2046
2048
|
Upload file from local path to Team Files asynchronously.
|
|
2047
2049
|
|
|
@@ -2057,8 +2059,8 @@ class FileApi(ModuleApiBase):
|
|
|
2057
2059
|
:type progress_cb: tqdm or callable, optional
|
|
2058
2060
|
:param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "size".
|
|
2059
2061
|
:type progress_cb_type: Literal["number", "size"], optional
|
|
2060
|
-
:return:
|
|
2061
|
-
:rtype: :class:`
|
|
2062
|
+
:return: None
|
|
2063
|
+
:rtype: :class:`NoneType`
|
|
2062
2064
|
:Usage example:
|
|
2063
2065
|
|
|
2064
2066
|
.. code-block:: python
|
|
@@ -2087,17 +2089,30 @@ class FileApi(ModuleApiBase):
|
|
|
2087
2089
|
}
|
|
2088
2090
|
if semaphore is None:
|
|
2089
2091
|
semaphore = self._api.get_default_semaphore()
|
|
2092
|
+
logger.debug(f"Uploading with async to: {dst}. Semaphore: {semaphore}")
|
|
2090
2093
|
async with semaphore:
|
|
2091
2094
|
async with aiofiles.open(src, "rb") as fd:
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2095
|
+
|
|
2096
|
+
async def file_chunk_generator():
|
|
2097
|
+
while True:
|
|
2098
|
+
chunk = await fd.read(8 * 1024 * 1024)
|
|
2099
|
+
if not chunk:
|
|
2100
|
+
break
|
|
2101
|
+
if progress_cb is not None and progress_cb_type == "size":
|
|
2102
|
+
progress_cb(len(chunk))
|
|
2103
|
+
yield chunk
|
|
2104
|
+
|
|
2105
|
+
async for chunk, _ in self._api.stream_async(
|
|
2106
|
+
method=api_method,
|
|
2107
|
+
method_type="POST",
|
|
2108
|
+
data=file_chunk_generator(), # added as required, but not used inside
|
|
2109
|
+
headers=headers,
|
|
2110
|
+
content=file_chunk_generator(), # used instead of data inside stream_async
|
|
2111
|
+
params=json_body,
|
|
2112
|
+
):
|
|
2113
|
+
pass
|
|
2098
2114
|
if progress_cb is not None and progress_cb_type == "number":
|
|
2099
2115
|
progress_cb(1)
|
|
2100
|
-
return response
|
|
2101
2116
|
|
|
2102
2117
|
async def upload_bulk_async(
|
|
2103
2118
|
self,
|
|
@@ -2109,6 +2124,7 @@ class FileApi(ModuleApiBase):
|
|
|
2109
2124
|
# check_hash: bool = True, #TODO add with resumaple api
|
|
2110
2125
|
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
2111
2126
|
progress_cb_type: Literal["number", "size"] = "size",
|
|
2127
|
+
enable_fallback: Optional[bool] = True,
|
|
2112
2128
|
) -> None:
|
|
2113
2129
|
"""
|
|
2114
2130
|
Upload multiple files from local paths to Team Files asynchronously.
|
|
@@ -2125,6 +2141,8 @@ class FileApi(ModuleApiBase):
|
|
|
2125
2141
|
:type progress_cb: tqdm or callable, optional
|
|
2126
2142
|
:param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "size".
|
|
2127
2143
|
:type progress_cb_type: Literal["number", "size"], optional
|
|
2144
|
+
:param enable_fallback: If True, the method will fallback to synchronous upload if an error occurs.
|
|
2145
|
+
:type enable_fallback: bool, optional
|
|
2128
2146
|
:return: None
|
|
2129
2147
|
:rtype: :class:`NoneType`
|
|
2130
2148
|
:Usage example:
|
|
@@ -2153,19 +2171,134 @@ class FileApi(ModuleApiBase):
|
|
|
2153
2171
|
api.file.upload_bulk_async(8, paths_to_files, paths_to_save)
|
|
2154
2172
|
)
|
|
2155
2173
|
"""
|
|
2156
|
-
|
|
2157
|
-
semaphore
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2174
|
+
try:
|
|
2175
|
+
if semaphore is None:
|
|
2176
|
+
semaphore = self._api.get_default_semaphore()
|
|
2177
|
+
tasks = []
|
|
2178
|
+
for src, dst in zip(src_paths, dst_paths):
|
|
2179
|
+
task = asyncio.create_task(
|
|
2180
|
+
self.upload_async(
|
|
2181
|
+
team_id=team_id,
|
|
2182
|
+
src=src,
|
|
2183
|
+
dst=dst,
|
|
2184
|
+
semaphore=semaphore,
|
|
2185
|
+
# chunk_size=chunk_size, #TODO add with resumaple api
|
|
2186
|
+
# check_hash=check_hash, #TODO add with resumaple api
|
|
2187
|
+
progress_cb=progress_cb,
|
|
2188
|
+
progress_cb_type=progress_cb_type,
|
|
2189
|
+
)
|
|
2190
|
+
)
|
|
2191
|
+
tasks.append(task)
|
|
2192
|
+
for task in tasks:
|
|
2193
|
+
await task
|
|
2194
|
+
except Exception as e:
|
|
2195
|
+
if enable_fallback:
|
|
2196
|
+
logger.warning(
|
|
2197
|
+
f"Upload files bulk asynchronously failed. Fallback to synchronous upload.",
|
|
2198
|
+
exc_info=True,
|
|
2199
|
+
)
|
|
2200
|
+
if progress_cb is not None and progress_cb_type == "number":
|
|
2201
|
+
logger.warning(
|
|
2202
|
+
"Progress callback type 'number' is not supported for synchronous upload. "
|
|
2203
|
+
"Progress callback will be disabled."
|
|
2204
|
+
)
|
|
2205
|
+
progress_cb = None
|
|
2206
|
+
self.upload_bulk(
|
|
2207
|
+
team_id=team_id,
|
|
2208
|
+
src_paths=src_paths,
|
|
2209
|
+
dst_paths=dst_paths,
|
|
2210
|
+
progress_cb=progress_cb,
|
|
2211
|
+
)
|
|
2212
|
+
else:
|
|
2213
|
+
raise e
|
|
2214
|
+
|
|
2215
|
+
async def upload_directory_async(
|
|
2216
|
+
self,
|
|
2217
|
+
team_id: int,
|
|
2218
|
+
local_dir: str,
|
|
2219
|
+
remote_dir: str,
|
|
2220
|
+
change_name_if_conflict: Optional[bool] = True,
|
|
2221
|
+
progress_size_cb: Optional[Union[tqdm, Callable]] = None,
|
|
2222
|
+
replace_if_conflict: Optional[bool] = False,
|
|
2223
|
+
enable_fallback: Optional[bool] = True,
|
|
2224
|
+
) -> str:
|
|
2225
|
+
"""
|
|
2226
|
+
Upload Directory to Team Files from local path.
|
|
2227
|
+
Files are uploaded asynchronously.
|
|
2228
|
+
|
|
2229
|
+
:param team_id: Team ID in Supervisely.
|
|
2230
|
+
:type team_id: int
|
|
2231
|
+
:param local_dir: Path to local Directory.
|
|
2232
|
+
:type local_dir: str
|
|
2233
|
+
:param remote_dir: Path to Directory in Team Files.
|
|
2234
|
+
:type remote_dir: str
|
|
2235
|
+
:param change_name_if_conflict: Checks if given name already exists and adds suffix to the end of the name.
|
|
2236
|
+
:type change_name_if_conflict: bool, optional
|
|
2237
|
+
:param progress_size_cb: Function for tracking download progress.
|
|
2238
|
+
:type progress_size_cb: Progress, optional
|
|
2239
|
+
:param replace_if_conflict: If True, replace existing dir.
|
|
2240
|
+
:type replace_if_conflict: bool, optional
|
|
2241
|
+
:param enable_fallback: If True, the method will fallback to synchronous upload if an error occurs.
|
|
2242
|
+
:type enable_fallback: bool, optional
|
|
2243
|
+
:return: Path to Directory in Team Files
|
|
2244
|
+
:rtype: :class:`str`
|
|
2245
|
+
:Usage example:
|
|
2246
|
+
|
|
2247
|
+
.. code-block:: python
|
|
2248
|
+
|
|
2249
|
+
import supervisely as sly
|
|
2250
|
+
|
|
2251
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
2252
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
2253
|
+
api = sly.Api.from_env()
|
|
2254
|
+
|
|
2255
|
+
path_to_dir = "/My_App_Test/ds1"
|
|
2256
|
+
local_path = "/home/admin/Downloads/My_local_test"
|
|
2257
|
+
|
|
2258
|
+
api.file.upload_directory(9, local_path, path_to_dir)
|
|
2259
|
+
"""
|
|
2260
|
+
try:
|
|
2261
|
+
if not remote_dir.startswith("/"):
|
|
2262
|
+
remote_dir = "/" + remote_dir
|
|
2263
|
+
|
|
2264
|
+
if self.dir_exists(team_id, remote_dir):
|
|
2265
|
+
if change_name_if_conflict is True:
|
|
2266
|
+
res_remote_dir = self.get_free_dir_name(team_id, remote_dir)
|
|
2267
|
+
elif replace_if_conflict is True:
|
|
2268
|
+
res_remote_dir = remote_dir
|
|
2269
|
+
else:
|
|
2270
|
+
raise FileExistsError(
|
|
2271
|
+
f"Directory {remote_dir} already exists in your team (id={team_id})"
|
|
2272
|
+
)
|
|
2273
|
+
else:
|
|
2274
|
+
res_remote_dir = remote_dir
|
|
2275
|
+
|
|
2276
|
+
local_files = await list_files_recursively_async(local_dir)
|
|
2277
|
+
dir_prefix = local_dir.rstrip("/") + "/"
|
|
2278
|
+
remote_files = [
|
|
2279
|
+
res_remote_dir.rstrip("/") + "/" + file[len(dir_prefix) :] for file in local_files
|
|
2280
|
+
]
|
|
2281
|
+
|
|
2282
|
+
await self.upload_bulk_async(
|
|
2283
|
+
team_id=team_id,
|
|
2284
|
+
src_paths=local_files,
|
|
2285
|
+
dst_paths=remote_files,
|
|
2286
|
+
progress_cb=progress_size_cb,
|
|
2169
2287
|
)
|
|
2170
|
-
|
|
2171
|
-
|
|
2288
|
+
except Exception as e:
|
|
2289
|
+
if enable_fallback:
|
|
2290
|
+
logger.warning(
|
|
2291
|
+
f"Upload directory asynchronously failed. Fallback to synchronous upload.",
|
|
2292
|
+
exc_info=True,
|
|
2293
|
+
)
|
|
2294
|
+
res_remote_dir = self.upload_directory(
|
|
2295
|
+
team_id=team_id,
|
|
2296
|
+
local_dir=local_dir,
|
|
2297
|
+
remote_dir=res_remote_dir,
|
|
2298
|
+
change_name_if_conflict=change_name_if_conflict,
|
|
2299
|
+
progress_size_cb=progress_size_cb,
|
|
2300
|
+
replace_if_conflict=replace_if_conflict,
|
|
2301
|
+
)
|
|
2302
|
+
else:
|
|
2303
|
+
raise e
|
|
2304
|
+
return res_remote_dir
|
|
@@ -7,3 +7,4 @@ from supervisely.convert.pointcloud_episodes.lyft.lyft_converter import LyftEpis
|
|
|
7
7
|
from supervisely.convert.pointcloud_episodes.nuscenes_conv.nuscenes_converter import (
|
|
8
8
|
NuscenesEpisodesConverter,
|
|
9
9
|
)
|
|
10
|
+
from supervisely.convert.pointcloud_episodes.kitti_360.kitti_360_converter import KITTI360Converter
|
|
File without changes
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from supervisely import PointcloudEpisodeAnnotation, ProjectMeta, is_development, logger, ObjClass, ObjClassCollection
|
|
5
|
+
from supervisely.geometry.cuboid_3d import Cuboid3d
|
|
6
|
+
from supervisely.api.api import Api, ApiField
|
|
7
|
+
from supervisely.convert.base_converter import AvailablePointcloudEpisodesConverters
|
|
8
|
+
from supervisely.convert.pointcloud_episodes.kitti_360.kitti_360_helper import *
|
|
9
|
+
from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import PointcloudEpisodeConverter
|
|
10
|
+
from supervisely.io.fs import (
|
|
11
|
+
file_exists,
|
|
12
|
+
get_file_name,
|
|
13
|
+
get_file_name_with_ext,
|
|
14
|
+
list_files_recursively,
|
|
15
|
+
silent_remove,
|
|
16
|
+
)
|
|
17
|
+
from supervisely.pointcloud_annotation.pointcloud_episode_frame_collection import PointcloudEpisodeFrameCollection
|
|
18
|
+
from supervisely.pointcloud_annotation.pointcloud_episode_object_collection import PointcloudEpisodeObjectCollection
|
|
19
|
+
from supervisely.pointcloud_annotation.pointcloud_episode_object import PointcloudEpisodeObject
|
|
20
|
+
from supervisely.pointcloud_annotation.pointcloud_episode_frame import PointcloudEpisodeFrame
|
|
21
|
+
from supervisely.pointcloud_annotation.pointcloud_figure import PointcloudFigure
|
|
22
|
+
|
|
23
|
+
class KITTI360Converter(PointcloudEpisodeConverter):
|
|
24
|
+
|
|
25
|
+
class Item:
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
scene_name: str,
|
|
30
|
+
frame_paths: List[str],
|
|
31
|
+
ann_data: Annotation3D,
|
|
32
|
+
poses_path: str,
|
|
33
|
+
related_images: Optional[tuple] = None,
|
|
34
|
+
custom_data: Optional[dict] = None,
|
|
35
|
+
):
|
|
36
|
+
self._scene_name = scene_name
|
|
37
|
+
self._frame_paths = frame_paths
|
|
38
|
+
self._ann_data = ann_data
|
|
39
|
+
self._poses_path = poses_path
|
|
40
|
+
self._related_images = related_images or []
|
|
41
|
+
|
|
42
|
+
self._type = "point_cloud_episode"
|
|
43
|
+
self._custom_data = custom_data if custom_data is not None else {}
|
|
44
|
+
|
|
45
|
+
def __init__(self, *args, **kwargs):
|
|
46
|
+
self._calib_path = None
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
return AvailablePointcloudEpisodesConverters.KITTI360
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def key_file_ext(self) -> str:
|
|
54
|
+
return ".bin"
|
|
55
|
+
|
|
56
|
+
def validate_format(self) -> bool:
|
|
57
|
+
try:
|
|
58
|
+
import kitti360scripts
|
|
59
|
+
except ImportError:
|
|
60
|
+
logger.warn("Please run 'pip install kitti360Scripts' to import KITTI-360 data.")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
self._items = []
|
|
64
|
+
subdirs = os.listdir(self._input_data)
|
|
65
|
+
if len(subdirs) == 1:
|
|
66
|
+
self._input_data = os.path.join(self._input_data, subdirs[0])
|
|
67
|
+
|
|
68
|
+
# * Get calibration path
|
|
69
|
+
calib_dir = next(iter([(Path(path).parent).as_posix() for path in list_files_recursively(self._input_data, [".txt"], None, True) if Path(path).stem.startswith("calib")]), None)
|
|
70
|
+
if calib_dir is None:
|
|
71
|
+
return False
|
|
72
|
+
self._calib_path = calib_dir
|
|
73
|
+
|
|
74
|
+
# * Get pointcloud files paths
|
|
75
|
+
velodyne_files = list_files_recursively(self._input_data, [".bin"], None, True)
|
|
76
|
+
if len(velodyne_files) == 0:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# * Get annotation files paths and related images
|
|
80
|
+
boxes_ann_files = list_files_recursively(self._input_data, [".xml"], None, True)
|
|
81
|
+
if len(boxes_ann_files) == 0:
|
|
82
|
+
return False
|
|
83
|
+
rimage_files = list_files_recursively(self._input_data, [".png"], None, True)
|
|
84
|
+
|
|
85
|
+
kitti_anns = []
|
|
86
|
+
for ann_file in boxes_ann_files:
|
|
87
|
+
key_name = Path(ann_file).stem
|
|
88
|
+
|
|
89
|
+
# * Get pointcloud files
|
|
90
|
+
frame_paths = []
|
|
91
|
+
for path in velodyne_files:
|
|
92
|
+
if key_name in Path(path).parts:
|
|
93
|
+
frame_paths.append(path)
|
|
94
|
+
if len(frame_paths) == 0:
|
|
95
|
+
logger.warn("No frames found for name: %s", key_name)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# * Get related images
|
|
99
|
+
rimages = []
|
|
100
|
+
for rimage in rimage_files:
|
|
101
|
+
path = Path(rimage)
|
|
102
|
+
if key_name in path.parts:
|
|
103
|
+
cam_name = path.parts[-3]
|
|
104
|
+
rimages.append((cam_name, rimage))
|
|
105
|
+
|
|
106
|
+
# * Get poses
|
|
107
|
+
poses_filter = (
|
|
108
|
+
lambda x: x.endswith("cam0_to_world.txt") and key_name in Path(x).parts
|
|
109
|
+
)
|
|
110
|
+
poses_path = next(
|
|
111
|
+
path
|
|
112
|
+
for path in list_files_recursively(self._input_data, [".txt"], None, True)
|
|
113
|
+
if poses_filter(path)
|
|
114
|
+
)
|
|
115
|
+
if poses_path is None:
|
|
116
|
+
logger.warn("No poses found for name: %s", key_name)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# * Parse annotation
|
|
120
|
+
ann = Annotation3D(ann_file)
|
|
121
|
+
kitti_anns.append(ann)
|
|
122
|
+
|
|
123
|
+
self._items.append(
|
|
124
|
+
self.Item(key_name, frame_paths, ann, poses_path, rimages)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# * Get object class names for meta
|
|
128
|
+
obj_class_names = set()
|
|
129
|
+
for ann in kitti_anns:
|
|
130
|
+
for obj in ann.get_objects():
|
|
131
|
+
obj_class_names.add(obj.name)
|
|
132
|
+
obj_classes = [ObjClass(obj_class, Cuboid3d) for obj_class in obj_class_names]
|
|
133
|
+
self._meta = ProjectMeta(obj_classes=ObjClassCollection(obj_classes))
|
|
134
|
+
return self.items_count > 0
|
|
135
|
+
|
|
136
|
+
def to_supervisely(
|
|
137
|
+
self,
|
|
138
|
+
item,
|
|
139
|
+
meta: ProjectMeta,
|
|
140
|
+
renamed_classes: dict = {},
|
|
141
|
+
renamed_tags: dict = {},
|
|
142
|
+
static_transformations: StaticTransformations = None,
|
|
143
|
+
) -> PointcloudEpisodeAnnotation:
|
|
144
|
+
static_transformations.set_cam2world(item._poses_path)
|
|
145
|
+
|
|
146
|
+
frame_cnt = len(item._frame_paths)
|
|
147
|
+
objs, frames = [], []
|
|
148
|
+
|
|
149
|
+
frame_idx_to_figures = {idx: [] for idx in range(frame_cnt)}
|
|
150
|
+
for obj in item._ann_data.get_objects():
|
|
151
|
+
pcd_obj = PointcloudEpisodeObject(meta.get_obj_class(obj.name))
|
|
152
|
+
objs.append(pcd_obj)
|
|
153
|
+
|
|
154
|
+
for idx in range(frame_cnt):
|
|
155
|
+
if obj.start_frame <= idx <= obj.end_frame:
|
|
156
|
+
tr_matrix = static_transformations.world_to_velo_transformation(obj, idx)
|
|
157
|
+
geom = convert_kitti_cuboid_to_supervisely_geometry(tr_matrix)
|
|
158
|
+
frame_idx_to_figures[idx].append(PointcloudFigure(pcd_obj, geom, idx))
|
|
159
|
+
for idx, figures in frame_idx_to_figures.items():
|
|
160
|
+
frame = PointcloudEpisodeFrame(idx, figures)
|
|
161
|
+
frames.append(frame)
|
|
162
|
+
obj_collection = PointcloudEpisodeObjectCollection(objs)
|
|
163
|
+
frame_collection = PointcloudEpisodeFrameCollection(frames)
|
|
164
|
+
return PointcloudEpisodeAnnotation(
|
|
165
|
+
frame_cnt, objects=obj_collection, frames=frame_collection
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def upload_dataset(self, api: Api, dataset_id: int, batch_size: int = 1, log_progress=True):
|
|
169
|
+
meta, renamed_classes, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
|
|
170
|
+
|
|
171
|
+
dataset_info = api.dataset.get_info_by_id(dataset_id)
|
|
172
|
+
if log_progress:
|
|
173
|
+
progress, progress_cb = self.get_progress(sum([len(item._frame_paths) for item in self._items]), "Converting pointcloud episodes...")
|
|
174
|
+
else:
|
|
175
|
+
progress_cb = None
|
|
176
|
+
static_transformations = StaticTransformations(self._calib_path)
|
|
177
|
+
scene_ds = dataset_info
|
|
178
|
+
multiple_items = self.items_count > 1
|
|
179
|
+
for item in self._items:
|
|
180
|
+
scene_ds = api.dataset.create(dataset_info.project_id, item._scene_name, parent_id=dataset_id) if multiple_items else dataset_info
|
|
181
|
+
frame_to_pcd_ids = {}
|
|
182
|
+
for idx, frame_path in enumerate(item._frame_paths):
|
|
183
|
+
# * Convert pointcloud from ".bin" to ".pcd"
|
|
184
|
+
pcd_path = str(Path(frame_path).with_suffix(".pcd"))
|
|
185
|
+
if file_exists(pcd_path):
|
|
186
|
+
logger.warning(f"Overwriting file with path: {pcd_path}")
|
|
187
|
+
convert_bin_to_pcd(frame_path, pcd_path)
|
|
188
|
+
|
|
189
|
+
# * Upload pointcloud
|
|
190
|
+
pcd_name = get_file_name_with_ext(pcd_path)
|
|
191
|
+
info = api.pointcloud_episode.upload_path(scene_ds.id, pcd_name, pcd_path, {"frame": idx})
|
|
192
|
+
pcd_id = info.id
|
|
193
|
+
frame_to_pcd_ids[idx] = pcd_id
|
|
194
|
+
|
|
195
|
+
# * Clean up
|
|
196
|
+
silent_remove(pcd_path)
|
|
197
|
+
|
|
198
|
+
if log_progress:
|
|
199
|
+
progress_cb(1)
|
|
200
|
+
|
|
201
|
+
# * Upload photocontext
|
|
202
|
+
rimage_jsons = []
|
|
203
|
+
cam_names = []
|
|
204
|
+
hashes = api.pointcloud_episode.upload_related_images(
|
|
205
|
+
[rimage_path for _, rimage_path in item._related_images]
|
|
206
|
+
)
|
|
207
|
+
for (cam_name, rimage_path), img, pcd_id in zip(
|
|
208
|
+
item._related_images, hashes, list(frame_to_pcd_ids.values())
|
|
209
|
+
):
|
|
210
|
+
cam_num = int(cam_name[-1])
|
|
211
|
+
rimage_info = convert_calib_to_image_meta(
|
|
212
|
+
get_file_name(rimage_path), static_transformations, cam_num
|
|
213
|
+
)
|
|
214
|
+
image_json = {
|
|
215
|
+
ApiField.ENTITY_ID: pcd_id,
|
|
216
|
+
ApiField.NAME: cam_name,
|
|
217
|
+
ApiField.HASH: img,
|
|
218
|
+
ApiField.META: rimage_info[ApiField.META],
|
|
219
|
+
}
|
|
220
|
+
rimage_jsons.append(image_json)
|
|
221
|
+
cam_names.append(cam_name)
|
|
222
|
+
if rimage_jsons:
|
|
223
|
+
api.pointcloud_episode.add_related_images(rimage_jsons, cam_names)
|
|
224
|
+
|
|
225
|
+
# * Convert annotation and upload
|
|
226
|
+
try:
|
|
227
|
+
ann = self.to_supervisely(
|
|
228
|
+
item, meta, renamed_classes, renamed_tags, static_transformations
|
|
229
|
+
)
|
|
230
|
+
api.pointcloud_episode.annotation.append(scene_ds.id, ann, frame_to_pcd_ids)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(
|
|
233
|
+
f"Failed to upload annotation for scene: {scene_ds.name}. Error: {repr(e)}",
|
|
234
|
+
stack_info=False,
|
|
235
|
+
)
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
logger.info(f"Dataset ID:{scene_ds.id} has been successfully uploaded.")
|
|
239
|
+
|
|
240
|
+
if log_progress:
|
|
241
|
+
if is_development():
|
|
242
|
+
progress.close()
|