supervisely 6.73.452__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.
Files changed (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {supervisely-6.73.452.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._select_dataset.hide()
399
+ self._select_dataset_field.hide()
357
400
  else:
358
401
  self._select_dataset.clear_selected()
359
- self._select_dataset.show()
402
+ self._select_dataset_field.show()
360
403
 
361
404
  if select_all_datasets:
362
- self._select_dataset.hide()
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._select_dataset)
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._select_dataset.hide()
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([self._select_team, self._select_workspace, self._select_project])
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