supervisely 6.73.390__py3-none-any.whl → 6.73.392__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 (34) hide show
  1. supervisely/app/widgets/experiment_selector/experiment_selector.py +21 -3
  2. supervisely/app/widgets/experiment_selector/template.html +49 -70
  3. supervisely/app/widgets/report_thumbnail/report_thumbnail.py +19 -4
  4. supervisely/decorators/profile.py +20 -0
  5. supervisely/nn/benchmark/utils/detection/utlis.py +7 -0
  6. supervisely/nn/experiments.py +4 -0
  7. supervisely/nn/inference/gui/serving_gui_template.py +71 -11
  8. supervisely/nn/inference/inference.py +108 -6
  9. supervisely/nn/training/gui/classes_selector.py +246 -27
  10. supervisely/nn/training/gui/gui.py +318 -234
  11. supervisely/nn/training/gui/hyperparameters_selector.py +2 -2
  12. supervisely/nn/training/gui/model_selector.py +42 -1
  13. supervisely/nn/training/gui/tags_selector.py +1 -1
  14. supervisely/nn/training/gui/train_val_splits_selector.py +8 -7
  15. supervisely/nn/training/gui/training_artifacts.py +10 -1
  16. supervisely/nn/training/gui/training_process.py +17 -1
  17. supervisely/nn/training/train_app.py +227 -72
  18. supervisely/template/__init__.py +2 -0
  19. supervisely/template/base_generator.py +90 -0
  20. supervisely/template/experiment/__init__.py +0 -0
  21. supervisely/template/experiment/experiment.html.jinja +537 -0
  22. supervisely/template/experiment/experiment_generator.py +996 -0
  23. supervisely/template/experiment/header.html.jinja +154 -0
  24. supervisely/template/experiment/sidebar.html.jinja +240 -0
  25. supervisely/template/experiment/sly-style.css +397 -0
  26. supervisely/template/experiment/template.html.jinja +18 -0
  27. supervisely/template/extensions.py +172 -0
  28. supervisely/template/template_renderer.py +253 -0
  29. {supervisely-6.73.390.dist-info → supervisely-6.73.392.dist-info}/METADATA +3 -1
  30. {supervisely-6.73.390.dist-info → supervisely-6.73.392.dist-info}/RECORD +34 -23
  31. {supervisely-6.73.390.dist-info → supervisely-6.73.392.dist-info}/LICENSE +0 -0
  32. {supervisely-6.73.390.dist-info → supervisely-6.73.392.dist-info}/WHEEL +0 -0
  33. {supervisely-6.73.390.dist-info → supervisely-6.73.392.dist-info}/entry_points.txt +0 -0
  34. {supervisely-6.73.390.dist-info → supervisely-6.73.392.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 Button, Card, ClassesTable, Container, Text
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 training and validation splits to unlock"
23
+ lock_message = "Select previous step to unlock"
9
24
 
10
- def __init__(self, project_id: int, classes: list, app_options: dict = {}):
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
- [self.qa_stats_text, self.classes_table, self.validator_text, self.button]
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
- project_classes = self.classes_table.project_meta.obj_classes
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
- table_data = self.classes_table._table_data
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 table_data
83
- if row[0]["data"] in selected_classes and row[2]["data"] == 0 and row[3]["data"] == 0
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
- n_classes = len(selected_classes)
87
- if n_classes == 0:
88
- message = "Please select at least one class"
89
- status = "error"
90
- else:
91
- class_text = "class" if n_classes == 1 else "classes"
92
- message = f"Selected {n_classes} {class_text}"
93
- status = "success"
94
- if empty_classes:
95
- intersections = set(selected_classes).intersection(empty_classes)
96
- if intersections:
97
- class_text = "class" if len(intersections) == 1 else "classes"
98
- message += (
99
- f". Selected {class_text} have no annotations: {', '.join(intersections)}"
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
- status = "warning"
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=message, status=status)
319
+ self.validator_text.set(text=". ".join(message_parts), status=status)
104
320
  self.validator_text.show()
105
- return n_classes > 0
321
+ return is_valid
322
+
323
+ def is_convert_class_shapes_enabled(self) -> bool:
324
+ return self.convert_class_shapes_checkbox.is_checked()