diff --git a/modules/dnn/include/opencv2/dnn/all_layers.hpp b/modules/dnn/include/opencv2/dnn/all_layers.hpp index 6294c48cf8..5a06982d5c 100644 --- a/modules/dnn/include/opencv2/dnn/all_layers.hpp +++ b/modules/dnn/include/opencv2/dnn/all_layers.hpp @@ -1337,6 +1337,12 @@ CV__DNN_INLINE_NS_BEGIN static Ptr create(const LayerParams& params); }; + class CV_EXPORTS UniqueLayer : public Layer + { + public: + static Ptr create(const LayerParams& params); + }; + class CV_EXPORTS LayerNormLayer : public Layer { public: diff --git a/modules/dnn/src/init.cpp b/modules/dnn/src/init.cpp index 27e1d1fc1d..805268b176 100644 --- a/modules/dnn/src/init.cpp +++ b/modules/dnn/src/init.cpp @@ -108,6 +108,7 @@ void initializeLayerFactory() CV_DNN_REGISTER_LAYER_CLASS(Split2, Split2Layer); CV_DNN_REGISTER_LAYER_CLASS(Squeeze, SqueezeLayer); CV_DNN_REGISTER_LAYER_CLASS(Tile2, Tile2Layer); + CV_DNN_REGISTER_LAYER_CLASS(Unique, UniqueLayer); CV_DNN_REGISTER_LAYER_CLASS(Transpose, TransposeLayer); CV_DNN_REGISTER_LAYER_CLASS(Unsqueeze, UnsqueezeLayer); CV_DNN_REGISTER_LAYER_CLASS(IsNaN, IsNaNLayer); diff --git a/modules/dnn/src/layers/unique_layer.cpp b/modules/dnn/src/layers/unique_layer.cpp new file mode 100644 index 0000000000..df0d8d7d8b --- /dev/null +++ b/modules/dnn/src/layers/unique_layer.cpp @@ -0,0 +1,341 @@ +// 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 + +// ONNX operator: Unique +// Spec: https://onnx.ai/onnx/operators/onnx__Unique.html +// Supported opsets: 11 (axis attribute introduced in opset 11) + +namespace cv { +namespace dnn { + +class UniqueLayerImpl CV_FINAL : public UniqueLayer +{ +public: + UniqueLayerImpl(const LayerParams& params) + { + setParamsFrom(params); + sorted_ = params.get("sorted", true); + hasAxis_ = params.has("axis"); + axis_ = hasAxis_ ? params.get("axis") : 0; + } + + bool supportBackend(int backendId) CV_OVERRIDE + { + return backendId == DNN_BACKEND_OPENCV; + } + + bool dynamicOutputShapes() const CV_OVERRIDE + { + return true; + } + + bool getMemoryShapes(const std::vector& inputs, + const int requiredOutputs, + std::vector& outputs, + std::vector& /*internals*/) const CV_OVERRIDE + { + CV_Assert(inputs.size() == 1); + outputs.resize(requiredOutputs); + return false; + } + + void getTypes(const std::vector& inputs, + const int requiredOutputs, + const int /*requiredInternals*/, + std::vector& outputs, + std::vector& /*internals*/) const CV_OVERRIDE + { + CV_Assert(inputs.size() == 1); + outputs.resize(requiredOutputs); + outputs[0] = inputs[0]; + for (int i = 1; i < requiredOutputs; ++i) + outputs[i] = MatType(CV_64S); + } + + void forward(InputArrayOfArrays inputs_arr, + OutputArrayOfArrays outputs_arr, + OutputArrayOfArrays /*internals_arr*/) CV_OVERRIDE + { + std::vector inputs; + inputs_arr.getMatVector(inputs); + CV_Assert(inputs.size() == 1); + + const Mat& Xin = inputs[0]; + CV_Assert(Xin.dims >= 1); + + int ax = hasAxis_ ? normalize_axis(axis_, Xin.dims) : -1; + + auto kind = outputs_arr.kind(); + if (kind == _InputArray::STD_VECTOR_MAT) + { + std::vector& outs = outputs_arr.getMatVecRef(); + CV_Assert(!outs.empty()); + dispatchByDepth(Xin, outs, ax, sorted_); + } + else if (kind == _InputArray::STD_VECTOR_UMAT) + { + std::vector& uouts = outputs_arr.getUMatVecRef(); + CV_Assert(!uouts.empty()); + std::vector tmp(uouts.size()); + dispatchByDepth(Xin, tmp, ax, sorted_); + for (size_t i = 0; i < uouts.size(); ++i) + { + if (tmp[i].empty()) + continue; + MatShape sh = shape(tmp[i]); + uouts[i].fit(sh, tmp[i].type()); + tmp[i].copyTo(uouts[i]); + } + } + else + { + CV_Error(cv::Error::StsBadArg, cv::format("Unsupported output array kind: %d", kind)); + } + } + +private: + template + void sortAndGroupKeys(std::vector>& keyed, + const LessKey& lessKey, + const EqualKey& equalKey, + std::vector& groupRepIndex, + std::vector& firstJ, + std::vector& counts, + std::vector& inv) const + { + std::sort(keyed.begin(), keyed.end(), [&](const std::pair& a, const std::pair& b){ + if (lessKey(a.second, b.second)) return true; + if (lessKey(b.second, a.second)) return false; + return a.first < b.first; + }); + + int g = 0; + const int A = (int)keyed.size(); + for (int pos = 0; pos < A; ) + { + int start = pos; + int minJ = keyed[pos].first; + int64 cnt = 0; + while (pos < A && equalKey(keyed[start].second, keyed[pos].second)) { + inv[ keyed[pos].first ] = g; + minJ = std::min(minJ, keyed[pos].first); + ++cnt; ++pos; + } + groupRepIndex.push_back(keyed[start].first); + firstJ.push_back(minJ); + counts.push_back(cnt); + ++g; + } + } + + template + void uniqueAxisImpl(const Mat& X, std::vector& outs, int ax, bool sorted) const + { + const int r = X.dims; + + MatShape inShape = shape(X); + int dimAxis; + size_t outer = 1, inner = 1; + if (ax < 0) { + dimAxis = (int)X.total(); + } else { + dimAxis = inShape[ax]; + outer = std::accumulate(inShape.begin(), inShape.begin() + ax, (size_t)1, std::multiplies()); + inner = std::accumulate(inShape.begin() + ax + 1, inShape.end(), (size_t)1, std::multiplies()); + } + + const int A = dimAxis; + const int64 sliceElems = (ax < 0) ? 1 : (int64)outer * (int64)inner; + + const T* inPtr = X.ptr(); + + std::vector groupRepIndex; groupRepIndex.reserve(A); + std::vector firstJ; firstJ.reserve(A); + std::vector counts; counts.reserve(A); + std::vector inv(A, -1); + + if (ax < 0) + { + using Pair = std::pair; // (original index, value) + std::vector keyed(A); + for (int u = 0; u < A; ++u) + keyed[u] = {u, static_cast(inPtr[(size_t)u])}; + + auto lessPair = [](const WT& a, const WT& b){ return a < b; }; + auto equalPair = [](const WT& a, const WT& b){ return a == b; }; + sortAndGroupKeys(keyed, lessPair, equalPair, groupRepIndex, firstJ, counts, inv); + } + else + { + const size_t S = (size_t)sliceElems; + std::vector flat((size_t)A * S); + for (int u = 0; u < A; ++u) + { + size_t wrote = 0; + for (size_t ob = 0; ob < outer; ++ob) + { + for (size_t ij = 0; ij < inner; ++ij) + { + size_t srcOff = ob * (size_t)dimAxis * inner + (size_t)u * inner + ij; + flat[(size_t)u * S + wrote] = static_cast(inPtr[srcOff]); + ++wrote; + } + } + } + + using Pair = std::pair; // (original index, offset into flat) + std::vector keyed(A); + for (int u = 0; u < A; ++u) + keyed[u] = {u, (size_t)u * S}; + + auto lessLex = [&](size_t aoff, size_t boff){ + const WT* pa = &flat[aoff]; + const WT* pb = &flat[boff]; + for (size_t t = 0; t < S; ++t) { + if (pa[t] < pb[t]) return true; + if (pa[t] > pb[t]) return false; + } + return false; + }; + auto equalLex = [&](size_t aoff, size_t boff){ + const WT* pa = &flat[aoff]; + const WT* pb = &flat[boff]; + for (size_t t = 0; t < S; ++t) { + if (pa[t] != pb[t]) return false; + } + return true; + }; + + sortAndGroupKeys(keyed, lessLex, equalLex, groupRepIndex, firstJ, counts, inv); + } + + std::vector order((int)groupRepIndex.size()); + std::iota(order.begin(), order.end(), 0); + if (!sorted) { + auto firstOccurLess = [&](int ga, int gb){ return firstJ[ga] < firstJ[gb]; }; + std::sort(order.begin(), order.end(), firstOccurLess); + } + + std::vector remap(order.size()); + for (int newi = 0; newi < (int)order.size(); ++newi) + remap[ order[newi] ] = newi; + + if (ax < 0) + { + MatShape yshape(1); yshape[0] = (int)order.size(); + outs[0].fit(yshape, X.type()); + T* yp = outs[0].ptr(); + for (int yi = 0; yi < (int)order.size(); ++yi) + { + int u = groupRepIndex[ order[yi] ]; + yp[yi] = inPtr[(size_t)u]; + } + } + else + { + std::vector ysz(r); + for (int k = 0; k < r; ++k) ysz[k] = X.size[k]; + ysz[ax] = (int)order.size(); + MatShape yshape(ysz); + outs[0].fit(yshape, X.type()); + + parallel_for_(Range(0, (int)order.size()), [&](const Range& rq){ + std::vector yidx(r, 0); + std::vector ystepB(r); + for (int k = 0; k < r; ++k) ystepB[k] = outs[0].step[k]; + + for (int q = rq.start; q < rq.end; ++q) { + int oldq = order[q]; + int oldu = groupRepIndex[oldq]; + + std::fill(yidx.begin(), yidx.end(), 0); + int64 wrote = 0; + for (int64 t = 0; t < sliceElems; ++t) + { + yidx[ax] = q; + size_t yoff = 0; + for (int k = 0; k < r; ++k) yoff += (size_t)yidx[k] * ystepB[k]; + T* yptrT = reinterpret_cast(outs[0].ptr() + yoff); + + size_t ob = (size_t)(wrote / (int64)inner); + size_t ij = (size_t)(wrote % (int64)inner); + size_t srcOff = ob * (size_t)dimAxis * inner + (size_t)oldu * inner + ij; + const T vs = inPtr[srcOff]; + *yptrT = vs; + wrote++; + + for (int k = r - 1; k >= 0; --k) { + if (k == ax) continue; + if (++yidx[k] < X.size[k]) break; + yidx[k] = 0; + } + } + } + }); + } + + if (outs.size() > 1) { + MatShape ishape(1); ishape[0] = (int)order.size(); + outs[1].fit(ishape, CV_64S); + auto ip = outs[1].ptr(); + for (int yi = 0; yi < (int)order.size(); ++yi) + ip[yi] = firstJ[ order[yi] ]; + } + + if (outs.size() > 2) { + MatShape invshape(1); invshape[0] = A; + outs[2].fit(invshape, CV_64S); + auto invp = outs[2].ptr(); + for (int j = 0; j < A; ++j) + invp[j] = remap[ inv[j] ]; + } + + if (outs.size() > 3) { + MatShape cshape(1); cshape[0] = (int)order.size(); + outs[3].fit(cshape, CV_64S); + auto cp = outs[3].ptr(); + for (int yi = 0; yi < (int)order.size(); ++yi) + cp[yi] = counts[ order[yi] ]; + } + } + + void dispatchByDepth(const Mat& X, std::vector& outs, int ax, bool sorted) const + { + switch (X.depth()) + { + case CV_8U: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_8S: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_16U: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_16S: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_16F: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_16BF: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_32U: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_32S: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_32F: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_64U: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_64S: uniqueAxisImpl(X, outs, ax, sorted); break; + case CV_64F: uniqueAxisImpl(X, outs, ax, sorted); break; + default: + CV_Error(Error::StsUnsupportedFormat, "Unsupported data type for Unique"); + } + } + + bool sorted_; + bool hasAxis_; + int axis_; +}; + +Ptr UniqueLayer::create(const LayerParams& params) +{ + return Ptr(new UniqueLayerImpl(params)); +} + +}} // namespace cv::dnn diff --git a/modules/dnn/src/onnx/onnx_importer2.cpp b/modules/dnn/src/onnx/onnx_importer2.cpp index 0b1790c3cc..87fe094726 100644 --- a/modules/dnn/src/onnx/onnx_importer2.cpp +++ b/modules/dnn/src/onnx/onnx_importer2.cpp @@ -216,7 +216,8 @@ protected: void parseDet (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseGridSample (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseResize (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); - void parseSize (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); + void parseSize (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); + void parseUnique (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseReshape (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseScatter (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); void parseShape (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto); @@ -1602,6 +1603,12 @@ void ONNXImporter2::parseResize(LayerParams& layerParams, const opencv_onnx::Nod addLayer(layerParams, node_proto, ninputs); } +void ONNXImporter2::parseUnique(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto) +{ + layerParams.type = "Unique"; + addLayer(layerParams, node_proto); +} + void ONNXImporter2::parseSize(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto) { layerParams.type = "Size"; @@ -2496,6 +2503,7 @@ void ONNXImporter2::buildDispatchMap_ONNX_AI(int opset_version) dispatch["If"] = &ONNXImporter2::parseIf; dispatch["Resize"] = &ONNXImporter2::parseResize; dispatch["Size"] = &ONNXImporter2::parseSize; + dispatch["Unique"] = &ONNXImporter2::parseUnique; dispatch["Trilu"] = &ONNXImporter2::parseTrilu; dispatch["IsNaN"] = &ONNXImporter2::parseIsNaN; dispatch["IsInf"] = &ONNXImporter2::parseIsInf; 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 eec1ba975a..cf222d5964 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 @@ -2094,15 +2094,15 @@ CASE(test_triu_square_neg) CASE(test_triu_zero) // no filter CASE(test_unique_not_sorted_without_axis) - // no filter + SKIP; CASE(test_unique_sorted_with_axis) - // no filter + SKIP; CASE(test_unique_sorted_with_axis_3d) - // no filter + SKIP; CASE(test_unique_sorted_with_negative_axis) - // no filter + SKIP; CASE(test_unique_sorted_without_axis) - // no filter + SKIP; CASE(test_unsqueeze_axis_0) SKIP; CASE(test_unsqueeze_axis_1) 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 681df3b86f..5c3716f1c9 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 @@ -140,3 +140,8 @@ "test_constantofshape_float_ones", "test_constantofshape_int_zeros", "test_nonzero_example", +"test_unique_not_sorted_without_axis", +"test_unique_sorted_with_axis", +"test_unique_sorted_with_axis_3d", +"test_unique_sorted_with_negative_axis", +"test_unique_sorted_without_axis", 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 0fd48db6cb..0a34d92f8b 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 @@ -287,8 +287,3 @@ "test_training_dropout_zero_ratio_mask", // ---- same as above --- "test_tril_zero", // ---- same as above --- "test_triu_zero", // ---- same as above --- -"test_unique_not_sorted_without_axis", // Issue:: Parser: Can't create layer "onnx_node_output_0!Y" of type "Unique" in function 'getLayerInstance' -"test_unique_sorted_with_axis", // ---- same as above --- -"test_unique_sorted_with_axis_3d", // ---- same as above --- -"test_unique_sorted_with_negative_axis", // ---- same as above --- -"test_unique_sorted_without_axis", // ---- same as above ---