From d85a77a546783ab28df9eb6347f855cc54f6192d Mon Sep 17 00:00:00 2001 From: Pablo Tello Date: Fri, 21 Dec 2018 16:47:23 +0000 Subject: COMPMID-1766: Implemented CPP Non Max Suppression Change-Id: I2d2b684d464f7b3bb1f91cfd29952f612d65f11f Signed-off-by: Pablo Tello Reviewed-on: https://review.mlplatform.org/708 Reviewed-by: VidhyaSudhan Loganathan Tested-by: Arm Jenkins --- .../CPP/functions/CPPDetectionOutputLayer.h | 52 ++++++- .../CPP/functions/CPPDetectionOutputLayer.cpp | 101 ++++++++++++- tests/AssetsLibrary.h | 37 +++++ tests/datasets/ShapeDatasets.h | 31 ++++ tests/validation/CPP/NonMaximalSuppression.cpp | 144 +++++++++++++++++++ .../validation/fixtures/NonMaxSuppressionFixture.h | 124 ++++++++++++++++ tests/validation/reference/NonMaxSuppression.cpp | 157 +++++++++++++++++++++ tests/validation/reference/NonMaxSuppression.h | 44 ++++++ 8 files changed, 683 insertions(+), 7 deletions(-) create mode 100644 tests/validation/CPP/NonMaximalSuppression.cpp create mode 100644 tests/validation/fixtures/NonMaxSuppressionFixture.h create mode 100644 tests/validation/reference/NonMaxSuppression.cpp create mode 100644 tests/validation/reference/NonMaxSuppression.h diff --git a/arm_compute/runtime/CPP/functions/CPPDetectionOutputLayer.h b/arm_compute/runtime/CPP/functions/CPPDetectionOutputLayer.h index 7f80948c81..8c610f3ec2 100644 --- a/arm_compute/runtime/CPP/functions/CPPDetectionOutputLayer.h +++ b/arm_compute/runtime/CPP/functions/CPPDetectionOutputLayer.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 ARM Limited. + * Copyright (c) 2018-2019 ARM Limited. * * SPDX-License-Identifier: MIT * @@ -39,6 +39,56 @@ using NormalizedBBox = std::array; // LabelBBox used for map label and bounding box using LabelBBox = std::map>; +/** CPP Function to perform non maximum suppression on the bounding boxes and scores + * + */ +class CPPNonMaximumSuppression : public IFunction +{ +public: + /** Default constructor */ + CPPNonMaximumSuppression(); + /** Configure the function to perform non maximal suppression + * + * @param[in] bboxes The input bounding boxes. Data types supported: F32. + * @param[in] scores The corresponding input confidence. Same as @p scores. + * @param[out] indices The kept indices of bboxes after nms. Data types supported: S32. + * @param[in] max_output_size An integer tensor representing the maximum number of boxes to be selected by non max suppression. + * @param[in] score_threshold The threshold used to filter detection results. + * @param[in] nms_threshold The threshold used in non maximum suppression. + * + */ + void configure(const ITensor *bboxes, const ITensor *scores, ITensor *indices, unsigned int max_output_size, const float score_threshold, const float nms_threshold); + + /** Static function to check if given arguments will lead to a valid configuration of @ref CPPNonMaximumSuppression + * + * @param[in] bboxes The input bounding boxes. Data types supported: F32. + * @param[in] scores The corresponding input confidence. Same as @p scores. + * @param[out] indices The kept indices of bboxes after nms. Data types supported: S32. + * @param[in] max_output_size An integer tensor representing the maximum number of boxes to be selected by non max suppression. + * @param[in] score_threshold The threshold used to filter detection results. + * @param[in] nms_threshold The threshold used in non maximum suppression. + * + */ + static Status validate(const ITensorInfo *bboxes, const ITensorInfo *scores, const ITensorInfo *indices, unsigned int max_output_size, + const float score_threshold, const float nms_threshold); + + // Inherited methods overridden: + void run() override; + /** Prevent instances of this class from being copied (As this class contains pointers) */ + CPPNonMaximumSuppression(const CPPNonMaximumSuppression &) = delete; + /** Prevent instances of this class from being copied (As this class contains pointers) */ + CPPNonMaximumSuppression &operator=(const CPPNonMaximumSuppression &) = delete; + +private: + const ITensor *_bboxes; + const ITensor *_scores; + ITensor *_indices; + unsigned int _max_output_size; + + float _score_threshold; + float _nms_threshold; +}; + /** CPP Function to generate the detection output based on location and confidence * predictions by doing non maximum suppression. * diff --git a/src/runtime/CPP/functions/CPPDetectionOutputLayer.cpp b/src/runtime/CPP/functions/CPPDetectionOutputLayer.cpp index 61005ab5fd..34a7294513 100644 --- a/src/runtime/CPP/functions/CPPDetectionOutputLayer.cpp +++ b/src/runtime/CPP/functions/CPPDetectionOutputLayer.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 ARM Limited. + * Copyright (c) 2018-2019 ARM Limited. * * SPDX-License-Identifier: MIT * @@ -34,7 +34,7 @@ namespace arm_compute { namespace { -Status validate_arguments(const ITensorInfo *input_loc, const ITensorInfo *input_conf, const ITensorInfo *input_priorbox, const ITensorInfo *output, DetectionOutputLayerInfo info) +Status detection_layer_validate_arguments(const ITensorInfo *input_loc, const ITensorInfo *input_conf, const ITensorInfo *input_priorbox, const ITensorInfo *output, DetectionOutputLayerInfo info) { ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(input_loc, input_conf, input_priorbox, output); ARM_COMPUTE_RETURN_ERROR_ON_DATA_TYPE_CHANNEL_NOT_IN(input_loc, 1, DataType::F32); @@ -366,14 +366,103 @@ void ApplyNMSFast(const std::vector &bboxes, indices.push_back(idx); } score_index_vec.erase(score_index_vec.begin()); - if(keep && eta < 1 && adaptive_threshold > 0.5) + if(keep && eta < 1.f && adaptive_threshold > 0.5f) { adaptive_threshold *= eta; } } } + +Status non_max_suppression_validate_arguments(const ITensorInfo *bboxes, const ITensorInfo *scores, const ITensorInfo *indices, unsigned int max_output_size, + const float score_threshold, const float nms_threshold) +{ + ARM_COMPUTE_RETURN_ERROR_ON_NULLPTR(bboxes, scores, indices); + ARM_COMPUTE_RETURN_ERROR_ON_DATA_TYPE_CHANNEL_NOT_IN(bboxes, 1, DataType::F32); + ARM_COMPUTE_RETURN_ERROR_ON_DATA_TYPE_CHANNEL_NOT_IN(scores, 1, DataType::F32); + ARM_COMPUTE_RETURN_ERROR_ON_DATA_TYPE_CHANNEL_NOT_IN(indices, 1, DataType::S32); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(bboxes->num_dimensions() > 2, "The bboxes tensor must be a 2-D float tensor of shape [4, num_boxes]."); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(scores->num_dimensions() > 1, "The scores tensor must be a 1-D float tensor of shape [num_boxes]."); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(indices->num_dimensions() > 1, "The indices must be 1-D integer tensor of shape [M], where max_output_size <= M"); + ARM_COMPUTE_RETURN_ERROR_ON_MISMATCHING_DATA_TYPES(bboxes, scores); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(scores->num_dimensions() > 1, "Scores must be a 1D float tensor"); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(indices->dimension(0) == 0, "Indices tensor must be bigger than 0"); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(max_output_size == 0, "Max size cannot be 0"); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(nms_threshold < 0.f || nms_threshold > 1.f, "Threshould must be in [0,1]"); + ARM_COMPUTE_RETURN_ERROR_ON_MSG(score_threshold < 0.f || score_threshold > 1.f, "Threshould must be in [0,1]"); + + return Status{}; +} } // namespace +CPPNonMaximumSuppression::CPPNonMaximumSuppression() + : _bboxes(nullptr), _scores(nullptr), _indices(nullptr), _max_output_size(0), _score_threshold(0.f), _nms_threshold(0.f) +{ +} + +void CPPNonMaximumSuppression::configure( + const ITensor *bboxes, const ITensor *scores, ITensor *indices, unsigned int max_output_size, + const float score_threshold, const float nms_threshold) +{ + ARM_COMPUTE_ERROR_ON_NULLPTR(bboxes, scores, indices); + ARM_COMPUTE_ERROR_THROW_ON(non_max_suppression_validate_arguments(bboxes->info(), scores->info(), indices->info(), max_output_size, score_threshold, nms_threshold)); + + // copy scores also to a vector + _bboxes = bboxes; + _scores = scores; + _indices = indices; + + _nms_threshold = nms_threshold; + _max_output_size = max_output_size; + _score_threshold = score_threshold; +} + +Status CPPNonMaximumSuppression::validate( + const ITensorInfo *bboxes, const ITensorInfo *scores, const ITensorInfo *indices, unsigned int max_output_size, + const float score_threshold, const float nms_threshold) +{ + ARM_COMPUTE_RETURN_ON_ERROR(non_max_suppression_validate_arguments(bboxes, scores, indices, max_output_size, score_threshold, nms_threshold)); + return Status{}; +} + +void extract_bounding_boxes_from_tensor(const ITensor *bboxes, std::vector &bboxes_vector) +{ + Window input_win; + input_win.use_tensor_dimensions(bboxes->info()->tensor_shape()); + input_win.set_dimension_step(0U, 4U); + input_win.set_dimension_step(1U, 1U); + Iterator input(bboxes, input_win); + auto f = [&bboxes_vector, &input](const Coordinates &) + { + const auto input_ptr = reinterpret_cast(input.ptr()); + bboxes_vector.push_back(NormalizedBBox({ *input_ptr, *(input_ptr + 1), *(2 + input_ptr), *(3 + input_ptr) })); + }; + execute_window_loop(input_win, f, input); +} + +void extract_scores_from_tensor(const ITensor *scores, std::vector &scores_vector) +{ + Window window; + window.use_tensor_dimensions(scores->info()->tensor_shape()); + Iterator it(scores, window); + auto f = [&it, &scores_vector](const Coordinates &) + { + const auto input_ptr = reinterpret_cast(it.ptr()); + scores_vector.push_back(*input_ptr); + }; + execute_window_loop(window, f, it); +} + +void CPPNonMaximumSuppression::run() +{ + std::vector bboxes_vector; + std::vector scores_vector; + std::vector indices_vector; + extract_bounding_boxes_from_tensor(_bboxes, bboxes_vector); + extract_scores_from_tensor(_scores, scores_vector); + ApplyNMSFast(bboxes_vector, scores_vector, _score_threshold, _nms_threshold, 1, -1 /* disable top_k */, indices_vector); + std::copy_n(indices_vector.begin(), std::min(indices_vector.size(), _indices->info()->dimension(0)), reinterpret_cast(_indices->ptr_to_element(Coordinates(0)))); +} + CPPDetectionOutputLayer::CPPDetectionOutputLayer() : _input_loc(nullptr), _input_conf(nullptr), _input_priorbox(nullptr), _output(nullptr), _info(), _num_priors(), _num(), _all_location_predictions(), _all_confidence_scores(), _all_prior_bboxes(), _all_prior_variances(), _all_decode_bboxes(), _all_indices() @@ -391,7 +480,7 @@ void CPPDetectionOutputLayer::configure(const ITensor *input_loc, const ITensor auto_init_if_empty(*output->info(), input_loc->info()->clone()->set_tensor_shape(TensorShape(7U, max_size))); // Perform validation step - ARM_COMPUTE_ERROR_THROW_ON(validate_arguments(input_loc->info(), input_conf->info(), input_priorbox->info(), output->info(), info)); + ARM_COMPUTE_ERROR_THROW_ON(detection_layer_validate_arguments(input_loc->info(), input_conf->info(), input_priorbox->info(), output->info(), info)); _input_loc = input_loc; _input_conf = input_conf; @@ -429,7 +518,7 @@ void CPPDetectionOutputLayer::configure(const ITensor *input_loc, const ITensor Status CPPDetectionOutputLayer::validate(const ITensorInfo *input_loc, const ITensorInfo *input_conf, const ITensorInfo *input_priorbox, const ITensorInfo *output, DetectionOutputLayerInfo info) { - ARM_COMPUTE_RETURN_ON_ERROR(validate_arguments(input_loc, input_conf, input_priorbox, output, info)); + ARM_COMPUTE_RETURN_ON_ERROR(detection_layer_validate_arguments(input_loc, input_conf, input_priorbox, output, info)); return Status{}; } @@ -582,4 +671,4 @@ void CPPDetectionOutputLayer::run() } } } -} // namespace arm_compute \ No newline at end of file +} // namespace arm_compute diff --git a/tests/AssetsLibrary.h b/tests/AssetsLibrary.h index d09e22762d..366c1450ba 100644 --- a/tests/AssetsLibrary.h +++ b/tests/AssetsLibrary.h @@ -207,6 +207,9 @@ public: template void fill(T &&tensor, D &&distribution, std::random_device::result_type seed_offset) const; + template + void fill_boxes(T &&tensor, D &&distribution, std::random_device::result_type seed_offset) const; + /** Fills the specified @p raw tensor with random values drawn from @p * distribution. * @@ -481,6 +484,40 @@ void AssetsLibrary::fill_borders_with_garbage(T &&tensor, D &&distribution, std: }); } +template +void AssetsLibrary::fill_boxes(T &&tensor, D &&distribution, std::random_device::result_type seed_offset) const +{ + using ResultType = typename std::remove_reference::type::result_type; + std::mt19937 gen(_seed + seed_offset); + TensorShape shape(tensor.shape()); + const int num_boxes = tensor.num_elements() / 4; + // Iterate over all elements + std::uniform_real_distribution<> size_dist(0.f, 1.f); + for(int element_idx = 0; element_idx < num_boxes * 4; element_idx += 4) + { + const ResultType delta = size_dist(gen); + const ResultType epsilon = size_dist(gen); + const ResultType left = distribution(gen); + const ResultType top = distribution(gen); + const ResultType right = left + delta; + const ResultType bottom = top + epsilon; + const std::tuple box(left, top, right, bottom); + Coordinates x1 = index2coord(shape, element_idx); + Coordinates y1 = index2coord(shape, element_idx + 1); + Coordinates x2 = index2coord(shape, element_idx + 2); + Coordinates y2 = index2coord(shape, element_idx + 3); + ResultType &target_value_x1 = reinterpret_cast(tensor(x1))[0]; + ResultType &target_value_y1 = reinterpret_cast(tensor(y1))[0]; + ResultType &target_value_x2 = reinterpret_cast(tensor(x2))[0]; + ResultType &target_value_y2 = reinterpret_cast(tensor(y2))[0]; + store_value_with_data_type(&target_value_x1, std::get<0>(box), tensor.data_type()); + store_value_with_data_type(&target_value_y1, std::get<1>(box), tensor.data_type()); + store_value_with_data_type(&target_value_x2, std::get<2>(box), tensor.data_type()); + store_value_with_data_type(&target_value_y2, std::get<3>(box), tensor.data_type()); + } + fill_borders_with_garbage(tensor, distribution, seed_offset); +} + template void AssetsLibrary::fill(T &&tensor, D &&distribution, std::random_device::result_type seed_offset) const { diff --git a/tests/datasets/ShapeDatasets.h b/tests/datasets/ShapeDatasets.h index 480df3c7eb..f461d7f9d2 100644 --- a/tests/datasets/ShapeDatasets.h +++ b/tests/datasets/ShapeDatasets.h @@ -946,6 +946,37 @@ public: { } }; + +/** Data set containing small 2D tensor shapes. */ +class Small2DNonMaxSuppressionShapes final : public ShapeDataset +{ +public: + Small2DNonMaxSuppressionShapes() + : ShapeDataset("Shape", + { + TensorShape{ 4U, 7U }, + TensorShape{ 4U, 13U }, + TensorShape{ 4U, 64U } + }) + { + } +}; + +/** Data set containing large 2D tensor shapes. */ +class Large2DNonMaxSuppressionShapes final : public ShapeDataset +{ +public: + Large2DNonMaxSuppressionShapes() + : ShapeDataset("Shape", + { + TensorShape{ 4U, 207U }, + TensorShape{ 4U, 113U }, + TensorShape{ 4U, 264U } + }) + { + } +}; + } // namespace datasets } // namespace test } // namespace arm_compute diff --git a/tests/validation/CPP/NonMaximalSuppression.cpp b/tests/validation/CPP/NonMaximalSuppression.cpp new file mode 100644 index 0000000000..6cd7b52066 --- /dev/null +++ b/tests/validation/CPP/NonMaximalSuppression.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019 ARM Limited. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "arm_compute/core/Types.h" +#include "arm_compute/runtime/CPP/functions/CPPDetectionOutputLayer.h" +#include "arm_compute/runtime/Tensor.h" +#include "arm_compute/runtime/TensorAllocator.h" +#include "tests/NEON/Accessor.h" +#include "tests/PaddingCalculator.h" +#include "tests/datasets/ShapeDatasets.h" +#include "tests/framework/Asserts.h" +#include "tests/framework/Macros.h" +#include "tests/framework/datasets/Datasets.h" +#include "tests/validation/Validation.h" +#include "tests/validation/fixtures/NonMaxSuppressionFixture.h" + +namespace arm_compute +{ +namespace test +{ +namespace validation +{ +namespace +{ +const auto max_output_boxes_dataset = framework::dataset::make("MaxOutputBoxes", 1, 10); +const auto score_threshold_dataset = framework::dataset::make("ScoreThreshold", { 0.1f, 0.5f, 0.f, 1.f }); +const auto nms_threshold_dataset = framework::dataset::make("NMSThreshold", { 0.1f, 0.5f, 0.f, 1.f }); +const auto NMSParametersSmall = datasets::Small2DNonMaxSuppressionShapes() * max_output_boxes_dataset * score_threshold_dataset * nms_threshold_dataset; +const auto NMSParametersBig = datasets::Large2DNonMaxSuppressionShapes() * max_output_boxes_dataset * score_threshold_dataset * nms_threshold_dataset; + +} // namespace + +TEST_SUITE(CPP) +TEST_SUITE(NMS) + +// *INDENT-OFF* +// clang-format off +DATA_TEST_CASE(Validate, framework::DatasetMode::ALL, zip(zip(zip(zip(zip(zip( + framework::dataset::make("BoundingBox",{ + TensorInfo(TensorShape(4U, 100U), 1, DataType::F32), + TensorInfo(TensorShape(1U, 4U, 2U), 1, DataType::F32), // invalid shape + TensorInfo(TensorShape(4U, 2U), 1, DataType::S32), // invalid data type + TensorInfo(TensorShape(4U, 3U), 1, DataType::F32), + TensorInfo(TensorShape(4U, 66U), 1, DataType::F32), + TensorInfo(TensorShape(4U, 100U), 1, DataType::F32), + TensorInfo(TensorShape(4U, 100U), 1, DataType::F32), + TensorInfo(TensorShape(4U, 100U), 1, DataType::F32), + TensorInfo(TensorShape(4U, 100U), 1, DataType::F32), + TensorInfo(TensorShape(4U, 100U), 1, DataType::F32), + }), + framework::dataset::make("Scores", { + TensorInfo(TensorShape(100U), 1, DataType::F32), + TensorInfo(TensorShape(37U, 2U, 13U, 27U), 1, DataType::F32), // invalid shape + TensorInfo(TensorShape(4U), 1, DataType::F32), + TensorInfo(TensorShape(3U), 1, DataType::U8), // invalid data type + TensorInfo(TensorShape(66U), 1, DataType::F32), // invalid data type + TensorInfo(TensorShape(100U), 1, DataType::F32), + TensorInfo(TensorShape(100U), 1, DataType::F32), + TensorInfo(TensorShape(100U), 1, DataType::F32), + TensorInfo(TensorShape(100U), 1, DataType::F32), + TensorInfo(TensorShape(100U), 1, DataType::F32), + })), + framework::dataset::make("Indices", { + TensorInfo(TensorShape(100U), 1, DataType::S32), + TensorInfo(TensorShape(100U), 1, DataType::S32), + TensorInfo(TensorShape(4U), 1, DataType::S32), + TensorInfo(TensorShape(3U), 1, DataType::S32), + TensorInfo(TensorShape(200U), 1, DataType::S32), // indices bigger than max bbs, OK because max_output is 66 + TensorInfo(TensorShape(100U), 1, DataType::F32), // invalid data type + TensorInfo(TensorShape(100U), 1, DataType::S32), + TensorInfo(TensorShape(100U), 1, DataType::S32), + TensorInfo(TensorShape(100U), 1, DataType::S32), + TensorInfo(TensorShape(100U), 1, DataType::S32), + + })), + framework::dataset::make("max_output", { + 10U, 2U,4U, 3U,66U, 1U, + 0U, /* invalid, must be greater than 0 */ + 10000U, /* OK, clamped to indices' size */ + 100U, + 10U, + })), + framework::dataset::make("score_threshold", { + 0.1f, 0.4f, 0.2f,0.8f,0.3f, 0.01f, 0.5f, 0.45f, + -1.f, /* invalid value, must be in [0,1] */ + 0.5f, + })), + framework::dataset::make("nms_threshold", { + 0.3f, 0.7f, 0.1f,0.13f,0.2f, 0.97f, 0.76f, 0.87f, 0.1f, + 10.f, /* invalid value, must be in [0,1]*/ + })), + framework::dataset::make("Expected", { + true, false, false, false, true, false, false,true, false, false + })), + + bbox_info, scores_info, indices_info, max_out, score_threshold, nms_threshold, expected) +{ + ARM_COMPUTE_EXPECT(bool(CPPNonMaximumSuppression::validate(&bbox_info.clone()->set_is_resizable(false), + &scores_info.clone()->set_is_resizable(false), + &indices_info.clone()->set_is_resizable(false), + max_out,score_threshold,nms_threshold)) == expected, framework::LogLevel::ERRORS); +} +// clang-format on +// *INDENT-ON* + +using CPPNonMaxSuppressionFixture = NMSValidationFixture; + +FIXTURE_DATA_TEST_CASE(RunSmall, CPPNonMaxSuppressionFixture, framework::DatasetMode::PRECOMMIT, NMSParametersSmall) +{ + // Validate output + validate(Accessor(_target), _reference); +} + +FIXTURE_DATA_TEST_CASE(RunLarge, CPPNonMaxSuppressionFixture, framework::DatasetMode::NIGHTLY, NMSParametersBig) +{ + // Validate output + validate(Accessor(_target), _reference); +} + +TEST_SUITE_END() // CPP +TEST_SUITE_END() // NMS +} // namespace validation +} // namespace test +} // namespace arm_compute diff --git a/tests/validation/fixtures/NonMaxSuppressionFixture.h b/tests/validation/fixtures/NonMaxSuppressionFixture.h new file mode 100644 index 0000000000..9299ed62a4 --- /dev/null +++ b/tests/validation/fixtures/NonMaxSuppressionFixture.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019 ARM Limited. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef ARM_COMPUTE_TEST_NON_MAX_SUPPRESSION_FIXTURE +#define ARM_COMPUTE_TEST_NON_MAX_SUPPRESSION_FIXTURE + +#include "arm_compute/core/Helpers.h" +#include "arm_compute/core/TensorShape.h" +#include "arm_compute/core/Types.h" +#include "arm_compute/runtime/Tensor.h" +#include "tests/AssetsLibrary.h" +#include "tests/Globals.h" +#include "tests/IAccessor.h" +#include "tests/framework/Asserts.h" +#include "tests/framework/Fixture.h" +#include "tests/validation/reference/NonMaxSuppression.h" + +namespace arm_compute +{ +namespace test +{ +namespace validation +{ +template + +class NMSValidationFixture : public framework::Fixture +{ +public: + template + void setup(TensorShape input_shape, unsigned int max_output_size, float score_threshold, float nms_threshold) + { + ARM_COMPUTE_ERROR_ON(max_output_size == 0); + ARM_COMPUTE_ERROR_ON(input_shape.num_dimensions() != 2); + const TensorShape output_shape(max_output_size); + const TensorShape scores_shape(input_shape[1]); + _target = compute_target(input_shape, scores_shape, output_shape, max_output_size, score_threshold, nms_threshold); + _reference = compute_reference(input_shape, scores_shape, output_shape, max_output_size, score_threshold, nms_threshold); + } + +protected: + template + void fill(U &&tensor, int i, int lo, int hi) + { + std::uniform_real_distribution<> distribution(lo, hi); + library->fill_boxes(tensor, distribution, i); + } + + TensorType compute_target(const TensorShape input_shape, const TensorShape scores_shape, const TensorShape output_shape, + unsigned int max_output_size, float score_threshold, float nms_threshold) + { + // Create tensors + TensorType bboxes = create_tensor(input_shape, DataType::F32); + TensorType scores = create_tensor(scores_shape, DataType::F32); + TensorType indices = create_tensor(output_shape, DataType::S32); + + // Create and configure function + FunctionType nms_func; + nms_func.configure(&bboxes, &scores, &indices, max_output_size, score_threshold, nms_threshold); + + ARM_COMPUTE_EXPECT(bboxes.info()->is_resizable(), framework::LogLevel::ERRORS); + ARM_COMPUTE_EXPECT(indices.info()->is_resizable(), framework::LogLevel::ERRORS); + ARM_COMPUTE_EXPECT(scores.info()->is_resizable(), framework::LogLevel::ERRORS); + + // Allocate tensors + bboxes.allocator()->allocate(); + indices.allocator()->allocate(); + scores.allocator()->allocate(); + + ARM_COMPUTE_EXPECT(!bboxes.info()->is_resizable(), framework::LogLevel::ERRORS); + ARM_COMPUTE_EXPECT(!indices.info()->is_resizable(), framework::LogLevel::ERRORS); + ARM_COMPUTE_EXPECT(!scores.info()->is_resizable(), framework::LogLevel::ERRORS); + + // Fill tensors + fill(AccessorType(bboxes), 0, 0.f, 1.f); + fill(AccessorType(scores), 1, 0.f, 1.f); + + // Compute function + nms_func.run(); + return indices; + } + + SimpleTensor compute_reference(const TensorShape input_shape, const TensorShape scores_shape, const TensorShape output_shape, + unsigned int max_output_size, float score_threshold, float nms_threshold) + { + // Create reference + SimpleTensor bboxes{ input_shape, DataType::F32 }; + SimpleTensor scores{ scores_shape, DataType::F32 }; + SimpleTensor indices{ output_shape, DataType::S32 }; + + // Fill reference + fill(bboxes, 0, 0.f, 1.f); + fill(scores, 1, 0.f, 1.f); + + return reference::non_max_suppression(bboxes, scores, indices, max_output_size, score_threshold, nms_threshold); + } + + TensorType _target{}; + SimpleTensor _reference{}; +}; + +} // namespace validation +} // namespace test +} // namespace arm_compute +#endif /* ARM_COMPUTE_TEST_NON_MAX_SUPPRESSION_FIXTURE */ diff --git a/tests/validation/reference/NonMaxSuppression.cpp b/tests/validation/reference/NonMaxSuppression.cpp new file mode 100644 index 0000000000..75929085b3 --- /dev/null +++ b/tests/validation/reference/NonMaxSuppression.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019 ARM Limited. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "NonMaxSuppression.h" + +#include "arm_compute/core/Types.h" +#include "tests/validation/Helpers.h" + +namespace arm_compute +{ +namespace test +{ +namespace validation +{ +namespace reference +{ +namespace +{ +using CandidateBox = std::pair; +using Box = std::tuple; + +inline float get_elem_by_coordinate(const SimpleTensor &tensor, Coordinates coord) +{ + return *static_cast(tensor(coord)); +} + +inline Box get_box(const SimpleTensor &boxes, size_t id) +{ + return std::make_tuple( + get_elem_by_coordinate(boxes, Coordinates(0, id)), + get_elem_by_coordinate(boxes, Coordinates(1, id)), + get_elem_by_coordinate(boxes, Coordinates(2, id)), + get_elem_by_coordinate(boxes, Coordinates(3, id))); +} + +inline std::pair get_min_yx(Box b) +{ + return std::make_pair( + std::min(std::get<0>(b), std::get<2>(b)), + std::min(std::get<1>(b), std::get<3>(b))); +} + +inline std::pair get_max_yx(Box b) +{ + return std::make_pair( + std::max(std::get<0>(b), std::get<2>(b)), + std::max(std::get<1>(b), std::get<3>(b))); +} + +inline float compute_size(const std::pair &min, const std::pair &max) +{ + return (max.first - min.first) * (max.second - min.second); +} + +inline float compute_intersection(const std::pair &b0_min, const std::pair &b0_max, + const std::pair &b1_min, const std::pair &b1_max, float b0_size, float b1_size) +{ + const float inter = std::max(std::min(b0_max.first, b1_max.first) - std::max(b0_min.first, b1_min.first), 0.0) * std::max(std::min(b0_max.second, + b1_max.second) + - std::max(b0_min.second, b1_min.second), + 0.0); + return inter / (b0_size + b1_size - inter); +} + +inline bool reject_box(Box b0, Box b1, float threshold) +{ + const auto b0_min = get_min_yx(b0); + const auto b0_max = get_max_yx(b0); + const auto b1_min = get_min_yx(b1); + const auto b1_max = get_max_yx(b1); + const float b0_size = compute_size(b0_min, b0_max); + const float b1_size = compute_size(b1_min, b1_max); + if(b0_size <= 0.f || b1_size <= 0.f) + { + return false; + } + else + { + return compute_intersection(b0_min, b0_max, b1_min, b1_max, b0_size, b1_size) > threshold; + } +} + +inline std::vector get_candidates(const SimpleTensor &scores, float threshold) +{ + std::vector candidates_vector; + for(int i = 0; i < scores.num_elements(); ++i) + { + if(scores[i] > threshold) + { + const auto cb = CandidateBox({ i, scores[i] }); + candidates_vector.push_back(cb); + } + } + std::sort(candidates_vector.begin(), candidates_vector.end(), [](const CandidateBox bb0, const CandidateBox bb1) + { + return bb0.second >= bb1.second; + }); + return candidates_vector; +} + +inline bool is_box_selected(const CandidateBox &cb, const SimpleTensor &bboxes, std::vector &selected_boxes, float threshold) +{ + for(int j = selected_boxes.size() - 1; j >= 0; --j) + { + if(reject_box(get_box(bboxes, cb.first), get_box(bboxes, selected_boxes[j]), threshold)) + { + return false; + } + } + return true; +} +} // namespace + +SimpleTensor non_max_suppression(const SimpleTensor &bboxes, const SimpleTensor &scores, SimpleTensor &indices, + unsigned int max_output_size, float score_threshold, float nms_threshold) +{ + const size_t num_boxes = bboxes.shape().y(); + const size_t output_size = std::min(static_cast(max_output_size), num_boxes); + const std::vector candidates_vector = get_candidates(scores, score_threshold); + std::vector selected; + size_t p(0); + while(selected.size() < output_size && p < candidates_vector.size() && selected.size() < candidates_vector.size()) + { + const auto nc = candidates_vector[p++]; + if(is_box_selected(nc, bboxes, selected, nms_threshold)) + { + selected.push_back(nc.first); + } + } + std::copy_n(selected.begin(), selected.size(), indices.data()); + return indices; +} + +} // namespace reference +} // namespace validation +} // namespace test +} // namespace arm_compute diff --git a/tests/validation/reference/NonMaxSuppression.h b/tests/validation/reference/NonMaxSuppression.h new file mode 100644 index 0000000000..0418412939 --- /dev/null +++ b/tests/validation/reference/NonMaxSuppression.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 ARM Limited. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef __ARM_COMPUTE_TEST_NON_MAX_SUPPRESION_H__ +#define __ARM_COMPUTE_TEST_NON_MAX_SUPPRESION_H__ + +#include "tests/SimpleTensor.h" + +namespace arm_compute +{ +namespace test +{ +namespace validation +{ +namespace reference +{ +SimpleTensor non_max_suppression(const SimpleTensor &bboxes, const SimpleTensor &scores, SimpleTensor &indices, + unsigned int max_output_size, float score_threshold, float nms_threshold); + +} // namespace reference +} // namespace validation +} // namespace test +} // namespace arm_compute +#endif /* __ARM_COMPUTE_TEST_NON_MAX_SUPPRESION_H__ */ -- cgit v1.2.1