supervisely 6.73.390__py3-none-any.whl → 6.73.391__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/app/widgets/experiment_selector/experiment_selector.py +20 -3
- supervisely/app/widgets/experiment_selector/template.html +49 -70
- supervisely/app/widgets/report_thumbnail/report_thumbnail.py +19 -4
- supervisely/decorators/profile.py +20 -0
- supervisely/nn/benchmark/utils/detection/utlis.py +7 -0
- supervisely/nn/experiments.py +4 -0
- supervisely/nn/inference/gui/serving_gui_template.py +71 -11
- supervisely/nn/inference/inference.py +108 -6
- supervisely/nn/training/gui/classes_selector.py +246 -27
- supervisely/nn/training/gui/gui.py +318 -234
- supervisely/nn/training/gui/hyperparameters_selector.py +2 -2
- supervisely/nn/training/gui/model_selector.py +42 -1
- supervisely/nn/training/gui/tags_selector.py +1 -1
- supervisely/nn/training/gui/train_val_splits_selector.py +8 -7
- supervisely/nn/training/gui/training_artifacts.py +10 -1
- supervisely/nn/training/gui/training_process.py +17 -1
- supervisely/nn/training/train_app.py +227 -72
- supervisely/template/__init__.py +2 -0
- supervisely/template/base_generator.py +90 -0
- supervisely/template/experiment/__init__.py +0 -0
- supervisely/template/experiment/experiment.html.jinja +537 -0
- supervisely/template/experiment/experiment_generator.py +996 -0
- supervisely/template/experiment/header.html.jinja +154 -0
- supervisely/template/experiment/sidebar.html.jinja +240 -0
- supervisely/template/experiment/sly-style.css +397 -0
- supervisely/template/experiment/template.html.jinja +18 -0
- supervisely/template/extensions.py +172 -0
- supervisely/template/template_renderer.py +253 -0
- {supervisely-6.73.390.dist-info → supervisely-6.73.391.dist-info}/METADATA +3 -1
- {supervisely-6.73.390.dist-info → supervisely-6.73.391.dist-info}/RECORD +34 -23
- {supervisely-6.73.390.dist-info → supervisely-6.73.391.dist-info}/LICENSE +0 -0
- {supervisely-6.73.390.dist-info → supervisely-6.73.391.dist-info}/WHEEL +0 -0
- {supervisely-6.73.390.dist-info → supervisely-6.73.391.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.390.dist-info → supervisely-6.73.391.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,36 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
|
|
1
3
|
from supervisely._utils import abs_url, is_debug_with_sly_net, is_development
|
|
2
|
-
from supervisely.app.widgets import
|
|
4
|
+
from supervisely.app.widgets import (
|
|
5
|
+
Button,
|
|
6
|
+
Card,
|
|
7
|
+
CheckboxField,
|
|
8
|
+
ClassesTable,
|
|
9
|
+
Container,
|
|
10
|
+
Text,
|
|
11
|
+
)
|
|
12
|
+
from supervisely.geometry.bitmap import Bitmap
|
|
13
|
+
from supervisely.geometry.graph import GraphNodes
|
|
14
|
+
from supervisely.geometry.polygon import Polygon
|
|
15
|
+
from supervisely.geometry.rectangle import Rectangle
|
|
16
|
+
from supervisely.nn.task_type import TaskType
|
|
17
|
+
from supervisely.nn.training.gui.model_selector import ModelSelector
|
|
3
18
|
|
|
4
19
|
|
|
5
20
|
class ClassesSelector:
|
|
6
21
|
title = "Classes Selector"
|
|
7
22
|
description = "Select classes that will be used for training"
|
|
8
|
-
lock_message = "Select
|
|
23
|
+
lock_message = "Select previous step to unlock"
|
|
9
24
|
|
|
10
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
project_id: int,
|
|
28
|
+
classes: list,
|
|
29
|
+
model_selector: ModelSelector = None,
|
|
30
|
+
app_options: dict = {},
|
|
31
|
+
):
|
|
11
32
|
# Init widgets
|
|
33
|
+
self.model_selector = model_selector
|
|
12
34
|
self.qa_stats_text = None
|
|
13
35
|
self.classes_table = None
|
|
14
36
|
self.validator_text = None
|
|
@@ -37,9 +59,22 @@ class ClassesSelector:
|
|
|
37
59
|
|
|
38
60
|
self.validator_text = Text("")
|
|
39
61
|
self.validator_text.hide()
|
|
62
|
+
|
|
63
|
+
self.convert_class_shapes_checkbox = CheckboxField(
|
|
64
|
+
title="Convert classes to model task type",
|
|
65
|
+
description="If enabled, classes with compatible shapes will be converted according to the model CV task type",
|
|
66
|
+
checked=False,
|
|
67
|
+
)
|
|
68
|
+
|
|
40
69
|
self.button = Button("Select")
|
|
41
70
|
self.display_widgets.extend(
|
|
42
|
-
[
|
|
71
|
+
[
|
|
72
|
+
self.qa_stats_text,
|
|
73
|
+
self.classes_table,
|
|
74
|
+
self.convert_class_shapes_checkbox,
|
|
75
|
+
self.validator_text,
|
|
76
|
+
self.button,
|
|
77
|
+
]
|
|
43
78
|
)
|
|
44
79
|
# -------------------------------- #
|
|
45
80
|
|
|
@@ -55,7 +90,7 @@ class ClassesSelector:
|
|
|
55
90
|
|
|
56
91
|
@property
|
|
57
92
|
def widgets_to_disable(self) -> list:
|
|
58
|
-
return [self.classes_table]
|
|
93
|
+
return [self.classes_table, self.convert_class_shapes_checkbox]
|
|
59
94
|
|
|
60
95
|
def get_selected_classes(self) -> list:
|
|
61
96
|
return self.classes_table.get_selected_classes()
|
|
@@ -66,40 +101,224 @@ class ClassesSelector:
|
|
|
66
101
|
def select_all_classes(self) -> None:
|
|
67
102
|
self.classes_table.select_all()
|
|
68
103
|
|
|
104
|
+
def get_wrong_shape_classes(self, task_type: str) -> List[str]:
|
|
105
|
+
allowed_shapes = {
|
|
106
|
+
TaskType.OBJECT_DETECTION: {Rectangle},
|
|
107
|
+
TaskType.INSTANCE_SEGMENTATION: {Bitmap},
|
|
108
|
+
TaskType.SEMANTIC_SEGMENTATION: {Bitmap},
|
|
109
|
+
TaskType.POSE_ESTIMATION: {GraphNodes},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if task_type not in allowed_shapes:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
selected_classes = self.get_selected_classes()
|
|
116
|
+
wrong_shape_classes = []
|
|
117
|
+
for class_name in selected_classes:
|
|
118
|
+
obj_class = self.classes_table.project_meta.get_obj_class(class_name)
|
|
119
|
+
|
|
120
|
+
from supervisely.annotation.obj_class import ObjClass
|
|
121
|
+
|
|
122
|
+
obj_class: ObjClass
|
|
123
|
+
if obj_class is None:
|
|
124
|
+
continue
|
|
125
|
+
if obj_class.geometry_type not in allowed_shapes[task_type]:
|
|
126
|
+
wrong_shape_classes.append(class_name)
|
|
127
|
+
|
|
128
|
+
return wrong_shape_classes
|
|
129
|
+
|
|
130
|
+
def classify_incompatible_classes(
|
|
131
|
+
self, task_type: str
|
|
132
|
+
) -> Tuple[List[str], List[str]]:
|
|
133
|
+
"""
|
|
134
|
+
Rules:
|
|
135
|
+
1) Detection – any shape can be converted, Rectangle is compatible.
|
|
136
|
+
2) Instance/Semantic segmentation – only Bitmap and Polygon (need conversion to Bitmap) are allowed; other shapes are not convertible.
|
|
137
|
+
3) Pose estimation – only GraphNodes are allowed; other shapes are not convertible.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
- convertible: List[str] – list of class names that can be converted to the task type
|
|
141
|
+
- non_convertible: List[str] – list of class names that cannot be converted to the task type
|
|
142
|
+
"""
|
|
143
|
+
selected_classes = self.get_selected_classes()
|
|
144
|
+
convertible: List[str] = []
|
|
145
|
+
non_convertible: List[str] = []
|
|
146
|
+
for class_name in selected_classes:
|
|
147
|
+
obj_class = self.classes_table.project_meta.get_obj_class(class_name)
|
|
148
|
+
if obj_class is None:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
geo_cls = obj_class.geometry_type
|
|
152
|
+
|
|
153
|
+
if task_type == TaskType.OBJECT_DETECTION:
|
|
154
|
+
if geo_cls is Rectangle:
|
|
155
|
+
# already compatible
|
|
156
|
+
continue
|
|
157
|
+
# Any other shape is converted to Rectangle (BBox)
|
|
158
|
+
convertible.append(class_name)
|
|
159
|
+
|
|
160
|
+
elif task_type in (
|
|
161
|
+
TaskType.INSTANCE_SEGMENTATION,
|
|
162
|
+
TaskType.SEMANTIC_SEGMENTATION,
|
|
163
|
+
):
|
|
164
|
+
if geo_cls is Bitmap:
|
|
165
|
+
# already compatible (bitmap mask)
|
|
166
|
+
continue
|
|
167
|
+
if geo_cls is Polygon:
|
|
168
|
+
# convertible to bitmap mask
|
|
169
|
+
convertible.append(class_name)
|
|
170
|
+
continue
|
|
171
|
+
# Other shapes cannot be converted
|
|
172
|
+
non_convertible.append(class_name)
|
|
173
|
+
|
|
174
|
+
elif task_type == TaskType.POSE_ESTIMATION:
|
|
175
|
+
if geo_cls is GraphNodes:
|
|
176
|
+
# already compatible
|
|
177
|
+
continue
|
|
178
|
+
# Other shapes cannot be converted
|
|
179
|
+
non_convertible.append(class_name)
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
# Unknown task type – treat all shapes as compatible
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
return convertible, non_convertible
|
|
186
|
+
|
|
69
187
|
def validate_step(self) -> bool:
|
|
188
|
+
# @TODO: Handle AnyShape classes
|
|
70
189
|
self.validator_text.hide()
|
|
190
|
+
task_type = (
|
|
191
|
+
self.model_selector.get_selected_task_type()
|
|
192
|
+
if self.model_selector
|
|
193
|
+
else None
|
|
194
|
+
)
|
|
71
195
|
|
|
72
|
-
|
|
73
|
-
if len(project_classes) == 0:
|
|
196
|
+
if len(self.classes_table.project_meta.obj_classes) == 0:
|
|
74
197
|
self.validator_text.set(text="Project has no classes", status="error")
|
|
75
198
|
self.validator_text.show()
|
|
76
199
|
return False
|
|
77
200
|
|
|
78
201
|
selected_classes = self.classes_table.get_selected_classes()
|
|
79
|
-
|
|
202
|
+
n_classes = len(selected_classes)
|
|
203
|
+
|
|
204
|
+
if n_classes == 0:
|
|
205
|
+
self.validator_text.set(
|
|
206
|
+
text="Please select at least one class", status="error"
|
|
207
|
+
)
|
|
208
|
+
self.validator_text.show()
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
class_word = "class" if n_classes == 1 else "classes"
|
|
212
|
+
message_parts = [f"Selected {n_classes} {class_word}"]
|
|
213
|
+
status = "success"
|
|
214
|
+
is_valid = True
|
|
215
|
+
|
|
80
216
|
empty_classes = [
|
|
81
217
|
row[0]["data"]
|
|
82
|
-
for row in
|
|
83
|
-
if row[0]["data"] in selected_classes
|
|
218
|
+
for row in self.classes_table._table_data
|
|
219
|
+
if row[0]["data"] in selected_classes
|
|
220
|
+
and row[2]["data"] == 0
|
|
221
|
+
and row[3]["data"] == 0
|
|
84
222
|
]
|
|
223
|
+
if empty_classes:
|
|
224
|
+
empty_word = "class" if len(empty_classes) == 1 else "classes"
|
|
225
|
+
message_parts.append(
|
|
226
|
+
f"{empty_word.capitalize()} with no annotations: {', '.join(empty_classes)}"
|
|
227
|
+
)
|
|
228
|
+
status = "warning"
|
|
85
229
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
230
|
+
convertible_classes, non_convertible_classes = (
|
|
231
|
+
self.classify_incompatible_classes(task_type)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
n_classes > 0
|
|
236
|
+
and not convertible_classes
|
|
237
|
+
and len(non_convertible_classes) == n_classes
|
|
238
|
+
):
|
|
239
|
+
available_task_types = []
|
|
240
|
+
if self.model_selector is not None and hasattr(
|
|
241
|
+
self.model_selector, "pretrained_models_table"
|
|
242
|
+
):
|
|
243
|
+
available_task_types = (
|
|
244
|
+
self.model_selector.pretrained_models_table.get_available_task_types()
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
other_suitable_exists = False
|
|
248
|
+
for other_task_type in available_task_types:
|
|
249
|
+
if other_task_type == task_type:
|
|
250
|
+
continue
|
|
251
|
+
other_conv, other_non_conv = self.classify_incompatible_classes(
|
|
252
|
+
other_task_type
|
|
253
|
+
)
|
|
254
|
+
if other_conv or len(other_non_conv) < n_classes:
|
|
255
|
+
other_suitable_exists = True
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
if other_suitable_exists:
|
|
259
|
+
self.validator_text.set(
|
|
260
|
+
text=(
|
|
261
|
+
f"No suitable classes for task type {task_type}. "
|
|
262
|
+
"Training is not possible. Please choose another task type."
|
|
263
|
+
),
|
|
264
|
+
status="error",
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
self.validator_text.set(
|
|
268
|
+
text=(
|
|
269
|
+
"Project contains no suitable classes for training. "
|
|
270
|
+
"Training is not possible. Please select another project."
|
|
271
|
+
),
|
|
272
|
+
status="error",
|
|
273
|
+
)
|
|
274
|
+
self.validator_text.show()
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
incompatible_exist = bool(convertible_classes or non_convertible_classes)
|
|
278
|
+
if incompatible_exist:
|
|
279
|
+
task_specific_texts = {
|
|
280
|
+
TaskType.OBJECT_DETECTION: "Only rectangle shapes are supported for object detection task, all other shape types can be converted to rectangle",
|
|
281
|
+
TaskType.INSTANCE_SEGMENTATION: "Only bitmap shapes are supported for instance segmentation task, polygon shapes can be converted to bitmap",
|
|
282
|
+
TaskType.SEMANTIC_SEGMENTATION: "Only bitmap shapes are supported for semantic segmentation task, polygon shapes can be converted to bitmap",
|
|
283
|
+
TaskType.POSE_ESTIMATION: "Only keypoint (graph) shape is supported for pose estimation task",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if non_convertible_classes:
|
|
287
|
+
specific_text = task_specific_texts.get(
|
|
288
|
+
task_type,
|
|
289
|
+
"Some selected classes have shapes that are incompatible with the chosen model task type.",
|
|
290
|
+
)
|
|
291
|
+
message_parts = [f"Model task type is {task_type}. {specific_text}"]
|
|
292
|
+
message_parts.append("Remove incompatible classes to continue.")
|
|
293
|
+
status = "error"
|
|
294
|
+
is_valid = False
|
|
295
|
+
else:
|
|
296
|
+
if self.is_convert_class_shapes_enabled():
|
|
297
|
+
message_parts.append(
|
|
298
|
+
f"Conversion enabled. Classes will be converted for task {task_type}."
|
|
299
|
+
)
|
|
300
|
+
status = "info" if status == "success" else status
|
|
301
|
+
is_valid = True
|
|
302
|
+
else:
|
|
303
|
+
specific_text = task_specific_texts.get(
|
|
304
|
+
task_type,
|
|
305
|
+
"Some selected classes have shapes that are incompatible with the chosen model task type.",
|
|
100
306
|
)
|
|
101
|
-
|
|
307
|
+
message_parts = [
|
|
308
|
+
f"Model task type is {task_type}. {specific_text}",
|
|
309
|
+
"Enable conversion checkbox or select compatible classes to continue.",
|
|
310
|
+
]
|
|
311
|
+
status = "error"
|
|
312
|
+
is_valid = False
|
|
313
|
+
else:
|
|
314
|
+
if self.is_convert_class_shapes_enabled():
|
|
315
|
+
message_parts.append(
|
|
316
|
+
"Conversion enabled, but no shape conversion required."
|
|
317
|
+
)
|
|
102
318
|
|
|
103
|
-
self.validator_text.set(text=
|
|
319
|
+
self.validator_text.set(text=". ".join(message_parts), status=status)
|
|
104
320
|
self.validator_text.show()
|
|
105
|
-
return
|
|
321
|
+
return is_valid
|
|
322
|
+
|
|
323
|
+
def is_convert_class_shapes_enabled(self) -> bool:
|
|
324
|
+
return self.convert_class_shapes_checkbox.is_checked()
|