supervisely 6.73.438__py3-none-any.whl → 6.73.513__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/__init__.py +137 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +14 -11
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +14 -11
- supervisely/api/api.py +59 -38
- supervisely/api/app_api.py +11 -2
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +52 -4
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +72 -1
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +9 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +24 -4
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +916 -222
- supervisely/nn/inference/inference_request.py +55 -10
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +10 -1
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/model/prediction.py +2 -1
- supervisely/nn/model/prediction_session.py +26 -14
- supervisely/nn/prediction_dto.py +19 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/utils.py +4 -5
- supervisely/nn/tracker/visualize.py +93 -93
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +46 -31
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- supervisely_lib/__init__.py +6 -1
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from typing import Callable, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from supervisely import ObjClass, ObjClassCollection
|
|
4
|
+
from supervisely.app import DataJson, StateJson
|
|
5
|
+
from supervisely.app.widgets import Text, Widget
|
|
6
|
+
from supervisely.geometry.alpha_mask import AlphaMask
|
|
7
|
+
from supervisely.geometry.any_geometry import AnyGeometry
|
|
8
|
+
from supervisely.geometry.bitmap import Bitmap
|
|
9
|
+
from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
|
|
10
|
+
from supervisely.geometry.cuboid_2d import Cuboid2d
|
|
11
|
+
from supervisely.geometry.cuboid_3d import Cuboid3d
|
|
12
|
+
from supervisely.geometry.graph import GraphNodes
|
|
13
|
+
from supervisely.geometry.mask_3d import Mask3D
|
|
14
|
+
from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
|
|
15
|
+
from supervisely.geometry.oriented_bbox import OrientedBBox
|
|
16
|
+
from supervisely.geometry.point import Point
|
|
17
|
+
from supervisely.geometry.point_3d import Point3d
|
|
18
|
+
from supervisely.geometry.pointcloud import Pointcloud
|
|
19
|
+
from supervisely.geometry.polygon import Polygon
|
|
20
|
+
from supervisely.geometry.polyline import Polyline
|
|
21
|
+
from supervisely.geometry.rectangle import Rectangle
|
|
22
|
+
from supervisely.imaging.color import generate_rgb
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from typing import Literal
|
|
26
|
+
except ImportError:
|
|
27
|
+
from typing_extensions import Literal
|
|
28
|
+
|
|
29
|
+
type_to_shape_text = {
|
|
30
|
+
AnyGeometry: "any shape",
|
|
31
|
+
Rectangle: "rectangle",
|
|
32
|
+
Polygon: "polygon",
|
|
33
|
+
AlphaMask: "alpha mask",
|
|
34
|
+
Bitmap: "bitmap (mask)",
|
|
35
|
+
Polyline: "polyline",
|
|
36
|
+
Point: "point",
|
|
37
|
+
Cuboid2d: "cuboid 2d",
|
|
38
|
+
Cuboid3d: "cuboid 3d",
|
|
39
|
+
Pointcloud: "pointcloud",
|
|
40
|
+
MultichannelBitmap: "n-channel mask",
|
|
41
|
+
Point3d: "point 3d",
|
|
42
|
+
GraphNodes: "keypoints",
|
|
43
|
+
ClosedSurfaceMesh: "volume (3d mask)",
|
|
44
|
+
Mask3D: "3d mask",
|
|
45
|
+
OrientedBBox: "oriented bbox",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
shape_text_to_type = {v: k for k, v in type_to_shape_text.items()}
|
|
49
|
+
|
|
50
|
+
# Geometry types available for creating new classes (excluding GraphNodes)
|
|
51
|
+
available_geometry_types = [
|
|
52
|
+
{"value": "rectangle", "label": "Rectangle"},
|
|
53
|
+
{"value": "polygon", "label": "Polygon"},
|
|
54
|
+
{"value": "bitmap (mask)", "label": "Bitmap (mask)"},
|
|
55
|
+
{"value": "polyline", "label": "Polyline"},
|
|
56
|
+
{"value": "point", "label": "Point"},
|
|
57
|
+
{"value": "any shape", "label": "Any shape"},
|
|
58
|
+
{"value": "oriented bbox", "label": "Oriented Bounding Box"},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SelectClass(Widget):
|
|
63
|
+
"""
|
|
64
|
+
SelectClass is a compact dropdown widget for selecting object classes with an option to create
|
|
65
|
+
new classes on the fly.
|
|
66
|
+
|
|
67
|
+
:param classes: Initial list of ObjClass instances
|
|
68
|
+
:type classes: Optional[Union[List[ObjClass], ObjClassCollection]]
|
|
69
|
+
:param filterable: Enable search/filter functionality in dropdown
|
|
70
|
+
:type filterable: Optional[bool]
|
|
71
|
+
:param placeholder: Placeholder text when no class is selected
|
|
72
|
+
:type placeholder: Optional[str]
|
|
73
|
+
:param show_add_new_class: Show "Add new class" option at the end of the list
|
|
74
|
+
:type show_add_new_class: Optional[bool]
|
|
75
|
+
:param size: Size of the select dropdown
|
|
76
|
+
:type size: Optional[Literal["large", "small", "mini"]]
|
|
77
|
+
:param multiple: Enable multiple selection
|
|
78
|
+
:type multiple: bool
|
|
79
|
+
:param widget_id: Unique widget identifier
|
|
80
|
+
:type widget_id: Optional[str]
|
|
81
|
+
|
|
82
|
+
:Usage example:
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
|
|
86
|
+
import supervisely as sly
|
|
87
|
+
from supervisely.app.widgets import SelectClass
|
|
88
|
+
|
|
89
|
+
# Create some initial classes
|
|
90
|
+
class_car = sly.ObjClass('car', sly.Rectangle, color=[255, 0, 0])
|
|
91
|
+
class_person = sly.ObjClass('person', sly.Polygon, color=[0, 255, 0])
|
|
92
|
+
|
|
93
|
+
# Create SelectClass widget
|
|
94
|
+
select_class = SelectClass(
|
|
95
|
+
classes=[class_car, class_person],
|
|
96
|
+
filterable=True,
|
|
97
|
+
show_add_new_class=True
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Handle selection changes
|
|
101
|
+
@select_class.value_changed
|
|
102
|
+
def on_class_selected(class_name):
|
|
103
|
+
print(f"Selected class: {class_name}")
|
|
104
|
+
selected_class = select_class.get_selected_class()
|
|
105
|
+
print(f"Class object: {selected_class}")
|
|
106
|
+
|
|
107
|
+
# Handle new class creation
|
|
108
|
+
@select_class.class_created
|
|
109
|
+
def on_class_created(new_class: sly.ObjClass):
|
|
110
|
+
print(f"New class created: {new_class.name}")
|
|
111
|
+
# Optionally update your project meta or perform other actions
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
class Routes:
|
|
115
|
+
VALUE_CHANGED = "value_changed"
|
|
116
|
+
CLASS_CREATED = "class_created_cb"
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
classes: Optional[Union[List[ObjClass], ObjClassCollection]] = [],
|
|
121
|
+
filterable: Optional[bool] = True,
|
|
122
|
+
placeholder: Optional[str] = "Select class",
|
|
123
|
+
show_add_new_class: Optional[bool] = True,
|
|
124
|
+
size: Optional[Literal["large", "small", "mini"]] = None,
|
|
125
|
+
multiple: bool = False,
|
|
126
|
+
widget_id: Optional[str] = None,
|
|
127
|
+
):
|
|
128
|
+
# Convert to list for internal use to allow mutations when adding new classes
|
|
129
|
+
if isinstance(classes, ObjClassCollection):
|
|
130
|
+
self._classes = list(classes)
|
|
131
|
+
else:
|
|
132
|
+
self._classes = list(classes) if classes else []
|
|
133
|
+
|
|
134
|
+
self._filterable = filterable
|
|
135
|
+
self._placeholder = placeholder
|
|
136
|
+
self._show_add_new_class = show_add_new_class
|
|
137
|
+
self._size = size
|
|
138
|
+
self._multiple = multiple
|
|
139
|
+
|
|
140
|
+
self._changes_handled = False
|
|
141
|
+
self._class_created_callback = None
|
|
142
|
+
|
|
143
|
+
# Store error message widget
|
|
144
|
+
self._error_message = Text("", status="error", font_size=13)
|
|
145
|
+
|
|
146
|
+
# Initialize parent Widget
|
|
147
|
+
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
148
|
+
|
|
149
|
+
# Register class_created route if show_add_new_class is enabled
|
|
150
|
+
if self._show_add_new_class:
|
|
151
|
+
self._register_class_created_route()
|
|
152
|
+
|
|
153
|
+
def get_json_data(self):
|
|
154
|
+
"""Build JSON data for the widget."""
|
|
155
|
+
# Build items list with class info
|
|
156
|
+
items = []
|
|
157
|
+
for cls in self._classes:
|
|
158
|
+
shape_text = type_to_shape_text.get(cls.geometry_type, "")
|
|
159
|
+
items.append(
|
|
160
|
+
{
|
|
161
|
+
"value": cls.name,
|
|
162
|
+
"label": cls.name,
|
|
163
|
+
"color": cls.color,
|
|
164
|
+
"geometryType": shape_text.upper() if shape_text else "",
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"items": items,
|
|
170
|
+
"placeholder": self._placeholder,
|
|
171
|
+
"filterable": self._filterable,
|
|
172
|
+
"multiple": self._multiple,
|
|
173
|
+
"size": self._size,
|
|
174
|
+
"showAddNewClass": self._show_add_new_class,
|
|
175
|
+
"availableGeometryTypes": available_geometry_types,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def get_json_state(self):
|
|
179
|
+
"""Build JSON state for the widget."""
|
|
180
|
+
return {
|
|
181
|
+
"value": self._classes[0].name if self._classes else None,
|
|
182
|
+
"createClassDialog": {
|
|
183
|
+
"visible": False,
|
|
184
|
+
"className": "",
|
|
185
|
+
"geometryType": "rectangle",
|
|
186
|
+
"showError": False,
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def get_value(self) -> Union[str, List[str], None]:
|
|
191
|
+
"""Get the currently selected class name(s)."""
|
|
192
|
+
return StateJson()[self.widget_id]["value"]
|
|
193
|
+
|
|
194
|
+
def get_selected_class(self) -> Union[ObjClass, List[ObjClass], None]:
|
|
195
|
+
"""Get the currently selected ObjClass object(s)."""
|
|
196
|
+
value = self.get_value()
|
|
197
|
+
if value is None:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
if self._multiple:
|
|
201
|
+
if not isinstance(value, list):
|
|
202
|
+
return []
|
|
203
|
+
result = []
|
|
204
|
+
for class_name in value:
|
|
205
|
+
for cls in self._classes:
|
|
206
|
+
if cls.name == class_name:
|
|
207
|
+
result.append(cls)
|
|
208
|
+
break
|
|
209
|
+
return result
|
|
210
|
+
else:
|
|
211
|
+
for cls in self._classes:
|
|
212
|
+
if cls.name == value:
|
|
213
|
+
return cls
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
def set_value(self, class_name: Union[str, List[str]]):
|
|
217
|
+
"""Set the selected class by name."""
|
|
218
|
+
StateJson()[self.widget_id]["value"] = class_name
|
|
219
|
+
StateJson().send_changes()
|
|
220
|
+
|
|
221
|
+
def get_all_classes(self) -> List[ObjClass]:
|
|
222
|
+
"""Get all available classes."""
|
|
223
|
+
return self._classes.copy()
|
|
224
|
+
|
|
225
|
+
def set(self, classes: Union[List[ObjClass], ObjClassCollection]):
|
|
226
|
+
"""Update the list of available classes."""
|
|
227
|
+
# Convert to list for internal use
|
|
228
|
+
if isinstance(classes, ObjClassCollection):
|
|
229
|
+
self._classes = list(classes)
|
|
230
|
+
else:
|
|
231
|
+
self._classes = list(classes) if classes else []
|
|
232
|
+
|
|
233
|
+
# Update data
|
|
234
|
+
DataJson()[self.widget_id] = self.get_json_data()
|
|
235
|
+
DataJson().send_changes()
|
|
236
|
+
|
|
237
|
+
# Reset value if current selection is not in new classes
|
|
238
|
+
current_value = StateJson()[self.widget_id]["value"]
|
|
239
|
+
if current_value:
|
|
240
|
+
if self._multiple:
|
|
241
|
+
if isinstance(current_value, list):
|
|
242
|
+
# Keep only valid selections
|
|
243
|
+
valid = [
|
|
244
|
+
v for v in current_value if any(cls.name == v for cls in self._classes)
|
|
245
|
+
]
|
|
246
|
+
if valid != current_value:
|
|
247
|
+
StateJson()[self.widget_id]["value"] = valid
|
|
248
|
+
StateJson().send_changes()
|
|
249
|
+
else:
|
|
250
|
+
if not any(cls.name == current_value for cls in self._classes):
|
|
251
|
+
StateJson()[self.widget_id]["value"] = (
|
|
252
|
+
self._classes[0].name if self._classes else None
|
|
253
|
+
)
|
|
254
|
+
StateJson().send_changes()
|
|
255
|
+
|
|
256
|
+
def _show_error(self, message: str):
|
|
257
|
+
"""Show error message in the create class dialog."""
|
|
258
|
+
self._error_message.text = message
|
|
259
|
+
StateJson()[self.widget_id]["createClassDialog"]["showError"] = True
|
|
260
|
+
StateJson().send_changes()
|
|
261
|
+
|
|
262
|
+
def _hide_dialog(self):
|
|
263
|
+
"""Hide the create class dialog and reset its state."""
|
|
264
|
+
state_obj = StateJson()[self.widget_id]["createClassDialog"]
|
|
265
|
+
state_obj["visible"] = False
|
|
266
|
+
state_obj["className"] = ""
|
|
267
|
+
state_obj["showError"] = False
|
|
268
|
+
StateJson().send_changes()
|
|
269
|
+
|
|
270
|
+
def _add_new_class(self, new_class: ObjClass):
|
|
271
|
+
"""Add a new class to the widget and update the UI."""
|
|
272
|
+
# Add to classes list
|
|
273
|
+
self._classes.append(new_class)
|
|
274
|
+
|
|
275
|
+
# Update data
|
|
276
|
+
DataJson()[self.widget_id] = self.get_json_data()
|
|
277
|
+
DataJson().send_changes()
|
|
278
|
+
|
|
279
|
+
# Set the new class as selected
|
|
280
|
+
if self._multiple:
|
|
281
|
+
current = StateJson()[self.widget_id]["value"]
|
|
282
|
+
if isinstance(current, list):
|
|
283
|
+
current.append(new_class.name)
|
|
284
|
+
else:
|
|
285
|
+
StateJson()[self.widget_id]["value"] = [new_class.name]
|
|
286
|
+
else:
|
|
287
|
+
StateJson()[self.widget_id]["value"] = new_class.name
|
|
288
|
+
StateJson().send_changes()
|
|
289
|
+
|
|
290
|
+
def value_changed(self, func: Callable[[Union[ObjClass, List[ObjClass]]], None]):
|
|
291
|
+
"""
|
|
292
|
+
Decorator to handle value change event.
|
|
293
|
+
The decorated function receives the selected ObjClass (or list of ObjClass if multiple=True).
|
|
294
|
+
|
|
295
|
+
:param func: Function to be called when selection changes
|
|
296
|
+
:type func: Callable[[Union[ObjClass, List[ObjClass]]], None]
|
|
297
|
+
"""
|
|
298
|
+
route_path = self.get_route_path(SelectClass.Routes.VALUE_CHANGED)
|
|
299
|
+
server = self._sly_app.get_server()
|
|
300
|
+
self._changes_handled = True
|
|
301
|
+
|
|
302
|
+
@server.post(route_path)
|
|
303
|
+
def _value_changed():
|
|
304
|
+
selected = self.get_selected_class()
|
|
305
|
+
if selected is not None:
|
|
306
|
+
func(selected)
|
|
307
|
+
|
|
308
|
+
return _value_changed
|
|
309
|
+
|
|
310
|
+
def _register_class_created_route(self):
|
|
311
|
+
"""Register the class_created route."""
|
|
312
|
+
route_path = self.get_route_path(SelectClass.Routes.CLASS_CREATED)
|
|
313
|
+
server = self._sly_app.get_server()
|
|
314
|
+
|
|
315
|
+
@server.post(route_path)
|
|
316
|
+
def _class_created():
|
|
317
|
+
state = StateJson()[self.widget_id]["createClassDialog"]
|
|
318
|
+
class_name = state["className"].strip()
|
|
319
|
+
geometry_type_str = state["geometryType"]
|
|
320
|
+
|
|
321
|
+
# Validate class name
|
|
322
|
+
if not class_name:
|
|
323
|
+
self._show_error("Class name cannot be empty")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
# Check if class with this name already exists
|
|
327
|
+
if any(cls.name == class_name for cls in self._classes):
|
|
328
|
+
self._show_error(f"Class '{class_name}' already exists")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
# Get geometry type from string
|
|
332
|
+
geometry_type = shape_text_to_type.get(geometry_type_str)
|
|
333
|
+
if geometry_type is None:
|
|
334
|
+
self._show_error("Invalid geometry type")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Generate color for the new class
|
|
338
|
+
existing_colors = [cls.color for cls in self._classes]
|
|
339
|
+
new_color = generate_rgb(existing_colors)
|
|
340
|
+
|
|
341
|
+
# Create new class
|
|
342
|
+
new_class = ObjClass(name=class_name, geometry_type=geometry_type, color=new_color)
|
|
343
|
+
|
|
344
|
+
# Add to widget
|
|
345
|
+
self._add_new_class(new_class)
|
|
346
|
+
|
|
347
|
+
# Hide dialog
|
|
348
|
+
self._hide_dialog()
|
|
349
|
+
|
|
350
|
+
# Call user's callback if set
|
|
351
|
+
if self._class_created_callback:
|
|
352
|
+
self._class_created_callback(new_class)
|
|
353
|
+
|
|
354
|
+
def class_created(self, func: Callable[[ObjClass], None]):
|
|
355
|
+
"""
|
|
356
|
+
Decorator to handle new class creation event.
|
|
357
|
+
The decorated function receives the newly created ObjClass.
|
|
358
|
+
|
|
359
|
+
:param func: Function to be called when a new class is created
|
|
360
|
+
:type func: Callable[[ObjClass], None]
|
|
361
|
+
"""
|
|
362
|
+
self._class_created_callback = func
|
|
363
|
+
return func
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<el-select v-model="state.{{{widget.widget_id}}}.value" {% if widget._changes_handled==true %}
|
|
2
|
+
@change="post('/{{{widget.widget_id}}}/value_changed')" {% endif %}
|
|
3
|
+
:placeholder="data.{{{widget.widget_id}}}.placeholder" :filterable="data.{{{widget.widget_id}}}.filterable"
|
|
4
|
+
:multiple="data.{{{widget.widget_id}}}.multiple" :size="data.{{{widget.widget_id}}}.size">
|
|
5
|
+
<el-option v-for="item in data.{{{widget.widget_id}}}.items" :key="item.value" :label="item.label"
|
|
6
|
+
:value="item.value">
|
|
7
|
+
<i class="zmdi zmdi-circle"
|
|
8
|
+
:style="{color: 'rgb(' + item.color[0] + ',' + item.color[1] + ',' + item.color[2] + ')'}"></i>
|
|
9
|
+
{{ item.label }}
|
|
10
|
+
<span v-if="item.geometryType" style="float: right; color: #8492a6; font-size: 12px; margin-right: 20px;">
|
|
11
|
+
{{ item.geometryType }}
|
|
12
|
+
</span>
|
|
13
|
+
</el-option>
|
|
14
|
+
<el-option v-if="data.{{{widget.widget_id}}}.showAddNewClass" value="__add_new_class__" label="+ Add new class"
|
|
15
|
+
disabled @click.native.stop="state.{{{widget.widget_id}}}.createClassDialog.visible = true"
|
|
16
|
+
style="cursor: pointer;">
|
|
17
|
+
<span style="color: #409EFF; font-weight: 500;">+ Add new class</span>
|
|
18
|
+
</el-option>
|
|
19
|
+
</el-select>
|
|
20
|
+
|
|
21
|
+
<!-- Dialog for creating new class -->
|
|
22
|
+
<el-dialog title="Create New Class" :visible.sync="state.{{{widget.widget_id}}}.createClassDialog.visible" width="450px"
|
|
23
|
+
:close-on-click-modal="false" @open="state.{{{widget.widget_id}}}.createClassDialog.showError = false">
|
|
24
|
+
<el-form label-width="120px" label-position="left">
|
|
25
|
+
<el-form-item label="Class Name">
|
|
26
|
+
<el-input v-model="state.{{{widget.widget_id}}}.createClassDialog.className" placeholder="Enter class name"
|
|
27
|
+
@input="state.{{{widget.widget_id}}}.createClassDialog.showError = false"
|
|
28
|
+
@keyup.enter.native="post('/{{{widget.widget_id}}}/class_created_cb')"></el-input>
|
|
29
|
+
</el-form-item>
|
|
30
|
+
<el-form-item label="Geometry Type">
|
|
31
|
+
<el-select v-model="state.{{{widget.widget_id}}}.createClassDialog.geometryType"
|
|
32
|
+
placeholder="Select geometry type" style="width: 100%">
|
|
33
|
+
<el-option v-for="geom in data.{{{widget.widget_id}}}.availableGeometryTypes" :key="geom.value"
|
|
34
|
+
:label="geom.label" :value="geom.value">
|
|
35
|
+
</el-option>
|
|
36
|
+
</el-select>
|
|
37
|
+
</el-form-item>
|
|
38
|
+
<div v-show="state.{{{widget.widget_id}}}.createClassDialog.showError" style="margin-top: 10px;">
|
|
39
|
+
{{{widget._error_message}}}
|
|
40
|
+
</div>
|
|
41
|
+
</el-form>
|
|
42
|
+
<span slot="footer" class="dialog-footer">
|
|
43
|
+
<el-button @click="state.{{{widget.widget_id}}}.createClassDialog.visible = false">
|
|
44
|
+
Cancel
|
|
45
|
+
</el-button>
|
|
46
|
+
<el-button type="primary" @click="post('/{{{widget.widget_id}}}/class_created_cb')">
|
|
47
|
+
Create
|
|
48
|
+
</el-button>
|
|
49
|
+
</span>
|
|
50
|
+
</el-dialog>
|
|
@@ -168,3 +168,25 @@ class SelectCudaDevice(Widget):
|
|
|
168
168
|
:return: None
|
|
169
169
|
"""
|
|
170
170
|
return self._select.set_value(value)
|
|
171
|
+
|
|
172
|
+
def disable(self) -> None:
|
|
173
|
+
"""Disables the widget.
|
|
174
|
+
|
|
175
|
+
This method disables the widget and grays out the selector.
|
|
176
|
+
|
|
177
|
+
:return: None
|
|
178
|
+
"""
|
|
179
|
+
self._disabled = True
|
|
180
|
+
self._select.disable()
|
|
181
|
+
self._refresh_button.disable()
|
|
182
|
+
|
|
183
|
+
def enable(self) -> None:
|
|
184
|
+
"""Enables the widget.
|
|
185
|
+
|
|
186
|
+
This method enables the widget and makes the selector clickable.
|
|
187
|
+
|
|
188
|
+
:return: None
|
|
189
|
+
"""
|
|
190
|
+
self._disabled = False
|
|
191
|
+
self._select.enable()
|
|
192
|
+
self._refresh_button.enable()
|
|
@@ -5,6 +5,7 @@ from supervisely.api.api import Api
|
|
|
5
5
|
from supervisely.app.widgets import Widget
|
|
6
6
|
from supervisely.app.widgets.checkbox.checkbox import Checkbox
|
|
7
7
|
from supervisely.app.widgets.container.container import Container
|
|
8
|
+
from supervisely.app.widgets.field.field import Field
|
|
8
9
|
from supervisely.app.widgets.select.select import Select
|
|
9
10
|
from supervisely.app.widgets.tree_select.tree_select import TreeSelect
|
|
10
11
|
from supervisely.project.project_type import ProjectType
|
|
@@ -97,6 +98,7 @@ class SelectDatasetTree(Widget):
|
|
|
97
98
|
widget_id: Union[str, None] = None,
|
|
98
99
|
show_select_all_datasets_checkbox: bool = True,
|
|
99
100
|
width: int = 193,
|
|
101
|
+
show_selectors_labels: bool = False,
|
|
100
102
|
):
|
|
101
103
|
self._api = Api.from_env()
|
|
102
104
|
|
|
@@ -114,11 +116,29 @@ class SelectDatasetTree(Widget):
|
|
|
114
116
|
# Using environment variables to set the default values if they are not provided.
|
|
115
117
|
self._project_id = project_id or env.project_id(raise_not_found=False)
|
|
116
118
|
self._dataset_id = default_id or env.dataset_id(raise_not_found=False)
|
|
119
|
+
if self._project_id:
|
|
120
|
+
project_info = self._api.project.get_info_by_id(self._project_id)
|
|
121
|
+
if allowed_project_types is not None:
|
|
122
|
+
allowed_values = []
|
|
123
|
+
if not isinstance(allowed_project_types, list):
|
|
124
|
+
allowed_project_types = [allowed_project_types]
|
|
125
|
+
|
|
126
|
+
for pt in allowed_project_types:
|
|
127
|
+
if isinstance(pt, (ProjectType, str)):
|
|
128
|
+
allowed_values.append(str(pt))
|
|
129
|
+
|
|
130
|
+
if project_info.type not in allowed_values:
|
|
131
|
+
self._project_id = None
|
|
117
132
|
|
|
118
133
|
self._multiselect = multiselect
|
|
119
134
|
self._compact = compact
|
|
120
135
|
self._append_to_body = append_to_body
|
|
121
136
|
|
|
137
|
+
# User-defined callbacks
|
|
138
|
+
self._team_changed_callbacks = []
|
|
139
|
+
self._workspace_changed_callbacks = []
|
|
140
|
+
self._project_changed_callbacks = []
|
|
141
|
+
|
|
122
142
|
# Extract values from Enum to match the .type property of the ProjectInfo object.
|
|
123
143
|
self._project_types = None
|
|
124
144
|
if allowed_project_types is not None:
|
|
@@ -160,6 +180,7 @@ class SelectDatasetTree(Widget):
|
|
|
160
180
|
if show_select_all_datasets_checkbox:
|
|
161
181
|
self._create_select_all_datasets_checkbox(select_all_datasets)
|
|
162
182
|
|
|
183
|
+
self._show_selectors_labels = show_selectors_labels
|
|
163
184
|
# Group the selectors and the dataset selector into a container.
|
|
164
185
|
self._content = Container(self._widgets)
|
|
165
186
|
super().__init__(widget_id=widget_id, file_path=__file__)
|
|
@@ -308,8 +329,30 @@ class SelectDatasetTree(Widget):
|
|
|
308
329
|
"""
|
|
309
330
|
if not self._multiselect:
|
|
310
331
|
raise ValueError("This method can only be called when multiselect is enabled.")
|
|
332
|
+
self._select_all_datasets_checkbox.uncheck()
|
|
311
333
|
self._select_dataset.set_selected_by_id(dataset_ids)
|
|
312
334
|
|
|
335
|
+
def team_changed(self, func: Callable) -> Callable:
|
|
336
|
+
"""Decorator to set the callback function for the team changed event."""
|
|
337
|
+
if self._compact:
|
|
338
|
+
raise ValueError("callback 'team_changed' is not available in compact mode.")
|
|
339
|
+
self._team_changed_callbacks.append(func)
|
|
340
|
+
return func
|
|
341
|
+
|
|
342
|
+
def workspace_changed(self, func: Callable) -> Callable:
|
|
343
|
+
"""Decorator to set the callback function for the workspace changed event."""
|
|
344
|
+
if self._compact:
|
|
345
|
+
raise ValueError("callback 'workspace_changed' is not available in compact mode.")
|
|
346
|
+
self._workspace_changed_callbacks.append(func)
|
|
347
|
+
return func
|
|
348
|
+
|
|
349
|
+
def project_changed(self, func: Callable) -> Callable:
|
|
350
|
+
"""Decorator to set the callback function for the project changed event."""
|
|
351
|
+
if self._compact:
|
|
352
|
+
raise ValueError("callback 'project_changed' is not available in compact mode.")
|
|
353
|
+
self._project_changed_callbacks.append(func)
|
|
354
|
+
return func
|
|
355
|
+
|
|
313
356
|
def value_changed(self, func: Callable) -> Callable:
|
|
314
357
|
"""Decorator to set the callback function for the value changed event.
|
|
315
358
|
|
|
@@ -353,13 +396,13 @@ class SelectDatasetTree(Widget):
|
|
|
353
396
|
|
|
354
397
|
if checked:
|
|
355
398
|
self._select_dataset.select_all()
|
|
356
|
-
self.
|
|
399
|
+
self._select_dataset_field.hide()
|
|
357
400
|
else:
|
|
358
401
|
self._select_dataset.clear_selected()
|
|
359
|
-
self.
|
|
402
|
+
self._select_dataset_field.show()
|
|
360
403
|
|
|
361
404
|
if select_all_datasets:
|
|
362
|
-
self.
|
|
405
|
+
self._select_dataset_field.hide()
|
|
363
406
|
select_all_datasets_checkbox.check()
|
|
364
407
|
|
|
365
408
|
self._widgets.append(select_all_datasets_checkbox)
|
|
@@ -390,9 +433,10 @@ class SelectDatasetTree(Widget):
|
|
|
390
433
|
self._select_dataset.set_selected_by_id(self._dataset_id)
|
|
391
434
|
if select_all_datasets:
|
|
392
435
|
self._select_dataset.select_all()
|
|
436
|
+
self._select_dataset_field = Field(self._select_dataset, title="Dataset")
|
|
393
437
|
|
|
394
438
|
# Adding the dataset selector to the list of widgets to be added to the container.
|
|
395
|
-
self._widgets.append(self.
|
|
439
|
+
self._widgets.append(self._select_dataset_field)
|
|
396
440
|
|
|
397
441
|
def _create_selectors(self, team_is_selectable: bool, workspace_is_selectable: bool):
|
|
398
442
|
"""Create the team, workspace, and project selectors.
|
|
@@ -412,6 +456,9 @@ class SelectDatasetTree(Widget):
|
|
|
412
456
|
self._select_workspace.set(items=self._get_select_items(team_id=team_id))
|
|
413
457
|
self._team_id = team_id
|
|
414
458
|
|
|
459
|
+
for callback in self._team_changed_callbacks:
|
|
460
|
+
callback(team_id)
|
|
461
|
+
|
|
415
462
|
def workspace_selector_handler(workspace_id: int):
|
|
416
463
|
"""Handler function for the event when the workspace selector value changes.
|
|
417
464
|
|
|
@@ -421,6 +468,9 @@ class SelectDatasetTree(Widget):
|
|
|
421
468
|
self._select_project.set(items=self._get_select_items(workspace_id=workspace_id))
|
|
422
469
|
self._workspace_id = workspace_id
|
|
423
470
|
|
|
471
|
+
for callback in self._workspace_changed_callbacks:
|
|
472
|
+
callback(workspace_id)
|
|
473
|
+
|
|
424
474
|
def project_selector_handler(project_id: int):
|
|
425
475
|
"""Handler function for the event when the project selector value changes.
|
|
426
476
|
|
|
@@ -435,7 +485,10 @@ class SelectDatasetTree(Widget):
|
|
|
435
485
|
and self._select_all_datasets_checkbox.is_checked()
|
|
436
486
|
):
|
|
437
487
|
self._select_dataset.select_all()
|
|
438
|
-
self.
|
|
488
|
+
self._select_dataset_field.hide()
|
|
489
|
+
|
|
490
|
+
for callback in self._project_changed_callbacks:
|
|
491
|
+
callback(project_id)
|
|
439
492
|
|
|
440
493
|
self._select_team = Select(
|
|
441
494
|
items=self._get_select_items(),
|
|
@@ -446,6 +499,7 @@ class SelectDatasetTree(Widget):
|
|
|
446
499
|
self._select_team.set_value(self._team_id)
|
|
447
500
|
if not team_is_selectable:
|
|
448
501
|
self._select_team.disable()
|
|
502
|
+
self._select_team_field = Field(self._select_team, title="Team")
|
|
449
503
|
|
|
450
504
|
self._select_workspace = Select(
|
|
451
505
|
items=self._get_select_items(team_id=self._team_id),
|
|
@@ -456,6 +510,7 @@ class SelectDatasetTree(Widget):
|
|
|
456
510
|
self._select_workspace.set_value(self._workspace_id)
|
|
457
511
|
if not workspace_is_selectable:
|
|
458
512
|
self._select_workspace.disable()
|
|
513
|
+
self._select_workspace_field = Field(self._select_workspace, title="Workspace")
|
|
459
514
|
|
|
460
515
|
self._select_project = Select(
|
|
461
516
|
items=self._get_select_items(workspace_id=self._workspace_id),
|
|
@@ -464,14 +519,17 @@ class SelectDatasetTree(Widget):
|
|
|
464
519
|
width_px=self._width,
|
|
465
520
|
)
|
|
466
521
|
self._select_project.set_value(self._project_id)
|
|
522
|
+
self._select_project_field = Field(self._select_project, title="Project")
|
|
467
523
|
|
|
468
|
-
# Register the event handlers.
|
|
524
|
+
# Register the event handlers._select_project
|
|
469
525
|
self._select_team.value_changed(team_selector_handler)
|
|
470
526
|
self._select_workspace.value_changed(workspace_selector_handler)
|
|
471
527
|
self._select_project.value_changed(project_selector_handler)
|
|
472
528
|
|
|
473
529
|
# Adding widgets to the list, so they can be added to the container.
|
|
474
|
-
self._widgets.extend(
|
|
530
|
+
self._widgets.extend(
|
|
531
|
+
[self._select_team_field, self._select_workspace_field, self._select_project_field]
|
|
532
|
+
)
|
|
475
533
|
|
|
476
534
|
def _get_select_items(self, **kwargs) -> List[Select.Item]:
|
|
477
535
|
"""Get the list of items for the team, workspace, and project selectors.
|
|
File without changes
|