From 914c0b2bc8321020bf73c17e89852f870e211aa3 Mon Sep 17 00:00:00 2001 From: Abhishek Gola Date: Wed, 13 May 2026 13:01:25 +0530 Subject: [PATCH] Merge pull request #28963 from abhishek-gola:custom_layers Added custom layer support in new DNN engine #28963 Closes: https://github.com/opencv/opencv/issues/26200 Merge with: https://github.com/opencv/opencv_extra/pull/1358 ### 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 --- .../dnn_custom_layers/dnn_custom_layers.md | 30 +++++ modules/dnn/src/onnx/onnx_importer2.cpp | 20 ++- samples/dnn/custom_layer_onnx.cpp | 121 ++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 samples/dnn/custom_layer_onnx.cpp diff --git a/doc/tutorials/dnn/dnn_custom_layers/dnn_custom_layers.md b/doc/tutorials/dnn/dnn_custom_layers/dnn_custom_layers.md index 319f4ab196..2435a64186 100644 --- a/doc/tutorials/dnn/dnn_custom_layers/dnn_custom_layers.md +++ b/doc/tutorials/dnn/dnn_custom_layers/dnn_custom_layers.md @@ -202,6 +202,36 @@ Next we register a layer and try to import the model. @snippet dnn/custom_layers.hpp Register ResizeBilinearLayer +## Example: custom layer from ONNX +ONNX groups operators into **domains**. The standard operators live in the +default domain `ai.onnx`; vendors and exporters often place their own ops in a +named domain such as `my.namespace`. When OpenCV imports an ONNX node, it looks +the op up in cv::dnn::LayerFactory by: + +- the op_type alone, for nodes in the default `ai.onnx` domain (or no domain), and +- `"."`, for nodes in any non-default domain. + +Node attributes are passed through to the layer constructor as +cv::dnn::LayerParams entries with the same names. Consider an op `MyCustomOp` +with attributes `scale` and `bias` that computes `y = scale * x + bias`. The +implementation can look like: + +@snippet dnn/custom_layer_onnx.cpp CustomScaleBiasLayer + +To import a model that uses this op, register the layer **before** calling +cv::dnn::readNetFromONNX. Use cv::dnn::LayerFactory::registerLayer for runtime +registration (and cv::dnn::LayerFactory::unregisterLayer when done) — pick the +right key for the domain of the op as described above: + +@snippet dnn/custom_layer_onnx.cpp Register CustomScaleBiasLayer + +A complete runnable example is available at +[samples/dnn/custom_layer_onnx.cpp](https://github.com/opencv/opencv/tree/5.x/samples/dnn/custom_layer_onnx.cpp). +Tiny ONNX models exercising both the default-domain and custom-domain registration +paths can be generated with +[generate_custom_layer_models.py](https://github.com/opencv/opencv_extra/tree/5.x/testdata/dnn/onnx/generate_custom_layer_models.py) +in the opencv_extra repository. + ## Define a custom layer in Python The following example shows how to customize OpenCV's layers in Python. diff --git a/modules/dnn/src/onnx/onnx_importer2.cpp b/modules/dnn/src/onnx/onnx_importer2.cpp index c58c8e25b6..5cf224acc5 100644 --- a/modules/dnn/src/onnx/onnx_importer2.cpp +++ b/modules/dnn/src/onnx/onnx_importer2.cpp @@ -894,11 +894,12 @@ void ONNXImporter2::parseNode(const opencv_onnx::NodeProto& node_proto) << node_proto.output_size() << " outputs from domain '" << layer_type_domain << "'");*/ + // Unknown domain: still try parseCustomLayer; addLayer() handles the miss. if (dispatch.empty()) { - CV_LOG_ERROR(NULL, "DNN/ONNX: missing dispatch map for domain='" << layer_type_domain << "'"); - rememberMissingOp(layer_type); - return; + CV_LOG_DEBUG(NULL, "DNN/ONNX: no built-in dispatch map for domain='" + << layer_type_domain << "', trying custom layer for '" + << layer_type << "'"); } node_inputs.clear(); @@ -990,8 +991,21 @@ void ONNXImporter2::parseNeg(LayerParams& layerParams, const opencv_onnx::NodePr addLayer(layerParams, node_proto); } +// Lookup unknown ONNX ops in the user-side LayerFactory; non-default +// domains are prefixed (e.g. "my.namespace.MyOp"), matching the old engine. void ONNXImporter2::parseCustomLayer(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto) { + const std::string& name = layerParams.name; + std::string& layer_type = layerParams.type; + const std::string& layer_type_domain = node_proto.has_domain() ? node_proto.domain() : std::string(); + + if (!LayerFactory::isLayerRegistered(layer_type)) + { + CV_LOG_INFO(NULL, "DNN/ONNX: unknown node type '" << layer_type + << "' (domain '" << layer_type_domain << "', node '" << name + << "'). Register a handler via CV_DNN_REGISTER_LAYER_CLASS() or LayerFactory::registerLayer()."); + } + parseSimpleLayers(layerParams, node_proto); } diff --git a/samples/dnn/custom_layer_onnx.cpp b/samples/dnn/custom_layer_onnx.cpp new file mode 100644 index 0000000000..a9f9bcbec9 --- /dev/null +++ b/samples/dnn/custom_layer_onnx.cpp @@ -0,0 +1,121 @@ +// 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) 2026, BigVision LLC, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Companion sample for tutorial: +// doc/tutorials/dnn/dnn_custom_layers/dnn_custom_layers.md (ONNX section) +// +// Models used by this sample are generated by: +// opencv_extra/testdata/dnn/onnx/generate_custom_layer_models.py +// and live at: +// opencv_extra/testdata/dnn/onnx/models/custom_layer_default_domain.onnx +// opencv_extra/testdata/dnn/onnx/models/custom_layer_custom_domain.onnx + +#include +#include +#include +#include +#include + +using namespace cv; +using namespace cv::dnn; +using namespace std; + +//! [CustomScaleBiasLayer] +// y = scale * x + bias, with scale/bias read from ONNX node attributes. +class CustomScaleBiasLayer CV_FINAL : public Layer +{ +public: + CustomScaleBiasLayer(const LayerParams& params) : Layer(params) + { + scale = params.get("scale", 1.f); + bias = params.get("bias", 0.f); + } + + static Ptr create(LayerParams& params) + { + return makePtr(params); + } + + bool getMemoryShapes(const vector& inpts, + const int /*requiredOutputs*/, + vector& outShapes, + vector& /*internals*/) const CV_OVERRIDE + { + outShapes.assign(1, inpts[0]); + return false; + } + + void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, + OutputArrayOfArrays) CV_OVERRIDE + { + vector inps, outs; + inputs_arr.getMatVector(inps); + outputs_arr.getMatVector(outs); + inps[0].convertTo(outs[0], outs[0].type(), scale, bias); + } + +private: + float scale, bias; +}; +//! [CustomScaleBiasLayer] + +static void printShape(const string& tag, const Mat& m) +{ + cout << tag; + for (int d = 0; d < m.dims; ++d) + cout << (d ? "x" : "") << m.size[d]; + cout << "\n"; +} + +int main(int argc, char** argv) +{ + const string keys = + "{ help h | | Print help message }" + "{ model m | | Path to ONNX model containing the custom op }" + "{ op o | MyCustomOp | Op key for registration. Default-domain: just the op_type " + "(e.g. MyCustomOp). Custom-domain: . " + "(e.g. my.namespace.MyDomainOp) }"; + CommandLineParser parser(argc, argv, keys); + parser.about("Demonstrates importing an ONNX model that contains a custom (non-standard) op " + "by registering a user-defined layer with cv::dnn::LayerFactory."); + if (parser.has("help") || !parser.has("model")) + { + parser.printMessage(); + return 0; + } + + const string modelPath = parser.get("model"); + const string opKey = parser.get("op"); + + //! [Register CustomScaleBiasLayer] + // ONNX op-type lookup: layers in the default `ai.onnx` domain are registered + // under their op_type; layers in a non-default domain are registered under + // "." (e.g. "my.namespace.MyDomainOp"). + LayerFactory::registerLayer(opKey, CustomScaleBiasLayer::create); + //! [Register CustomScaleBiasLayer] + + Net net = readNetFromONNX(modelPath); + if (net.empty()) + { + cerr << "Failed to load model: " << modelPath << "\n"; + LayerFactory::unregisterLayer(opKey); + return 1; + } + + // The companion ONNX models accept a 1x3x4x4 float input. + Mat input(vector{1, 3, 4, 4}, CV_32F, Scalar(1.0f)); + net.setInput(input); + Mat out = net.forward(); + + cout << "Loaded " << modelPath << " using custom op key '" << opKey << "'.\n"; + printShape("Input shape: ", input); + printShape("Output shape: ", out); + cout << "Output[0,0,0,0] = " << out.ptr()[0] + << " (= scale * 1.0 + bias for the registered op)\n"; + + LayerFactory::unregisterLayer(opKey); + return 0; +}