1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-29 23:33:05 +04:00

Merge pull request #27674 from abhishek-gola:nonmaxsuppression_layer_add

Added nonmaxsuppression (NMS) layer to new DNN engine #27674

### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
Abhishek Gola
2025-09-19 15:25:37 +05:30
committed by GitHub
parent 07b7dd02cd
commit d79e95c018
7 changed files with 259 additions and 18 deletions
@@ -1414,6 +1414,12 @@ CV__DNN_INLINE_NS_BEGIN
static Ptr<CastLayer> create(const LayerParams &params);
};
class CV_EXPORTS NonMaxSuppressionLayer : public Layer
{
public:
static Ptr<NonMaxSuppressionLayer> create(const LayerParams& params);
};
class CV_EXPORTS ClipLayer : public Layer {
public:
static Ptr<ClipLayer> create(const LayerParams &params);
+1
View File
@@ -96,6 +96,7 @@ void initializeLayerFactory()
CV_DNN_REGISTER_LAYER_CLASS(Pad2, Pad2Layer);
CV_DNN_REGISTER_LAYER_CLASS(NonZero, NonZeroLayer);
CV_DNN_REGISTER_LAYER_CLASS(QuantizeLinear, QuantizeLinearLayer);
CV_DNN_REGISTER_LAYER_CLASS(NonMaxSuppression, NonMaxSuppressionLayer);
CV_DNN_REGISTER_LAYER_CLASS(Range, RangeLayer);
CV_DNN_REGISTER_LAYER_CLASS(Reshape, ReshapeLayer);
CV_DNN_REGISTER_LAYER_CLASS(Reshape2, Reshape2Layer);
@@ -0,0 +1,226 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
// Copyright (C) 2025, BigVision LLC, all rights reserved.
// Third party copyrights are property of their respective owners.
#include "../precomp.hpp"
#include "layers_common.hpp"
#include <opencv2/dnn/shape_utils.hpp>
#include <limits>
#include <numeric>
#include <cstdint>
namespace cv {
namespace dnn {
// ONNX NonMaxSuppression (opset 10 and 11)
// Spec: https://onnx.ai/onnx/operators/onnx__NonMaxSuppression.html
// Inputs: boxes [B, N, 4], scores [B, C, N], (optional) max_output_boxes_per_class, iou_threshold, score_threshold
// Output: selected_indices [K, 3] (int64): [batch_idx, class_idx, box_idx]
class NonMaxSuppressionLayerImpl CV_FINAL : public NonMaxSuppressionLayer
{
public:
NonMaxSuppressionLayerImpl(const LayerParams& p) {
setParamsFrom(p);
centerPointBox = p.get<int>("center_point_box", 0);
defaultMaxOut = p.get<int>("default_max_output_boxes_per_class", 0);
defaultIouThr = p.get<float>("default_iou_threshold", 0.f);
defaultScoreThr = p.get<float>("default_score_threshold", 0.f);
}
bool supportBackend(int backendId) CV_OVERRIDE {
return backendId == DNN_BACKEND_OPENCV;
}
virtual bool dynamicOutputShapes() const CV_OVERRIDE { return true; }
bool getMemoryShapes(const std::vector<MatShape>& inputs, const int, std::vector<MatShape>& outputs, std::vector<MatShape>&) const CV_OVERRIDE {
CV_Assert(inputs.size() >= 2);
const MatShape& boxes = inputs[0];
const MatShape& scores = inputs[1];
CV_Assert(boxes.size() == 3 && boxes[2] == 4);
CV_Assert(scores.size() == 3 && boxes[0] == scores[0] && boxes[1] == scores[2]);
outputs.assign(1, MatShape({1, 3}));
return false;
}
void getTypes(const std::vector<MatType>&, const int requiredOutputs, const int requiredInternals,
std::vector<MatType>& outputs, std::vector<MatType>& internals) const CV_OVERRIDE {
outputs.assign(requiredOutputs, MatType(CV_64S));
}
void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, OutputArrayOfArrays) CV_OVERRIDE {
std::vector<Mat> inputs, outputs;
Mat boxesBlob, scoresBlob;
int maxOut = defaultMaxOut;
float iouThr = defaultIouThr;
float scoreThr = defaultScoreThr;
auto inKind = inputs_arr.kind();
if (inKind == _InputArray::STD_VECTOR_UMAT) {
std::vector<UMat> uinputs;
inputs_arr.getUMatVector(uinputs);
CV_Assert(uinputs.size() >= 2);
boxesBlob = uinputs[0].getMat(ACCESS_READ);
scoresBlob = uinputs[1].getMat(ACCESS_READ);
if (uinputs.size() >= 3 && !uinputs[2].empty()) {
Mat tmp = uinputs[2].getMat(ACCESS_READ);
CV_Assert(tmp.channels() == 1 && tmp.total() == 1);
maxOut = tensorToScalar<int>(tmp);
}
if (uinputs.size() >= 4 && !uinputs[3].empty()) {
Mat tmp = uinputs[3].getMat(ACCESS_READ);
CV_Assert(tmp.channels() == 1 && tmp.total() == 1);
iouThr = tensorToScalar<float>(tmp);
}
if (uinputs.size() >= 5 && !uinputs[4].empty()) {
Mat tmp = uinputs[4].getMat(ACCESS_READ);
CV_Assert(tmp.channels() == 1 && tmp.total() == 1);
scoreThr = tensorToScalar<float>(tmp);
}
} else {
inputs_arr.getMatVector(inputs);
CV_Assert(inputs.size() >= 2);
boxesBlob = inputs[0];
scoresBlob = inputs[1];
if (inputs.size() >= 3 && !inputs[2].empty()) {
CV_Assert(inputs[2].channels() == 1 && inputs[2].total() == 1);
maxOut = tensorToScalar<int>(inputs[2]);
}
if (inputs.size() >= 4 && !inputs[3].empty()) {
CV_Assert(inputs[3].channels() == 1 && inputs[3].total() == 1);
iouThr = tensorToScalar<float>(inputs[3]);
}
if (inputs.size() >= 5 && !inputs[4].empty()) {
CV_Assert(inputs[4].channels() == 1 && inputs[4].total() == 1);
scoreThr = tensorToScalar<float>(inputs[4]);
}
}
const int B = boxesBlob.size[0];
const int C = scoresBlob.size[1];
const int tasks = B * C;
std::vector<std::vector<Vec<int64_t,3>>> tripletsPerBC;
run(boxesBlob, scoresBlob, maxOut, iouThr, scoreThr, tripletsPerBC);
int K = 0;
for (int t = 0; t < tasks; ++t) K += (int)tripletsPerBC[t].size();
Mat outMat;
auto outKind = outputs_arr.kind();
if (outKind == _InputArray::STD_VECTOR_MAT) {
std::vector<Mat>& outs = outputs_arr.getMatVecRef();
CV_Assert(outs.size() == 1);
outs[0].create(K, 3, CV_64S);
outMat = outs[0];
} else if (outKind == _InputArray::STD_VECTOR_UMAT) {
std::vector<UMat> uouts;
outputs_arr.getUMatVector(uouts);
CV_Assert(uouts.size() == 1);
uouts[0].fit(MatShape({K, 3}), CV_64S);
outMat = uouts[0].getMat(ACCESS_WRITE);
} else {
outputs_arr.getMatRef(0).create(K, 3, CV_64S);
outMat = outputs_arr.getMatRef(0);
}
auto* out = outMat.ptr<int64_t>();
std::vector<int> offsets(tasks + 1, 0);
for (int t = 0; t < tasks; ++t)
offsets[t + 1] = offsets[t] + (int)tripletsPerBC[t].size() * 3;
parallel_for_(Range(0, tasks), [&](const Range& r){
for (int t = r.start; t < r.end; ++t) {
int64_t* dst = out + offsets[t];
const auto& v = tripletsPerBC[t];
for (const auto& trip : v) {
*dst++ = trip[0]; *dst++ = trip[1]; *dst++ = trip[2];
}
}
});
}
private:
void run(const Mat& boxesBlob, const Mat& scoresBlob, int maxOut, float iouThr, float scoreThr,
std::vector<std::vector<Vec<int64_t,3>>>& tripletsPerBC) const
{
const int B = boxesBlob.size[0];
const int N = boxesBlob.size[1];
const int C = scoresBlob.size[1];
const int tasks = B * C;
tripletsPerBC.clear();
tripletsPerBC.resize(tasks);
parallel_for_(Range(0, tasks), [&](const Range& r){
std::vector<Rect2f> rects;
std::vector<float> scores;
std::vector<int> globalIndices;
rects.reserve(N); scores.reserve(N); globalIndices.reserve(N);
std::vector<Rect2d> rects2d;
std::vector<int> keep;
rects2d.reserve(N);
keep.reserve(N);
for (int taskIdx = r.start; taskIdx < r.end; ++taskIdx) {
rects.clear(); scores.clear(); globalIndices.clear();
rects2d.clear(); keep.clear();
const int b = taskIdx / C;
const int c = taskIdx % C;
const float* sPtr = scoresBlob.ptr<float>(b, c);
for (int n = 0; n < N; ++n) {
float sc = sPtr[n];
const float* bx = boxesBlob.ptr<float>(b, n);
Rect2f r;
if (centerPointBox == 0) {
const float y1 = bx[0], x1 = bx[1], y2 = bx[2], x2 = bx[3];
const float x = std::min(x1, x2), y = std::min(y1, y2);
const float w = std::max(0.f, std::abs(x2 - x1));
const float h = std::max(0.f, std::abs(y2 - y1));
r = Rect2f(x, y, w, h);
} else {
const float yc = bx[0], xc = bx[1];
const float h = std::max(0.f, bx[2]), w = std::max(0.f, bx[3]);
r = Rect2f(xc - 0.5f*w, yc - 0.5f*h, w, h);
}
rects.push_back(r);
scores.push_back(sc);
globalIndices.push_back(n);
}
if (rects.empty())
continue;
for (const Rect2f& rf : rects) {
rects2d.emplace_back((double)rf.x, (double)rf.y, (double)rf.width, (double)rf.height);
}
NMSBoxes(rects2d, scores, /*score_threshold*/ scoreThr, /*nms_threshold*/ iouThr, keep, 1.f, 0);
if (maxOut > 0 && (int)keep.size() > maxOut)
keep.resize(maxOut);
auto& local = tripletsPerBC[taskIdx];
local.reserve(keep.size());
for (int kept : keep) {
const int globalIdx = globalIndices[kept];
local.push_back({(int64_t)b, (int64_t)c, (int64_t)globalIdx});
}
}
});
}
int centerPointBox = 0;
int defaultMaxOut = 0;
float defaultIouThr = 0.f;
float defaultScoreThr = 0.f;
};
Ptr<NonMaxSuppressionLayer> NonMaxSuppressionLayer::create(const LayerParams& params) {
return Ptr<NonMaxSuppressionLayer>(new NonMaxSuppressionLayerImpl(params));
}
}} // namespace dnn
+8
View File
@@ -232,6 +232,7 @@ protected:
void parseTranspose (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseUnsqueeze (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseUpsample (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseNonMaxSuprression (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseTopK2 (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseBitShift (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
@@ -1704,6 +1705,12 @@ void ONNXImporter2::parseGridSample(LayerParams& layerParams, const opencv_onnx:
addLayer(layerParams, node_proto);
}
void ONNXImporter2::parseNonMaxSuprression(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
{
layerParams.type = "NonMaxSuprression";
addLayer(layerParams, node_proto);
}
void ONNXImporter2::parseUpsample(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
{
int n_inputs = node_proto.input_size();
@@ -2548,6 +2555,7 @@ void ONNXImporter2::buildDispatchMap_ONNX_AI(int opset_version)
dispatch["GridSample"] = &ONNXImporter2::parseGridSample;
dispatch["Upsample"] = &ONNXImporter2::parseUpsample;
dispatch["BitShift"] = &ONNXImporter2::parseBitShift;
dispatch["NonMaxSuprression"] = &ONNXImporter2::parseNonMaxSuprression;
dispatch["SoftMax"] = dispatch["Softmax"] = dispatch["LogSoftmax"] = &ONNXImporter2::parseSoftMax;
dispatch["DetectionOutput"] = &ONNXImporter2::parseDetectionOutput;
dispatch["CumSum"] = &ONNXImporter2::parseCumSum;
@@ -1200,23 +1200,23 @@ CASE(test_nllloss_NCd1d2d3d4d5_none_no_weight)
CASE(test_nllloss_NCd1d2d3d4d5_none_no_weight_expanded)
// no filter
CASE(test_nonmaxsuppression_center_point_box_format)
// no filter
SKIP;
CASE(test_nonmaxsuppression_flipped_coordinates)
// no filter
SKIP;
CASE(test_nonmaxsuppression_identical_boxes)
// no filter
SKIP;
CASE(test_nonmaxsuppression_limit_output_size)
// no filter
SKIP;
CASE(test_nonmaxsuppression_single_box)
// no filter
SKIP;
CASE(test_nonmaxsuppression_suppress_by_IOU)
// no filter
SKIP;
CASE(test_nonmaxsuppression_suppress_by_IOU_and_scores)
// no filter
SKIP;
CASE(test_nonmaxsuppression_two_batches)
// no filter
SKIP;
CASE(test_nonmaxsuppression_two_classes)
// no filter
SKIP;
CASE(test_nonzero_example)
SKIP;
CASE(test_not_2d)
@@ -166,3 +166,12 @@
"test_resize_downsample_sizes_linear_pytorch_half_pixel",
"test_resize_downsample_sizes_nearest_tf_half_pixel_for_nn",
"test_resize_tf_crop_and_resize",
"test_nonmaxsuppression_center_point_box_format",
"test_nonmaxsuppression_flipped_coordinates",
"test_nonmaxsuppression_identical_boxes",
"test_nonmaxsuppression_limit_output_size",
"test_nonmaxsuppression_single_box",
"test_nonmaxsuppression_suppress_by_IOU",
"test_nonmaxsuppression_suppress_by_IOU_and_scores",
"test_nonmaxsuppression_two_batches",
"test_nonmaxsuppression_two_classes",
@@ -123,15 +123,6 @@
"test_nllloss_NCd1d2d3d4d5_mean_weight", // Issue:: Layer does not exist (NegativeLogLikelihoodLoss, SoftmaxCrossEntropyLoss)
"test_nllloss_NCd1d2d3d4d5_mean_weight_expanded", // Issue::Wrong output
"test_nllloss_NCd1d2d3d4d5_none_no_weight", // Issue:: Layer does not exist (NegativeLogLikelihoodLoss, SoftmaxCrossEntropyLoss)
"test_nonmaxsuppression_center_point_box_format", // Issue:: Layer does not exist (NonMaxSuppression)::Can't create layer "onnx_node_output_0!selected_indices" of type "NonMaxSuppression" in function 'getLayerInstance'
"test_nonmaxsuppression_flipped_coordinates", // ---- same as above ---
"test_nonmaxsuppression_identical_boxes", // ---- same as above ---
"test_nonmaxsuppression_limit_output_size", // ---- same as above ---
"test_nonmaxsuppression_single_box", // ---- same as above ---
"test_nonmaxsuppression_suppress_by_IOU", // ---- same as above ---
"test_nonmaxsuppression_suppress_by_IOU_and_scores", // ---- same as above ---
"test_nonmaxsuppression_two_batches", // ---- same as above ---
"test_nonmaxsuppression_two_classes", // ---- same as above ---
"test_onehot_negative_indices", // Issue:: Layer does not exist (OneHot) :: Can't create layer "onnx_node_output_0!y" of type "OneHot" in function 'getLayerInstance'
"test_onehot_with_axis", // ---- same as above ---
"test_onehot_with_negative_axis", // ---- same as above ---