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,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.
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
{{{widget._team_selector}}}
|
|
4
4
|
{% endif %}
|
|
5
|
-
<sly-select-team-workspace
|
|
6
|
-
{
|
|
7
|
-
:team-id.sync="
|
|
8
|
-
{%
|
|
9
|
-
:
|
|
10
|
-
{% endif %}
|
|
11
|
-
:
|
|
12
|
-
|
|
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
|
|
58
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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]
|
|
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]
|
|
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.
|
|
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]
|
|
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.
|
|
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):
|
supervisely/aug/aug.py
CHANGED
|
@@ -526,7 +526,9 @@ def instance_crop(
|
|
|
526
526
|
|
|
527
527
|
|
|
528
528
|
# Resize
|
|
529
|
-
def resize(
|
|
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
|
|
supervisely/convert/converter.py
CHANGED
|
@@ -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
|