From 73bec44360be9d4facd403fef3c2f2b4c0b35b97 Mon Sep 17 00:00:00 2001 From: "bogunowicz@arrival.com" Date: Tue, 10 Jan 2023 16:48:37 +0100 Subject: [PATCH 1/6] start working on the task --- .../loggers/metric_functions/__init__.py | 2 +- .../loggers/metric_functions/cv/__init__.py | 0 .../loggers/metric_functions/cv/built_ins.py | 206 ++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/deepsparse/loggers/metric_functions/cv/__init__.py create mode 100644 src/deepsparse/loggers/metric_functions/cv/built_ins.py diff --git a/src/deepsparse/loggers/metric_functions/__init__.py b/src/deepsparse/loggers/metric_functions/__init__.py index ef2739def2..425ec06191 100644 --- a/src/deepsparse/loggers/metric_functions/__init__.py +++ b/src/deepsparse/loggers/metric_functions/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. # flake8: noqa -from .built_ins import * +from src.deepsparse.loggers.metric_functions.cv.built_ins import * diff --git a/src/deepsparse/loggers/metric_functions/cv/__init__.py b/src/deepsparse/loggers/metric_functions/cv/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/deepsparse/loggers/metric_functions/cv/built_ins.py b/src/deepsparse/loggers/metric_functions/cv/built_ins.py new file mode 100644 index 0000000000..da3084c63d --- /dev/null +++ b/src/deepsparse/loggers/metric_functions/cv/built_ins.py @@ -0,0 +1,206 @@ +# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +The set of all the built-in metric functions +""" +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy + + +__all__ = [ + "identity", + "image_shape", + "mean_pixels_per_channel", + "std_pixels_per_channel", + "max_pixels_per_channel", + "fraction_zeros", + "bounding_box_count", +] + + +def identity(x: Any): + """ + Simple identity function + + :param x: Any object + :return: The same object + """ + return x + + +def image_shape( + img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 +) -> Dict[str, int]: + """ + Return the shape of the image. + + :param img: An image represented as a numpy array or a torch tensor. + Assumptions: + - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) + tensor/array + - the image has 3 or 1 channels + :return: Dictionary that maps "height", "width", "channels" keys + to the appropriate integers + """ + img_numpy = _assert_numpy_image(img) + num_dims, _ = _check_valid_image(img_numpy) + if num_dims == 4: + img_numpy = img_numpy[0] + return dict(zip(["height", "width", "channels"], img_numpy.shape)) + + +def mean_pixels_per_channel( + img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 +) -> Dict[str, float]: + """ + Return the mean pixel value per image channel + + :param img: An image represented as a numpy array or a torch tensor. + Assumptions: + - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) + tensor/array + - the image has 3 or 1 channels + :return: Dictionary that maps channel number to the mean pixel value + """ + img_numpy = _assert_numpy_image(img) + num_dims, channel_dim = _check_valid_image(img_numpy) + dims = numpy.arange(0, num_dims, 1) + dims = numpy.delete(dims, channel_dim) + means = numpy.mean(img_numpy, axis=tuple(dims)) + keys = ["mean_channel_{}".format(i) for i in range(len(means))] + return dict(zip(keys, means)) + + +def std_pixels_per_channel( + img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 +) -> Dict[str, float]: + """ + Return the standard deviation of pixel values per image channel + :param img: An image represented as a numpy array or a torch tensor. + Assumptions: + - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) + tensor/array + - the image has 3 or 1 channels + :return: Dictionary that maps channel number to the std pixel value + """ + img_numpy = _assert_numpy_image(img) + num_dims, channel_dim = _check_valid_image(img) + dims = numpy.arange(0, num_dims, 1) + dims = numpy.delete(dims, channel_dim) + stds = tuple(numpy.std(img_numpy, axis=tuple(dims))) + keys = ["std_channel_{}".format(i) for i in range(len(stds))] + return dict(zip(keys, stds)) + + +def max_pixels_per_channel( + img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 +) -> Union[Tuple[float, float, float], Tuple[float]]: + """ + Return the max pixel value per image channel + :param img: An image represented as a numpy array or a torch tensor. + Assumptions: + - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) + tensor/array + - the image has 3 or 1 channels + :return: Tuple containing the max pixel values: + - 3 floats if image has 3 channels + - 1 float if image has 1 channel + """ + img_numpy = _assert_numpy_image(img) + num_dims, channel_dim = _check_valid_image(img) + dims = numpy.arange(0, num_dims, 1) + dims = numpy.delete(dims, channel_dim) + return tuple(numpy.max(img_numpy, axis=tuple(dims))) + + +def fraction_zeros(img: Union[numpy.ndarray, "torch.tensor"]) -> float: # noqa F821 + """ + Return the float the represents the fraction of zeros in the + image tensor/array + + :param img: An image represented as a numpy array or a torch tensor. + Assumptions: + - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) + tensor/array + - the image has 3 or 1 channels + :return: A float in range from 0. to 1. + """ + image_numpy = _assert_numpy_image(img) + _check_valid_image(image_numpy) + return (image_numpy.size - numpy.count_nonzero(image_numpy)) / image_numpy.size + +def num_classes_predicted + +def bounding_box_count(bboxes: List[List[Optional[List[float]]]]) -> Dict[int, int]: + """ + Extract the number of bounding boxes from the (nested) list of bbox corners + + :param bboxes: A (nested) list, where the leaf list has length four and contains + float values (top left and bottom right coordinates of the bounding box corners) + :return: Dictionary, where the keys are image indices within + a batch and the values are the bbox counts + """ + if not bboxes or _is_nested_list_empty(bboxes): + return 0 + + if not (isinstance(bboxes[0][0][0], float) and len(bboxes[0][0]) == 4): + raise ValueError( + "A valid argument `bboxes` should be of " + "type: List[List[Optional[List[float]]]])." + ) + + bboxes_count = {} + for batch_idx, bboxes_ in enumerate(bboxes): + num_bboxes = len(bboxes_) + bboxes_count[batch_idx] = num_bboxes + + return bboxes_count + + +def _check_valid_image(img: numpy.ndarray) -> Tuple[int, int]: + num_dims = img.ndim + if num_dims == 4: + img = img[0] + + channel_dim = [i for i, dim in enumerate(img.shape) if (dim == 1) or (dim == 3)] + + if img.ndim != 3: + raise ValueError( + "A valid image must have three or four (incl. batch dimension) dimensions" + ) + + if len(channel_dim) != 1: + raise ValueError( + "Could not infer a channel dimension from the image tensor/array" + ) + + channel_dim = channel_dim[0] + return num_dims, channel_dim if num_dims == 3 else channel_dim + 1 + + +def _assert_numpy_image( + img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 +) -> numpy.ndarray: + if hasattr(img, "numpy"): + img = img.numpy() + return img + + +def _is_nested_list_empty(nested_list: List) -> bool: + if not nested_list: + return True + if isinstance(nested_list[0], list): + return _is_nested_list_empty(nested_list[0]) + return False From dae5b690cd7b59edaafc2c92fcf84d03d2ffface Mon Sep 17 00:00:00 2001 From: "bogunowicz@arrival.com" Date: Wed, 11 Jan 2023 15:43:33 +0100 Subject: [PATCH 2/6] initial commit --- .../loggers/metric_functions/__init__.py | 3 +- .../loggers/metric_functions/built_ins.py | 180 +-------------- .../loggers/metric_functions/cv/__init__.py | 17 ++ .../loggers/metric_functions/cv/built_ins.py | 195 ++++++++++------ src/deepsparse/yolo/schemas.py | 4 +- .../loggers/metric_functions/cv/__init__.py | 13 ++ .../metric_functions/cv/test_built_ins.py | 216 ++++++++++++++++++ .../metric_functions/test_built_ins.py | 153 ------------- 8 files changed, 384 insertions(+), 397 deletions(-) create mode 100644 tests/deepsparse/loggers/metric_functions/cv/__init__.py create mode 100644 tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py delete mode 100644 tests/deepsparse/loggers/metric_functions/test_built_ins.py diff --git a/src/deepsparse/loggers/metric_functions/__init__.py b/src/deepsparse/loggers/metric_functions/__init__.py index 425ec06191..3901bb6ec4 100644 --- a/src/deepsparse/loggers/metric_functions/__init__.py +++ b/src/deepsparse/loggers/metric_functions/__init__.py @@ -13,4 +13,5 @@ # limitations under the License. # flake8: noqa -from src.deepsparse.loggers.metric_functions.cv.built_ins import * +from .built_ins import * +from .cv import * diff --git a/src/deepsparse/loggers/metric_functions/built_ins.py b/src/deepsparse/loggers/metric_functions/built_ins.py index 3dd18b529c..865ca88c58 100644 --- a/src/deepsparse/loggers/metric_functions/built_ins.py +++ b/src/deepsparse/loggers/metric_functions/built_ins.py @@ -12,22 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -The set of all the built-in metric functions +The set of the general built-in metric functions """ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any -import numpy - -__all__ = [ - "identity", - "image_shape", - "mean_pixels_per_channel", - "std_pixels_per_channel", - "max_pixels_per_channel", - "fraction_zeros", - "bounding_box_count", -] +__all__ = ["identity"] def identity(x: Any): @@ -38,167 +28,3 @@ def identity(x: Any): :return: The same object """ return x - - -def image_shape( - img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 -) -> Tuple[int, int, int]: - """ - Return the shape of the image. - - :param img: An image represented as a numpy array or a torch tensor. - Assumptions: - - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) - tensor/array - - the image has 3 or 1 channels - :return: Tuple containing the image shape; three integers - """ - img_numpy = _assert_numpy_image(img) - num_dims, _ = _check_valid_image(img_numpy) - if num_dims == 4: - img_numpy = img_numpy[0] - return img_numpy.shape - - -def mean_pixels_per_channel( - img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 -) -> Union[Tuple[float, float, float], Tuple[float]]: - """ - Return the mean pixel value per image channel - - :param img: An image represented as a numpy array or a torch tensor. - Assumptions: - - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) - tensor/array - - the image has 3 or 1 channels - :return: Tuple containing the mean pixel values: - - 3 floats if image has 3 channels - - 1 float if image has 1 channel - """ - img_numpy = _assert_numpy_image(img) - num_dims, channel_dim = _check_valid_image(img_numpy) - dims = numpy.arange(0, num_dims, 1) - dims = numpy.delete(dims, channel_dim) - return tuple(numpy.mean(img_numpy, axis=tuple(dims))) - - -def std_pixels_per_channel( - img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 -) -> Union[Tuple[float, float, float], Tuple[float]]: - """ - Return the standard deviation of pixel values per image channel - :param img: An image represented as a numpy array or a torch tensor. - Assumptions: - - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) - tensor/array - - the image has 3 or 1 channels - :return: Tuple containing the standard deviation of pixel values: - - 3 floats if image has 3 channels - - 1 float if image has 1 channel - """ - img_numpy = _assert_numpy_image(img) - num_dims, channel_dim = _check_valid_image(img) - dims = numpy.arange(0, num_dims, 1) - dims = numpy.delete(dims, channel_dim) - return tuple(numpy.std(img_numpy, axis=tuple(dims))) - - -def max_pixels_per_channel( - img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 -) -> Union[Tuple[float, float, float], Tuple[float]]: - """ - Return the max pixel value per image channel - :param img: An image represented as a numpy array or a torch tensor. - Assumptions: - - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) - tensor/array - - the image has 3 or 1 channels - :return: Tuple containing the max pixel values: - - 3 floats if image has 3 channels - - 1 float if image has 1 channel - """ - img_numpy = _assert_numpy_image(img) - num_dims, channel_dim = _check_valid_image(img) - dims = numpy.arange(0, num_dims, 1) - dims = numpy.delete(dims, channel_dim) - return tuple(numpy.max(img_numpy, axis=tuple(dims))) - - -def fraction_zeros(img: Union[numpy.ndarray, "torch.tensor"]) -> float: # noqa F821 - """ - Return the float the represents the fraction of zeros in the - image tensor/array - - :param img: An image represented as a numpy array or a torch tensor. - Assumptions: - - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) - tensor/array - - the image has 3 or 1 channels - :return: A float in range from 0. to 1. - """ - image_numpy = _assert_numpy_image(img) - _check_valid_image(image_numpy) - return (image_numpy.size - numpy.count_nonzero(image_numpy)) / image_numpy.size - - -def bounding_box_count(bboxes: List[List[Optional[List[float]]]]) -> Dict[int, int]: - """ - Extract the number of bounding boxes from the (nested) list of bbox corners - - :param bboxes: A (nested) list, where the leaf list has length four and contains - float values (top left and bottom right coordinates of the bounding box corners) - :return: Dictionary, where the keys are image indices within - a batch and the values are the bbox counts - """ - if not bboxes or _is_nested_list_empty(bboxes): - return 0 - - if not (isinstance(bboxes[0][0][0], float) and len(bboxes[0][0]) == 4): - raise ValueError( - "A valid argument `bboxes` should be of " - "type: List[List[Optional[List[float]]]])." - ) - - bboxes_count = {} - for batch_idx, bboxes_ in enumerate(bboxes): - num_bboxes = len(bboxes_) - bboxes_count[batch_idx] = num_bboxes - - return bboxes_count - - -def _check_valid_image(img: numpy.ndarray) -> Tuple[int, int]: - num_dims = img.ndim - if num_dims == 4: - img = img[0] - - channel_dim = [i for i, dim in enumerate(img.shape) if (dim == 1) or (dim == 3)] - - if img.ndim != 3: - raise ValueError( - "A valid image must have three or four (incl. batch dimension) dimensions" - ) - - if len(channel_dim) != 1: - raise ValueError( - "Could not infer a channel dimension from the image tensor/array" - ) - - channel_dim = channel_dim[0] - return num_dims, channel_dim if num_dims == 3 else channel_dim + 1 - - -def _assert_numpy_image( - img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 -) -> numpy.ndarray: - if hasattr(img, "numpy"): - img = img.numpy() - return img - - -def _is_nested_list_empty(nested_list: List) -> bool: - if not nested_list: - return True - if isinstance(nested_list[0], list): - return _is_nested_list_empty(nested_list[0]) - return False diff --git a/src/deepsparse/loggers/metric_functions/cv/__init__.py b/src/deepsparse/loggers/metric_functions/cv/__init__.py index e69de29bb2..9a29ee8d11 100644 --- a/src/deepsparse/loggers/metric_functions/cv/__init__.py +++ b/src/deepsparse/loggers/metric_functions/cv/__init__.py @@ -0,0 +1,17 @@ +# flake8: noqa + +# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .built_ins import * diff --git a/src/deepsparse/loggers/metric_functions/cv/built_ins.py b/src/deepsparse/loggers/metric_functions/cv/built_ins.py index da3084c63d..15fc299e74 100644 --- a/src/deepsparse/loggers/metric_functions/cv/built_ins.py +++ b/src/deepsparse/loggers/metric_functions/cv/built_ins.py @@ -14,32 +14,24 @@ """ The set of all the built-in metric functions """ -from typing import Any, Dict, List, Optional, Tuple, Union +from collections import Counter +from typing import Dict, List, Optional, Tuple, Union import numpy __all__ = [ - "identity", "image_shape", "mean_pixels_per_channel", "std_pixels_per_channel", - "max_pixels_per_channel", + "mean_score_per_detection", + "std_score_per_detection", "fraction_zeros", - "bounding_box_count", + "count_classes_detected", + "count_number_objects_detected", ] -def identity(x: Any): - """ - Simple identity function - - :param x: Any object - :return: The same object - """ - return x - - def image_shape( img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 ) -> Dict[str, int]: @@ -51,14 +43,22 @@ def image_shape( - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) tensor/array - the image has 3 or 1 channels - :return: Dictionary that maps "height", "width", "channels" keys - to the appropriate integers + :return: Dictionary that maps "dim_0", "dim_1" and "channels" keys to the + appropriate integers """ img_numpy = _assert_numpy_image(img) - num_dims, _ = _check_valid_image(img_numpy) + num_dims, channel_dim = _check_valid_image(img_numpy) if num_dims == 4: img_numpy = img_numpy[0] - return dict(zip(["height", "width", "channels"], img_numpy.shape)) + channel_dim -= 1 + + result = {"channels": img_numpy.shape[channel_dim]} + dims_counter = 0 + for index, dim in enumerate(img_numpy.shape): + if index != channel_dim: + result[f"dim_{dims_counter}"] = dim + dims_counter += 1 + return result def mean_pixels_per_channel( @@ -104,27 +104,6 @@ def std_pixels_per_channel( return dict(zip(keys, stds)) -def max_pixels_per_channel( - img: Union[numpy.ndarray, "torch.tensor"] # noqa F821 -) -> Union[Tuple[float, float, float], Tuple[float]]: - """ - Return the max pixel value per image channel - :param img: An image represented as a numpy array or a torch tensor. - Assumptions: - - 3 dimensional or 4 dimensional (num_batches in zeroth dimension) - tensor/array - - the image has 3 or 1 channels - :return: Tuple containing the max pixel values: - - 3 floats if image has 3 channels - - 1 float if image has 1 channel - """ - img_numpy = _assert_numpy_image(img) - num_dims, channel_dim = _check_valid_image(img) - dims = numpy.arange(0, num_dims, 1) - dims = numpy.delete(dims, channel_dim) - return tuple(numpy.max(img_numpy, axis=tuple(dims))) - - def fraction_zeros(img: Union[numpy.ndarray, "torch.tensor"]) -> float: # noqa F821 """ Return the float the represents the fraction of zeros in the @@ -141,32 +120,128 @@ def fraction_zeros(img: Union[numpy.ndarray, "torch.tensor"]) -> float: # noqa _check_valid_image(image_numpy) return (image_numpy.size - numpy.count_nonzero(image_numpy)) / image_numpy.size -def num_classes_predicted -def bounding_box_count(bboxes: List[List[Optional[List[float]]]]) -> Dict[int, int]: +def count_classes_detected( + classes: List[List[Optional[Union[int, str]]]] +) -> Dict[str, int]: + """ + Count the number of unique classes detected in the image batch + + :param classes: A nested list, where: + - first level is the batch information + - second level is the sample information. Can be + either `None` (no detection) or an integer/string + representation of the class label + :return: Dictionary, where the keys are class labels + and the values are their counts across the batch """ - Extract the number of bounding boxes from the (nested) list of bbox corners + _check_valid_classes(classes) + detected_classes = [] + for sample_idx, sample in enumerate(classes): + if sample != [None]: + detected_classes += sample + counter = Counter(detected_classes) + # convert keys to strings if required + counter = {str(class_label): count for class_label, count in counter.items()} + return counter + + +def count_number_objects_detected( + classes: List[List[Optional[Union[int, str]]]] +) -> Dict[str, int]: + """ + Count the number of successful detections per image - :param bboxes: A (nested) list, where the leaf list has length four and contains - float values (top left and bottom right coordinates of the bounding box corners) + :param classes: A nested list, where: + - first level is the batch information + - second level is the sample information. Can be + either `None` (no detection) or an integer/string + representation of the class label :return: Dictionary, where the keys are image indices within - a batch and the values are the bbox counts + a batch and the values are the number of detected objects per image """ - if not bboxes or _is_nested_list_empty(bboxes): - return 0 + _check_valid_classes(classes) - if not (isinstance(bboxes[0][0][0], float) and len(bboxes[0][0]) == 4): - raise ValueError( - "A valid argument `bboxes` should be of " - "type: List[List[Optional[List[float]]]])." + number_objects_per_batch = {} + for sample_idx, sample in enumerate(classes): + number_objects_per_batch[str(sample_idx)] = ( + 0 if sample == [None] else len(sample) ) - bboxes_count = {} - for batch_idx, bboxes_ in enumerate(bboxes): - num_bboxes = len(bboxes_) - bboxes_count[batch_idx] = num_bboxes + return number_objects_per_batch + + +def mean_score_per_detection(scores: List[List[Optional[float]]]) -> Dict[str, float]: + """ + Return the mean score per detection + + :param scores: A nested list, where: + - first level is the batch information + - second level is the sample information. Can be + either `None` (no detection) or a float score + :return: Dictionary, where the keys are image indices within + a batch and the values are the mean score per detection + """ + _check_valid_scores(scores) + + mean_scores_per_batch = {} + for sample_idx, sample in enumerate(scores): + if sample == [None]: + mean_scores_per_batch[str(sample_idx)] = 0.0 + else: + mean_scores_per_batch[str(sample_idx)] = numpy.mean(sample) + + return mean_scores_per_batch + + +def std_score_per_detection(scores: List[List[Optional[float]]]) -> Dict[str, float]: + """ + Return the standard deviation of scores per detection - return bboxes_count + :param scores: A nested list, where: + - first level is the batch information + - second level is the sample information. Can be + either `None` (no detection) or a float score + :return: Dictionary, where the keys are image indices within + a batch and the values are the standard deviation of scores per detection + """ + _check_valid_scores(scores) + + std_scores_per_batch = {} + for sample_idx, sample in enumerate(scores): + if sample == [None]: + std_scores_per_batch[str(sample_idx)] = 0.0 + else: + std_scores_per_batch[str(sample_idx)] = numpy.std(sample) + + return std_scores_per_batch + + +def _check_valid_classes(classes: List[List[Optional[Union[int, str]]]]): + for sample in classes: + if ( + all(isinstance(i, int) for i in sample) + or all(isinstance(i, str) for i in sample) + or sample == [None] + ): + pass + else: + raise ValueError( + "Detection for a sample must be either a " + "list of integers or a list of strings or a list " + "with a single `None` value" + ) + + +def _check_valid_scores(scores: List[List[Optional[float]]]): + for sample in scores: + if all(isinstance(i, float) for i in sample) or sample == [None]: + pass + else: + raise ValueError( + "Scores for a sample must be either a " + "list of floats or a list with a single `None` value" + ) def _check_valid_image(img: numpy.ndarray) -> Tuple[int, int]: @@ -196,11 +271,3 @@ def _assert_numpy_image( if hasattr(img, "numpy"): img = img.numpy() return img - - -def _is_nested_list_empty(nested_list: List) -> bool: - if not nested_list: - return True - if isinstance(nested_list[0], list): - return _is_nested_list_empty(nested_list[0]) - return False diff --git a/src/deepsparse/yolo/schemas.py b/src/deepsparse/yolo/schemas.py index e97d7f26f7..2b4872c0f1 100644 --- a/src/deepsparse/yolo/schemas.py +++ b/src/deepsparse/yolo/schemas.py @@ -67,7 +67,7 @@ class YOLOOutput(BaseModel): scores: List[List[float]] = Field( description="List of scores, one for each prediction" ) - labels: List[List[str]] = Field( + classes: List[List[str]] = Field( description="List of labels, one for each prediction" ) @@ -78,7 +78,7 @@ def __getitem__(self, index): return _YOLOImageOutput( self.boxes[index], self.scores[index], - self.labels[index], + self.classes[index], ) def __iter__(self): diff --git a/tests/deepsparse/loggers/metric_functions/cv/__init__.py b/tests/deepsparse/loggers/metric_functions/cv/__init__.py new file mode 100644 index 0000000000..0c44f887a4 --- /dev/null +++ b/tests/deepsparse/loggers/metric_functions/cv/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py b/tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py new file mode 100644 index 0000000000..318e4f8d8f --- /dev/null +++ b/tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py @@ -0,0 +1,216 @@ +# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy + +import pytest +from deepsparse.loggers.metric_functions import ( + count_classes_detected, + count_number_objects_detected, + fraction_zeros, + image_shape, + mean_pixels_per_channel, + mean_score_per_detection, + std_pixels_per_channel, + std_score_per_detection, +) + + +def _generate_array_and_fill_with_n_zeros(fill_value, shape, n_zeros): + array = numpy.full(fill_value=fill_value, shape=shape) + array = array.flatten() + array[:n_zeros] = 0.0 + array = numpy.reshape(array, shape) + return array + + +BBOX = [500.0, 500.0, 400.0, 400.0] + + +@pytest.mark.parametrize( + "image, expected_shape", + [ + (numpy.random.rand(2, 3, 16, 16), {"channels": 3, "dim_0": 16, "dim_1": 16}), + (numpy.random.rand(2, 16, 16, 3), {"channels": 3, "dim_0": 16, "dim_1": 16}), + (numpy.random.rand(16, 16, 1), {"channels": 1, "dim_0": 16, "dim_1": 16}), + (numpy.random.rand(16, 1, 16, 15), {"channels": 1, "dim_0": 16, "dim_1": 15}), + ], +) +def test_image_shape(image, expected_shape): + assert expected_shape == image_shape(image) + + +@pytest.mark.parametrize( + "image, expected_means", + [ + ( + numpy.full(fill_value=0.3, shape=(16, 3, 16, 16)), + numpy.full(fill_value=0.3, shape=3), + ), + ( + numpy.full(fill_value=0.3, shape=(2, 1, 16, 16)), + numpy.full(fill_value=0.3, shape=1), + ), + ( + numpy.full(fill_value=0.5, shape=(1, 16, 16)), + numpy.full(fill_value=0.5, shape=1), + ), + ( + numpy.full(fill_value=0.5, shape=(3, 16, 16)), + numpy.full(fill_value=0.5, shape=3), + ), + ( + numpy.full(fill_value=0.5, shape=(16, 16, 1)), + numpy.full(fill_value=0.5, shape=1), + ), + ( + numpy.full(fill_value=0.5, shape=(16, 16, 3)), + numpy.full(fill_value=0.5, shape=3), + ), + ( + numpy.full(fill_value=0.5, shape=(16, 16, 15, 3)), + numpy.full(fill_value=0.5, shape=3), + ), + ( + numpy.full(fill_value=0.5, shape=(16, 15, 16, 3)), + numpy.full(fill_value=0.5, shape=3), + ), + ], +) +def test_mean_pixel_per_channel(image, expected_means): + result = mean_pixels_per_channel(image) + for mean, expected_mean in zip(result.values(), expected_means): + numpy.testing.assert_allclose(mean, expected_mean) + assert list(result.keys()) == [ + f"mean_channel_{idx}" for idx in range(len(expected_means)) + ] + + +@pytest.mark.parametrize( + "image, expected_stds", + [ + (numpy.full(fill_value=0.3, shape=(2, 3, 16, 16)), numpy.zeros(3)), + (numpy.full(fill_value=0.3, shape=(2, 1, 16, 16)), numpy.zeros(1)), + (numpy.full(fill_value=0.5, shape=(1, 16, 16)), numpy.zeros(1)), + (numpy.full(fill_value=0.5, shape=(16, 16, 1)), numpy.zeros(1)), + (numpy.full(fill_value=0.5, shape=(3, 16, 16)), numpy.zeros(3)), + ], +) +def test_std_pixels_per_channel(image, expected_stds): + result = std_pixels_per_channel(image) + for std, expected_std in zip(result.values(), expected_stds): + numpy.testing.assert_allclose(std, expected_std, atol=1e-16) + assert list(result.keys()) == [ + f"std_channel_{idx}" for idx in range(len(expected_stds)) + ] + + +@pytest.mark.parametrize( + "image, expected_percentage", + [ + ( + _generate_array_and_fill_with_n_zeros( + fill_value=0.5, shape=(2, 16, 16, 3), n_zeros=0 + ), + 0.0, + ), + ( + _generate_array_and_fill_with_n_zeros( + fill_value=120, shape=(3, 3, 16, 16), n_zeros=3 * 3 * 16 * 8 + ), + 0.5, + ), + ( + _generate_array_and_fill_with_n_zeros( + fill_value=0.1, shape=(16, 16, 1), n_zeros=16 * 16 * 1 + ), + 1.0, + ), + ], +) +def test_percentage_zeros_per_channel(image, expected_percentage): + assert expected_percentage == fraction_zeros(image) + + +@pytest.mark.parametrize( + "classes, expected_count_classes, should_raise_error", + [ + ([[None], [0, 1, 3], [3, 3, 0]], {"0": 2, "1": 1, "3": 3}, False), + ([[None], [None]], {}, False), + ( + [["foo", "bar"], ["foo", "bar", "alice"]], + {"foo": 2, "bar": 2, "alice": 1}, + False, + ), + ( + [["foo", "bar"], ["foo", "bar", "alice"], [None]], + {"foo": 2, "bar": 2, "alice": 1}, + False, + ), + ([[6.666], [0, 1, 3], [3, 3, 0]], None, True), + ([[None, None], [0, 1, 3], [3, 3, 0]], None, True), + ], +) +def test_count_classes_detected(classes, expected_count_classes, should_raise_error): + if should_raise_error: + with pytest.raises(ValueError): + count_classes_detected(classes) + return + assert expected_count_classes == count_classes_detected(classes) + + +@pytest.mark.parametrize( + "classes, expected_count_classes", + [ + ([[None], [0, 1, 3], [3, 3, 0]], {"0": 0, "1": 3, "2": 3}), + ([[None], [None]], {"0": 0, "1": 0}), + ([["foo", "bar"], ["foo", "bar", "alice"], ["bar"]], {"0": 2, "1": 3, "2": 1}), + ([["foo", "bar"], ["foo", "bar", "alice"], [None]], {"0": 2, "1": 3, "2": 0}), + ], +) +def test_count_number_objects_detected(classes, expected_count_classes): + assert expected_count_classes == count_number_objects_detected(classes) + + +@pytest.mark.parametrize( + "scores, expected_mean_score", + [ + ([[None], [0.5, 0.5, 0.5], [0.3, 0.3, 0.3]], {"0": 0.0, "1": 0.5, "2": 0.3}), + ([[None], [None]], {"0": 0.0, "1": 0.0}), + ([[0.5, 0.5], [0.9, 0.9, 0.9], [1.0]], {"0": 0.5, "1": 0.9, "2": 1.0}), + ([[1.0, 0.0], [None]], {"0": 0.5, "1": 0.0}), + ], +) +def test_mean_score_per_detection(scores, expected_mean_score): + assert expected_mean_score == mean_score_per_detection(scores) + + +@pytest.mark.parametrize( + "scores, expected_std_score", + [ + ( + [[None], [0.5, 0.5, 0.5], [0.6, 0.7, 0.8]], + {"0": 0.0, "1": 0.0, "2": 0.08164}, + ), + ([[None], [None]], {"0": 0.0, "1": 0.0}), + ([[1.0, 0.0], [None]], {"0": 0.5, "1": 0.0}), + ], +) +def test_std_score_per_detection(scores, expected_std_score): + result = std_score_per_detection(scores) + for (result_keys, results_values), (keys, values) in zip( + result.items(), expected_std_score.items() + ): + numpy.testing.assert_allclose(results_values, values, atol=1e-5) + assert result_keys == keys diff --git a/tests/deepsparse/loggers/metric_functions/test_built_ins.py b/tests/deepsparse/loggers/metric_functions/test_built_ins.py deleted file mode 100644 index be01f10e62..0000000000 --- a/tests/deepsparse/loggers/metric_functions/test_built_ins.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy - -import pytest -from deepsparse.loggers.metric_functions import ( - bounding_box_count, - fraction_zeros, - image_shape, - max_pixels_per_channel, - mean_pixels_per_channel, - std_pixels_per_channel, -) - - -def _generate_array_and_fill_with_n_zeros(fill_value, shape, n_zeros): - array = numpy.full(fill_value=fill_value, shape=shape) - array = array.flatten() - array[:n_zeros] = 0.0 - array = numpy.reshape(array, shape) - return array - - -BBOX = [500.0, 500.0, 400.0, 400.0] - - -@pytest.mark.parametrize( - "image, expected_shape", - [ - (numpy.random.rand(2, 3, 16, 16), (3, 16, 16)), - (numpy.random.rand(2, 16, 16, 3), (16, 16, 3)), - (numpy.random.rand(16, 16, 1), (16, 16, 1)), - ], -) -def test_image_shape(image, expected_shape): - assert expected_shape == image_shape(image) - - -@pytest.mark.parametrize( - "image, expected_means", - [ - ( - numpy.full(fill_value=0.3, shape=(2, 3, 16, 16)), - numpy.full(fill_value=0.3, shape=3), - ), - ( - numpy.full(fill_value=0.3, shape=(2, 1, 16, 16)), - numpy.full(fill_value=0.3, shape=1), - ), - ( - numpy.full(fill_value=0.5, shape=(1, 16, 16)), - numpy.full(fill_value=0.5, shape=1), - ), - ( - numpy.full(fill_value=0.5, shape=(16, 16, 1)), - numpy.full(fill_value=0.5, shape=1), - ), - ( - numpy.full(fill_value=0.5, shape=(3, 16, 16)), - numpy.full(fill_value=0.5, shape=3), - ), - ], -) -def test_mean_pixel_per_channel(image, expected_means): - numpy.testing.assert_allclose(expected_means, mean_pixels_per_channel(image)) - - -@pytest.mark.parametrize( - "image, expected_stds", - [ - (numpy.full(fill_value=0.3, shape=(2, 3, 16, 16)), numpy.zeros(3)), - (numpy.full(fill_value=0.3, shape=(2, 1, 16, 16)), numpy.zeros(1)), - (numpy.full(fill_value=0.5, shape=(1, 16, 16)), numpy.zeros(1)), - (numpy.full(fill_value=0.5, shape=(16, 16, 1)), numpy.zeros(1)), - (numpy.full(fill_value=0.5, shape=(3, 16, 16)), numpy.zeros(3)), - ], -) -def test_std_pixels_per_channel(image, expected_stds): - numpy.testing.assert_allclose( - expected_stds, std_pixels_per_channel(image), atol=1e-16 - ) - - -@pytest.mark.parametrize( - "image, expected_max", - [ - ( - numpy.full(fill_value=0.3, shape=(2, 3, 16, 16)), - numpy.full(fill_value=0.3, shape=3), - ), - ( - numpy.full(fill_value=0.5, shape=(3, 16, 16)), - numpy.full(fill_value=0.5, shape=3), - ), - ( - numpy.full(fill_value=120, shape=(16, 16, 1)), - numpy.full(fill_value=120, shape=1), - ), - ], -) -def test_max_pixels_per_channel(image, expected_max): - numpy.testing.assert_allclose( - expected_max, max_pixels_per_channel(image), atol=1e-16 - ) - - -@pytest.mark.parametrize( - "image, expected_percentage", - [ - (_generate_array_and_fill_with_n_zeros(0.5, (2, 16, 16, 3), 0), 0.0), - ( - _generate_array_and_fill_with_n_zeros(120, (3, 3, 16, 16), 3 * 3 * 16 * 8), - 0.5, - ), - (_generate_array_and_fill_with_n_zeros(0.1, (16, 16, 1), 16 * 16 * 1), 1.0), - ], -) -def test_percentage_zeros_per_channel(image, expected_percentage): - assert expected_percentage == fraction_zeros(image) - - -@pytest.mark.parametrize( - "bboxes, expected_num_bboxes, should_raise_error", - [ - ([[]], 0, False), - ([], 0, False), - ([[BBOX, BBOX, BBOX], [BBOX, BBOX, BBOX, BBOX]], {0: 3, 1: 4}, False), - ([[BBOX, BBOX, BBOX]], {0: 3}, False), - ([[[10, 10, 10, 10]]], None, True), - ([[BBOX[:3]]], None, True), - ([["string"]], None, True), - (["string"], None, True), - ], -) -def test_num_bounding_boxes(bboxes, expected_num_bboxes, should_raise_error): - if should_raise_error: - with pytest.raises(ValueError): - bounding_box_count(bboxes) - return - - assert expected_num_bboxes == bounding_box_count(bboxes) From eabc5039aff2d2c194f10110f7a8882aaabb6007 Mon Sep 17 00:00:00 2001 From: dbogunowicz <97082108+dbogunowicz@users.noreply.github.com> Date: Thu, 12 Jan 2023 09:15:58 +0100 Subject: [PATCH 3/6] Update src/deepsparse/loggers/metric_functions/cv/built_ins.py Co-authored-by: Benjamin Fineran --- src/deepsparse/loggers/metric_functions/cv/built_ins.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/deepsparse/loggers/metric_functions/cv/built_ins.py b/src/deepsparse/loggers/metric_functions/cv/built_ins.py index 15fc299e74..a0ac022964 100644 --- a/src/deepsparse/loggers/metric_functions/cv/built_ins.py +++ b/src/deepsparse/loggers/metric_functions/cv/built_ins.py @@ -136,11 +136,9 @@ def count_classes_detected( and the values are their counts across the batch """ _check_valid_classes(classes) - detected_classes = [] - for sample_idx, sample in enumerate(classes): - if sample != [None]: - detected_classes += sample - counter = Counter(detected_classes) + counter = Counter() + for detections in classes: + counter.update(detections) # convert keys to strings if required counter = {str(class_label): count for class_label, count in counter.items()} return counter From 24c6e002899822dd2125051d51993a127546a931 Mon Sep 17 00:00:00 2001 From: dbogunowicz <97082108+dbogunowicz@users.noreply.github.com> Date: Thu, 12 Jan 2023 09:16:27 +0100 Subject: [PATCH 4/6] Update src/deepsparse/loggers/metric_functions/cv/built_ins.py Co-authored-by: Benjamin Fineran --- src/deepsparse/loggers/metric_functions/cv/built_ins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deepsparse/loggers/metric_functions/cv/built_ins.py b/src/deepsparse/loggers/metric_functions/cv/built_ins.py index a0ac022964..35ab538ac1 100644 --- a/src/deepsparse/loggers/metric_functions/cv/built_ins.py +++ b/src/deepsparse/loggers/metric_functions/cv/built_ins.py @@ -145,7 +145,7 @@ def count_classes_detected( def count_number_objects_detected( - classes: List[List[Optional[Union[int, str]]]] + classes: List[List[Union[int, str, None]]] ) -> Dict[str, int]: """ Count the number of successful detections per image From 2766bf86fa39978ec6633a2761aa0315026d9de7 Mon Sep 17 00:00:00 2001 From: "bogunowicz@arrival.com" Date: Thu, 12 Jan 2023 10:19:31 +0100 Subject: [PATCH 5/6] PR changes --- src/deepsparse/loggers/helpers.py | 2 +- .../loggers/metric_functions/__init__.py | 2 +- .../{cv => computer_vision}/__init__.py | 3 +- .../{cv => computer_vision}/built_ins.py | 177 +++++++++--------- src/deepsparse/yolo/schemas.py | 4 +- .../{cv => computer_vision}/__init__.py | 0 .../{cv => computer_vision}/test_built_ins.py | 24 +-- 7 files changed, 109 insertions(+), 103 deletions(-) rename src/deepsparse/loggers/metric_functions/{cv => computer_vision}/__init__.py (99%) rename src/deepsparse/loggers/metric_functions/{cv => computer_vision}/built_ins.py (60%) rename tests/deepsparse/loggers/metric_functions/{cv => computer_vision}/__init__.py (100%) rename tests/deepsparse/loggers/metric_functions/{cv => computer_vision}/test_built_ins.py (91%) diff --git a/src/deepsparse/loggers/helpers.py b/src/deepsparse/loggers/helpers.py index bf83009cee..8fdad639c3 100644 --- a/src/deepsparse/loggers/helpers.py +++ b/src/deepsparse/loggers/helpers.py @@ -23,7 +23,7 @@ import numpy -import deepsparse.loggers.metric_functions.built_ins as built_ins +import deepsparse.loggers.metric_functions as built_ins from deepsparse.loggers import MetricCategories diff --git a/src/deepsparse/loggers/metric_functions/__init__.py b/src/deepsparse/loggers/metric_functions/__init__.py index 3901bb6ec4..562b5c2531 100644 --- a/src/deepsparse/loggers/metric_functions/__init__.py +++ b/src/deepsparse/loggers/metric_functions/__init__.py @@ -14,4 +14,4 @@ # flake8: noqa from .built_ins import * -from .cv import * +from .computer_vision import * diff --git a/src/deepsparse/loggers/metric_functions/cv/__init__.py b/src/deepsparse/loggers/metric_functions/computer_vision/__init__.py similarity index 99% rename from src/deepsparse/loggers/metric_functions/cv/__init__.py rename to src/deepsparse/loggers/metric_functions/computer_vision/__init__.py index 9a29ee8d11..ef2739def2 100644 --- a/src/deepsparse/loggers/metric_functions/cv/__init__.py +++ b/src/deepsparse/loggers/metric_functions/computer_vision/__init__.py @@ -1,5 +1,3 @@ -# flake8: noqa - # Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,4 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. +# flake8: noqa from .built_ins import * diff --git a/src/deepsparse/loggers/metric_functions/cv/built_ins.py b/src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py similarity index 60% rename from src/deepsparse/loggers/metric_functions/cv/built_ins.py rename to src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py index 35ab538ac1..51b1eb13ef 100644 --- a/src/deepsparse/loggers/metric_functions/cv/built_ins.py +++ b/src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py @@ -27,8 +27,8 @@ "mean_score_per_detection", "std_score_per_detection", "fraction_zeros", - "count_classes_detected", - "count_number_objects_detected", + "detected_classes", + "number_detected_objects", ] @@ -79,7 +79,7 @@ def mean_pixels_per_channel( dims = numpy.arange(0, num_dims, 1) dims = numpy.delete(dims, channel_dim) means = numpy.mean(img_numpy, axis=tuple(dims)) - keys = ["mean_channel_{}".format(i) for i in range(len(means))] + keys = ["channel_{}".format(i) for i in range(len(means))] return dict(zip(keys, means)) @@ -100,7 +100,7 @@ def std_pixels_per_channel( dims = numpy.arange(0, num_dims, 1) dims = numpy.delete(dims, channel_dim) stds = tuple(numpy.std(img_numpy, axis=tuple(dims))) - keys = ["std_channel_{}".format(i) for i in range(len(stds))] + keys = ["channel_{}".format(i) for i in range(len(stds))] return dict(zip(keys, stds)) @@ -121,125 +121,132 @@ def fraction_zeros(img: Union[numpy.ndarray, "torch.tensor"]) -> float: # noqa return (image_numpy.size - numpy.count_nonzero(image_numpy)) / image_numpy.size -def count_classes_detected( - classes: List[List[Optional[Union[int, str]]]] +def detected_classes( + detected_classes: List[List[Union[int, str, None]]] ) -> Dict[str, int]: """ Count the number of unique classes detected in the image batch - :param classes: A nested list, where: - - first level is the batch information - - second level is the sample information. Can be - either `None` (no detection) or an integer/string - representation of the class label + :param detected_classes: A nested list, that contains + the detected classes for each sample in the batch. + Every element of the inner list pertains to one + set of detections in the batch. The inner list can + either contain a single `None` value (no detection) + or a number of integer/string representation of the + detected classes. :return: Dictionary, where the keys are class labels and the values are their counts across the batch """ - _check_valid_classes(classes) counter = Counter() - for detections in classes: - counter.update(detections) + for detection in detected_classes: + _check_valid_detection(detection) + counter.update(detection) # convert keys to strings if required counter = {str(class_label): count for class_label, count in counter.items()} return counter -def count_number_objects_detected( - classes: List[List[Union[int, str, None]]] +def number_detected_objects( + detected_classes: List[List[Union[int, str, None]]] ) -> Dict[str, int]: """ - Count the number of successful detections per image - - :param classes: A nested list, where: - - first level is the batch information - - second level is the sample information. Can be - either `None` (no detection) or an integer/string - representation of the class label + Count the number of successful detections per sample + + :param detected_classes: A nested list, that contains + the detected classes for each sample in the batch. + Every element of the inner list pertains to one + set of detections in the batch. The inner list can + either contain a single `None` value (no detection) + or a number of integer/string representation of the + detected classes. :return: Dictionary, where the keys are image indices within a batch and the values are the number of detected objects per image + Example: + {"0": 3, # 3 objects detected in the zeroth image + "1": 0, # no objects detected in the first image + "2": 1 # 1 object detected in the second image + ... + } """ - _check_valid_classes(classes) - number_objects_per_batch = {} - for sample_idx, sample in enumerate(classes): - number_objects_per_batch[str(sample_idx)] = ( - 0 if sample == [None] else len(sample) + number_objects_per_image = {} + for detection_idx, detection in enumerate(detected_classes): + _check_valid_detection(detection) + number_objects_per_image[str(detection_idx)] = ( + 0 if detection == [None] else len(detection) ) - - return number_objects_per_batch + return number_objects_per_image -def mean_score_per_detection(scores: List[List[Optional[float]]]) -> Dict[str, float]: +def mean_score_per_detection( + scores: List[List[Union[None, float]]] +) -> Dict[str, float]: """ Return the mean score per detection - :param scores: A nested list, where: - - first level is the batch information - - second level is the sample information. Can be - either `None` (no detection) or a float score + :param scores: A nested list, that contains + the detected classes for each sample in the batch. + Every element of the inner list pertains to one + set of detections in the batch. The inner list can + either contain a single `None` value (no detection) + or a number of float representation of the + score (confidence) of the detected classes. :return: Dictionary, where the keys are image indices within a batch and the values are the mean score per detection """ - _check_valid_scores(scores) - - mean_scores_per_batch = {} - for sample_idx, sample in enumerate(scores): - if sample == [None]: - mean_scores_per_batch[str(sample_idx)] = 0.0 - else: - mean_scores_per_batch[str(sample_idx)] = numpy.mean(sample) + mean_scores_per_image = {} + for score_idx, score in enumerate(scores): + _check_valid_score(score) + mean_scores_per_image[str(score_idx)] = ( + 0.0 if score == [None] else numpy.mean(score) + ) - return mean_scores_per_batch + return mean_scores_per_image def std_score_per_detection(scores: List[List[Optional[float]]]) -> Dict[str, float]: """ Return the standard deviation of scores per detection - :param scores: A nested list, where: - - first level is the batch information - - second level is the sample information. Can be - either `None` (no detection) or a float score + :param scores: A nested list, that contains + the detected classes for each sample in the batch. + Every element of the inner list pertains to one + set of detections in the batch. The inner list can + either contain a single `None` value (no detection) + or a number of float representation of the + score (confidence) of the detected classes. :return: Dictionary, where the keys are image indices within a batch and the values are the standard deviation of scores per detection """ - _check_valid_scores(scores) - - std_scores_per_batch = {} - for sample_idx, sample in enumerate(scores): - if sample == [None]: - std_scores_per_batch[str(sample_idx)] = 0.0 - else: - std_scores_per_batch[str(sample_idx)] = numpy.std(sample) - - return std_scores_per_batch - - -def _check_valid_classes(classes: List[List[Optional[Union[int, str]]]]): - for sample in classes: - if ( - all(isinstance(i, int) for i in sample) - or all(isinstance(i, str) for i in sample) - or sample == [None] - ): - pass - else: - raise ValueError( - "Detection for a sample must be either a " - "list of integers or a list of strings or a list " - "with a single `None` value" - ) - - -def _check_valid_scores(scores: List[List[Optional[float]]]): - for sample in scores: - if all(isinstance(i, float) for i in sample) or sample == [None]: - pass - else: - raise ValueError( - "Scores for a sample must be either a " - "list of floats or a list with a single `None` value" - ) + std_scores_per_image = {} + for score_idx, score in enumerate(scores): + _check_valid_score(score) + std_scores_per_image[str(score_idx)] = ( + 0.0 if score == [None] else numpy.std(score) + ) + + return std_scores_per_image + + +def _check_valid_detection(detection: List[Union[int, str, None]]): + if not ( + all(isinstance(det, int) for det in detection) + or all(isinstance(det, str) for det in detection) + or detection == [None] + ): + raise ValueError( + "Detection for a sample must be either a " + "list of integers or a list of strings or a list " + "with a single `None` value" + ) + + +def _check_valid_score(score: List[Union[float, None]]): + if not (all(isinstance(score_, float) for score_ in score) or score == [None]): + raise ValueError( + "Scores for a sample must be either a " + "list of floats or a list with a single `None` value" + ) def _check_valid_image(img: numpy.ndarray) -> Tuple[int, int]: diff --git a/src/deepsparse/yolo/schemas.py b/src/deepsparse/yolo/schemas.py index 2b4872c0f1..e97d7f26f7 100644 --- a/src/deepsparse/yolo/schemas.py +++ b/src/deepsparse/yolo/schemas.py @@ -67,7 +67,7 @@ class YOLOOutput(BaseModel): scores: List[List[float]] = Field( description="List of scores, one for each prediction" ) - classes: List[List[str]] = Field( + labels: List[List[str]] = Field( description="List of labels, one for each prediction" ) @@ -78,7 +78,7 @@ def __getitem__(self, index): return _YOLOImageOutput( self.boxes[index], self.scores[index], - self.classes[index], + self.labels[index], ) def __iter__(self): diff --git a/tests/deepsparse/loggers/metric_functions/cv/__init__.py b/tests/deepsparse/loggers/metric_functions/computer_vision/__init__.py similarity index 100% rename from tests/deepsparse/loggers/metric_functions/cv/__init__.py rename to tests/deepsparse/loggers/metric_functions/computer_vision/__init__.py diff --git a/tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py b/tests/deepsparse/loggers/metric_functions/computer_vision/test_built_ins.py similarity index 91% rename from tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py rename to tests/deepsparse/loggers/metric_functions/computer_vision/test_built_ins.py index 318e4f8d8f..ef73a5581d 100644 --- a/tests/deepsparse/loggers/metric_functions/cv/test_built_ins.py +++ b/tests/deepsparse/loggers/metric_functions/computer_vision/test_built_ins.py @@ -16,12 +16,12 @@ import pytest from deepsparse.loggers.metric_functions import ( - count_classes_detected, - count_number_objects_detected, + detected_classes, fraction_zeros, image_shape, mean_pixels_per_channel, mean_score_per_detection, + number_detected_objects, std_pixels_per_channel, std_score_per_detection, ) @@ -93,7 +93,7 @@ def test_mean_pixel_per_channel(image, expected_means): for mean, expected_mean in zip(result.values(), expected_means): numpy.testing.assert_allclose(mean, expected_mean) assert list(result.keys()) == [ - f"mean_channel_{idx}" for idx in range(len(expected_means)) + f"channel_{idx}" for idx in range(len(expected_means)) ] @@ -112,7 +112,7 @@ def test_std_pixels_per_channel(image, expected_stds): for std, expected_std in zip(result.values(), expected_stds): numpy.testing.assert_allclose(std, expected_std, atol=1e-16) assert list(result.keys()) == [ - f"std_channel_{idx}" for idx in range(len(expected_stds)) + f"channel_{idx}" for idx in range(len(expected_stds)) ] @@ -146,8 +146,8 @@ def test_percentage_zeros_per_channel(image, expected_percentage): @pytest.mark.parametrize( "classes, expected_count_classes, should_raise_error", [ - ([[None], [0, 1, 3], [3, 3, 0]], {"0": 2, "1": 1, "3": 3}, False), - ([[None], [None]], {}, False), + ([[None], [0, 1, 3], [3, 3, 0]], {"0": 2, "1": 1, "3": 3, "None": 1}, False), + ([[None], [None]], {"None": 2}, False), ( [["foo", "bar"], ["foo", "bar", "alice"]], {"foo": 2, "bar": 2, "alice": 1}, @@ -155,19 +155,19 @@ def test_percentage_zeros_per_channel(image, expected_percentage): ), ( [["foo", "bar"], ["foo", "bar", "alice"], [None]], - {"foo": 2, "bar": 2, "alice": 1}, + {"foo": 2, "bar": 2, "alice": 1, "None": 1}, False, ), ([[6.666], [0, 1, 3], [3, 3, 0]], None, True), ([[None, None], [0, 1, 3], [3, 3, 0]], None, True), ], ) -def test_count_classes_detected(classes, expected_count_classes, should_raise_error): +def test_detected_classes(classes, expected_count_classes, should_raise_error): if should_raise_error: with pytest.raises(ValueError): - count_classes_detected(classes) + detected_classes(classes) return - assert expected_count_classes == count_classes_detected(classes) + assert expected_count_classes == detected_classes(classes) @pytest.mark.parametrize( @@ -179,8 +179,8 @@ def test_count_classes_detected(classes, expected_count_classes, should_raise_er ([["foo", "bar"], ["foo", "bar", "alice"], [None]], {"0": 2, "1": 3, "2": 0}), ], ) -def test_count_number_objects_detected(classes, expected_count_classes): - assert expected_count_classes == count_number_objects_detected(classes) +def test_number_detected_objects(classes, expected_count_classes): + assert expected_count_classes == number_detected_objects(classes) @pytest.mark.parametrize( From 3ae3fe924a2d3506dc84b70a9c0e1c15aa694ef0 Mon Sep 17 00:00:00 2001 From: "bogunowicz@arrival.com" Date: Thu, 12 Jan 2023 10:23:25 +0100 Subject: [PATCH 6/6] error message --- .../loggers/metric_functions/computer_vision/built_ins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py b/src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py index 51b1eb13ef..c93e1f49d3 100644 --- a/src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py +++ b/src/deepsparse/loggers/metric_functions/computer_vision/built_ins.py @@ -235,7 +235,7 @@ def _check_valid_detection(detection: List[Union[int, str, None]]): or detection == [None] ): raise ValueError( - "Detection for a sample must be either a " + "Detection must be either a " "list of integers or a list of strings or a list " "with a single `None` value" ) @@ -244,7 +244,7 @@ def _check_valid_detection(detection: List[Union[int, str, None]]): def _check_valid_score(score: List[Union[float, None]]): if not (all(isinstance(score_, float) for score_ in score) or score == [None]): raise ValueError( - "Scores for a sample must be either a " + "Score must be either a " "list of floats or a list with a single `None` value" )