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.
- supervisely/_utils.py +9 -0
- supervisely/api/entity_annotation/figure_api.py +3 -0
- supervisely/api/module_api.py +35 -1
- supervisely/api/video/video_api.py +1 -1
- supervisely/api/video_annotation_tool_api.py +58 -7
- supervisely/nn/benchmark/base_benchmark.py +13 -2
- supervisely/nn/benchmark/base_evaluator.py +2 -0
- supervisely/nn/benchmark/comparison/detection_visualization/text_templates.py +5 -0
- supervisely/nn/benchmark/comparison/detection_visualization/vis_metrics/overview.py +25 -0
- supervisely/nn/benchmark/comparison/detection_visualization/visualizer.py +9 -3
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -0
- supervisely/nn/benchmark/instance_segmentation/text_templates.py +7 -0
- supervisely/nn/benchmark/object_detection/evaluator.py +15 -3
- supervisely/nn/benchmark/object_detection/metric_provider.py +21 -1
- supervisely/nn/benchmark/object_detection/text_templates.py +7 -0
- supervisely/nn/benchmark/object_detection/vis_metrics/key_metrics.py +12 -0
- supervisely/nn/benchmark/object_detection/vis_metrics/overview.py +41 -2
- supervisely/nn/benchmark/object_detection/visualizer.py +20 -0
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +1 -0
- supervisely/nn/benchmark/utils/detection/calculate_metrics.py +31 -33
- supervisely/nn/benchmark/visualization/renderer.py +2 -0
- supervisely/nn/inference/cache.py +19 -1
- supervisely/nn/inference/inference.py +22 -0
- supervisely/nn/inference/tracking/base_tracking.py +362 -0
- supervisely/nn/inference/tracking/bbox_tracking.py +179 -129
- supervisely/nn/inference/tracking/mask_tracking.py +420 -329
- supervisely/nn/inference/tracking/point_tracking.py +325 -288
- supervisely/nn/inference/tracking/tracker_interface.py +346 -13
- {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/METADATA +1 -1
- {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/RECORD +34 -33
- {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/LICENSE +0 -0
- {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/WHEEL +0 -0
- {supervisely-6.73.283.dist-info → supervisely-6.73.285.dist-info}/entry_points.txt +0 -0
- {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
|
"""
|
supervisely/api/module_api.py
CHANGED
|
@@ -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
|
|
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:
|
|
@@ -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("
|
|
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
|
|
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) ->
|
|
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)
|
|
@@ -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),
|
|
@@ -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
|
|
126
|
-
f"iou_threshold must be one of
|
|
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.
|
|
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
|
-
|
|
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),
|