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.
Files changed (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,270 @@
1
+ from typing import Callable, List, Optional, Union
2
+
3
+ from supervisely.api.user_api import UserInfo
4
+ from supervisely.app import DataJson, StateJson
5
+ from supervisely.app.widgets import Widget
6
+
7
+ try:
8
+ from typing import Literal
9
+ except ImportError:
10
+ from typing_extensions import Literal
11
+
12
+
13
+ class SelectUser(Widget):
14
+ """
15
+ SelectUser is a dropdown widget for selecting users from a team.
16
+ Extends the Select widget with user-specific functionality.
17
+
18
+ :param users: Initial list of UserInfo instances
19
+ :type users: Optional[List[UserInfo]]
20
+ :param team_id: Team ID to fetch users from
21
+ :type team_id: Optional[int]
22
+ :param roles: List of allowed user roles to filter by (e.g., ['admin', 'developer'])
23
+ :type roles: Optional[List[str]]
24
+ :param filterable: Enable search/filter functionality in dropdown
25
+ :type filterable: Optional[bool]
26
+ :param placeholder: Placeholder text when no user is selected
27
+ :type placeholder: Optional[str]
28
+ :param size: Size of the select dropdown
29
+ :type size: Optional[Literal["large", "small", "mini"]]
30
+ :param multiple: Enable multiple selection
31
+ :type multiple: bool
32
+ :param widget_id: Unique widget identifier
33
+ :type widget_id: Optional[str]
34
+
35
+ :Usage example:
36
+
37
+ .. code-block:: python
38
+
39
+ import supervisely as sly
40
+ from supervisely.app.widgets import SelectUser
41
+
42
+ # Initialize with team_id and filter by roles
43
+ select_user = SelectUser(
44
+ team_id=123,
45
+ roles=['admin', 'developer'],
46
+ multiple=True
47
+ )
48
+
49
+ # Or initialize empty and set users later
50
+ select_user = SelectUser(roles=['annotator', 'reviewer'])
51
+ select_user.set_users(user_list)
52
+
53
+ # Handle selection changes
54
+ @select_user.value_changed
55
+ def on_user_selected(users):
56
+ if isinstance(users, list):
57
+ print(f"Selected users: {[u.login for u in users]}")
58
+ else:
59
+ print(f"Selected user: {users.login}")
60
+ """
61
+
62
+ class Routes:
63
+ VALUE_CHANGED = "value_changed"
64
+
65
+ def __init__(
66
+ self,
67
+ users: Optional[List[UserInfo]] = None,
68
+ team_id: Optional[int] = None,
69
+ roles: Optional[List[str]] = None,
70
+ filterable: Optional[bool] = True,
71
+ placeholder: Optional[str] = "Select user",
72
+ size: Optional[Literal["large", "small", "mini"]] = None,
73
+ multiple: bool = False,
74
+ widget_id: Optional[str] = None,
75
+ ):
76
+ self._users = []
77
+ self._team_id = team_id
78
+ self._allowed_roles = roles
79
+ self._filterable = filterable
80
+ self._placeholder = placeholder
81
+ self._size = size
82
+ self._multiple = multiple
83
+ self._changes_handled = False
84
+
85
+ # Load users from team_id if provided
86
+ if team_id is not None:
87
+ self._load_users_from_team(team_id)
88
+ elif users is not None:
89
+ self._users = self._filter_users_by_role(list(users))
90
+
91
+ # Initialize parent Widget
92
+ super().__init__(widget_id=widget_id, file_path=__file__)
93
+
94
+ def _filter_users_by_role(self, users: List[UserInfo]) -> List[UserInfo]:
95
+ """Filter users by allowed roles."""
96
+ if self._allowed_roles is None:
97
+ return users
98
+
99
+ return [user for user in users if user.role in self._allowed_roles]
100
+
101
+ def _load_users_from_team(self, team_id: int):
102
+ """Load users from a team using the API."""
103
+ from supervisely import Api
104
+
105
+ api = Api.from_env()
106
+ all_users = api.user.get_team_members(team_id)
107
+ self._users = self._filter_users_by_role(all_users)
108
+
109
+ def get_json_data(self):
110
+ """Build JSON data for the widget."""
111
+ items = []
112
+ for user in self._users:
113
+ user_name = None
114
+ if user.name:
115
+ name = user.name.strip()
116
+ if name:
117
+ user_name = name[:15] + "…" if len(name) > 15 else name
118
+
119
+ right_text = ""
120
+ user_role = user.role.upper() if user.role else "NONE"
121
+ if len(user_role) > 10:
122
+ user_role = user_role[:10] + "…"
123
+ if user_name and user_name != user.login:
124
+ right_text = f"{user_name} • {user_role}"
125
+ else:
126
+ right_text = user_role
127
+
128
+ items.append(
129
+ {
130
+ "value": user.login,
131
+ "label": user.login,
132
+ "rightText": right_text,
133
+ }
134
+ )
135
+
136
+ return {
137
+ "items": items,
138
+ "placeholder": self._placeholder,
139
+ "filterable": self._filterable,
140
+ "multiple": self._multiple,
141
+ "size": self._size,
142
+ }
143
+
144
+ def get_json_state(self):
145
+ """Build JSON state for the widget."""
146
+ value = None
147
+ if self._multiple:
148
+ value = []
149
+ return {"value": value}
150
+
151
+ def get_value(self) -> Union[str, List[str], None]:
152
+ """Get the currently selected user login(s)."""
153
+ return StateJson()[self.widget_id]["value"]
154
+
155
+ def get_selected_user(self) -> Union[UserInfo, List[UserInfo], None]:
156
+ """Get the currently selected UserInfo object(s)."""
157
+ value = self.get_value()
158
+ if value is None:
159
+ return None
160
+
161
+ if self._multiple:
162
+ if not isinstance(value, list):
163
+ return []
164
+ result = []
165
+ for login in value:
166
+ for user in self._users:
167
+ if user.login == login:
168
+ result.append(user)
169
+ break
170
+ return result
171
+ else:
172
+ for user in self._users:
173
+ if user.login == value:
174
+ return user
175
+ return None
176
+
177
+ def set_value(self, login: Union[str, List[str]]):
178
+ """Set the selected user by login."""
179
+ StateJson()[self.widget_id]["value"] = login
180
+ StateJson().send_changes()
181
+
182
+ def get_all_users(self) -> List[UserInfo]:
183
+ """Get all available users."""
184
+ return self._users.copy()
185
+
186
+ def set_users(self, users: List[UserInfo]):
187
+ """Update the list of available users."""
188
+ self._users = self._filter_users_by_role(list(users))
189
+
190
+ # Update data
191
+ DataJson()[self.widget_id] = self.get_json_data()
192
+ DataJson().send_changes()
193
+
194
+ # Reset value if current selection is not in new users
195
+ current_value = StateJson()[self.widget_id]["value"]
196
+ if current_value:
197
+ if self._multiple:
198
+ if isinstance(current_value, list):
199
+ # Keep only valid selections
200
+ valid = [v for v in current_value if any(u.login == v for u in self._users)]
201
+ if valid != current_value:
202
+ StateJson()[self.widget_id]["value"] = valid
203
+ StateJson().send_changes()
204
+ else:
205
+ if not any(u.login == current_value for u in self._users):
206
+ StateJson()[self.widget_id]["value"] = (
207
+ self._users[0].login if self._users else None
208
+ )
209
+ StateJson().send_changes()
210
+
211
+ def set_team_id(self, team_id: int):
212
+ """Load users from a team by team_id."""
213
+ self._team_id = team_id
214
+ self._load_users_from_team(team_id)
215
+
216
+ # Update data
217
+ DataJson()[self.widget_id] = self.get_json_data()
218
+ DataJson().send_changes()
219
+
220
+ # Reset selection
221
+ if self._multiple:
222
+ StateJson()[self.widget_id]["value"] = []
223
+ else:
224
+ StateJson()[self.widget_id]["value"] = self._users[0].login if self._users else None
225
+ StateJson().send_changes()
226
+
227
+ def set_selected_users_by_ids(self, user_ids: Union[int, List[int]]):
228
+ """Set the selected user(s) by user ID(s).
229
+
230
+ :param user_ids: Single user ID or list of user IDs to select
231
+ :type user_ids: Union[int, List[int]]
232
+ """
233
+ if isinstance(user_ids, int):
234
+ user_ids = [user_ids]
235
+
236
+ # Find logins for the given user IDs
237
+ selected_logins = []
238
+ for user_id in user_ids:
239
+ for user in self._users:
240
+ if user.id == user_id:
241
+ selected_logins.append(user.login)
242
+ break
243
+
244
+ # Set value based on multiple mode
245
+ if self._multiple:
246
+ StateJson()[self.widget_id]["value"] = selected_logins
247
+ else:
248
+ StateJson()[self.widget_id]["value"] = selected_logins[0] if selected_logins else None
249
+
250
+ StateJson().send_changes()
251
+
252
+ def value_changed(self, func: Callable[[Union[UserInfo, List[UserInfo]]], None]):
253
+ """
254
+ Decorator to handle value change event.
255
+ The decorated function receives the selected UserInfo (or list of UserInfo if multiple=True).
256
+
257
+ :param func: Function to be called when selection changes
258
+ :type func: Callable[[Union[UserInfo, List[UserInfo]]], None]
259
+ """
260
+ route_path = self.get_route_path(SelectUser.Routes.VALUE_CHANGED)
261
+ server = self._sly_app.get_server()
262
+ self._changes_handled = True
263
+
264
+ @server.post(route_path)
265
+ def _value_changed():
266
+ selected = self.get_selected_user()
267
+ if selected is not None:
268
+ func(selected)
269
+
270
+ return _value_changed
@@ -0,0 +1,13 @@
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" style="font-size: 12px;">
7
+ <i class="zmdi zmdi-account" style="margin-right: 8px;"></i>
8
+ {{ item.label }}
9
+ <span v-if="item.rightText" style="float: right; color: #8492a6; font-size: 12px; margin-right: 20px; margin-left: 10px;">
10
+ {{ item.rightText }}
11
+ </span>
12
+ </el-option>
13
+ </el-select>
@@ -1,18 +1,20 @@
1
- from typing import Dict
1
+ from typing import Callable, Dict
2
2
 
3
3
  try:
4
4
  from typing import Literal
5
5
  except ImportError:
6
6
  from typing_extensions import Literal
7
7
 
8
- from supervisely.app import DataJson, StateJson
9
- from supervisely.app.widgets import Widget, SelectTeam, generate_id
10
8
  from supervisely.api.api import Api
11
- from supervisely.sly_logger import logger
9
+ from supervisely.app import DataJson, StateJson
10
+ from supervisely.app.widgets import SelectTeam, Widget, generate_id
12
11
  from supervisely.app.widgets.select_sly_utils import _get_int_or_env
13
12
 
14
13
 
15
14
  class SelectWorkspace(Widget):
15
+ class Routes:
16
+ VALUE_CHANGED = "value_changed"
17
+
16
18
  def __init__(
17
19
  self,
18
20
  default_id: int = None,
@@ -30,20 +32,18 @@ class SelectWorkspace(Widget):
30
32
  self._size = size
31
33
  self._team_selector = None
32
34
  self._disabled = False
35
+ self._changes_handled = False
33
36
 
34
37
  self._default_id = _get_int_or_env(self._default_id, "context.workspaceId")
35
38
  if self._default_id is not None:
36
- info = self._api.workspace.get_info_by_id(
37
- self._default_id, raise_error=True
38
- )
39
+ info = self._api.workspace.get_info_by_id(self._default_id, raise_error=True)
39
40
  self._team_id = info.team_id
40
41
  self._team_id = _get_int_or_env(self._team_id, "context.teamId")
41
42
 
42
43
  if compact is True:
44
+ # If team_id is not provided in compact mode, start disabled
43
45
  if self._team_id is None:
44
- raise ValueError(
45
- '"team_id" have to be passed as argument or "compact" has to be False'
46
- )
46
+ self._disabled = True
47
47
  else:
48
48
  # if self._show_label is False:
49
49
  # logger.warn(
@@ -74,6 +74,35 @@ class SelectWorkspace(Widget):
74
74
  res["options"]["size"] = self._size
75
75
  return res
76
76
 
77
+ def get_team_id(self):
78
+ if self._compact is True:
79
+ return self._team_id
80
+ else:
81
+ return self._team_selector.get_selected_id()
82
+
83
+ def set_team_id(self, team_id: int):
84
+ """Set the team ID and update the UI. Automatically enables the widget if it was disabled."""
85
+ self._team_id = team_id
86
+ if self._compact is False and self._team_selector is not None:
87
+ self._team_selector.set_team_id(team_id)
88
+ else:
89
+ DataJson()[self.widget_id]["teamId"] = team_id
90
+ DataJson().send_changes()
91
+
92
+ # Auto-enable the widget when team_id is set
93
+ if self._disabled and team_id is not None:
94
+ self.enable()
95
+
96
+ def set_workspace_id(self, workspace_id: int):
97
+ """Set the workspace ID and update the UI."""
98
+ StateJson()[self.widget_id]["workspaceId"] = workspace_id
99
+ StateJson().send_changes()
100
+
101
+ def set_ids(self, team_id: int, workspace_id: int):
102
+ """Set both team ID and workspace ID and update the UI."""
103
+ self.set_team_id(team_id)
104
+ self.set_workspace_id(workspace_id)
105
+
77
106
  def get_json_state(self) -> Dict:
78
107
  return {
79
108
  "workspaceId": self._default_id,
@@ -95,3 +124,23 @@ class SelectWorkspace(Widget):
95
124
  self._disabled = False
96
125
  DataJson()[self.widget_id]["disabled"] = self._disabled
97
126
  DataJson().send_changes()
127
+
128
+ def value_changed(self, func: Callable[[int], None]):
129
+ """
130
+ Decorator to handle workspace selection change event.
131
+ The decorated function receives the selected workspace ID.
132
+
133
+ :param func: Function to be called when workspace selection changes
134
+ :type func: Callable[[int], None]
135
+ """
136
+ route_path = self.get_route_path(SelectWorkspace.Routes.VALUE_CHANGED)
137
+ server = self._sly_app.get_server()
138
+ self._changes_handled = True
139
+
140
+ @server.post(route_path)
141
+ def _value_changed():
142
+ workspace_id = self.get_selected_id()
143
+ if workspace_id is not None:
144
+ func(workspace_id)
145
+
146
+ return _value_changed
@@ -1,15 +1,12 @@
1
1
  <div>
2
2
  {% if widget._compact == false %}
3
- {{{widget._team_selector}}}
3
+ {{{widget._team_selector}}}
4
4
  {% endif %}
5
- <sly-select-team-workspace
6
- {% if widget._compact == false %}
7
- :team-id.sync="state.{{{widget._team_selector.widget_id}}}.teamId"
8
- {% else %}
9
- :team-id.sync="data.{{{widget.widget_id}}}.teamId"
10
- {% endif %}
11
- :workspace-id.sync="state.{{{widget.widget_id}}}.workspaceId"
12
- :options="data.{{{widget.widget_id}}}.options"
13
- :disabled="data.{{{widget.widget_id}}}.disabled"
14
- ></sly-select-team-workspace>
15
- </div>
5
+ <sly-select-team-workspace {% if widget._compact==false %}
6
+ :team-id.sync="state.{{{widget._team_selector.widget_id}}}.teamId" {% else %}
7
+ :team-id.sync="data.{{{widget.widget_id}}}.teamId" {% endif %}
8
+ :workspace-id.sync="state.{{{widget.widget_id}}}.workspaceId" {% if widget._changes_handled==true %}
9
+ @update:workspace-id="state.{{{widget.widget_id}}}.workspaceId = $event; post('/{{{widget.widget_id}}}/value_changed')"
10
+ {% endif %} :options="data.{{{widget.widget_id}}}.options"
11
+ :disabled="data.{{{widget.widget_id}}}.disabled"></sly-select-team-workspace>
12
+ </div>
@@ -1,3 +1,5 @@
1
+ # isort: skip_file
2
+
1
3
  import copy
2
4
  import io
3
5
 
@@ -54,9 +56,8 @@ class PackerUnpacker:
54
56
 
55
57
  @staticmethod
56
58
  def pandas_unpacker(data: pd.DataFrame):
57
- data = data.replace({np.nan: None})
58
- # data = data.astype(object).replace(np.nan, "-") # TODO: replace None later
59
-
59
+ # Keep None/NaN values in source data, don't replace them
60
+ # They will be converted to "" only when sending to frontend
60
61
  unpacked_data = {
61
62
  "columns": data.columns.to_list(),
62
63
  "data": data.values.tolist(),
@@ -169,9 +170,35 @@ class Table(Widget):
169
170
 
170
171
  super().__init__(widget_id=widget_id, file_path=__file__)
171
172
 
173
+ def _prepare_data_for_frontend(self, data_dict):
174
+ """Convert None and NaN values to empty strings for frontend display.
175
+ This preserves the original None/NaN values in _parsed_data.
176
+ """
177
+ import math
178
+
179
+ display_data = copy.deepcopy(data_dict)
180
+
181
+ # Convert None/NaN in data rows
182
+ for row in display_data.get("data", []):
183
+ for i in range(len(row)):
184
+ value = row[i]
185
+ # Check for None or NaN (NaN is a float that doesn't equal itself)
186
+ if value is None or (isinstance(value, float) and math.isnan(value)):
187
+ row[i] = ""
188
+
189
+ # Convert None/NaN in summary row if present
190
+ if "summaryRow" in display_data and display_data["summaryRow"] is not None:
191
+ summary_row = display_data["summaryRow"]
192
+ for i in range(len(summary_row)):
193
+ value = summary_row[i]
194
+ if value is None or (isinstance(value, float) and math.isnan(value)):
195
+ summary_row[i] = ""
196
+
197
+ return display_data
198
+
172
199
  def get_json_data(self):
173
200
  return {
174
- "table_data": self._parsed_data,
201
+ "table_data": self._prepare_data_for_frontend(self._parsed_data),
175
202
  "table_options": {
176
203
  "perPage": self._per_page,
177
204
  "pageSizes": self._page_sizes,
@@ -255,13 +282,17 @@ class Table(Widget):
255
282
 
256
283
  def read_json(self, value: dict) -> None:
257
284
  self._update_table_data(input_data=value)
258
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
285
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
286
+ self._parsed_data
287
+ )
259
288
  DataJson().send_changes()
260
289
  self.clear_selection()
261
290
 
262
291
  def read_pandas(self, value: pd.DataFrame) -> None:
263
292
  self._update_table_data(input_data=value)
264
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
293
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
294
+ self._parsed_data
295
+ )
265
296
  DataJson().send_changes()
266
297
  self.clear_selection()
267
298
 
@@ -272,7 +303,9 @@ class Table(Widget):
272
303
  index = len(table_data) if index > len(table_data) or index < 0 else index
273
304
 
274
305
  self._parsed_data["data"].insert(index, data)
275
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
306
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
307
+ self._parsed_data
308
+ )
276
309
  DataJson().send_changes()
277
310
 
278
311
  def pop_row(self, index=-1):
@@ -284,7 +317,9 @@ class Table(Widget):
284
317
 
285
318
  if len(self._parsed_data["data"]) != 0:
286
319
  popped_row = self._parsed_data["data"].pop(index)
287
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
320
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
321
+ self._parsed_data
322
+ )
288
323
  DataJson().send_changes()
289
324
  return popped_row
290
325
 
@@ -382,11 +417,27 @@ class Table(Widget):
382
417
  StateJson()[self.widget_id]["selected_row"] = {}
383
418
  StateJson().send_changes()
384
419
 
420
+ @staticmethod
421
+ def _values_equal(val1, val2):
422
+ """Compare two values, handling NaN specially."""
423
+ import math
424
+
425
+ # Check if both are NaN
426
+ is_nan1 = isinstance(val1, float) and math.isnan(val1)
427
+ is_nan2 = isinstance(val2, float) and math.isnan(val2)
428
+ if is_nan1 and is_nan2:
429
+ return True
430
+ # Check if both are None
431
+ if val1 is None and val2 is None:
432
+ return True
433
+ # Regular comparison
434
+ return val1 == val2
435
+
385
436
  def delete_row(self, key_column_name, key_cell_value):
386
437
  col_index = self._parsed_data["columns"].index(key_column_name)
387
438
  row_indices = []
388
439
  for idx, row in enumerate(self._parsed_data["data"]):
389
- if row[col_index] == key_cell_value:
440
+ if self._values_equal(row[col_index], key_cell_value):
390
441
  row_indices.append(idx)
391
442
  if len(row_indices) == 0:
392
443
  raise ValueError('Column "{key_column_name}" does not have value "{key_cell_value}"')
@@ -400,7 +451,7 @@ class Table(Widget):
400
451
  key_col_index = self._parsed_data["columns"].index(key_column_name)
401
452
  row_indices = []
402
453
  for idx, row in enumerate(self._parsed_data["data"]):
403
- if row[key_col_index] == key_cell_value:
454
+ if self._values_equal(row[key_col_index], key_cell_value):
404
455
  row_indices.append(idx)
405
456
  if len(row_indices) == 0:
406
457
  raise ValueError('Column "{key_column_name}" does not have value "{key_cell_value}"')
@@ -411,20 +462,24 @@ class Table(Widget):
411
462
 
412
463
  col_index = self._parsed_data["columns"].index(column_name)
413
464
  self._parsed_data["data"][row_indices[0]][col_index] = new_value
414
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
465
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
466
+ self._parsed_data
467
+ )
415
468
  DataJson().send_changes()
416
469
 
417
470
  def update_matching_cells(self, key_column_name, key_cell_value, column_name, new_value):
418
471
  key_col_index = self._parsed_data["columns"].index(key_column_name)
419
472
  row_indices = []
420
473
  for idx, row in enumerate(self._parsed_data["data"]):
421
- if row[key_col_index] == key_cell_value:
474
+ if self._values_equal(row[key_col_index], key_cell_value):
422
475
  row_indices.append(idx)
423
476
 
424
477
  col_index = self._parsed_data["columns"].index(column_name)
425
478
  for row_idx in row_indices:
426
479
  self._parsed_data["data"][row_idx][col_index] = new_value
427
- DataJson()[self.widget_id]["table_data"] = self._parsed_data
480
+ DataJson()[self.widget_id]["table_data"] = self._prepare_data_for_frontend(
481
+ self._parsed_data
482
+ )
428
483
  DataJson().send_changes()
429
484
 
430
485
  def sort(self, column_id: int = None, direction: Optional[Literal["asc", "desc"]] = None):
@@ -264,6 +264,8 @@ class TreeSelect(Widget):
264
264
 
265
265
  def _get_all_items(items: List[TreeSelect.Item]) -> List[TreeSelect.Item]:
266
266
  res = []
267
+ if not items:
268
+ return res
267
269
  for item in items:
268
270
  res.append(item)
269
271
  res.extend(_get_all_items(item.children))
supervisely/aug/aug.py CHANGED
@@ -526,7 +526,9 @@ def instance_crop(
526
526
 
527
527
 
528
528
  # Resize
529
- def resize(img: np.ndarray, ann: Annotation, size: Tuple) -> Tuple[np.ndarray, Annotation]:
529
+ def resize(
530
+ img: np.ndarray, ann: Annotation, size: Tuple, skip_empty_masks: bool = False
531
+ ) -> Tuple[np.ndarray, Annotation]:
530
532
  """
531
533
  Resizes an input Image and Annotation to a given size.
532
534
 
@@ -536,6 +538,8 @@ def resize(img: np.ndarray, ann: Annotation, size: Tuple) -> Tuple[np.ndarray, A
536
538
  :type ann: Annotation
537
539
  :param size: Desired size (height, width) in pixels or -1.
538
540
  :type size: Tuple[int, int]
541
+ :param skip_empty_masks: If True, skips resizing of empty masks in Annotation.
542
+ :type skip_empty_masks: bool, optional
539
543
  :raises: :class:`RuntimeError` if Image shape does not match img_size in Annotation
540
544
  :return: Tuple containing resized Image and Annotation
541
545
  :rtype: :class:`Tuple[np.ndarray, Annotation]`
@@ -576,7 +580,7 @@ def resize(img: np.ndarray, ann: Annotation, size: Tuple) -> Tuple[np.ndarray, A
576
580
 
577
581
  new_size = sly_image.restore_proportional_size(in_size=ann.img_size, out_size=size)
578
582
  res_img = sly_image.resize(img, new_size)
579
- res_ann = ann.resize(new_size)
583
+ res_ann = ann.resize(new_size, skip_empty_masks=skip_empty_masks)
580
584
  return res_img, res_ann
581
585
 
582
586
 
@@ -49,6 +49,7 @@ class AvailableVideoConverters:
49
49
  SLY = "supervisely"
50
50
  MOT = "mot"
51
51
  DAVIS = "davis"
52
+ MULTI_VIEW = "multi_view"
52
53
 
53
54
 
54
55
  class AvailablePointcloudConverters:
@@ -105,9 +105,9 @@ class ImportManager:
105
105
  )
106
106
  return modality_converter.detect_format()
107
107
 
108
- def upload_dataset(self, dataset_id):
108
+ def upload_dataset(self, dataset_id) -> Optional[int]:
109
109
  """Upload converted data to Supervisely"""
110
- self.converter.upload_dataset(self._api, dataset_id)
110
+ return self.converter.upload_dataset(self._api, dataset_id)
111
111
 
112
112
  # def validate_format(self):
113
113
  # raise NotImplementedError