From de851d24a581b7c4cde9fb085b735d5791bf8722 Mon Sep 17 00:00:00 2001 From: Abhishek Gola Date: Tue, 14 Apr 2026 11:45:46 +0530 Subject: [PATCH] Merge pull request #28121 from abhishek-gola:loop_layer_add Add Loop layer to new DNN engine #28121 Addition of loop layer in 5.x for issue: https://github.com/opencv/opencv/issues/26179 and https://github.com/opencv/opencv/issues/26141 and https://github.com/opencv/opencv/issues/25200 OpenCV Extra: https://github.com/opencv/opencv_extra/pull/1335 ### 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 --- modules/core/src/matrix_transform.cpp | 8 ++ .../dnn/include/opencv2/dnn/all_layers.hpp | 16 ++- modules/dnn/src/graph_buffer_allocator.cpp | 20 +++- modules/dnn/src/graph_const_fold.cpp | 1 + modules/dnn/src/graph_simplifier.cpp | 29 +++-- modules/dnn/src/init.cpp | 1 + modules/dnn/src/layers/layers_common.cpp | 28 +++-- modules/dnn/src/layers/loop_layer.cpp | 80 +++++++++++++ modules/dnn/src/net_impl2.cpp | 84 +++++++++++++ modules/dnn/src/onnx/onnx_importer2.cpp | 34 +++++- modules/dnn/test/test_model.cpp | 113 ++++++++++++++++++ ...conformance_layer_filter__openvino.inl.hpp | 2 +- ...yer_filter_opencv_classic_denylist.inl.hpp | 4 +- ..._conformance_layer_parser_denylist.inl.hpp | 5 +- 14 files changed, 393 insertions(+), 32 deletions(-) create mode 100644 modules/dnn/src/layers/loop_layer.cpp diff --git a/modules/core/src/matrix_transform.cpp b/modules/core/src/matrix_transform.cpp index cf7eddc3c5..aa1eb29c09 100644 --- a/modules/core/src/matrix_transform.cpp +++ b/modules/core/src/matrix_transform.cpp @@ -1209,6 +1209,14 @@ void broadcast(InputArray _src, InputArray _shape, OutputArray _dst) { } // other cases int max_ndims = std::max(dims_src, dims_shape); + if (max_ndims < 2 && src.total() == 1) { + const char* p_src = src.ptr(); + char* p_dst = dst.ptr(); + size_t esz = src.elemSize(); + for (size_t j = 0; j < dst.total(); j++) + std::memcpy(p_dst + j * esz, p_src, esz); + return; + } const int all_ndims[2] = {src.dims, dst.dims}; const int* orig_shapes[2] = {src.size.p, dst.size.p}; cv::AutoBuffer buff(max_ndims * 4); diff --git a/modules/dnn/include/opencv2/dnn/all_layers.hpp b/modules/dnn/include/opencv2/dnn/all_layers.hpp index 773f7a301f..6b18f41518 100644 --- a/modules/dnn/include/opencv2/dnn/all_layers.hpp +++ b/modules/dnn/include/opencv2/dnn/all_layers.hpp @@ -749,7 +749,7 @@ CV__DNN_INLINE_NS_BEGIN static Ptr create(const LayerParams ¶ms); }; - // Forward declaration for computational Graph used by IfLayer + // Forward declaration for computational Graph used by If/Loop layers class Graph; class CV_EXPORTS IfLayer : public Layer @@ -761,6 +761,20 @@ CV__DNN_INLINE_NS_BEGIN static Ptr create(const LayerParams& params); }; + class CV_EXPORTS LoopLayer : public Layer + { + public: + /** + * @brief Evaluate loop condition tensor as a boolean flag. + * + * The input tensor must contain exactly one element of an integral or floating type. + */ + virtual bool cond(InputArray arr) const = 0; + + /** Factory: creates a LoopLayer implementation. */ + static Ptr create(const LayerParams& params); + }; + class CV_EXPORTS ConcatLayer : public Layer { public: diff --git a/modules/dnn/src/graph_buffer_allocator.cpp b/modules/dnn/src/graph_buffer_allocator.cpp index 97cacb58a8..51ef22c7c6 100644 --- a/modules/dnn/src/graph_buffer_allocator.cpp +++ b/modules/dnn/src/graph_buffer_allocator.cpp @@ -288,8 +288,15 @@ struct BufferAllocator shareBuffer(outarg, elseOutarg); } + // Isolate subgraph buffers: prevent parent's freed buffers from + // being reused here, which causes overwrites on re-execution. + std::vector saved_freebufs = freebufs; + freebufs.clear(); assign(thenBranch); + freebufs.clear(); assign(elseBranch); + freebufs = saved_freebufs; + for (size_t i = 0; i < noutputs; i++) { Arg thenOutarg = thenOutargs[i]; Arg elseOutarg = elseOutargs[i]; @@ -320,9 +327,9 @@ struct BufferAllocator CV_Assert(body_noutputs == noutputs+1); CV_Assert(n_state_vars >= 0 && n_accums >= 0); Arg inp0 = inputs[0]; - if (inp0.idx > 0 && usecounts[inp0.idx] > 0) { - CV_Assert(!netimpl->isConstArg(inp0)); - if (!netimpl->isConstArg(trip_count)) + if (inp0.idx > 0 && usecounts[inp0.idx] > 0 && + !netimpl->isConstArg(inp0)) { + if (!netimpl->isConstArg(trip_count) && bufidxs[trip_count.idx] >= 0) shareBuffer(trip_count, inputs[0]); else bufidxs.at(inputs[0].idx) = getFreeBuffer(); @@ -335,7 +342,7 @@ struct BufferAllocator Arg v_out = i >= 0 ? outputs[i] : Arg(); if (inparg.idx > 0 && usecounts[inparg.idx] > 0) { CV_Assert(!netimpl->isConstArg(inparg)); - if (!netimpl->isConstArg(v_inp)) + if (!netimpl->isConstArg(v_inp) && v_inp.idx > 0 && bufidxs[v_inp.idx] >= 0) shareBuffer(v_inp, inparg); else bufidxs[inparg.idx] = getFreeBuffer(); @@ -346,9 +353,10 @@ struct BufferAllocator } } + std::vector saved_freebufs = freebufs; + freebufs.clear(); assign(body); - for (auto body_out: body_outputs) - releaseBuffer(bufidxs.at(body_out.idx)); + freebufs = saved_freebufs; } for (auto out: outputs) { diff --git a/modules/dnn/src/graph_const_fold.cpp b/modules/dnn/src/graph_const_fold.cpp index 2fd2f93c01..7b999bc64e 100644 --- a/modules/dnn/src/graph_const_fold.cpp +++ b/modules/dnn/src/graph_const_fold.cpp @@ -63,6 +63,7 @@ struct ConstFolding if (processGraph(g)) modified = true; } + newprog.push_back(layer); continue; } const std::vector& inputs = layer->inputs; diff --git a/modules/dnn/src/graph_simplifier.cpp b/modules/dnn/src/graph_simplifier.cpp index 2e1dc400be..b984e2ad9a 100644 --- a/modules/dnn/src/graph_simplifier.cpp +++ b/modules/dnn/src/graph_simplifier.cpp @@ -73,7 +73,7 @@ int Subgraph::getInputNodeId(const Ptr& net, return i; } } - CV_Error(Error::StsParseError, "Input node with name " + name + " not found"); + return -1; // not found in this graph — input comes from outer scope (valid for subgraphs) } bool Subgraph::match(const Ptr& net, int nodeId, @@ -139,18 +139,24 @@ bool Subgraph::match(const Ptr& net, int nodeId, if (inputNodes.size() != 2) CV_Error(Error::StsNotImplemented, "Commutative op fusion with more than 2 inputs"); + int inp0 = getInputNodeId(net, node, 0); + int inp1 = getInputNodeId(net, node, 1); auto newMatchings = makePtr>(*matchings); matchCandidates.push_back(newMatchings); state.matchings.push_back(newMatchings); - states.push({getInputNodeId(net, node, 0), inputNodes[0], state.matchings}); - states.push({getInputNodeId(net, node, 1), inputNodes[1], state.matchings}); + if (inp0 >= 0) + states.push({inp0, inputNodes[0], state.matchings}); + if (inp1 >= 0) + states.push({inp1, inputNodes[1], state.matchings}); state.matchings.pop_back(); newMatchings = makePtr>(*matchings); matchCandidates.push_back(newMatchings); state.matchings.push_back(newMatchings); - states.push({getInputNodeId(net, node, 0), inputNodes[1], state.matchings}); - states.push({getInputNodeId(net, node, 1), inputNodes[0], state.matchings}); + if (inp0 >= 0) + states.push({inp0, inputNodes[1], state.matchings}); + if (inp1 >= 0) + states.push({inp1, inputNodes[0], state.matchings}); state.matchings.pop_back(); } else @@ -158,6 +164,8 @@ bool Subgraph::match(const Ptr& net, int nodeId, for (int j = 0; j < inputNodes.size(); ++j) { nodeId = getInputNodeId(net, node, j); + if (nodeId < 0) + continue; states.push({nodeId, inputNodes[j], state.matchings}); } } @@ -264,8 +272,10 @@ void simplifySubgraphs(const Ptr& net, std::string inpName = node->getInputName(i); if (inpName.empty()) continue; - CV_Assert(nodeIds.find(inpName) != nodeIds.end()); - refcounts[nodeIds[inpName]] += 1; + auto it = nodeIds.find(inpName); + if (it == nodeIds.end()) + continue; + refcounts[it->second] += 1; } } @@ -277,7 +287,10 @@ void simplifySubgraphs(const Ptr& net, auto node = net->getNode(nodeId); for (int i = 0; i < node->getNumInputs(); ++i) { std::string inpName = node->getInputName(i); - refcounts[nodeIds[inpName]] -= 1; + auto it = nodeIds.find(inpName); + if (it == nodeIds.end()) + continue; + refcounts[it->second] -= 1; } net->removeNode(nodeId); refcounts[nodeId] = -1; // Same node cannot be removed twice diff --git a/modules/dnn/src/init.cpp b/modules/dnn/src/init.cpp index 930b7a4726..8deae9ca5a 100644 --- a/modules/dnn/src/init.cpp +++ b/modules/dnn/src/init.cpp @@ -85,6 +85,7 @@ void initializeLayerFactory() #endif CV_DNN_REGISTER_LAYER_CLASS(If, IfLayer); + CV_DNN_REGISTER_LAYER_CLASS(Loop, LoopLayer); CV_DNN_REGISTER_LAYER_CLASS(Concat, ConcatLayer); CV_DNN_REGISTER_LAYER_CLASS(Concat2, Concat2Layer); CV_DNN_REGISTER_LAYER_CLASS(ConstantOfShape, ConstantOfShapeLayer); diff --git a/modules/dnn/src/layers/layers_common.cpp b/modules/dnn/src/layers/layers_common.cpp index f3c017011d..ff063827b7 100644 --- a/modules/dnn/src/layers/layers_common.cpp +++ b/modules/dnn/src/layers/layers_common.cpp @@ -271,12 +271,18 @@ void tensorToIntVec(const Mat& tensor, std::vector& vec) } else { int type = tensor.type(); CV_Assert(type == CV_32S || type == CV_64S); - CV_Assert(tensor.dims <= 1); + // Accept tensors of any dimensionality; treat them as a flat vector. + CV_Assert(tensor.isContinuous()); int size = (int)tensor.total(); vec.resize(size); - for (int i = 0; i < size; i++) { - vec[i] = type == CV_32S ? tensor.at(i) : - saturate_cast(tensor.at(i)); + if (type == CV_32S) { + const int* p = tensor.ptr(); + for (int i = 0; i < size; i++) + vec[i] = p[i]; + } else { + const int64_t* p = tensor.ptr(); + for (int i = 0; i < size; i++) + vec[i] = saturate_cast(p[i]); } } } @@ -289,12 +295,18 @@ void tensorToFloatVec(const Mat& tensor, std::vector& vec) int type = tensor.type(); MatShape shape = tensor.shape(); CV_Assert(type == CV_32F || type == CV_16F); - CV_Assert(shape.dims <= 1); + // Accept tensors of any dimensionality; treat them as a flat vector. + CV_Assert(tensor.isContinuous()); int size = (int)shape.total(); vec.resize(size); - for (int i = 0; i < size; i++) { - vec[i] = type == CV_32F ? tensor.at(i) : - (float)tensor.at(i); + if (type == CV_32F) { + const float* p = tensor.ptr(); + for (int i = 0; i < size; i++) + vec[i] = p[i]; + } else { + const hfloat* p = tensor.ptr(); + for (int i = 0; i < size; i++) + vec[i] = (float)p[i]; } } } diff --git a/modules/dnn/src/layers/loop_layer.cpp b/modules/dnn/src/layers/loop_layer.cpp new file mode 100644 index 0000000000..8f3e4656f9 --- /dev/null +++ b/modules/dnn/src/layers/loop_layer.cpp @@ -0,0 +1,80 @@ +// 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. + +#include "../precomp.hpp" +#include "../net_impl.hpp" +#include "layers_common.hpp" +#include + +namespace cv { namespace dnn { +CV__DNN_INLINE_NS_BEGIN + +class LoopLayerImpl CV_FINAL : public LoopLayer +{ +public: + explicit LoopLayerImpl(const LayerParams& params) { setParamsFrom(params); } + virtual ~LoopLayerImpl() = default; + + // Single subgraph: the loop body + std::vector >* subgraphs() const CV_OVERRIDE { return &body_; } + + bool getMemoryShapes(const std::vector&, + const int requiredOutputs, + std::vector& outputs, + std::vector& internals) const CV_OVERRIDE + { + outputs.assign(std::max(1, requiredOutputs), MatShape()); + internals.clear(); + return false; + } + + bool dynamicOutputShapes() const CV_OVERRIDE { return true; } + + // OPTIMIZATION: Reduced to a simple numeric check via double + bool cond(InputArray arr) const CV_OVERRIDE + { + Mat buf, *inp; + if (arr.kind() == _InputArray::MAT) { + inp = (Mat*)arr.getObj(); + } else { + buf = arr.getMat(); + inp = &buf; + } + CV_Assert(inp->total() == 1u); + bool flag; + switch (inp->depth()) + { + case CV_8U: case CV_8S: case CV_Bool: + flag = *inp->ptr() != 0; break; + case CV_16U: case CV_16S: + flag = *inp->ptr() != 0; break; + case CV_16F: + case CV_16BF: + flag = *inp->ptr() != 0; break; + case CV_32U: case CV_32S: + flag = *inp->ptr() != 0; break; + case CV_32F: + flag = *inp->ptr() != 0; break; + case CV_64U: case CV_64S: + flag = *inp->ptr() != 0; break; + case CV_64F: + flag = *inp->ptr() != 0; break; + default: + CV_Error_(Error::StsBadArg, + ("Loop condition: unsupported tensor type %s", + typeToString(inp->type()).c_str())); + } + return flag; + } + +private: + mutable std::vector > body_; +}; + +Ptr LoopLayer::create(const LayerParams& params) +{ + return makePtr(params); +} +CV__DNN_INLINE_NS_END +}} // namespace cv::dnn diff --git a/modules/dnn/src/net_impl2.cpp b/modules/dnn/src/net_impl2.cpp index 434c387476..4207a92eb7 100644 --- a/modules/dnn/src/net_impl2.cpp +++ b/modules/dnn/src/net_impl2.cpp @@ -1188,6 +1188,7 @@ void Net::Impl::forwardGraph(Ptr& graph, InputArrayOfArrays inputs_, } else { Ptr iflayer = layer.dynamicCast(); + Ptr loopLayer = layer.dynamicCast(); if (iflayer) { int branch = iflayer->branch(inpMats[0]); Ptr subgraph = subgraphs->at(branch); @@ -1196,6 +1197,89 @@ void Net::Impl::forwardGraph(Ptr& graph, InputArrayOfArrays inputs_, branchInputs.assign(inpMats.begin() + 1, inpMats.end()); forwardGraph(subgraph, branchInputs, outMats, false); } + else if (loopLayer) { + CV_Assert(subgraphs->size() == 1); + Ptr body = subgraphs->at(0); + + int n_in = (int)inpMats.size(); + int n_state = n_in - 2; + int n_accum = (int)body->outputs().size() - n_state - 1; + CV_Assert(n_in >= 2 && n_state >= 0 && n_accum >= 0); + + Mat mIter, mCond, tmp; + mIter.create(1, 1, CV_64S); + if (inpMats[1].empty()) + { + mCond.create(1, 1, CV_8U); + mCond.at(0) = 1; + } + else + { + mCond = inpMats[1]; + } + int64 iter = 0; + int64 max_iter = inpMats[0].empty() ? -1 : + (inpMats[0].convertTo(tmp, CV_64S), *tmp.ptr()); + bool active = loopLayer->cond(mCond); + + std::vector state(inpMats.begin() + 2, inpMats.end()); + std::vector > history(n_accum); + std::vector inputs(n_in), outputs; + + while (active && (max_iter < 0 || iter < max_iter)) { + mIter.at(0) = iter++; + inputs[0] = mIter; + inputs[1] = mCond; + std::copy(state.begin(), state.end(), inputs.begin() + 2); + + forwardGraph(body, inputs, outputs, false); + + mCond = outputs[0]; + active = loopLayer->cond(mCond); + + for (int i = 0; i < n_state; i++) + state[i] = outputs[1 + i]; + for (int i = 0; i < n_accum; i++) + history[i].push_back(outputs[1 + n_state + i].clone()); + } + + outMats.assign(state.begin(), state.end()); + outMats.resize(n_state + n_accum); + + for (int i = 0; i < n_accum; ++i) { + const std::vector& perIter = history[i]; + if (perIter.empty()) { + outMats[n_state + i] = Mat(); + continue; + } + + const Mat& first = perIter[0]; + MatShape elemShape = first.shape(); + int ndims = elemShape.dims; + CV_Assert(ndims >= 0); + + MatShape outShape(ndims + 1); + outShape[0] = (int)perIter.size(); + for (int d = 0; d < ndims; ++d) + outShape[d + 1] = elemShape[d]; + + Mat stacked; + stacked.create(outShape, first.type()); + + for (size_t k = 0; k < perIter.size(); ++k) + { + const Mat& src = perIter[k]; + CV_Assert(src.type() == first.type()); + CV_Assert(src.total() == first.total()); + + Mat dst = stacked.reshape(0, (int)perIter.size()) + .row((int)k) + .reshape(first.channels(), elemShape.dims, &elemShape[0]); + src.copyTo(dst); + } + outMats[n_state + i] = stacked; + } + } else { CV_Error_(Error::StsNotImplemented, ("unknown layer type '%s' with subgraphs", layer->type.c_str())); diff --git a/modules/dnn/src/onnx/onnx_importer2.cpp b/modules/dnn/src/onnx/onnx_importer2.cpp index a74251cd5e..a03fae45dc 100644 --- a/modules/dnn/src/onnx/onnx_importer2.cpp +++ b/modules/dnn/src/onnx/onnx_importer2.cpp @@ -180,6 +180,7 @@ protected: void parseCastLike (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseClip (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseConcat (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); + void parseLoop (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseIf (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseConstant (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseConstantOfShape (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); @@ -1499,10 +1500,6 @@ void ONNXImporter2::parseGather(LayerParams& layerParams, const opencv_onnx::Nod { layerParams.type = "Gather2"; CV_CheckEQ(node_proto.input_size(), 2, ""); - // Diagnostics: log axis used by this Gather node (attribute may be absent -> default 0) - int axis = layerParams.get("axis", 0); - const std::string node_name = node_proto.has_name() ? node_proto.name() : std::string(); - CV_LOG_WARNING(NULL, "DNN/ONNX: Gather node '" << node_name << "' axis=" << axis << ", outputs=" << (node_proto.output_size() > 0 ? node_proto.output(0) : std::string(""))); addLayer(layerParams, node_proto); } @@ -1519,6 +1516,34 @@ void ONNXImporter2::parseConcat(LayerParams& layerParams, const opencv_onnx::Nod addLayer(layerParams, node_proto); } +void ONNXImporter2::parseLoop(LayerParams& layerParams, + const opencv_onnx::NodeProto& node_proto) +{ + // ONNX Loop: inputs = [M, cond, v0, v1, ...]; attribute "body" is a GraphProto. + CV_Assert(node_proto.input_size() >= 2); + layerParams.type = "Loop"; + + // Create Loop layer node in the current graph. + addLayer(layerParams, node_proto); + + std::vector > subgraphs(1); + for (int i = 0; i < node_proto.attribute_size(); ++i) + { + const auto& attr = node_proto.attribute(i); + if (attr.name() == "body") + { + opencv_onnx::GraphProto body = attr.g(); + Ptr graph = parseGraph(&body, false); + subgraphs[0] = graph; + } + } + + CV_Assert(!subgraphs[0].empty()); + + Ptr& loopLayer = curr_prog.back(); + *loopLayer->subgraphs() = subgraphs; +} + void ONNXImporter2::parseIf(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto) { @@ -2694,6 +2719,7 @@ void ONNXImporter2::buildDispatchMap_ONNX_AI() dispatch["Gather"] = &ONNXImporter2::parseGather; dispatch["GatherElements"] = &ONNXImporter2::parseGatherElements; dispatch["Concat"] = &ONNXImporter2::parseConcat; + dispatch["Loop"] = &ONNXImporter2::parseLoop; dispatch["If"] = &ONNXImporter2::parseIf; dispatch["Resize"] = &ONNXImporter2::parseResize2; dispatch["Size"] = &ONNXImporter2::parseSize; diff --git a/modules/dnn/test/test_model.cpp b/modules/dnn/test/test_model.cpp index 12a0098afb..62c622e3fe 100644 --- a/modules/dnn/test/test_model.cpp +++ b/modules/dnn/test/test_model.cpp @@ -978,4 +978,117 @@ TEST_P(Reproducibility_ResNet50_QDQ_ONNX, Accuracy) INSTANTIATE_TEST_CASE_P(/**/, Reproducibility_ResNet50_QDQ_ONNX, testing::ValuesIn(getAvailableTargets(DNN_BACKEND_OPENCV))); +typedef testing::TestWithParam Reproducibility_MobileNetSSD_ONNX; +TEST_P(Reproducibility_MobileNetSSD_ONNX, Accuracy) +{ + Target targetId = GetParam(); + auto engine_forced = static_cast( + cv::utils::getConfigurationParameterSizeT("OPENCV_FORCE_DNN_ENGINE", ENGINE_AUTO)); + if (engine_forced == ENGINE_CLASSIC) + { + applyTestTag(CV_TEST_TAG_DNN_SKIP_PARSER); + return; + } + + applyTestTag(targetId == DNN_TARGET_CPU ? CV_TEST_TAG_MEMORY_512MB : CV_TEST_TAG_MEMORY_1GB); + ASSERT_TRUE(ocl::useOpenCL() || targetId == DNN_TARGET_CPU || targetId == DNN_TARGET_CPU_FP16); + + std::string modelname = _tf("onnx/models/ssd_mobilenet_v1_12.onnx", false); + Net net = readNetFromONNX(modelname); + + net.setPreferableBackend(DNN_BACKEND_OPENCV); + net.setPreferableTarget(targetId); + + if (targetId == DNN_TARGET_CPU_FP16) + net.enableWinograd(false); + + std::string imgname = _tf("dog_orig_size.png"); + Mat image = imread(imgname); + ASSERT_TRUE(!image.empty()); + Mat input; + resize(image, input, Size(300, 300)); + int imsize[] = {1, input.rows, input.cols, 3}; + Mat input8dim4(4, imsize, CV_8U, input.data); + + std::vector outNames = net.getUnconnectedOutLayersNames(); + std::vector outs; + double min_t = 0; + const int niters = +#ifdef _DEBUG + 1; +#else + 30; +#endif + + for (int i = 0; i < niters; i++) { + double t = (double)getTickCount(); + net.setInput(input8dim4); + net.forward(outs, outNames); + t = (double)getTickCount() - t; + min_t = i == 0 ? t : std::min(min_t, t); + } + printf("run time = %.2fms\n", min_t*1000./getTickFrequency()); + + // Model outputs: detection_boxes [1,N,4], detection_classes [1,N], + // detection_scores [1,N], num_detections [1] + ASSERT_EQ(outs.size(), (size_t)4); + + Mat boxes, classes, scores, numDet; + for (size_t i = 0; i < outs.size(); i++) { + if (outs[i].dims == 3 && outs[i].size[2] == 4) + boxes = outs[i]; + else if (outs[i].total() == 1) + numDet = outs[i]; + else if (outs[i].dims == 2) { + float first = outs[i].at(0, 0); + if (first == std::round(first) && first > 0) + classes = outs[i]; + else + scores = outs[i]; + } + } + ASSERT_FALSE(boxes.empty()); + ASSERT_FALSE(scores.empty()); + ASSERT_FALSE(classes.empty()); + ASSERT_FALSE(numDet.empty()); + + int ndet = (int)numDet.at(0); + printf("num_detections = %d\n", ndet); + ASSERT_GT(ndet, 0); + + // Build test detection vectors from model outputs + // Model boxes are normalized (y1, x1, y2, x2) — convert to Rect2d(x, y, w, h) + std::vector testClassIds; + std::vector testScores; + std::vector testBoxes; + for (int j = 0; j < ndet; j++) { + testClassIds.push_back((int)classes.at(0, j)); + testScores.push_back(scores.at(0, j)); + float y1 = boxes.at(0, j, 0); + float x1 = boxes.at(0, j, 1); + float y2 = boxes.at(0, j, 2); + float x2 = boxes.at(0, j, 3); + testBoxes.push_back(Rect2d(x1, y1, x2 - x1, y2 - y1)); + } + + // Reference detections for dog_orig_size.png + // COCO 1-indexed: 2=bicycle, 3=car, 18=dog + std::vector refClassIds = {2, 18, 3}; + std::vector refScores = {0.944377f, 0.877805f, 0.787824f}; + std::vector refBoxes = { + Rect2d(0.157917, 0.219984, 0.742909 - 0.157917, 0.739280 - 0.219984), // bicycle + Rect2d(0.168082, 0.360803, 0.426304 - 0.168082, 0.919625 - 0.360803), // dog + Rect2d(0.600506, 0.114612, 0.899101 - 0.600506, 0.298757 - 0.114612), // car + }; + + float confThreshold = 0.5f; + double scoreDiff = 0.1; + double iouDiff = 0.05; + normAssertDetections(refClassIds, refScores, refBoxes, + testClassIds, testScores, testBoxes, + "", confThreshold, scoreDiff, iouDiff); +} +INSTANTIATE_TEST_CASE_P(/**/, Reproducibility_MobileNetSSD_ONNX, + testing::ValuesIn(getAvailableTargets(DNN_BACKEND_OPENCV))); + }} // namespace diff --git a/modules/dnn/test/test_onnx_conformance_layer_filter__openvino.inl.hpp b/modules/dnn/test/test_onnx_conformance_layer_filter__openvino.inl.hpp index 67336a2ea2..8380090602 100644 --- a/modules/dnn/test/test_onnx_conformance_layer_filter__openvino.inl.hpp +++ b/modules/dnn/test/test_onnx_conformance_layer_filter__openvino.inl.hpp @@ -1435,7 +1435,7 @@ CASE(test_logsoftmax_negative_axis_expanded) CASE(test_logsoftmax_negative_axis_expanded_ver18) SKIP; CASE(test_loop11) - // no filter + SKIP; CASE(test_loop13_seq) // no filter CASE(test_loop16_seq_none) diff --git a/modules/dnn/test/test_onnx_conformance_layer_filter_opencv_classic_denylist.inl.hpp b/modules/dnn/test/test_onnx_conformance_layer_filter_opencv_classic_denylist.inl.hpp index 6f1a9ad09d..705c0207d6 100644 --- a/modules/dnn/test/test_onnx_conformance_layer_filter_opencv_classic_denylist.inl.hpp +++ b/modules/dnn/test/test_onnx_conformance_layer_filter_opencv_classic_denylist.inl.hpp @@ -600,7 +600,8 @@ "test_rotary_embedding_with_interleaved_rotary_dim", "test_rotary_embedding_with_interleaved_rotary_dim_expanded", "test_rotary_embedding_with_rotary_dim", -"test_rotary_embedding_with_rotary_dim_expanded","test_attention_3d", +"test_rotary_embedding_with_rotary_dim_expanded", +"test_attention_3d", "test_attention_3d_attn_mask", "test_attention_3d_causal", "test_attention_3d_diff_heads_sizes", @@ -733,3 +734,4 @@ "test_reduce_sum_square_empty_set", "test_reduce_sum_square_empty_set_expanded", "test_reduce_log_sum_exp_empty_set_expanded", +"test_loop11", diff --git a/modules/dnn/test/test_onnx_conformance_layer_parser_denylist.inl.hpp b/modules/dnn/test/test_onnx_conformance_layer_parser_denylist.inl.hpp index 62cd2624e3..31907751b8 100644 --- a/modules/dnn/test/test_onnx_conformance_layer_parser_denylist.inl.hpp +++ b/modules/dnn/test/test_onnx_conformance_layer_parser_denylist.inl.hpp @@ -331,9 +331,8 @@ "test_l1normalization_axis_last", "test_l2normalization_axis_0", "test_l2normalization_axis_1", -"test_loop11", // Issue::'Graph' is not supported in function 'getLayerParams' -"test_loop13_seq", // Issue::typeProto.has_tensor_type() in function 'populateNet' -"test_loop16_seq_none", // Issue::Failed to allocate 179812654996800 bytes in function 'OutOfMemoryError' +"test_loop13_seq", // Loop with tensor sequences output, not yet supported in OpenCV +"test_loop16_seq_none", // Loop with optional tensor sequences, not yet supported in OpenCV "test_lpnormalization_default", "test_lppool_1d_default", "test_lppool_2d_default",