Skip to content

Commit 93aa605

Browse files
maxxgxleoll2
andauthored
bugfix: classification not working with label that contain spaces (#540)
* apply label sorting by default and add legacy name for classification --------- Co-authored-by: Leonardo Lai <leonardo.lai@live.com>
1 parent 5904a4b commit 93aa605

File tree

4 files changed

+132
-46
lines changed

4 files changed

+132
-46
lines changed

CHANGELOG.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
# v2.6.2 Intel® Geti™ SDK (08-01-2024)
2+
## What's Changed
3+
* Bugfix: inference not working for classification projects with label containing spaces in their name by @maxxgx in https://github.com/openvinotoolkit/geti-sdk/pull/540
4+
5+
**Full Changelog**: https://github.com/openvinotoolkit/geti-sdk/compare/v2.6.1...v2.6.2
6+
7+
18
# v2.6.1 Intel® Geti™ SDK (02-01-2024)
29
## What's Changed
3-
* CVS-159908 - Fix empty label ID in configuration [develop] by @maxxgx in https://github.com/openvinotoolkit/geti-sdk/pull/534
10+
* Bugfix: empty label sometimes not recognized during inference by @maxxgx in https://github.com/openvinotoolkit/geti-sdk/pull/535
411

512
## New Contributors
6-
* @maxxgx made their first contribution in https://github.com/openvinotoolkit/geti-sdk/pull/534
13+
* @maxxgx made their first contribution in https://github.com/openvinotoolkit/geti-sdk/pull/535
714

815
**Full Changelog**: https://github.com/openvinotoolkit/geti-sdk/compare/v2.6.0...v2.6.1
916

geti_sdk/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
# See the License for the specific language governing permissions
1313
# and limitations under the License.
1414

15-
__version__ = "2.6.1"
15+
__version__ = "2.6.2"

geti_sdk/deployment/predictions_postprocessing/results_converter/results_to_prediction_converter.py

+63-40
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Module implements the InferenceResultsToPredictionConverter class."""
1616

1717
import abc
18+
import logging
1819
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union
1920

2021
import cv2
@@ -31,7 +32,7 @@
3132
from geti_sdk.data_models.annotations import Annotation
3233
from geti_sdk.data_models.containers import LabelList
3334
from geti_sdk.data_models.enums.domain import Domain
34-
from geti_sdk.data_models.label import ScoredLabel
35+
from geti_sdk.data_models.label import Label, ScoredLabel
3536
from geti_sdk.data_models.predictions import Prediction
3637
from geti_sdk.data_models.shapes import (
3738
Ellipse,
@@ -48,12 +49,25 @@
4849
class InferenceResultsToPredictionConverter(metaclass=abc.ABCMeta):
4950
"""Interface for the converter"""
5051

51-
def __init__(
52-
self, labels: LabelList, configuration: Optional[Dict[str, Any]] = None
53-
):
52+
def __init__(self, labels: LabelList, configuration: Dict[str, Any]):
5453
self.labels = labels.get_non_empty_labels()
5554
self.empty_label = labels.get_empty_label()
5655
self.configuration = configuration
56+
self.is_labels_sorted = "label_ids" in configuration
57+
if self.is_labels_sorted:
58+
# Make sure the list of labels is sorted according to the order
59+
# defined in the ModelAPI configuration.
60+
# - If the 'label_ids' field only contains a single label,
61+
# it will be typed as string. No need to sort in that case.
62+
# - Filter out the empty label ID, as it is managed separately by the base converter class.
63+
ids = configuration["label_ids"]
64+
if not isinstance(ids, str):
65+
ids = [
66+
id_
67+
for id_ in ids
68+
if not self.empty_label or id_ != self.empty_label.id
69+
]
70+
self.labels.sort_by_ids(ids)
5771

5872
@abc.abstractmethod
5973
def convert_to_prediction(
@@ -89,9 +103,7 @@ class ClassificationToPredictionConverter(InferenceResultsToPredictionConverter)
89103
parameters
90104
"""
91105

92-
def __init__(
93-
self, labels: LabelList, configuration: Optional[Dict[str, Any]] = None
94-
):
106+
def __init__(self, labels: LabelList, configuration: Dict[str, Any]):
95107
super().__init__(labels, configuration)
96108

97109
def convert_to_prediction(
@@ -110,11 +122,18 @@ def convert_to_prediction(
110122
labels = []
111123
for label in inference_results.top_labels:
112124
label_idx, label_name, label_prob = label
113-
# label_idx does not necessarily match the label index in the project
114-
# labels. Therefore, we map the label by name instead.
115-
labels.append(
116-
self.labels.create_scored_label(id_or_name=label_name, score=label_prob)
117-
)
125+
if self.is_labels_sorted:
126+
scored_label = ScoredLabel.from_label(
127+
label=self.labels[label_idx], probability=label_prob
128+
)
129+
else:
130+
# label_idx does not necessarily match the label index in the project
131+
# labels. Therefore, we map the label by name instead.
132+
_label = self._get_label_by_prediction_name(name=label_name)
133+
scored_label = ScoredLabel.from_label(
134+
label=_label, probability=label_prob
135+
)
136+
labels.append(scored_label)
118137

119138
if not labels and self.empty_label:
120139
labels = [ScoredLabel.from_label(self.empty_label, probability=0)]
@@ -153,6 +172,27 @@ def convert_saliency_map(
153172
for i, label in enumerate(self.labels.get_non_empty_labels())
154173
}
155174

175+
def _get_label_by_prediction_name(self, name: str) -> Label:
176+
"""
177+
Get a Label object by its predicted name.
178+
179+
:param name: predicted name of the label
180+
:return: Label corresponding to the name
181+
:raises KeyError: if the label is not found in the LabelList
182+
"""
183+
try:
184+
return self.labels.get_by_name(name=name)
185+
except KeyError:
186+
# If the label is not found, we try to find it by legacy name (replacing spaces with underscores)
187+
for label in self.labels:
188+
legacy_name = label.name.replace(" ", "_")
189+
if legacy_name == name:
190+
logging.warning(
191+
f"Found label `{label.name}` using its legacy name `{legacy_name}`."
192+
)
193+
return label
194+
raise KeyError(f"Label named `{name}` was not found in the LabelList")
195+
156196

157197
class DetectionToPredictionConverter(InferenceResultsToPredictionConverter):
158198
"""
@@ -162,27 +202,14 @@ class DetectionToPredictionConverter(InferenceResultsToPredictionConverter):
162202
:param configuration: optional model configuration setting
163203
"""
164204

165-
def __init__(
166-
self, labels: LabelList, configuration: Optional[Dict[str, Any]] = None
167-
):
205+
def __init__(self, labels: LabelList, configuration: Dict[str, Any]):
168206
super().__init__(labels, configuration)
169207
self.use_ellipse_shapes = False
170208
self.confidence_threshold = 0.0
171-
if configuration is not None:
172-
if "use_ellipse_shapes" in configuration:
173-
self.use_ellipse_shapes = configuration["use_ellipse_shapes"]
174-
if "confidence_threshold" in configuration:
175-
self.confidence_threshold = configuration["confidence_threshold"]
176-
if "label_ids" in configuration:
177-
# Make sure the list of labels is sorted according to the order
178-
# defined in the ModelAPI configuration.
179-
# - If the 'label_ids' field only contains a single label,
180-
# it will be typed as string. No need to sort in that case.
181-
# - Filter out the empty label ID, as it is managed separately by the base converter class.
182-
ids = configuration["label_ids"]
183-
if not isinstance(ids, str):
184-
ids = [id_ for id_ in ids if id_ != self.empty_label.id]
185-
self.labels.sort_by_ids(ids)
209+
if "use_ellipse_shapes" in configuration:
210+
self.use_ellipse_shapes = configuration["use_ellipse_shapes"]
211+
if "confidence_threshold" in configuration:
212+
self.confidence_threshold = configuration["confidence_threshold"]
186213

187214
def _detection2array(self, detections: List[Detection]) -> np.ndarray:
188215
"""
@@ -468,9 +495,7 @@ class SegmentationToPredictionConverter(InferenceResultsToPredictionConverter):
468495
:param configuration: optional model configuration setting
469496
"""
470497

471-
def __init__(
472-
self, labels: LabelList, configuration: Optional[Dict[str, Any]] = None
473-
):
498+
def __init__(self, labels: LabelList, configuration: Dict[str, Any]):
474499
super().__init__(labels, configuration)
475500
# NB: index=0 is reserved for the background label
476501
self.label_map = dict(enumerate(self.labels, 1))
@@ -518,9 +543,7 @@ class AnomalyToPredictionConverter(InferenceResultsToPredictionConverter):
518543
:param configuration: optional model configuration setting
519544
"""
520545

521-
def __init__(
522-
self, labels: LabelList, configuration: Optional[Dict[str, Any]] = None
523-
):
546+
def __init__(self, labels: LabelList, configuration: Dict[str, Any]):
524547
super().__init__(labels, configuration)
525548
self.normal_label = next(
526549
label for label in self.labels if not label.is_anomalous
@@ -629,14 +652,14 @@ class ConverterFactory:
629652
def create_converter(
630653
labels: LabelList,
631654
domain: Domain,
632-
configuration: Optional[Dict[str, Any]] = None,
655+
configuration: Dict[str, Any],
633656
) -> InferenceResultsToPredictionConverter:
634657
"""
635-
Create the appropriate inferencer object according to the model's task.
658+
Create the appropriate inference converter object according to the model's task.
636659
637-
:param label_schema: The label schema containing the label info of the task.
660+
:param labels: The labels of the model
638661
:param domain: The domain to which the converter applies
639-
:param configuration: Optional configuration for the converter. Defaults to None.
662+
:param configuration: configuration for the converter
640663
:return: The created inference result to prediction converter.
641664
:raises ValueError: If the task type cannot be determined from the label schema.
642665
"""

tests/pre-merge/unit/deployment/test_prediction_converter.py

+59-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
SegmentedObject,
2626
)
2727

28+
from geti_sdk.data_models.containers import LabelList
2829
from geti_sdk.data_models.enums.domain import Domain
29-
from geti_sdk.data_models.label import ScoredLabel
30+
from geti_sdk.data_models.label import Label, ScoredLabel
3031
from geti_sdk.data_models.shapes import (
3132
Ellipse,
3233
Point,
@@ -64,7 +65,7 @@ def test_classification_to_prediction_converter(self, fxt_label_list_factory):
6465
)
6566

6667
# Act
67-
converter = ClassificationToPredictionConverter(labels)
68+
converter = ClassificationToPredictionConverter(labels, configuration={})
6869
prediction = converter.convert_to_prediction(
6970
raw_prediction, image_shape=(10, 10)
7071
)
@@ -196,7 +197,7 @@ def test_segmentation_to_prediction_converter(self, fxt_label_list_factory):
196197
)
197198

198199
# Act
199-
converter = SegmentationToPredictionConverter(labels)
200+
converter = SegmentationToPredictionConverter(labels, configuration={})
200201
prediction = converter.convert_to_prediction(raw_prediction)
201202

202203
# Assert
@@ -257,3 +258,58 @@ def test_anomaly_to_prediction_converter(self, domain, fxt_label_list_factory):
257258
assert prediction.annotations[0].shape == Rectangle(
258259
*coords_to_xmin_xmax_width_height(pred_boxes[0])
259260
)
261+
262+
@pytest.mark.parametrize(
263+
"label_ids, label_names, predicted_labels, configuration",
264+
[
265+
(
266+
["1", "2"],
267+
["foo bar", "foo_bar"],
268+
["foo_bar", "foo_bar"],
269+
{"label_ids": ["1", "2"]},
270+
),
271+
(["1", "2"], ["label 1", "label 2"], ["label_1", "label_2"], {}),
272+
(["1", "2"], ["?", "@"], ["@", "?"], {}),
273+
(["1", "2", "3", "4"], ["c", "b", "a", "empty"], ["a", "b", "c"], {}),
274+
],
275+
)
276+
def test_legacy_label_conversion(
277+
self, label_ids, label_names, predicted_labels, configuration
278+
):
279+
# Arrange
280+
labels = LabelList(
281+
[
282+
Label(
283+
id=_id,
284+
name=name,
285+
color="",
286+
group="",
287+
domain=Domain.CLASSIFICATION,
288+
is_empty="empty" in name,
289+
)
290+
for _id, name in zip(label_ids, label_names)
291+
]
292+
)
293+
raw_prediction = ClassificationResult(
294+
top_labels=[
295+
(i, p_label, 0.7) for i, p_label in enumerate(predicted_labels)
296+
],
297+
raw_scores=[0.7] * len(predicted_labels),
298+
saliency_map=None,
299+
feature_vector=None,
300+
)
301+
302+
# Act
303+
converter = ClassificationToPredictionConverter(
304+
labels=labels, configuration=configuration
305+
)
306+
pred = converter.convert_to_prediction(
307+
inference_results=raw_prediction,
308+
image_shape=(10, 10, 10),
309+
)
310+
311+
# Assert
312+
assert len(pred.annotations[0].labels) == len(predicted_labels)
313+
assert {label.name for label in converter.labels} == {
314+
p_label.name for p_label in pred.annotations[0].labels
315+
}

0 commit comments

Comments
 (0)