supervisely 6.73.368__py3-none-any.whl → 6.73.370__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/api/volume/volume_figure_api.py +1 -15
- supervisely/app/widgets/experiment_selector/experiment_selector.py +2 -2
- supervisely/convert/volume/nii/nii_volume_helper.py +4 -4
- supervisely/geometry/constants.py +2 -0
- supervisely/geometry/mask_3d.py +219 -45
- supervisely/nn/inference/inference.py +17 -8
- supervisely/nn/training/gui/gui.py +6 -3
- supervisely/nn/training/train_app.py +102 -14
- supervisely/volume/volume.py +59 -32
- supervisely/volume_annotation/volume_figure.py +3 -3
- {supervisely-6.73.368.dist-info → supervisely-6.73.370.dist-info}/METADATA +1 -1
- {supervisely-6.73.368.dist-info → supervisely-6.73.370.dist-info}/RECORD +16 -16
- {supervisely-6.73.368.dist-info → supervisely-6.73.370.dist-info}/LICENSE +0 -0
- {supervisely-6.73.368.dist-info → supervisely-6.73.370.dist-info}/WHEEL +0 -0
- {supervisely-6.73.368.dist-info → supervisely-6.73.370.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.368.dist-info → supervisely-6.73.370.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import tempfile
|
|
5
|
-
from collections import OrderedDict
|
|
6
5
|
from typing import Dict, List
|
|
7
6
|
from uuid import UUID
|
|
8
7
|
|
|
@@ -470,7 +469,7 @@ class VolumeFigureApi(FigureApi):
|
|
|
470
469
|
for figure in figures:
|
|
471
470
|
if figure.key() == key:
|
|
472
471
|
geometry_data = figure.geometry.data
|
|
473
|
-
header =
|
|
472
|
+
header = figure.geometry.create_header()
|
|
474
473
|
geometry_bytes = encode(geometry_data.astype(uint8), header)
|
|
475
474
|
self.upload_sf_geometries([key], {key: geometry_bytes}, key_id_map)
|
|
476
475
|
|
|
@@ -625,19 +624,6 @@ class VolumeFigureApi(FigureApi):
|
|
|
625
624
|
geometry = Mask3D.create_from_file(figure_path)
|
|
626
625
|
spatial_figure._set_3d_geometry(geometry)
|
|
627
626
|
|
|
628
|
-
def _create_header_for_geometry(self, geometry: Mask3D) -> OrderedDict:
|
|
629
|
-
"""
|
|
630
|
-
Create header for encoding Mask3D to NRRD bytes
|
|
631
|
-
"""
|
|
632
|
-
header = OrderedDict()
|
|
633
|
-
if geometry._space is not None:
|
|
634
|
-
header["space"] = geometry._space
|
|
635
|
-
if geometry._space_directions is not None:
|
|
636
|
-
header["space directions"] = geometry._space_directions
|
|
637
|
-
if geometry._space_origin is not None:
|
|
638
|
-
header["space origin"] = geometry._space_origin.to_json()["space_origin"]
|
|
639
|
-
return header
|
|
640
|
-
|
|
641
627
|
def download(
|
|
642
628
|
self, dataset_id: int, volume_ids: List[int] = None, skip_geometry: bool = False, **kwargs
|
|
643
629
|
) -> Dict[int, List[FigureInfo]]:
|
|
@@ -52,7 +52,7 @@ class ExperimentSelector(Widget):
|
|
|
52
52
|
self._experiment_info = experiment_info
|
|
53
53
|
|
|
54
54
|
task_id = experiment_info.task_id
|
|
55
|
-
if task_id == "debug-session":
|
|
55
|
+
if task_id == "debug-session" or task_id == -1:
|
|
56
56
|
pass
|
|
57
57
|
elif type(task_id) is str:
|
|
58
58
|
if task_id.isdigit():
|
|
@@ -392,7 +392,7 @@ class ExperimentSelector(Widget):
|
|
|
392
392
|
if result:
|
|
393
393
|
task_type, model_row = result
|
|
394
394
|
if task_type is not None and model_row is not None:
|
|
395
|
-
if model_row.task_id == "debug-session":
|
|
395
|
+
if model_row.task_id == "debug-session" or model_row.task_id == -1:
|
|
396
396
|
self.__debug_row = (task_type, model_row)
|
|
397
397
|
continue
|
|
398
398
|
table_rows[task_type].append(model_row)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from collections import defaultdict, namedtuple
|
|
3
|
+
from pathlib import Path
|
|
2
4
|
from typing import Generator
|
|
3
5
|
|
|
4
6
|
import nrrd
|
|
5
7
|
import numpy as np
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from collections import defaultdict, namedtuple
|
|
8
8
|
|
|
9
9
|
from supervisely import Api
|
|
10
10
|
from supervisely.collection.str_enum import StrEnum
|
|
@@ -107,13 +107,13 @@ def nifti_to_nrrd(nii_file_path: str, converted_dir: str) -> str:
|
|
|
107
107
|
def get_annotation_from_nii(path: str) -> Generator[Mask3D, None, None]:
|
|
108
108
|
"""Get annotation from NIfTI 3D volume file."""
|
|
109
109
|
|
|
110
|
-
data,
|
|
110
|
+
data, header = convert_3d_nifti_to_nrrd(path)
|
|
111
111
|
unique_classes = np.unique(data)
|
|
112
112
|
|
|
113
113
|
for class_id in unique_classes:
|
|
114
114
|
if class_id == 0:
|
|
115
115
|
continue
|
|
116
|
-
mask = Mask3D(data == class_id)
|
|
116
|
+
mask = Mask3D(data == class_id, volume_header=header)
|
|
117
117
|
yield mask, class_id
|
|
118
118
|
|
|
119
119
|
|
supervisely/geometry/mask_3d.py
CHANGED
|
@@ -6,9 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
import base64
|
|
7
7
|
import gzip
|
|
8
8
|
import tempfile
|
|
9
|
+
from collections import OrderedDict
|
|
9
10
|
from typing import Dict, List, Literal, Optional, Tuple, Union
|
|
10
11
|
|
|
11
|
-
import nrrd
|
|
12
12
|
import numpy as np
|
|
13
13
|
|
|
14
14
|
from supervisely import logger
|
|
@@ -22,6 +22,8 @@ from supervisely.geometry.constants import (
|
|
|
22
22
|
ID,
|
|
23
23
|
LABELER_LOGIN,
|
|
24
24
|
MASK_3D,
|
|
25
|
+
SPACE,
|
|
26
|
+
SPACE_DIRECTIONS,
|
|
25
27
|
SPACE_ORIGIN,
|
|
26
28
|
UPDATED_AT,
|
|
27
29
|
)
|
|
@@ -183,6 +185,10 @@ class Mask3D(Geometry):
|
|
|
183
185
|
:type updated_at: str, optional
|
|
184
186
|
:param created_at: Date and Time when Mask 3D was created. Date Format is the same as in "updated_at" parameter.
|
|
185
187
|
:type created_at: str, optional
|
|
188
|
+
:param volume_header: NRRD header dictionary. Optional.
|
|
189
|
+
:type volume_header: dict, optional
|
|
190
|
+
:param convert_to_ras: If True, converts the mask to RAS orientation. Default is True.
|
|
191
|
+
:type convert_to_ras: bool, optional
|
|
186
192
|
:raises: :class:`ValueError`, if data is not bool or no pixels set to True in data
|
|
187
193
|
:Usage example:
|
|
188
194
|
|
|
@@ -219,6 +225,8 @@ class Mask3D(Geometry):
|
|
|
219
225
|
labeler_login: Optional[str] = None,
|
|
220
226
|
updated_at: Optional[str] = None,
|
|
221
227
|
created_at: Optional[str] = None,
|
|
228
|
+
volume_header: Optional[Dict] = None,
|
|
229
|
+
convert_to_ras: bool = True,
|
|
222
230
|
):
|
|
223
231
|
super().__init__(
|
|
224
232
|
sly_id=sly_id,
|
|
@@ -253,6 +261,98 @@ class Mask3D(Geometry):
|
|
|
253
261
|
self._space = None
|
|
254
262
|
self._space_directions = None
|
|
255
263
|
|
|
264
|
+
if volume_header is not None:
|
|
265
|
+
self.set_volume_space_meta(volume_header)
|
|
266
|
+
if self.space is not None and self.space != "right-anterior-superior":
|
|
267
|
+
if convert_to_ras:
|
|
268
|
+
self.orient_ras()
|
|
269
|
+
else:
|
|
270
|
+
logger.debug(
|
|
271
|
+
"Mask3D is not in RAS orientation. It is recommended to use RAS orientation for 3D masks."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def space_origin(self) -> Optional[List[float]]:
|
|
276
|
+
"""
|
|
277
|
+
Get the space origin of the Mask3D as a list of floats.
|
|
278
|
+
|
|
279
|
+
:return: Space origin of the Mask3D.
|
|
280
|
+
:rtype: List[float] or None
|
|
281
|
+
"""
|
|
282
|
+
if self._space_origin is not None:
|
|
283
|
+
return [self._space_origin.x, self._space_origin.y, self._space_origin.z]
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
@space_origin.setter
|
|
287
|
+
def space_origin(self, value: Union[PointVolume, List[float], np.array]):
|
|
288
|
+
"""
|
|
289
|
+
Set the space origin of the Mask3D.
|
|
290
|
+
|
|
291
|
+
:param value: Space origin of the Mask3D. If provided as a list or array, it should contain 3 floats in the order [x, y, z].
|
|
292
|
+
:type value: :class:`PointVolume<PointVolume>` or List[float]
|
|
293
|
+
"""
|
|
294
|
+
if isinstance(value, PointVolume):
|
|
295
|
+
self._space_origin = value
|
|
296
|
+
elif isinstance(value, list) and len(value) == 3:
|
|
297
|
+
self._space_origin = PointVolume(x=value[0], y=value[1], z=value[2])
|
|
298
|
+
elif isinstance(value, np.ndarray) and value.shape == (3,):
|
|
299
|
+
self._space_origin = PointVolume(x=value[0], y=value[1], z=value[2])
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError("Space origin must be a PointVolume or a list of 3 floats.")
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def space(self) -> Optional[str]:
|
|
305
|
+
"""
|
|
306
|
+
Get the space of the Mask3D.
|
|
307
|
+
|
|
308
|
+
:return: Space of the Mask3D.
|
|
309
|
+
:rtype: :class:`str`
|
|
310
|
+
"""
|
|
311
|
+
return self._space
|
|
312
|
+
|
|
313
|
+
@space.setter
|
|
314
|
+
def space(self, value: str):
|
|
315
|
+
"""
|
|
316
|
+
Set the space of the Mask3D.
|
|
317
|
+
|
|
318
|
+
:param value: Space of the Mask3D.
|
|
319
|
+
:type value: str
|
|
320
|
+
"""
|
|
321
|
+
if not isinstance(value, str):
|
|
322
|
+
raise ValueError("Space must be a string.")
|
|
323
|
+
self._space = value
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def space_directions(self) -> Optional[List[List[float]]]:
|
|
327
|
+
"""
|
|
328
|
+
Get the space directions of the Mask3D.
|
|
329
|
+
|
|
330
|
+
:return: Space directions of the Mask3D.
|
|
331
|
+
:rtype: :class:`List[List[float]]`
|
|
332
|
+
"""
|
|
333
|
+
return self._space_directions
|
|
334
|
+
|
|
335
|
+
@space_directions.setter
|
|
336
|
+
def space_directions(self, value: Union[List[List[float]], np.ndarray]):
|
|
337
|
+
"""
|
|
338
|
+
Set the space directions of the Mask3D.
|
|
339
|
+
|
|
340
|
+
:param value: Space directions of the Mask3D. Should be a 3x3 array-like structure.
|
|
341
|
+
:type value: List[List[float]] or np.ndarray
|
|
342
|
+
"""
|
|
343
|
+
if isinstance(value, np.ndarray):
|
|
344
|
+
if value.shape != (3, 3):
|
|
345
|
+
raise ValueError("Space directions must be a 3x3 array.")
|
|
346
|
+
self._space_directions = value.tolist()
|
|
347
|
+
elif (
|
|
348
|
+
isinstance(value, list)
|
|
349
|
+
and len(value) == 3
|
|
350
|
+
and all(isinstance(row, (list, np.ndarray)) and len(row) == 3 for row in value)
|
|
351
|
+
):
|
|
352
|
+
self._space_directions = [list(row) for row in value]
|
|
353
|
+
else:
|
|
354
|
+
raise ValueError("Space directions must be a 3x3 array or list of lists.")
|
|
355
|
+
|
|
256
356
|
@staticmethod
|
|
257
357
|
def geometry_name():
|
|
258
358
|
"""Return geometry name"""
|
|
@@ -268,22 +368,8 @@ class Mask3D(Geometry):
|
|
|
268
368
|
:param file_path: Path to nrrd file with data
|
|
269
369
|
:type file_path: str
|
|
270
370
|
"""
|
|
271
|
-
|
|
272
|
-
figure.
|
|
273
|
-
try:
|
|
274
|
-
figure.geometry._space_origin = PointVolume(
|
|
275
|
-
x=mask3d_header["space origin"][0],
|
|
276
|
-
y=mask3d_header["space origin"][1],
|
|
277
|
-
z=mask3d_header["space origin"][2],
|
|
278
|
-
)
|
|
279
|
-
figure.geometry._space = mask3d_header["space"]
|
|
280
|
-
figure.geometry._space_directions = mask3d_header["space directions"]
|
|
281
|
-
except KeyError as e:
|
|
282
|
-
header_keys = ["'space'", "'space directions'", "'space origin'"]
|
|
283
|
-
if str(e) in header_keys:
|
|
284
|
-
logger.warning(
|
|
285
|
-
f"The Mask3D geometry for figure ID '{get_file_name(file_path)}' doesn't contain optional space attributes that have similar names to {', '.join(header_keys)}. To set the values for these attributes, you can use information from the Volume associated with this figure object."
|
|
286
|
-
)
|
|
371
|
+
mask3d = Mask3D.create_from_file(file_path)
|
|
372
|
+
figure._set_3d_geometry(mask3d)
|
|
287
373
|
path_without_filename = "/".join(file_path.split("/")[:-1])
|
|
288
374
|
remove_dir(path_without_filename)
|
|
289
375
|
|
|
@@ -295,22 +381,26 @@ class Mask3D(Geometry):
|
|
|
295
381
|
:param file_path: Path to nrrd file with data
|
|
296
382
|
:type file_path: str
|
|
297
383
|
"""
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
384
|
+
from supervisely.volume.volume import read_nrrd_serie_volume_np
|
|
385
|
+
|
|
386
|
+
mask3d_data, meta = read_nrrd_serie_volume_np(file_path)
|
|
387
|
+
direction = np.array(meta["directions"]).reshape(3, 3)
|
|
388
|
+
spacing = np.array(meta["spacing"])
|
|
389
|
+
space_directions = (direction.T * spacing[:, None]).tolist()
|
|
390
|
+
mask3d_header = {
|
|
391
|
+
"space": "right-anterior-superior",
|
|
392
|
+
"space directions": space_directions,
|
|
393
|
+
"space origin": meta.get("origin", None),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
geometry = cls(data=mask3d_data, volume_header=mask3d_header)
|
|
397
|
+
|
|
398
|
+
fields_to_check = ["space", "space_directions", "space_origin"]
|
|
399
|
+
if any([getattr(geometry, value) is None for value in fields_to_check]):
|
|
309
400
|
header_keys = ["'space'", "'space directions'", "'space origin'"]
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
)
|
|
401
|
+
logger.debug(
|
|
402
|
+
f"The Mask3D geometry created from the file '{file_path}' doesn't contain optional space attributes that have similar names to {', '.join(header_keys)}. To set the values for these attributes, you can use information from the Volume associated with this figure object."
|
|
403
|
+
)
|
|
314
404
|
return geometry
|
|
315
405
|
|
|
316
406
|
@classmethod
|
|
@@ -323,7 +413,7 @@ class Mask3D(Geometry):
|
|
|
323
413
|
:return: A Mask3D geometry object.
|
|
324
414
|
:rtype: Mask3D
|
|
325
415
|
"""
|
|
326
|
-
with tempfile.NamedTemporaryFile(delete=True) as temp_file:
|
|
416
|
+
with tempfile.NamedTemporaryFile(delete=True, suffix=".nrrd") as temp_file:
|
|
327
417
|
temp_file.write(geometry_bytes)
|
|
328
418
|
return cls.create_from_file(temp_file.name)
|
|
329
419
|
|
|
@@ -368,12 +458,14 @@ class Mask3D(Geometry):
|
|
|
368
458
|
GEOMETRY_TYPE: self.name(),
|
|
369
459
|
}
|
|
370
460
|
|
|
371
|
-
if self.
|
|
372
|
-
res[f"{self._impl_json_class_name()}"][f"{SPACE_ORIGIN}"] =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
461
|
+
if self.space_origin:
|
|
462
|
+
res[f"{self._impl_json_class_name()}"][f"{SPACE_ORIGIN}"] = self.space_origin
|
|
463
|
+
|
|
464
|
+
if self.space:
|
|
465
|
+
res[f"{self._impl_json_class_name()}"][f"{SPACE}"] = self.space
|
|
466
|
+
|
|
467
|
+
if self.space_directions:
|
|
468
|
+
res[f"{self._impl_json_class_name()}"][f"{SPACE_DIRECTIONS}"] = self.space_directions
|
|
377
469
|
|
|
378
470
|
self._add_creation_info(res)
|
|
379
471
|
return res
|
|
@@ -426,18 +518,30 @@ class Mask3D(Geometry):
|
|
|
426
518
|
created_at = json_data.get(CREATED_AT, None)
|
|
427
519
|
sly_id = json_data.get(ID, None)
|
|
428
520
|
class_id = json_data.get(CLASS_ID, None)
|
|
429
|
-
|
|
521
|
+
|
|
522
|
+
header = {}
|
|
523
|
+
|
|
524
|
+
space_origin = json_data[json_root_key].get(SPACE_ORIGIN, None)
|
|
525
|
+
if space_origin is not None:
|
|
526
|
+
header["space origin"] = space_origin
|
|
527
|
+
|
|
528
|
+
space = json_data[json_root_key].get(SPACE, None)
|
|
529
|
+
if space is not None:
|
|
530
|
+
header["space"] = space
|
|
531
|
+
|
|
532
|
+
space_directions = json_data[json_root_key].get(SPACE_DIRECTIONS, None)
|
|
533
|
+
if space_directions is not None:
|
|
534
|
+
header["space directions"] = space_directions
|
|
535
|
+
|
|
536
|
+
return cls(
|
|
430
537
|
data=data.astype(np.bool_),
|
|
431
538
|
sly_id=sly_id,
|
|
432
539
|
class_id=class_id,
|
|
433
540
|
labeler_login=labeler_login,
|
|
434
541
|
updated_at=updated_at,
|
|
435
542
|
created_at=created_at,
|
|
543
|
+
volume_header=header,
|
|
436
544
|
)
|
|
437
|
-
if SPACE_ORIGIN in json_data[json_root_key]:
|
|
438
|
-
x, y, z = json_data[json_root_key][SPACE_ORIGIN]
|
|
439
|
-
instance._space_origin = PointVolume(x=x, y=y, z=z)
|
|
440
|
-
return instance
|
|
441
545
|
|
|
442
546
|
@classmethod
|
|
443
547
|
def _impl_json_class_name(cls):
|
|
@@ -474,7 +578,7 @@ class Mask3D(Geometry):
|
|
|
474
578
|
path_for_mesh = f"meshes/{figure_id}.nrrd"
|
|
475
579
|
api.volume.figure.download_stl_meshes([figure_id], [path_for_mesh])
|
|
476
580
|
|
|
477
|
-
mask3d_data, _ =
|
|
581
|
+
mask3d_data, _ = sly.volume.volume.read_nrrd_serie_volume_np(path_for_mesh)
|
|
478
582
|
encoded_string = sly.Mask3D.data_2_base64(mask3d_data)
|
|
479
583
|
|
|
480
584
|
print(encoded_string)
|
|
@@ -619,3 +723,73 @@ class Mask3D(Geometry):
|
|
|
619
723
|
continue
|
|
620
724
|
geometries_dict[key] = geometry_bytes
|
|
621
725
|
return geometries_dict
|
|
726
|
+
|
|
727
|
+
def set_volume_space_meta(self, header: Dict):
|
|
728
|
+
"""
|
|
729
|
+
Set space, space directions, and space origin attributes from a NRRD header dictionary.
|
|
730
|
+
|
|
731
|
+
:param header: NRRD header dictionary.
|
|
732
|
+
:type header: dict
|
|
733
|
+
"""
|
|
734
|
+
if "space" in header:
|
|
735
|
+
self.space = header["space"]
|
|
736
|
+
if "space directions" in header:
|
|
737
|
+
self.space_directions = header["space directions"]
|
|
738
|
+
if "space origin" in header:
|
|
739
|
+
self.space_origin = PointVolume(
|
|
740
|
+
x=header["space origin"][0],
|
|
741
|
+
y=header["space origin"][1],
|
|
742
|
+
z=header["space origin"][2],
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def create_header(self) -> OrderedDict:
|
|
746
|
+
"""
|
|
747
|
+
Create header for encoding Mask3D to NRRD bytes
|
|
748
|
+
|
|
749
|
+
:return: Header for NRRD file
|
|
750
|
+
:rtype: OrderedDict
|
|
751
|
+
"""
|
|
752
|
+
header = OrderedDict()
|
|
753
|
+
if self.space is not None:
|
|
754
|
+
header["space"] = self.space
|
|
755
|
+
if self.space_directions is not None:
|
|
756
|
+
header["space directions"] = self.space_directions
|
|
757
|
+
if self.space_origin is not None:
|
|
758
|
+
header["space origin"] = self.space_origin
|
|
759
|
+
return header
|
|
760
|
+
|
|
761
|
+
def orient_ras(self) -> None:
|
|
762
|
+
"""
|
|
763
|
+
Transforms the mask data and updates spatial metadata (origin, directions, spacing)
|
|
764
|
+
to align with the RAS coordinate system using SimpleITK.
|
|
765
|
+
|
|
766
|
+
:rtype: None
|
|
767
|
+
"""
|
|
768
|
+
import SimpleITK as sitk
|
|
769
|
+
|
|
770
|
+
from supervisely.volume.volume import _sitk_image_orient_ras
|
|
771
|
+
|
|
772
|
+
sitk_volume = sitk.GetImageFromArray(self.data)
|
|
773
|
+
if self.space_origin is not None:
|
|
774
|
+
sitk_volume.SetOrigin(self.space_origin)
|
|
775
|
+
if self.space_directions is not None:
|
|
776
|
+
# Convert space directions to spacing and direction
|
|
777
|
+
space_directions = np.array(self.space_directions)
|
|
778
|
+
spacing = np.linalg.norm(space_directions, axis=1)
|
|
779
|
+
direction = space_directions / spacing[:, np.newaxis]
|
|
780
|
+
sitk_volume.SetSpacing(spacing)
|
|
781
|
+
sitk_volume.SetDirection(direction.flatten())
|
|
782
|
+
|
|
783
|
+
sitk_volume = _sitk_image_orient_ras(sitk_volume)
|
|
784
|
+
|
|
785
|
+
# Extract transformed data and update object
|
|
786
|
+
self.data = sitk.GetArrayFromImage(sitk_volume)
|
|
787
|
+
new_direction = np.array(sitk_volume.GetDirection()).reshape(3, 3)
|
|
788
|
+
new_spacing = np.array(sitk_volume.GetSpacing())
|
|
789
|
+
new_space_directions = (new_direction.T * new_spacing[:, None]).tolist()
|
|
790
|
+
new_header = {
|
|
791
|
+
"space": "right-anterior-superior",
|
|
792
|
+
"space directions": new_space_directions,
|
|
793
|
+
"space origin": sitk_volume.GetOrigin(),
|
|
794
|
+
}
|
|
795
|
+
self.set_volume_space_meta(new_header)
|
|
@@ -878,10 +878,16 @@ class Inference:
|
|
|
878
878
|
|
|
879
879
|
try:
|
|
880
880
|
if is_production():
|
|
881
|
-
|
|
881
|
+
without_workflow = deploy_params.get("without_workflow", False)
|
|
882
|
+
if without_workflow is False:
|
|
883
|
+
self._add_workflow_input(model_source, model_files, model_info)
|
|
882
884
|
except Exception as e:
|
|
883
885
|
logger.warning(f"Failed to add input to the workflow: {repr(e)}")
|
|
884
886
|
|
|
887
|
+
# remove is_benchmark from deploy_params
|
|
888
|
+
if "without_workflow" in deploy_params:
|
|
889
|
+
deploy_params.pop("without_workflow")
|
|
890
|
+
|
|
885
891
|
self._load_model(deploy_params)
|
|
886
892
|
if self._model_meta is None:
|
|
887
893
|
self._set_model_meta_from_classes()
|
|
@@ -3709,17 +3715,20 @@ class Inference:
|
|
|
3709
3715
|
)
|
|
3710
3716
|
|
|
3711
3717
|
app_name = sly_env.app_name()
|
|
3712
|
-
meta = WorkflowMeta(node_settings=WorkflowSettings(title=
|
|
3718
|
+
meta = WorkflowMeta(node_settings=WorkflowSettings(title=app_name))
|
|
3713
3719
|
|
|
3714
3720
|
logger.debug(
|
|
3715
3721
|
f"Workflow Input: Checkpoint URL - {checkpoint_url}, Checkpoint Name - {checkpoint_name}"
|
|
3716
3722
|
)
|
|
3717
|
-
if
|
|
3718
|
-
self.api.
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
+
if model_source == ModelSource.CUSTOM:
|
|
3724
|
+
if checkpoint_url and self.api.file.exists(sly_env.team_id(), checkpoint_url):
|
|
3725
|
+
# self.api.app.workflow.add_input_file(checkpoint_url, model_weight=True, meta=meta)
|
|
3726
|
+
remote_checkpoint_dir = os.path.dirname(checkpoint_url)
|
|
3727
|
+
self.api.app.workflow.add_input_folder(remote_checkpoint_dir, meta=meta)
|
|
3728
|
+
else:
|
|
3729
|
+
logger.debug(
|
|
3730
|
+
f"Checkpoint {checkpoint_url} not found in Team Files. Cannot set workflow input"
|
|
3731
|
+
)
|
|
3723
3732
|
|
|
3724
3733
|
|
|
3725
3734
|
def _exclude_duplicated_predictions(
|
|
@@ -242,7 +242,7 @@ class TrainGUI:
|
|
|
242
242
|
else:
|
|
243
243
|
self.task_id = sly_env.task_id(raise_not_found=False)
|
|
244
244
|
if self.task_id is None:
|
|
245
|
-
self.task_id =
|
|
245
|
+
self.task_id = -1
|
|
246
246
|
|
|
247
247
|
self.framework_name = framework_name
|
|
248
248
|
self.models = models
|
|
@@ -257,7 +257,7 @@ class TrainGUI:
|
|
|
257
257
|
self.project_info = self._api.project.get_info_by_id(self.project_id)
|
|
258
258
|
if self.project_info.type is None:
|
|
259
259
|
raise ValueError(f"Project with ID: '{self.project_id}' does not exist or was archived")
|
|
260
|
-
|
|
260
|
+
|
|
261
261
|
self.project_meta = ProjectMeta.from_json(self._api.project.get_meta(self.project_id))
|
|
262
262
|
|
|
263
263
|
if self.workspace_id is None:
|
|
@@ -350,7 +350,10 @@ class TrainGUI:
|
|
|
350
350
|
if model_name is None:
|
|
351
351
|
experiment_name = "Enter experiment name"
|
|
352
352
|
else:
|
|
353
|
-
|
|
353
|
+
if self.task_id == -1:
|
|
354
|
+
experiment_name = f"debug_{self.project_info.name}_{model_name}"
|
|
355
|
+
else:
|
|
356
|
+
experiment_name = f"{self.task_id}_{self.project_info.name}_{model_name}"
|
|
354
357
|
|
|
355
358
|
if experiment_name == self.training_process.get_experiment_name():
|
|
356
359
|
return
|
|
@@ -9,7 +9,7 @@ import shutil
|
|
|
9
9
|
import subprocess
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from os import getcwd, listdir, walk
|
|
12
|
-
from os.path import basename, exists, expanduser, isdir, isfile, join
|
|
12
|
+
from os.path import basename, dirname, exists, expanduser, isdir, isfile, join
|
|
13
13
|
from typing import Any, Dict, List, Literal, Optional, Union
|
|
14
14
|
from urllib.request import urlopen
|
|
15
15
|
|
|
@@ -128,7 +128,7 @@ class TrainApp:
|
|
|
128
128
|
self._app_name = "custom-app"
|
|
129
129
|
self.task_id = sly_env.task_id(raise_not_found=False)
|
|
130
130
|
if self.task_id is None:
|
|
131
|
-
self.task_id =
|
|
131
|
+
self.task_id = -1
|
|
132
132
|
logger.info("TrainApp is running in debug mode")
|
|
133
133
|
|
|
134
134
|
self.framework_name = framework_name
|
|
@@ -580,7 +580,7 @@ class TrainApp:
|
|
|
580
580
|
|
|
581
581
|
# Step 6. Upload artifacts
|
|
582
582
|
self._set_text_status("uploading")
|
|
583
|
-
remote_dir,
|
|
583
|
+
remote_dir, session_link_file_info = self._upload_artifacts()
|
|
584
584
|
|
|
585
585
|
# Step 7. [Optional] Run Model Benchmark
|
|
586
586
|
mb_eval_lnk_file_info, mb_eval_report = None, None
|
|
@@ -650,12 +650,35 @@ class TrainApp:
|
|
|
650
650
|
|
|
651
651
|
# Step 10. Set output widgets
|
|
652
652
|
self._set_text_status("reset")
|
|
653
|
-
self._set_training_output(
|
|
653
|
+
self._set_training_output(
|
|
654
|
+
experiment_info, remote_dir, session_link_file_info, mb_eval_report
|
|
655
|
+
)
|
|
654
656
|
self._set_ws_progress_status("completed")
|
|
655
657
|
|
|
656
658
|
# Step 11. Workflow output
|
|
657
659
|
if is_production():
|
|
658
|
-
self.
|
|
660
|
+
best_checkpoint_file_info = self._get_best_checkpoint_info(experiment_info, remote_dir)
|
|
661
|
+
self._workflow_output(
|
|
662
|
+
remote_dir, best_checkpoint_file_info, mb_eval_lnk_file_info, mb_eval_report_id
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def _get_best_checkpoint_info(self, experiment_info: dict, remote_dir: str) -> FileInfo:
|
|
666
|
+
"""
|
|
667
|
+
Returns the best checkpoint info.
|
|
668
|
+
|
|
669
|
+
:param experiment_info: Experiment info.
|
|
670
|
+
:type experiment_info: dict
|
|
671
|
+
:param remote_dir: Remote directory.
|
|
672
|
+
:type remote_dir: str
|
|
673
|
+
:return: Best checkpoint info.
|
|
674
|
+
:rtype: FileInfo
|
|
675
|
+
"""
|
|
676
|
+
best_checkpoint_name = experiment_info.get("best_checkpoint")
|
|
677
|
+
remote_best_checkpoint_path = join(remote_dir, "checkpoints", best_checkpoint_name)
|
|
678
|
+
best_checkpoint_file_info = self._api.file.get_info_by_path(
|
|
679
|
+
self.team_id, remote_best_checkpoint_path
|
|
680
|
+
)
|
|
681
|
+
return best_checkpoint_file_info
|
|
659
682
|
|
|
660
683
|
def register_inference_class(
|
|
661
684
|
self, inference_class: Inference, inference_settings: Union[str, dict] = None
|
|
@@ -686,6 +709,8 @@ class TrainApp:
|
|
|
686
709
|
"""
|
|
687
710
|
Returns the current state of the application.
|
|
688
711
|
|
|
712
|
+
:param experiment_info: Experiment info.
|
|
713
|
+
:type experiment_info: dict
|
|
689
714
|
:return: Application state.
|
|
690
715
|
:rtype: dict
|
|
691
716
|
"""
|
|
@@ -748,6 +773,27 @@ class TrainApp:
|
|
|
748
773
|
"""
|
|
749
774
|
self.gui.load_from_app_state(app_state)
|
|
750
775
|
|
|
776
|
+
def add_output_files(self, paths: List[str]) -> None:
|
|
777
|
+
"""
|
|
778
|
+
Copies files or directories to the output directory, which will be uploaded to the team files upon training completion.
|
|
779
|
+
If path is a file, it will be uploaded to the root artifacts directory.
|
|
780
|
+
If path is a directory, it will be uploded to the root artifacts directory with the same directory name and structure.
|
|
781
|
+
|
|
782
|
+
:param paths: List of paths to files or directories to be copied to the output directory.
|
|
783
|
+
:type paths: List[str]
|
|
784
|
+
:return: None
|
|
785
|
+
:rtype: None
|
|
786
|
+
"""
|
|
787
|
+
|
|
788
|
+
for path in paths:
|
|
789
|
+
if sly_fs.file_exists(path):
|
|
790
|
+
shutil.copyfile(path, join(self.output_dir, sly_fs.get_file_name_with_ext(path)))
|
|
791
|
+
elif sly_fs.dir_exists(path):
|
|
792
|
+
shutil.copytree(path, join(self.output_dir, basename(path)))
|
|
793
|
+
else:
|
|
794
|
+
logger.warning(f"Provided path: '{path}' does not exist. Skipping...")
|
|
795
|
+
continue
|
|
796
|
+
|
|
751
797
|
# Loaders
|
|
752
798
|
def _load_models(self, models: Union[str, List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
|
|
753
799
|
"""
|
|
@@ -1549,7 +1595,7 @@ class TrainApp:
|
|
|
1549
1595
|
logger.debug(f"Uploading '{local_path}' to Supervisely")
|
|
1550
1596
|
total_size = sly_fs.get_file_size(local_path)
|
|
1551
1597
|
with self.progress_bar_main(
|
|
1552
|
-
message=message, total=total_size, unit="
|
|
1598
|
+
message=message, total=total_size, unit="B", unit_scale=True, unit_divisor=1024
|
|
1553
1599
|
) as upload_artifacts_pbar:
|
|
1554
1600
|
self.progress_bar_main.show()
|
|
1555
1601
|
file_info = self._api.file.upload(
|
|
@@ -1774,8 +1820,9 @@ class TrainApp:
|
|
|
1774
1820
|
with self.progress_bar_main(
|
|
1775
1821
|
message="Uploading demo files to Team Files",
|
|
1776
1822
|
total=total_size,
|
|
1777
|
-
unit="
|
|
1823
|
+
unit="B",
|
|
1778
1824
|
unit_scale=True,
|
|
1825
|
+
unit_divisor=1024,
|
|
1779
1826
|
) as upload_artifacts_pbar:
|
|
1780
1827
|
self.progress_bar_main.show()
|
|
1781
1828
|
remote_dir = self._api.file.upload_directory_fast(
|
|
@@ -1863,7 +1910,7 @@ class TrainApp:
|
|
|
1863
1910
|
f"Uploading artifacts directory: '{self.output_dir}' to Supervisely Team Files directory '{remote_artifacts_dir}'"
|
|
1864
1911
|
)
|
|
1865
1912
|
# Clean debug directory if exists
|
|
1866
|
-
if task_id ==
|
|
1913
|
+
if task_id == -1:
|
|
1867
1914
|
if self._api.file.dir_exists(self.team_id, f"{remote_artifacts_dir}/", True):
|
|
1868
1915
|
with self.progress_bar_main(
|
|
1869
1916
|
message=f"[Debug] Cleaning train artifacts: '{remote_artifacts_dir}/'",
|
|
@@ -1888,8 +1935,9 @@ class TrainApp:
|
|
|
1888
1935
|
with self.progress_bar_main(
|
|
1889
1936
|
message="Uploading train artifacts to Team Files",
|
|
1890
1937
|
total=total_size,
|
|
1891
|
-
unit="
|
|
1938
|
+
unit="B",
|
|
1892
1939
|
unit_scale=True,
|
|
1940
|
+
unit_divisor=1024,
|
|
1893
1941
|
) as upload_artifacts_pbar:
|
|
1894
1942
|
self.progress_bar_main.show()
|
|
1895
1943
|
remote_dir = self._api.file.upload_directory_fast(
|
|
@@ -1907,6 +1955,7 @@ class TrainApp:
|
|
|
1907
1955
|
"state": {"slyFolder": f"{join(remote_dir, 'logs')}"}
|
|
1908
1956
|
}
|
|
1909
1957
|
self.gui.training_logs.tensorboard_offline_button.enable()
|
|
1958
|
+
|
|
1910
1959
|
return remote_dir, file_info
|
|
1911
1960
|
|
|
1912
1961
|
def _set_training_output(
|
|
@@ -2095,6 +2144,7 @@ class TrainApp:
|
|
|
2095
2144
|
"model_meta": model_meta.to_json(),
|
|
2096
2145
|
"task_type": task_type,
|
|
2097
2146
|
}
|
|
2147
|
+
self._benchmark_params["without_workflow"] = True
|
|
2098
2148
|
|
|
2099
2149
|
logger.info(f"Deploy parameters: {self._benchmark_params}")
|
|
2100
2150
|
|
|
@@ -2162,16 +2212,19 @@ class TrainApp:
|
|
|
2162
2212
|
raise ValueError(f"Task type: '{task_type}' is not supported for Model Benchmark")
|
|
2163
2213
|
|
|
2164
2214
|
if self._has_splits_selector:
|
|
2215
|
+
app_session_id = self.task_id
|
|
2216
|
+
if app_session_id == -1:
|
|
2217
|
+
app_session_id = None
|
|
2165
2218
|
if self.gui.train_val_splits_selector.get_split_method() == "Based on datasets":
|
|
2166
2219
|
train_info = {
|
|
2167
|
-
"app_session_id":
|
|
2220
|
+
"app_session_id": app_session_id,
|
|
2168
2221
|
"train_dataset_ids": train_dataset_ids,
|
|
2169
2222
|
"train_images_ids": None,
|
|
2170
2223
|
"images_count": len(self._train_split),
|
|
2171
2224
|
}
|
|
2172
2225
|
else:
|
|
2173
2226
|
train_info = {
|
|
2174
|
-
"app_session_id":
|
|
2227
|
+
"app_session_id": app_session_id,
|
|
2175
2228
|
"train_dataset_ids": None,
|
|
2176
2229
|
"train_images_ids": train_images_ids,
|
|
2177
2230
|
"images_count": len(self._train_split),
|
|
@@ -2223,6 +2276,25 @@ class TrainApp:
|
|
|
2223
2276
|
|
|
2224
2277
|
except Exception as e:
|
|
2225
2278
|
logger.error(f"Model benchmark failed. {repr(e)}", exc_info=True)
|
|
2279
|
+
pred_error_message = (
|
|
2280
|
+
"Not found any predictions. Please make sure that your model produces predictions."
|
|
2281
|
+
)
|
|
2282
|
+
if isinstance(e, ValueError) and str(e) == pred_error_message:
|
|
2283
|
+
self.gui.training_artifacts.model_benchmark_fail_text.set(
|
|
2284
|
+
"The Model Evaluation report cannot be generated: The model is not making predictions. "
|
|
2285
|
+
"This indicates that your model may not have trained successfully or is underfitted. "
|
|
2286
|
+
"You can try increasing the number of epochs or adjusting the hyperparameters more carefully.",
|
|
2287
|
+
"warning",
|
|
2288
|
+
)
|
|
2289
|
+
|
|
2290
|
+
lnk_file_info, report, report_id, eval_metrics, primary_metric_name = (
|
|
2291
|
+
None,
|
|
2292
|
+
None,
|
|
2293
|
+
None,
|
|
2294
|
+
{},
|
|
2295
|
+
None,
|
|
2296
|
+
)
|
|
2297
|
+
|
|
2226
2298
|
self._set_text_status("finalizing")
|
|
2227
2299
|
self.progress_bar_main.hide()
|
|
2228
2300
|
self.progress_bar_secondary.hide()
|
|
@@ -2288,6 +2360,14 @@ class TrainApp:
|
|
|
2288
2360
|
):
|
|
2289
2361
|
"""
|
|
2290
2362
|
Adds the output data to the workflow.
|
|
2363
|
+
|
|
2364
|
+
:param team_files_dir: Team files directory.
|
|
2365
|
+
:type team_files_dir: str
|
|
2366
|
+
:param file_info: FileInfo of the best checkpoint.
|
|
2367
|
+
:type file_info: FileInfo
|
|
2368
|
+
:param model_benchmark_report: FileInfo of the model benchmark report link (.lnk).
|
|
2369
|
+
:type model_benchmark_report: Optional[FileInfo]
|
|
2370
|
+
:param model_benchmark_report_id: Model benchmark report ID.
|
|
2291
2371
|
"""
|
|
2292
2372
|
try:
|
|
2293
2373
|
module_id = (
|
|
@@ -2310,7 +2390,7 @@ class TrainApp:
|
|
|
2310
2390
|
|
|
2311
2391
|
if file_info:
|
|
2312
2392
|
relation_settings = WorkflowSettings(
|
|
2313
|
-
title="
|
|
2393
|
+
title="Checkpoints",
|
|
2314
2394
|
icon="folder",
|
|
2315
2395
|
icon_color="#FFA500",
|
|
2316
2396
|
icon_bg_color="#FFE8BE",
|
|
@@ -2321,7 +2401,10 @@ class TrainApp:
|
|
|
2321
2401
|
relation_settings=relation_settings, node_settings=node_settings
|
|
2322
2402
|
)
|
|
2323
2403
|
logger.debug(f"Workflow Output: meta \n {meta}")
|
|
2324
|
-
self._api.app.workflow.add_output_file(file_info, model_weight=True, meta=meta)
|
|
2404
|
+
# self._api.app.workflow.add_output_file(file_info, model_weight=True, meta=meta)
|
|
2405
|
+
|
|
2406
|
+
remote_checkpoint_dir = dirname(file_info.path)
|
|
2407
|
+
self._api.app.workflow.add_output_folder(remote_checkpoint_dir, meta=meta)
|
|
2325
2408
|
else:
|
|
2326
2409
|
logger.debug(
|
|
2327
2410
|
f"File with checkpoints not found in Team Files. Cannot set workflow output."
|
|
@@ -2370,13 +2453,14 @@ class TrainApp:
|
|
|
2370
2453
|
logger.debug("Tensorboard server is already running")
|
|
2371
2454
|
return
|
|
2372
2455
|
self._register_routes()
|
|
2456
|
+
|
|
2373
2457
|
args = [
|
|
2374
2458
|
"tensorboard",
|
|
2375
2459
|
"--logdir",
|
|
2376
2460
|
self.log_dir,
|
|
2377
2461
|
"--host=localhost",
|
|
2378
2462
|
f"--port={self._tensorboard_port}",
|
|
2379
|
-
"--load_fast=
|
|
2463
|
+
"--load_fast=auto",
|
|
2380
2464
|
"--reload_multifile=true",
|
|
2381
2465
|
]
|
|
2382
2466
|
self._tensorboard_process = subprocess.Popen(args)
|
|
@@ -2522,6 +2606,9 @@ class TrainApp:
|
|
|
2522
2606
|
self._set_ws_progress_status("finalizing")
|
|
2523
2607
|
self._finalize(experiment_info)
|
|
2524
2608
|
self.gui.training_process.start_button.loading = False
|
|
2609
|
+
|
|
2610
|
+
# Shutdown the app after training is finished
|
|
2611
|
+
self.app.shutdown()
|
|
2525
2612
|
except Exception as e:
|
|
2526
2613
|
message = f"Error occurred during finalizing and uploading training artifacts. {check_logs_text}"
|
|
2527
2614
|
self._show_error(message, e)
|
|
@@ -2706,6 +2793,7 @@ class TrainApp:
|
|
|
2706
2793
|
total=size,
|
|
2707
2794
|
unit="B",
|
|
2708
2795
|
unit_scale=True,
|
|
2796
|
+
unit_divisor=1024,
|
|
2709
2797
|
) as export_upload_main_pbar:
|
|
2710
2798
|
logger.debug(f"Uploading {len(export_weights)} export weights of size {size} bytes")
|
|
2711
2799
|
logger.debug(f"Destination paths: {file_dest_paths}")
|
supervisely/volume/volume.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
from typing import List, Tuple, Union
|
|
6
|
+
from typing import List, Optional, Tuple, Union
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pydicom
|
|
@@ -15,6 +15,7 @@ import supervisely.volume.nrrd_encoder as nrrd_encoder
|
|
|
15
15
|
from supervisely import logger
|
|
16
16
|
from supervisely.geometry.mask_3d import Mask3D
|
|
17
17
|
from supervisely.io.fs import get_file_ext, get_file_name, list_files_recursively
|
|
18
|
+
from supervisely.volume.stl_converter import matrix_from_nrrd_header
|
|
18
19
|
|
|
19
20
|
# Do NOT use directly for extension validation. Use is_valid_ext() / has_valid_ext() below instead.
|
|
20
21
|
ALLOWED_VOLUME_EXTENSIONS = [".nrrd", ".dcm"]
|
|
@@ -455,7 +456,7 @@ def inspect_dicom_series(root_dir: str, logging: bool = True) -> dict:
|
|
|
455
456
|
return found_series
|
|
456
457
|
|
|
457
458
|
|
|
458
|
-
def _sitk_image_orient_ras(sitk_volume):
|
|
459
|
+
def _sitk_image_orient_ras(sitk_volume: sitk.Image) -> sitk.Image:
|
|
459
460
|
import SimpleITK as sitk
|
|
460
461
|
|
|
461
462
|
if sitk_volume.GetDimension() == 4 and sitk_volume.GetSize()[3] == 1:
|
|
@@ -704,7 +705,7 @@ def read_nrrd_serie_volume(path: str) -> Tuple[sitk.Image, dict]:
|
|
|
704
705
|
"""
|
|
705
706
|
Read NRRD volume with given path.
|
|
706
707
|
|
|
707
|
-
:param path:
|
|
708
|
+
:param path: Path to NRRD volume files.
|
|
708
709
|
:type path: List[str]
|
|
709
710
|
:return: Volume data in SimpleITK.Image format and dictionary with metadata.
|
|
710
711
|
:rtype: Tuple[SimpleITK.Image, dict]
|
|
@@ -741,12 +742,12 @@ def read_nrrd_serie_volume(path: str) -> Tuple[sitk.Image, dict]:
|
|
|
741
742
|
return sitk_volume, meta
|
|
742
743
|
|
|
743
744
|
|
|
744
|
-
def read_nrrd_serie_volume_np(paths:
|
|
745
|
+
def read_nrrd_serie_volume_np(paths: str) -> Tuple[np.ndarray, dict]:
|
|
745
746
|
"""
|
|
746
747
|
Read NRRD volume with given path.
|
|
747
748
|
|
|
748
|
-
:param
|
|
749
|
-
:type
|
|
749
|
+
:param paths: Path to NRRD volume file.
|
|
750
|
+
:type paths: str
|
|
750
751
|
:return: Volume data in NumPy array format and dictionary with metadata.
|
|
751
752
|
:rtype: Tuple[np.ndarray, dict]
|
|
752
753
|
:Usage example:
|
|
@@ -868,17 +869,18 @@ def is_nifti_file(path: str) -> bool:
|
|
|
868
869
|
|
|
869
870
|
def convert_3d_geometry_to_mesh(
|
|
870
871
|
geometry: Mask3D,
|
|
871
|
-
spacing: tuple =
|
|
872
|
+
spacing: tuple = (1.0, 1.0, 1.0),
|
|
872
873
|
level: float = 0.5,
|
|
873
874
|
apply_decimation: bool = False,
|
|
874
875
|
decimation_fraction: float = 0.5,
|
|
876
|
+
volume_meta: Optional[dict] = None,
|
|
875
877
|
) -> Trimesh:
|
|
876
878
|
"""
|
|
877
879
|
Converts a 3D geometry (Mask3D) to a Trimesh mesh.
|
|
878
880
|
|
|
879
881
|
:param geometry: The 3D geometry to convert.
|
|
880
882
|
:type geometry: supervisely.geometry.mask_3d.Mask3D
|
|
881
|
-
:param spacing: Voxel spacing in (x, y, z).
|
|
883
|
+
:param spacing: Voxel spacing in (x, y, z).
|
|
882
884
|
:type spacing: tuple
|
|
883
885
|
:param level: Isosurface value for marching cubes. Default is 0.5.
|
|
884
886
|
:type level: float
|
|
@@ -886,6 +888,8 @@ def convert_3d_geometry_to_mesh(
|
|
|
886
888
|
:type apply_decimation: bool
|
|
887
889
|
:param decimation_fraction: Fraction of faces to keep if decimation is applied. Default is 0.5.
|
|
888
890
|
:type decimation_fraction: float
|
|
891
|
+
:param volume_meta: Metadata of the volume. Used for mesh alignment if geometry lacks specific fields. Default is None.
|
|
892
|
+
:type volume_meta: dict, optional
|
|
889
893
|
:return: The resulting Trimesh mesh.
|
|
890
894
|
:rtype: trimesh.Trimesh
|
|
891
895
|
|
|
@@ -893,37 +897,40 @@ def convert_3d_geometry_to_mesh(
|
|
|
893
897
|
|
|
894
898
|
.. code-block:: python
|
|
895
899
|
|
|
900
|
+
volume_header = nrrd.read_header("path/to/volume.nrrd")
|
|
896
901
|
mask3d = Mask3D.create_from_file("path/to/mask3d")
|
|
897
|
-
mesh = convert_3d_geometry_to_mesh(mask3d, spacing=(1.0, 1.0, 1.0), level=0.7, apply_decimation=True)
|
|
902
|
+
mesh = convert_3d_geometry_to_mesh(mask3d, spacing=(1.0, 1.0, 1.0), level=0.7, apply_decimation=True, volume_meta=volume_header)
|
|
898
903
|
"""
|
|
899
904
|
from skimage import measure
|
|
900
905
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
except Exception as e:
|
|
909
|
-
logger.warning(
|
|
910
|
-
"Failed to get spacing from geometry meta. Using (1.0, 1.0, 1.0).", exc_info=1
|
|
911
|
-
)
|
|
912
|
-
spacing = (1.0, 1.0, 1.0)
|
|
913
|
-
|
|
914
|
-
# marching_cubes expects (z, y, x) order
|
|
915
|
-
verts, faces, normals, _ = measure.marching_cubes(
|
|
916
|
-
mask.astype(np.float32), level=level, spacing=spacing
|
|
917
|
-
)
|
|
906
|
+
if volume_meta is None:
|
|
907
|
+
volume_meta = {}
|
|
908
|
+
|
|
909
|
+
space_directions = geometry.space_directions or volume_meta.get("space directions")
|
|
910
|
+
space_origin = geometry.space_origin or volume_meta.get("space origin")
|
|
911
|
+
|
|
912
|
+
verts, faces, normals, _ = measure.marching_cubes(geometry.data, level=level, spacing=spacing)
|
|
918
913
|
mesh = Trimesh(vertices=verts, faces=faces, vertex_normals=normals, process=False)
|
|
919
914
|
|
|
920
915
|
if apply_decimation and 0 < decimation_fraction < 1:
|
|
921
916
|
mesh = mesh.simplify_quadric_decimation(int(len(mesh.faces) * decimation_fraction))
|
|
922
917
|
|
|
918
|
+
if space_directions is not None and space_origin is not None:
|
|
919
|
+
header = {
|
|
920
|
+
"space directions": space_directions,
|
|
921
|
+
"space origin": space_origin,
|
|
922
|
+
}
|
|
923
|
+
align_mesh_to_volume(mesh, header)
|
|
924
|
+
|
|
925
|
+
# flip x and y axes to match initial mask orientation
|
|
926
|
+
mesh.apply_transform(np.diag([-1, -1, 1, 1]))
|
|
927
|
+
|
|
928
|
+
mesh.fix_normals()
|
|
929
|
+
|
|
923
930
|
return mesh
|
|
924
931
|
|
|
925
932
|
|
|
926
|
-
def export_3d_as_mesh(geometry: Mask3D, output_path: str, kwargs
|
|
933
|
+
def export_3d_as_mesh(geometry: Mask3D, output_path: str, **kwargs):
|
|
927
934
|
"""
|
|
928
935
|
Exports the 3D mesh representation of the object to a file in either STL or OBJ format.
|
|
929
936
|
|
|
@@ -936,7 +943,7 @@ def export_3d_as_mesh(geometry: Mask3D, output_path: str, kwargs=None):
|
|
|
936
943
|
- level (float): Isosurface value for marching cubes. Default is 0.5.
|
|
937
944
|
- apply_decimation (bool): Whether to simplify the mesh. Default is False.
|
|
938
945
|
- decimation_fraction (float): Fraction of faces to keep if decimation is applied. Default is 0.5.
|
|
939
|
-
|
|
946
|
+
- volume_meta (dict): Metadata of the volume. Used for mesh alignment if geometry lacks specific fields. Default is None.
|
|
940
947
|
:return: None
|
|
941
948
|
|
|
942
949
|
:Usage example:
|
|
@@ -946,14 +953,34 @@ def export_3d_as_mesh(geometry: Mask3D, output_path: str, kwargs=None):
|
|
|
946
953
|
mask3d_path = "path/to/mask3d"
|
|
947
954
|
mask3d = Mask3D.create_from_file(mask3d_path)
|
|
948
955
|
|
|
949
|
-
mask3d.export_3d_as_mesh(mask3d, "output.stl",
|
|
956
|
+
mask3d.export_3d_as_mesh(mask3d, "output.stl", spacing=(1.0, 1.0, 1.0), level=0.7, apply_decimation=True)
|
|
950
957
|
"""
|
|
951
958
|
|
|
952
|
-
if kwargs is None:
|
|
953
|
-
kwargs = {}
|
|
954
|
-
|
|
955
959
|
if get_file_ext(output_path).lower() not in [".stl", ".obj"]:
|
|
956
960
|
raise ValueError('File extension must be either ".stl" or ".obj"')
|
|
957
961
|
|
|
958
962
|
mesh = convert_3d_geometry_to_mesh(geometry, **kwargs)
|
|
959
963
|
mesh.export(output_path)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def align_mesh_to_volume(mesh: Trimesh, volume_header: dict) -> None:
|
|
967
|
+
"""
|
|
968
|
+
Transforms the given mesh in-place using spatial information from an NRRD header.
|
|
969
|
+
The mesh will be tranformed to match the coordinate system defined in the header.
|
|
970
|
+
|
|
971
|
+
:param mesh: The mesh object to be transformed. The transformation is applied in-place.
|
|
972
|
+
:type mesh: Trimesh
|
|
973
|
+
:param volume_header: The NRRD header containing spatial metadata, including "space directions",
|
|
974
|
+
"space origin", and "space". Field "space" should be in the format of
|
|
975
|
+
"right-anterior-superior", "left-anterior-superior", etc.
|
|
976
|
+
:type volume_header: dict
|
|
977
|
+
:returns: None
|
|
978
|
+
:rtype: None
|
|
979
|
+
"""
|
|
980
|
+
from supervisely.geometry.constants import SPACE_ORIGIN
|
|
981
|
+
from supervisely.geometry.mask_3d import PointVolume
|
|
982
|
+
|
|
983
|
+
if isinstance(volume_header["space origin"], PointVolume):
|
|
984
|
+
volume_header["space origin"] = volume_header["space origin"].to_json()[SPACE_ORIGIN]
|
|
985
|
+
transform_mat = matrix_from_nrrd_header(volume_header)
|
|
986
|
+
mesh.apply_transform(transform_mat)
|
|
@@ -675,6 +675,6 @@ class VolumeFigure(VideoFigure):
|
|
|
675
675
|
)
|
|
676
676
|
|
|
677
677
|
self.geometry.data = new_geometry.data
|
|
678
|
-
self.geometry.
|
|
679
|
-
self.geometry.
|
|
680
|
-
self.geometry.
|
|
678
|
+
self.geometry.space = new_geometry.space
|
|
679
|
+
self.geometry.space_origin = new_geometry.space_origin
|
|
680
|
+
self.geometry.space_directions = new_geometry.space_directions
|
|
@@ -77,7 +77,7 @@ supervisely/api/video/video_tag_api.py,sha256=wPe1HeJyg9kV1z2UJq6BEte5sKBoPJ2UGA
|
|
|
77
77
|
supervisely/api/volume/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
78
|
supervisely/api/volume/volume_annotation_api.py,sha256=NOHpLeqHLCeRs1KlXWoG91vtIXdUVTO69wh1ws0VmOQ,22246
|
|
79
79
|
supervisely/api/volume/volume_api.py,sha256=rz_yaBbbTkVeAHmF449zPI8Va_YpDHfHYjXgjGAjMJg,55390
|
|
80
|
-
supervisely/api/volume/volume_figure_api.py,sha256=
|
|
80
|
+
supervisely/api/volume/volume_figure_api.py,sha256=upjIdiiQgOJ6och0KUg0rQo-q-PIipL5RX2V3fOBPvI,25437
|
|
81
81
|
supervisely/api/volume/volume_object_api.py,sha256=F7pLV2MTlBlyN6fEKdxBSUatIMGWSuu8bWj3Hvcageo,2139
|
|
82
82
|
supervisely/api/volume/volume_tag_api.py,sha256=yNGgXz44QBSW2VGlNDOVLqLXnH8Q2fFrxDFb_girYXA,3639
|
|
83
83
|
supervisely/app/__init__.py,sha256=4yW79U_xvo7vjg6-vRhjtt0bO8MxMSx2PD8dMamS9Q8,633
|
|
@@ -255,7 +255,7 @@ supervisely/app/widgets/empty/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
255
255
|
supervisely/app/widgets/empty/empty.py,sha256=fCr8I7CQ2XLo59bl2txjDrblOGiu0TzUcM-Pq6s7gKY,1285
|
|
256
256
|
supervisely/app/widgets/empty/template.html,sha256=aDBKkin5aLuqByzNN517-rTYCGIg5SPKgnysYMPYjv8,40
|
|
257
257
|
supervisely/app/widgets/experiment_selector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
258
|
-
supervisely/app/widgets/experiment_selector/experiment_selector.py,sha256=
|
|
258
|
+
supervisely/app/widgets/experiment_selector/experiment_selector.py,sha256=MButdiR6j6bpvJRI9iYtO5UjQu_Dc4ABzcrPsy2YcRg,19933
|
|
259
259
|
supervisely/app/widgets/experiment_selector/style.css,sha256=-zPPXHnJvatYj_xVVAb7T8uoSsUTyhm5xCKWkkFQ78E,548
|
|
260
260
|
supervisely/app/widgets/experiment_selector/template.html,sha256=k7f_Xl6nDUXXwu6IY_RblYni5TbZRRxCBduY5O_SyFs,2908
|
|
261
261
|
supervisely/app/widgets/fast_table/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -671,7 +671,7 @@ supervisely/convert/volume/dicom/dicom_helper.py,sha256=OrKlyt1hA5BOXKhE1LF1WxBI
|
|
|
671
671
|
supervisely/convert/volume/nii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
672
672
|
supervisely/convert/volume/nii/nii_planes_volume_converter.py,sha256=dXoBA8AYUOEjLpV2cZJ5n1HDq4_gNhnD__NVsgfc_Qc,14551
|
|
673
673
|
supervisely/convert/volume/nii/nii_volume_converter.py,sha256=BAOKX96-bp6WfTFLrCQNrXk2YhKqIFSU5LJ-auKiAfc,8514
|
|
674
|
-
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=
|
|
674
|
+
supervisely/convert/volume/nii/nii_volume_helper.py,sha256=_FepXNu1RIFuzBOv0aAtrG-J2xN3uyyEMcq6pUD9fsk,14159
|
|
675
675
|
supervisely/convert/volume/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
676
676
|
supervisely/convert/volume/sly/sly_volume_converter.py,sha256=XmSuxnRqxchG87b244f3h0UHvOt6IkajMquL1drWlCM,5595
|
|
677
677
|
supervisely/convert/volume/sly/sly_volume_helper.py,sha256=gUY0GW3zDMlO2y-zQQG36uoXMrKkKz4-ErM1CDxFCxE,5620
|
|
@@ -687,7 +687,7 @@ supervisely/geometry/any_geometry.py,sha256=BOZBsuMYgtkA7cOKp-URjzV9zQkpHuvfp2QP
|
|
|
687
687
|
supervisely/geometry/bitmap.py,sha256=-tyIXCfgvNn3c7jHs18aq693JR5xdvhfNf6Vmf4952g,21869
|
|
688
688
|
supervisely/geometry/bitmap_base.py,sha256=lNamVL3gZ355oYlIEPc0yC84k1bxuCbUVI0ouaZ_Q4k,13814
|
|
689
689
|
supervisely/geometry/closed_surface_mesh.py,sha256=3ZplCm3Q2bhPcxNmtv2U1UfdezRkC3_BxjwH4yl7wrs,1558
|
|
690
|
-
supervisely/geometry/constants.py,sha256=
|
|
690
|
+
supervisely/geometry/constants.py,sha256=6lXpwTTFuswuH9WXMy4akHKshQ5C6fgQhdY-XCdVIMA,842
|
|
691
691
|
supervisely/geometry/conversions.py,sha256=ZY6xWYFWaDA5KDJkcIBBP8LAmMfZwxMeVFfYUYEM6fw,1170
|
|
692
692
|
supervisely/geometry/cuboid.py,sha256=GVHeUrVgfjUjE3PorV_vtge6_thDvvUYI5-9_HZjfWs,21077
|
|
693
693
|
supervisely/geometry/cuboid_2d.py,sha256=enQ-7ZVix5SqC7ZEwxgC0Kvmz9J_wXL7NH3m02snNvc,13444
|
|
@@ -697,7 +697,7 @@ supervisely/geometry/graph.py,sha256=kSShcGU4kZgwAbvTrqGzC55qha0nI7M5luiMZSbNx_4
|
|
|
697
697
|
supervisely/geometry/helpers.py,sha256=2gdYMFWTAr836gVXcp-lkDQs9tdaV0ou33kj3mzJBQA,5132
|
|
698
698
|
supervisely/geometry/image_rotator.py,sha256=wrU8cXEUfuNcmPms2myUV4BpZqz_2oDArsEUFeiTpxs,6888
|
|
699
699
|
supervisely/geometry/main_tests.py,sha256=K3Olsz9igHDW2IfIA5JOpjoE8bZ3ex2PXvVR2ZCDrHU,27199
|
|
700
|
-
supervisely/geometry/mask_3d.py,sha256=
|
|
700
|
+
supervisely/geometry/mask_3d.py,sha256=gaac4wUoG-qmpVcttgfAh2WhS3VUWjcdNqw5V-Aa5GA,26720
|
|
701
701
|
supervisely/geometry/multichannel_bitmap.py,sha256=dL0igkOCVZiIZ9LDU7srFLA50XGo4doE-B5_E1uboXM,4968
|
|
702
702
|
supervisely/geometry/point.py,sha256=7ed_Ipd-Ab8ZeqJF5ft0kP9pKVb2iWXCxPuRhMuweMc,13228
|
|
703
703
|
supervisely/geometry/point_3d.py,sha256=0ico0aV4fuKNBVrysDjUy1Cx1S9CEzBlEVE3AsbVd0E,1669
|
|
@@ -888,7 +888,7 @@ supervisely/nn/benchmark/visualization/widgets/table/__init__.py,sha256=47DEQpj8
|
|
|
888
888
|
supervisely/nn/benchmark/visualization/widgets/table/table.py,sha256=atmDnF1Af6qLQBUjLhK18RMDKAYlxnsuVHMSEa5a-e8,4319
|
|
889
889
|
supervisely/nn/inference/__init__.py,sha256=QFukX2ip-U7263aEPCF_UCFwj6EujbMnsgrXp5Bbt8I,1623
|
|
890
890
|
supervisely/nn/inference/cache.py,sha256=rc_CRlCuTCzLDtcl1paTJib7ALTer0ge9o32WtoUMkY,34795
|
|
891
|
-
supervisely/nn/inference/inference.py,sha256=
|
|
891
|
+
supervisely/nn/inference/inference.py,sha256=gq0yvMiFZ2FfJvvEmw7PRHa0GikCwRX9S2CE_0fuGX4,177798
|
|
892
892
|
supervisely/nn/inference/inference_request.py,sha256=y6yw0vbaRRcEBS27nq3y0sL6Gmq2qLA_Bm0GrnJGegE,14267
|
|
893
893
|
supervisely/nn/inference/session.py,sha256=dIg2F-OBl68pUzcmtmcI0YQIp1WWNnrJTVMjwFN91Q4,35824
|
|
894
894
|
supervisely/nn/inference/uploader.py,sha256=21a9coOimCHhEqAbV-llZWcp12847DEMoQp3N16bpK0,5425
|
|
@@ -994,10 +994,10 @@ supervisely/nn/tracking/__init__.py,sha256=Ld1ed7ZZQZPkhX-5Xr-UbHZx5zLCm2-tInHnP
|
|
|
994
994
|
supervisely/nn/tracking/boxmot.py,sha256=H9cQjYGL9nX_TLrfKDChhljTIiE9lffcgbwWCf_4PJU,4277
|
|
995
995
|
supervisely/nn/tracking/tracking.py,sha256=WNrNm02B1pspA3d_AmzSJ-54RZTqWV2NZiC7FHe88bo,857
|
|
996
996
|
supervisely/nn/training/__init__.py,sha256=gY4PCykJ-42MWKsqb9kl-skemKa8yB6t_fb5kzqR66U,111
|
|
997
|
-
supervisely/nn/training/train_app.py,sha256=
|
|
997
|
+
supervisely/nn/training/train_app.py,sha256=D2Fuy1SzoHTqeMWrdLLVLqeZN5Eu6M_CzU85y78na6I,116077
|
|
998
998
|
supervisely/nn/training/gui/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
|
999
999
|
supervisely/nn/training/gui/classes_selector.py,sha256=Bpp-RFDQqcZ0kLJmS6ZnExkdscWwRusvF4vbWjEsKlQ,3926
|
|
1000
|
-
supervisely/nn/training/gui/gui.py,sha256=
|
|
1000
|
+
supervisely/nn/training/gui/gui.py,sha256=Z68uMPNkOyb70rpxfVDfJuGSzcoOhrqqDog8PABF2JQ,43312
|
|
1001
1001
|
supervisely/nn/training/gui/hyperparameters_selector.py,sha256=5dUCYAx4E0HBLguj2B_s2nWeGGCWzv6vJeT0XvDJO3M,7746
|
|
1002
1002
|
supervisely/nn/training/gui/input_selector.py,sha256=rmirJzpdxuYONI6y5_cvMdGWBJ--T20YTsISghATHu4,2510
|
|
1003
1003
|
supervisely/nn/training/gui/model_selector.py,sha256=I6KRKyylpwUEC3CApEnzDKkWe5xqju0Az3D0Eg32Jdc,5352
|
|
@@ -1075,13 +1075,13 @@ supervisely/volume/__init__.py,sha256=EBZBY_5mzabXzMUQh5akusIGd16XnX9n8J0jIi_JmW
|
|
|
1075
1075
|
supervisely/volume/nrrd_encoder.py,sha256=1lqwwyqxEvctw1ysQ70x4xPSV1uy1g5YcH5CURwL7-c,4084
|
|
1076
1076
|
supervisely/volume/nrrd_loader.py,sha256=_yqahKcqSRxunHZ5LtnUWIRA7UvIhPKOhAUwYijSGY4,9065
|
|
1077
1077
|
supervisely/volume/stl_converter.py,sha256=WIMQgHO_u4JT58QdcMXcb_euF1BFhM7D52IVX_0QTxE,6285
|
|
1078
|
-
supervisely/volume/volume.py,sha256=
|
|
1078
|
+
supervisely/volume/volume.py,sha256=jDu_p1zPQxCojjtdJlVVTxfuKgVCYmMSY13Xz99k7pA,30765
|
|
1079
1079
|
supervisely/volume_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1080
1080
|
supervisely/volume_annotation/constants.py,sha256=BdFIh56fy7vzLIjt0gH8xP01EIU-qgQIwbSHVUcABCU,569
|
|
1081
1081
|
supervisely/volume_annotation/plane.py,sha256=wyezAcc8tLp38O44CwWY0wjdQxf3VjRdFLWooCrk-Nw,16301
|
|
1082
1082
|
supervisely/volume_annotation/slice.py,sha256=9m3jtUYz4PYKV3rgbeh2ofDebkyg4TomNbkC6BwZ0lA,4635
|
|
1083
1083
|
supervisely/volume_annotation/volume_annotation.py,sha256=pGu6n8_5JkFpir4HTVRf302gGD2EqJ96Gh4M0_236Qg,32047
|
|
1084
|
-
supervisely/volume_annotation/volume_figure.py,sha256=
|
|
1084
|
+
supervisely/volume_annotation/volume_figure.py,sha256=3iFyknF8TlLCMBwgg8gJ26wnNTDTRaw8uEJFVkJGh78,25331
|
|
1085
1085
|
supervisely/volume_annotation/volume_object.py,sha256=rWzOnycoSJ4-CvFgDOP_rPortU4CdcYR26txe5wJHNo,3577
|
|
1086
1086
|
supervisely/volume_annotation/volume_object_collection.py,sha256=Tc4AovntgoFj5hpTLBv7pCQ3eL0BjorOVpOh2nAE_tA,5706
|
|
1087
1087
|
supervisely/volume_annotation/volume_tag.py,sha256=MEk1ky7X8zWe2JgV-j8jXt14e8yu2g1kScU26n9lOMk,9494
|
|
@@ -1097,9 +1097,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
1097
1097
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
1098
1098
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
1099
1099
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1100
|
-
supervisely-6.73.
|
|
1101
|
-
supervisely-6.73.
|
|
1102
|
-
supervisely-6.73.
|
|
1103
|
-
supervisely-6.73.
|
|
1104
|
-
supervisely-6.73.
|
|
1105
|
-
supervisely-6.73.
|
|
1100
|
+
supervisely-6.73.370.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1101
|
+
supervisely-6.73.370.dist-info/METADATA,sha256=fcZ2RfmecBVHdsVjrjwfDZ4Uv2z21M0Y-jJYbnaiFRE,35154
|
|
1102
|
+
supervisely-6.73.370.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
1103
|
+
supervisely-6.73.370.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1104
|
+
supervisely-6.73.370.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1105
|
+
supervisely-6.73.370.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|