supervisely 6.73.283__py3-none-any.whl → 6.73.285__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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

Files changed (34) hide show
  1. supervisely/_utils.py +9 -0
  2. supervisely/api/entity_annotation/figure_api.py +3 -0
  3. supervisely/api/module_api.py +35 -1
  4. supervisely/api/video/video_api.py +1 -1
  5. supervisely/api/video_annotation_tool_api.py +58 -7
  6. supervisely/nn/benchmark/base_benchmark.py +13 -2
  7. supervisely/nn/benchmark/base_evaluator.py +2 -0
  8. supervisely/nn/benchmark/comparison/detection_visualization/text_templates.py +5 -0
  9. supervisely/nn/benchmark/comparison/detection_visualization/vis_metrics/overview.py +25 -0
  10. supervisely/nn/benchmark/comparison/detection_visualization/visualizer.py +9 -3
  11. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -0
  12. supervisely/nn/benchmark/instance_segmentation/text_templates.py +7 -0
  13. supervisely/nn/benchmark/object_detection/evaluator.py +15 -3
  14. supervisely/nn/benchmark/object_detection/metric_provider.py +21 -1
  15. supervisely/nn/benchmark/object_detection/text_templates.py +7 -0
  16. supervisely/nn/benchmark/object_detection/vis_metrics/key_metrics.py +12 -0
  17. supervisely/nn/benchmark/object_detection/vis_metrics/overview.py +41 -2
  18. supervisely/nn/benchmark/object_detection/visualizer.py +20 -0
  19. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +1 -0
  20. supervisely/nn/benchmark/utils/detection/calculate_metrics.py +31 -33
  21. supervisely/nn/benchmark/visualization/renderer.py +2 -0
  22. supervisely/nn/inference/cache.py +19 -1
  23. supervisely/nn/inference/inference.py +22 -0
  24. supervisely/nn/inference/tracking/base_tracking.py +362 -0
  25. supervisely/nn/inference/tracking/bbox_tracking.py +179 -129
  26. supervisely/nn/inference/tracking/mask_tracking.py +420 -329
  27. supervisely/nn/inference/tracking/point_tracking.py +325 -288
  28. supervisely/nn/inference/tracking/tracker_interface.py +346 -13
  29. {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/METADATA +1 -1
  30. {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/RECORD +34 -33
  31. {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/LICENSE +0 -0
  32. {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/WHEEL +0 -0
  33. {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/entry_points.txt +0 -0
  34. {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/top_level.txt +0 -0
supervisely/_utils.py CHANGED
@@ -83,6 +83,15 @@ def take_with_default(v, default):
83
83
  return v if v is not None else default
84
84
 
85
85
 
86
+ def find_value_by_keys(d: Dict, keys: List[str], default=object()):
87
+ for key in keys:
88
+ if key in d:
89
+ return d[key]
90
+ if default is object():
91
+ raise KeyError(f"None of the keys {keys} are in the dictionary.")
92
+ return default
93
+
94
+
86
95
  def batched(seq, batch_size=50):
87
96
  for i in range(0, len(seq), batch_size):
88
97
  yield seq[i : i + batch_size]
@@ -81,6 +81,9 @@ class FigureInfo(NamedTuple):
81
81
  if self.geometry_meta is not None:
82
82
  return Rectangle(*self.geometry_meta["bbox"], sly_id=self.id)
83
83
 
84
+ def to_json(self):
85
+ return FigureApi.convert_info_to_json(self)
86
+
84
87
 
85
88
  class FigureApi(RemoveableBulkModuleApi):
86
89
  """
@@ -3,7 +3,16 @@ import asyncio
3
3
  from collections import namedtuple
4
4
  from copy import deepcopy
5
5
  from math import ceil
6
- from typing import TYPE_CHECKING, Any, AsyncGenerator, List, NamedTuple, Optional, Tuple
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ AsyncGenerator,
10
+ Dict,
11
+ List,
12
+ NamedTuple,
13
+ Optional,
14
+ Tuple,
15
+ )
7
16
 
8
17
  import requests
9
18
 
@@ -602,6 +611,8 @@ class ApiField:
602
611
  """"""
603
612
  WITH_SHARED = "withShared"
604
613
  """"""
614
+ USE_DIRECT_PROGRESS_MESSAGES = "useDirectProgressMessages"
615
+ """"""
605
616
  EXTRA_FIELDS = "extraFields"
606
617
  """"""
607
618
  CUSTOM_SORT = "customSort"
@@ -861,6 +872,29 @@ class ModuleApiBase(_JsonConvertibleModule):
861
872
  raise RuntimeError("Can not parse field {!r}".format(field_name))
862
873
  return self.InfoType(*field_values)
863
874
 
875
+ @classmethod
876
+ def convert_info_to_json(cls, info: NamedTuple) -> Dict:
877
+ """_convert_info_to_json"""
878
+
879
+ def _create_nested_dict(keys, value):
880
+ if len(keys) == 1:
881
+ return {keys[0]: value}
882
+ else:
883
+ return {keys[0]: _create_nested_dict(keys[1:], value)}
884
+
885
+ json_info = {}
886
+ for field_name, value in zip(cls.info_sequence(), info):
887
+ if type(field_name) is str:
888
+ json_info[field_name] = value
889
+ elif isinstance(field_name, tuple):
890
+ if len(field_name[0]) == 0:
891
+ json_info[field_name[1]] = value
892
+ else:
893
+ json_info.update(_create_nested_dict(field_name[0], value))
894
+ else:
895
+ raise RuntimeError("Can not parse field {!r}".format(field_name))
896
+ return json_info
897
+
864
898
  def _get_response_by_id(self, id, method, id_field, fields=None):
865
899
  """_get_response_by_id"""
866
900
  try:
@@ -1361,7 +1361,7 @@ class VideoApi(RemoveableBulkModuleApi):
1361
1361
  :return: None
1362
1362
  """
1363
1363
 
1364
- response = self._api.post(
1364
+ self._api.post(
1365
1365
  "videos.notify-annotation-tool",
1366
1366
  {
1367
1367
  "type": "videos:tracking-error",
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- from typing import Any, Dict, Optional
3
+ from typing import Any, Dict, Optional, Tuple
4
4
 
5
5
  from supervisely.api.module_api import ApiField, ModuleApiBase
6
6
  from supervisely.collection.str_enum import StrEnum
@@ -21,6 +21,7 @@ class VideoAnnotationToolAction(StrEnum):
21
21
  """"""
22
22
  ENTITIES_SET_INTITY = "entities/setEntity"
23
23
  """"""
24
+ DIRECT_TRACKING_PROGRESS = "figures/setDirectTrackingProgress"
24
25
 
25
26
 
26
27
  class VideoAnnotationToolApi(ModuleApiBase):
@@ -51,7 +52,7 @@ class VideoAnnotationToolApi(ModuleApiBase):
51
52
  VideoAnnotationToolAction.JOBS_ENABLE_CONTROLS,
52
53
  {},
53
54
  )
54
-
55
+
55
56
  def disable_submit_button(self, session_id: str) -> Dict[str, Any]:
56
57
  """Disables submit button of the labeling jobs.
57
58
 
@@ -65,7 +66,7 @@ class VideoAnnotationToolApi(ModuleApiBase):
65
66
  VideoAnnotationToolAction.JOBS_DISABLE_SUBMIT,
66
67
  {},
67
68
  )
68
-
69
+
69
70
  def enable_submit_button(self, session_id: str) -> Dict[str, Any]:
70
71
  """Enables submit button of the labeling jobs.
71
72
 
@@ -79,7 +80,7 @@ class VideoAnnotationToolApi(ModuleApiBase):
79
80
  VideoAnnotationToolAction.JOBS_ENABLE_SUBMIT,
80
81
  {},
81
82
  )
82
-
83
+
83
84
  def disable_confirm_button(self, session_id: str) -> Dict[str, Any]:
84
85
  """Disables confirm button of the labeling jobs.
85
86
 
@@ -93,10 +94,10 @@ class VideoAnnotationToolApi(ModuleApiBase):
93
94
  VideoAnnotationToolAction.JOBS_DISABLE_CONFIRM,
94
95
  {},
95
96
  )
96
-
97
+
97
98
  def enable_confirm_button(self, session_id: str) -> Dict[str, Any]:
98
99
  """Enables confirm button of the labeling jobs.
99
-
100
+
100
101
  :param session_id: ID of the session in the Video Labeling Tool which confirm button should be enabled.
101
102
  :type session_id: str
102
103
  :return: Response from API server in JSON format.
@@ -132,12 +133,62 @@ class VideoAnnotationToolApi(ModuleApiBase):
132
133
  },
133
134
  )
134
135
 
136
+ def set_direct_tracking_progress(
137
+ self,
138
+ session_id: str,
139
+ video_id: int,
140
+ track_id: str,
141
+ frame_range: Tuple,
142
+ progress_current: int,
143
+ progress_total: int,
144
+ ):
145
+ payload = {
146
+ ApiField.TRACK_ID: track_id,
147
+ ApiField.VIDEO_ID: video_id,
148
+ ApiField.FRAME_RANGE: frame_range,
149
+ ApiField.PROGRESS: {
150
+ ApiField.CURRENT: progress_current,
151
+ ApiField.TOTAL: progress_total,
152
+ },
153
+ }
154
+ return self._act(session_id, VideoAnnotationToolAction.DIRECT_TRACKING_PROGRESS, payload)
155
+
156
+ def set_direct_tracking_error(
157
+ self,
158
+ session_id: str,
159
+ video_id: int,
160
+ track_id: str,
161
+ message: str,
162
+ ):
163
+ payload = {
164
+ ApiField.TRACK_ID: track_id,
165
+ ApiField.VIDEO_ID: video_id,
166
+ ApiField.TYPE: "error",
167
+ ApiField.ERROR: {ApiField.MESSAGE: message},
168
+ }
169
+ return self._act(session_id, VideoAnnotationToolAction.DIRECT_TRACKING_PROGRESS, payload)
170
+
171
+ def set_direct_tracking_warning(
172
+ self,
173
+ session_id: str,
174
+ video_id: int,
175
+ track_id: str,
176
+ message: str,
177
+ ):
178
+ payload = {
179
+ ApiField.TRACK_ID: track_id,
180
+ ApiField.VIDEO_ID: video_id,
181
+ ApiField.TYPE: "warning",
182
+ ApiField.MESSAGE: message,
183
+ }
184
+ return self._act(session_id, VideoAnnotationToolAction.DIRECT_TRACKING_PROGRESS, payload)
185
+
135
186
  def _act(self, session_id: int, action: VideoAnnotationToolAction, payload: dict):
136
187
  data = {
137
188
  ApiField.SESSION_ID: session_id,
138
189
  ApiField.ACTION: str(action),
139
190
  ApiField.PAYLOAD: payload,
140
191
  }
141
- resp = self._api.post("/annotation-tool.run-action", data)
192
+ resp = self._api.post("annotation-tool.run-action", data)
142
193
 
143
194
  return resp.json()
@@ -1,5 +1,6 @@
1
1
  import os
2
- from typing import Callable, List, Optional, Tuple, Union
2
+ from pathlib import Path
3
+ from typing import Callable, List, Optional, Tuple, Union, Type
3
4
 
4
5
  import numpy as np
5
6
 
@@ -80,7 +81,7 @@ class BaseBenchmark:
80
81
  self.report_id = None
81
82
  self._validate_evaluation_params()
82
83
 
83
- def _get_evaluator_class(self) -> type:
84
+ def _get_evaluator_class(self) -> Type[BaseEvaluator]:
84
85
  raise NotImplementedError()
85
86
 
86
87
  @property
@@ -95,6 +96,10 @@ class BaseBenchmark:
95
96
  def key_metrics(self):
96
97
  eval_results = self.get_eval_result()
97
98
  return eval_results.key_metrics
99
+
100
+ @property
101
+ def primary_metric_name(self) -> str:
102
+ return self._get_evaluator_class().eval_result_cls.PRIMARY_METRIC
98
103
 
99
104
  def run_evaluation(
100
105
  self,
@@ -492,6 +497,8 @@ class BaseBenchmark:
492
497
  "It should be defined in the subclass of BaseBenchmark (e.g. ObjectDetectionBenchmark)."
493
498
  )
494
499
  eval_result = self.get_eval_result()
500
+ self._dump_key_metrics(eval_result)
501
+
495
502
  layout_dir = self.get_layout_results_dir()
496
503
  self.visualizer = self.visualizer_cls( # pylint: disable=not-callable
497
504
  self.api, [eval_result], layout_dir, self.pbar
@@ -621,3 +628,7 @@ class BaseBenchmark:
621
628
  self.diff_project_info = eval_result.diff_project_info
622
629
  return self.diff_project_info
623
630
  return None
631
+
632
+ def _dump_key_metrics(self, eval_result: BaseEvaluator):
633
+ path = str(Path(self.get_eval_results_dir(), "key_metrics.json"))
634
+ json.dump_json_file(eval_result.key_metrics, path)
@@ -12,6 +12,8 @@ from supervisely.task.progress import tqdm_sly
12
12
 
13
13
 
14
14
  class BaseEvalResult:
15
+ PRIMARY_METRIC = None
16
+
15
17
  def __init__(self, directory: Optional[str] = None):
16
18
  self.directory = directory
17
19
  self.inference_info: Dict = None
@@ -87,6 +87,11 @@ In this section you can visually assess the model performance through examples.
87
87
  > Filtering options allow you to adjust the confidence threshold (only for predictions) and the model's false outcomes (only for differences). Differences are calculated only for the optimal confidence threshold, allowing you to focus on the most accurate predictions made by the model.
88
88
  """
89
89
 
90
+ markdown_different_iou_thresholds_warning = """### IoU Thresholds Mismatch
91
+
92
+ <i class="zmdi zmdi-alert-polygon" style="color: #f5a623; margin-right: 5px"></i> The models were evaluated using different IoU thresholds. Since these thresholds varied between models and classes, it may have led to unfair comparison. For fair model comparison, we suggest using the same IoU threshold across models.
93
+ """
94
+
90
95
  markdown_explore_difference = """## Explore Predictions
91
96
 
92
97
  In this section, you can explore predictions made by different models side-by-side. This helps you to understand the differences in predictions made by each model, and to identify which model performs better in different scenarios.
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from typing import List
2
3
 
3
4
  from supervisely._utils import abs_url
@@ -15,6 +16,7 @@ class Overview(BaseVisMetrics):
15
16
  MARKDOWN_OVERVIEW = "markdown_overview"
16
17
  MARKDOWN_OVERVIEW_INFO = "markdown_overview_info"
17
18
  MARKDOWN_COMMON_OVERVIEW = "markdown_common_overview"
19
+ MARKDOWN_DIFF_IOU = "markdown_different_iou_thresholds_warning"
18
20
  CHART = "chart_key_metrics"
19
21
 
20
22
  def __init__(self, vis_texts, eval_results: List[EvalResult]) -> None:
@@ -237,3 +239,26 @@ class Overview(BaseVisMetrics):
237
239
  ),
238
240
  )
239
241
  return fig
242
+
243
+ @property
244
+ def not_matched_iou_per_class_thresholds_md(self) -> MarkdownWidget:
245
+ if all([not r.different_iou_thresholds_per_class for r in self.eval_results]):
246
+ return None
247
+
248
+ iou_thrs_map = defaultdict(set)
249
+ matched = True
250
+ for eval_result in self.eval_results:
251
+ for cat_id, iou_thr in eval_result.mp.iou_threshold_per_class.items():
252
+ iou_thrs_map[cat_id].add(iou_thr)
253
+ if len(iou_thrs_map[cat_id]) > 1:
254
+ matched = False
255
+ break
256
+
257
+ if matched:
258
+ return None
259
+
260
+ return MarkdownWidget(
261
+ name="markdown_different_iou_thresholds_warning",
262
+ title="IoU per class thresholds mismatch",
263
+ text=self.vis_texts.markdown_different_iou_thresholds_warning,
264
+ )
@@ -50,10 +50,9 @@ class DetectionComparisonVisualizer(BaseComparisonVisualizer):
50
50
  self.overviews = self._create_overviews(overview)
51
51
  self.overview_md = overview.overview_md
52
52
  self.key_metrics_md = self._create_key_metrics()
53
- self.key_metrics_table = overview.get_table_widget(
54
- latency=speedtest.latency, fps=speedtest.fps
55
- )
53
+ self.key_metrics_table = overview.get_table_widget(speedtest.latency, speedtest.fps)
56
54
  self.overview_chart = overview.chart_widget
55
+ self.iou_per_class_thresholds_md = overview.not_matched_iou_per_class_thresholds_md
57
56
 
58
57
  columns_number = len(self.comparison.eval_results) + 1 # +1 for GT
59
58
  self.explore_predictions_modal_gallery = self._create_explore_modal_table(columns_number)
@@ -154,6 +153,13 @@ class DetectionComparisonVisualizer(BaseComparisonVisualizer):
154
153
  (0, self.header),
155
154
  (1, self.overview_md),
156
155
  (0, self.overviews),
156
+ ]
157
+
158
+ if self.iou_per_class_thresholds_md is not None:
159
+ is_anchors_widgets.append((0, self.iou_per_class_thresholds_md))
160
+
161
+ is_anchors_widgets += [
162
+ # Key Metrics
157
163
  (1, self.key_metrics_md),
158
164
  (0, self.key_metrics_table),
159
165
  (0, self.overview_chart),
@@ -14,6 +14,7 @@ from supervisely.nn.benchmark.utils import calculate_metrics, read_coco_datasets
14
14
 
15
15
  class InstanceSegmentationEvalResult(ObjectDetectionEvalResult):
16
16
  mp_cls = MetricProvider
17
+ PRIMARY_METRIC = "mAP"
17
18
 
18
19
  @classmethod
19
20
  def from_evaluator(
@@ -60,6 +60,13 @@ Here, we comprehensively assess the model's performance by presenting a broad se
60
60
  - **Calibration Score**: This score represents the consistency of predicted probabilities (or <abbr title="{}">confidence scores</abbr>) made by the model. We evaluate how well predicted probabilities align with actual outcomes. A well-calibrated model means that when it predicts an object with, say, 80% confidence, approximately 80% of those predictions should actually be correct.
61
61
  """
62
62
 
63
+ markdown_AP_custom_description = """> * AP_custom - Average Precision with different IoU thresholds for each class, that was set in evaluation params by the user."""
64
+
65
+ markdown_iou_per_class = """### IoU Threshold per Class
66
+
67
+ The model is evaluated using different IoU thresholds for each class.
68
+ """
69
+
63
70
  markdown_explorer = """## Explore Predictions
64
71
  In this section you can visually assess the model performance through examples. This helps users better understand model capabilities and limitations, giving an intuitive grasp of prediction quality in different scenarios.
65
72
 
@@ -19,6 +19,7 @@ from supervisely.nn.benchmark.visualization.vis_click_data import ClickData, IdM
19
19
 
20
20
  class ObjectDetectionEvalResult(BaseEvalResult):
21
21
  mp_cls = MetricProvider
22
+ PRIMARY_METRIC = "mAP"
22
23
 
23
24
  def _read_files(self, path: str) -> None:
24
25
  """Read all necessary files from the directory"""
@@ -92,6 +93,10 @@ class ObjectDetectionEvalResult(BaseEvalResult):
92
93
  def key_metrics(self):
93
94
  return self.mp.key_metrics()
94
95
 
96
+ @property
97
+ def different_iou_thresholds_per_class(self) -> bool:
98
+ return self.mp.iou_threshold_per_class is not None
99
+
95
100
 
96
101
  class ObjectDetectionEvaluator(BaseEvaluator):
97
102
  EVALUATION_PARAMS_YAML_PATH = f"{Path(__file__).parent}/evaluation_params.yaml"
@@ -120,12 +125,19 @@ class ObjectDetectionEvaluator(BaseEvaluator):
120
125
 
121
126
  @classmethod
122
127
  def validate_evaluation_params(cls, evaluation_params: dict) -> None:
128
+ available_thres = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95]
123
129
  iou_threshold = evaluation_params.get("iou_threshold")
124
130
  if iou_threshold is not None:
125
- assert iou_threshold in [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95], (
126
- f"iou_threshold must be one of [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95], "
127
- f"but got {iou_threshold}"
131
+ assert iou_threshold in available_thres, (
132
+ f"iou_threshold must be one of {available_thres}, " f"but got {iou_threshold}"
128
133
  )
134
+ iou_threshold_per_class = evaluation_params.get("iou_threshold_per_class")
135
+ if iou_threshold_per_class is not None:
136
+ for class_name, iou_thres in iou_threshold_per_class.items():
137
+ assert iou_thres in available_thres, (
138
+ f"class {class_name}: iou_threshold_per_class must be one of {available_thres}, "
139
+ f"but got {iou_thres}"
140
+ )
129
141
 
130
142
  def _convert_to_coco(self):
131
143
  cocoGt_json = sly2coco(
@@ -92,7 +92,11 @@ class MetricProvider:
92
92
 
93
93
  eval_params = params.get("evaluation_params", {})
94
94
  self.iou_threshold = eval_params.get("iou_threshold", 0.5)
95
- self.iou_threshold_idx = np.searchsorted(self.iouThrs, self.iou_threshold)
95
+ self.iou_threshold_idx = np.where(np.isclose(self.iouThrs, self.iou_threshold))[0][0]
96
+
97
+ # IoU per class (optional)
98
+ self.iou_threshold_per_class = eval_params.get("iou_threshold_per_class")
99
+ self.iou_idx_per_class = params.get("iou_idx_per_class") # {cat id: iou_idx}
96
100
 
97
101
  def calculate(self):
98
102
  self.m_full = _MetricProvider(
@@ -142,6 +146,8 @@ class MetricProvider:
142
146
  def json_metrics(self):
143
147
  base = self.base_metrics()
144
148
  iou_name = int(self.iou_threshold * 100)
149
+ if self.iou_threshold_per_class is not None:
150
+ iou_name = "_custom"
145
151
  ap_by_class = self.AP_per_class().tolist()
146
152
  ap_by_class = dict(zip(self.cat_names, ap_by_class))
147
153
  ap_custom_by_class = self.AP_custom_per_class().tolist()
@@ -166,6 +172,8 @@ class MetricProvider:
166
172
 
167
173
  def key_metrics(self):
168
174
  iou_name = int(self.iou_threshold * 100)
175
+ if self.iou_threshold_per_class is not None:
176
+ iou_name = "_custom"
169
177
  json_metrics = self.json_metrics()
170
178
  json_metrics.pop("AP_by_class")
171
179
  json_metrics.pop(f"AP{iou_name}_by_class")
@@ -174,6 +182,8 @@ class MetricProvider:
174
182
  def metric_table(self):
175
183
  table = self.json_metrics()
176
184
  iou_name = int(self.iou_threshold * 100)
185
+ if self.iou_threshold_per_class is not None:
186
+ iou_name = "_custom"
177
187
  return {
178
188
  "mAP": table["mAP"],
179
189
  "AP50": table["AP50"],
@@ -196,6 +206,10 @@ class MetricProvider:
196
206
 
197
207
  def AP_custom_per_class(self):
198
208
  s = self.coco_precision[self.iou_threshold_idx, :, :, 0, 2]
209
+ s = s.copy()
210
+ if self.iou_threshold_per_class is not None:
211
+ for cat_id, iou_idx in self.iou_idx_per_class.items():
212
+ s[:, cat_id - 1] = self.coco_precision[iou_idx, :, cat_id - 1, 0, 2]
199
213
  s[s == -1] = np.nan
200
214
  ap = np.nanmean(s, axis=0)
201
215
  return ap
@@ -280,6 +294,9 @@ class _MetricProvider:
280
294
  ious.append(match["iou"])
281
295
  cats.append(cat_id_to_idx[match["category_id"]])
282
296
  ious = np.array(ious) + np.spacing(1)
297
+ if 0.8999999999999999 in iouThrs:
298
+ iouThrs = iouThrs.copy()
299
+ iouThrs[iouThrs == 0.8999999999999999] = 0.9
283
300
  iou_idxs = np.searchsorted(iouThrs, ious) - 1
284
301
  cats = np.array(cats)
285
302
  # TP
@@ -452,6 +469,9 @@ class _MetricProvider:
452
469
  )
453
470
  scores = np.array([m["score"] for m in matches_sorted])
454
471
  ious = np.array([m["iou"] if m["type"] == "TP" else 0.0 for m in matches_sorted])
472
+ if 0.8999999999999999 in iouThrs:
473
+ iouThrs = iouThrs.copy()
474
+ iouThrs[iouThrs == 0.8999999999999999] = 0.9
455
475
  iou_idxs = np.searchsorted(iouThrs, ious + np.spacing(1))
456
476
 
457
477
  # Check
@@ -65,6 +65,13 @@ Here, we comprehensively assess the model's performance by presenting a broad se
65
65
  - **Calibration Score**: This score represents the consistency of predicted probabilities (or <abbr title="{}">confidence scores</abbr>) made by the model. We evaluate how well predicted probabilities align with actual outcomes. A well-calibrated model means that when it predicts an object with, say, 80% confidence, approximately 80% of those predictions should actually be correct.
66
66
  """
67
67
 
68
+ markdown_AP_custom_description = """> * AP_custom - Average Precision with different IoU thresholds for each class, that was set in evaluation params by the user."""
69
+
70
+ markdown_iou_per_class = """### IoU Threshold per Class
71
+
72
+ The model is evaluated using different IoU thresholds for each class.
73
+ """
74
+
68
75
  markdown_explorer = """## Explore Predictions
69
76
  In this section you can visually assess the model performance through examples. This helps users better understand model capabilities and limitations, giving an intuitive grasp of prediction quality in different scenarios.
70
77
 
@@ -29,6 +29,8 @@ class KeyMetrics(DetectionVisMetric):
29
29
  columns = ["metrics", "values"]
30
30
  content = []
31
31
  for metric, value in self.eval_result.mp.metric_table().items():
32
+ if metric == "AP_custom":
33
+ metric += "*"
32
34
  row = [metric, round(value, 2)]
33
35
  dct = {
34
36
  "row": row,
@@ -134,3 +136,13 @@ class KeyMetrics(DetectionVisMetric):
134
136
  ]
135
137
 
136
138
  return res
139
+
140
+ @property
141
+ def custom_ap_description_md(self) -> MarkdownWidget:
142
+ if not self.eval_result.different_iou_thresholds_per_class:
143
+ return None
144
+ return MarkdownWidget(
145
+ "custom_ap_description",
146
+ "Custom AP per Class",
147
+ self.vis_texts.markdown_AP_custom_description,
148
+ )
@@ -2,7 +2,7 @@ import datetime
2
2
  from typing import List
3
3
 
4
4
  from supervisely.nn.benchmark.object_detection.base_vis_metric import DetectionVisMetric
5
- from supervisely.nn.benchmark.visualization.widgets import MarkdownWidget
5
+ from supervisely.nn.benchmark.visualization.widgets import MarkdownWidget, TableWidget
6
6
 
7
7
 
8
8
  class Overview(DetectionVisMetric):
@@ -32,6 +32,10 @@ class Overview(DetectionVisMetric):
32
32
  # link to scroll to the optimal confidence section
33
33
  opt_conf_url = self.vis_texts.docs_url + "#f1-optimal-confidence-threshold"
34
34
 
35
+ iou_threshold = self.eval_result.mp.iou_threshold
36
+ if self.eval_result.different_iou_thresholds_per_class:
37
+ iou_threshold = "Different IoU thresholds for each class (see the table below)"
38
+
35
39
  formats = [
36
40
  model_name.replace("_", "\_"),
37
41
  checkpoint_name.replace("_", "\_"),
@@ -45,7 +49,7 @@ class Overview(DetectionVisMetric):
45
49
  classes_str,
46
50
  note_about_images,
47
51
  starter_app_info,
48
- self.eval_result.mp.iou_threshold,
52
+ iou_threshold,
49
53
  round(self.eval_result.mp.f1_optimal_conf, 4),
50
54
  opt_conf_url,
51
55
  self.vis_texts.docs_url,
@@ -112,3 +116,38 @@ class Overview(DetectionVisMetric):
112
116
  starter_app_info = train_session or evaluator_session or ""
113
117
 
114
118
  return classes_str, images_str, starter_app_info
119
+
120
+ @property
121
+ def iou_per_class_md(self) -> List[MarkdownWidget]:
122
+ if not self.eval_result.different_iou_thresholds_per_class:
123
+ return None
124
+
125
+ return MarkdownWidget(
126
+ "markdown_iou_per_class",
127
+ "Different IoU thresholds for each class",
128
+ text=self.vis_texts.markdown_iou_per_class,
129
+ )
130
+
131
+ @property
132
+ def iou_per_class_table(self) -> TableWidget:
133
+ if not self.eval_result.different_iou_thresholds_per_class:
134
+ return None
135
+
136
+ content = []
137
+ for name, thr in self.eval_result.mp.iou_threshold_per_class.items():
138
+ row = [name, round(thr, 2)]
139
+ dct = {"row": row, "id": name, "items": row}
140
+ content.append(dct)
141
+
142
+ data = {
143
+ "columns": ["Class name", "IoU threshold"],
144
+ "columnsOptions": [{"disableSort": True}, {}],
145
+ "content": content,
146
+ }
147
+ return TableWidget(
148
+ name="table_iou_per_class",
149
+ data=data,
150
+ fix_columns=1,
151
+ width="60%",
152
+ main_column="Class name",
153
+ )
@@ -90,11 +90,16 @@ class ObjectDetectionVisualizer(BaseVisualizer):
90
90
  self.header = overview.get_header(me.login)
91
91
  self.overview_md = overview.md
92
92
 
93
+ # IOU Per Class (optional)
94
+ self.iou_per_class_md = overview.iou_per_class_md
95
+ self.iou_per_class_table = overview.iou_per_class_table
96
+
93
97
  # Key Metrics
94
98
  key_metrics = KeyMetrics(self.vis_texts, self.eval_result)
95
99
  self.key_metrics_md = key_metrics.md
96
100
  self.key_metrics_table = key_metrics.table
97
101
  self.overview_chart = key_metrics.chart
102
+ self.custom_ap_description = key_metrics.custom_ap_description_md
98
103
 
99
104
  # Explore Predictions
100
105
  explore_predictions = ExplorePredictions(
@@ -238,9 +243,24 @@ class ObjectDetectionVisualizer(BaseVisualizer):
238
243
  # Overview
239
244
  (0, self.header),
240
245
  (1, self.overview_md),
246
+ ]
247
+
248
+ if self.iou_per_class_table is not None:
249
+ is_anchors_widgets += [
250
+ (0, self.iou_per_class_md),
251
+ (0, self.iou_per_class_table),
252
+ ]
253
+
254
+ is_anchors_widgets += [
241
255
  # KeyMetrics
242
256
  (1, self.key_metrics_md),
243
257
  (0, self.key_metrics_table),
258
+ ]
259
+
260
+ if self.custom_ap_description is not None:
261
+ is_anchors_widgets.append((0, self.custom_ap_description))
262
+
263
+ is_anchors_widgets += [
244
264
  (0, self.overview_chart),
245
265
  # ExplorePredictions
246
266
  (1, self.explore_predictions_md),
@@ -25,6 +25,7 @@ from supervisely.sly_logger import logger
25
25
 
26
26
  class SemanticSegmentationEvalResult(BaseEvalResult):
27
27
  mp_cls = MetricProvider
28
+ PRIMARY_METRIC = "mIoU"
28
29
 
29
30
  def _read_files(self, path: str) -> None:
30
31
  """Read all necessary files from the directory"""