From 6420d2b9295513b908f4e865d9576bd3f3b7b724 Mon Sep 17 00:00:00 2001 From: Abhishek Gola Date: Wed, 11 Feb 2026 15:48:18 +0530 Subject: [PATCH] Added batchnorm layer --- .../dnn/include/opencv2/dnn/all_layers.hpp | 9 + modules/dnn/src/init.cpp | 1 + modules/dnn/src/layers/batch_norm2_layer.cpp | 166 ++++++++++++++++++ modules/dnn/src/onnx/onnx_importer2.cpp | 45 ++--- ...conformance_layer_filter__openvino.inl.hpp | 4 +- ...yer_filter_opencv_classic_denylist.inl.hpp | 2 + ..._conformance_layer_parser_denylist.inl.hpp | 2 - 7 files changed, 199 insertions(+), 30 deletions(-) create mode 100644 modules/dnn/src/layers/batch_norm2_layer.cpp diff --git a/modules/dnn/include/opencv2/dnn/all_layers.hpp b/modules/dnn/include/opencv2/dnn/all_layers.hpp index 6afe873768..b068250e67 100644 --- a/modules/dnn/include/opencv2/dnn/all_layers.hpp +++ b/modules/dnn/include/opencv2/dnn/all_layers.hpp @@ -1161,6 +1161,15 @@ CV__DNN_INLINE_NS_BEGIN static Ptr create(const LayerParams ¶ms); }; + class CV_EXPORTS BatchNorm2Layer : public Layer + { + public: + float epsilon; + bool useGlobalStats, hasWeights, hasBias; + + static Ptr create(const LayerParams& params); + }; + class CV_EXPORTS BatchNormLayerInt8 : public BatchNormLayer { public: diff --git a/modules/dnn/src/init.cpp b/modules/dnn/src/init.cpp index d8ba3c6056..7dc0cfbdba 100644 --- a/modules/dnn/src/init.cpp +++ b/modules/dnn/src/init.cpp @@ -189,6 +189,7 @@ void initializeLayerFactory() CV_DNN_REGISTER_LAYER_CLASS(Gelu, GeluLayer); CV_DNN_REGISTER_LAYER_CLASS(GeluApproximation, GeluApproximationLayer); CV_DNN_REGISTER_LAYER_CLASS(BatchNorm, BatchNormLayer); + CV_DNN_REGISTER_LAYER_CLASS(BatchNorm2, BatchNorm2Layer); CV_DNN_REGISTER_LAYER_CLASS(MaxUnpool, MaxUnpoolLayer); CV_DNN_REGISTER_LAYER_CLASS(Dropout, BlankLayer); CV_DNN_REGISTER_LAYER_CLASS(Identity, BlankLayer); diff --git a/modules/dnn/src/layers/batch_norm2_layer.cpp b/modules/dnn/src/layers/batch_norm2_layer.cpp new file mode 100644 index 0000000000..1ae756c411 --- /dev/null +++ b/modules/dnn/src/layers/batch_norm2_layer.cpp @@ -0,0 +1,166 @@ +// 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. + +#include "../precomp.hpp" +#include "layers_common.hpp" + +namespace cv { +namespace dnn { + +class BatchNorm2LayerImpl CV_FINAL : public BatchNorm2Layer { +public: + BatchNorm2LayerImpl(const LayerParams& params) { + setParamsFrom(params); + + epsilon = params.get("epsilon", params.get("eps", 1e-5f)); + useGlobalStats = params.get("use_global_stats", true); + hasWeights = params.get("has_weight", false); + hasBias = params.get("has_bias", false); + + if (blobs.size() >= 4) { + dynamicInputs = false; + + const Mat& mean = blobs[0]; + const Mat& var = blobs[1]; + const Mat& scale = blobs[2]; + const Mat& beta = blobs[3]; + + weights_.create(scale.size(), CV_32F); + bias_.create(scale.size(), CV_32F); + + cv::sqrt(var + epsilon, bias_); + cv::divide(scale, bias_, weights_); + bias_ = beta - mean.mul(weights_); + } else { + dynamicInputs = true; + } + } + + bool supportBackend(int backendId) CV_OVERRIDE + { + return backendId == DNN_BACKEND_OPENCV; + } + + bool dynamicOutputShapes() const CV_OVERRIDE + { + return dynamicInputs; + } + + bool getMemoryShapes(const std::vector& inputs, + const int requiredOutputs, + std::vector& outputs, + std::vector& internals) const CV_OVERRIDE + { + CV_Assert(!inputs.empty()); + outputs.assign(requiredOutputs, inputs[0]); + 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.empty()); + outputs.assign(requiredOutputs, inputs[0]); + } + + void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, OutputArrayOfArrays internals_arr) CV_OVERRIDE + { + if (inputs_arr.depth() == CV_16F) + { + forward_fallback(inputs_arr, outputs_arr, internals_arr); + return; + } + + std::vector inputs; + inputs_arr.getMatVector(inputs); + + const Mat &X = inputs[0]; + Mat Y; + Mat w, b; + + if (dynamicInputs) { + CV_Assert(inputs.size() == 5); + + const Mat& scale = inputs[1]; + const Mat& beta = inputs[2]; + const Mat& mean = inputs[3]; + const Mat& var = inputs[4]; + + w.create(scale.size(), CV_32F); + b.create(scale.size(), CV_32F); + + cv::sqrt(var + epsilon, b); + cv::divide(scale, b, w); + b = beta - mean.mul(w); + } else { + w = weights_; + b = bias_; + } + + if (w.empty() || b.empty()) + CV_Error(Error::StsBadArg, "BatchNorm2Layer: Weights not initialized"); + + MatShape outShape = shape(X); + auto kind = outputs_arr.kind(); + if (kind == _InputArray::STD_VECTOR_MAT) { + std::vector& outs = outputs_arr.getMatVecRef(); + CV_Assert(outs.size() >= 1); + outs[0].fit(outShape, X.type()); + Y = outs[0]; + } else if (kind == _InputArray::STD_VECTOR_UMAT) { + std::vector& uouts = outputs_arr.getUMatVecRef(); + CV_Assert(uouts.size() >= 1); + uouts[0].fit(outShape, X.type()); + Y = uouts[0].getMat(ACCESS_WRITE); + } else { + CV_Error(Error::StsBadArg, "Unsupported output array kind"); + } + + const int C = (X.dims >= 2) ? X.size[1] : 1; + const int N = X.size[0]; + const size_t planeSize = X.total() / (N * C); + + CV_Assert(w.total() == C); + + parallel_for_(Range(0, N * C), [&](const Range& r) { + for (int i = r.start; i < r.end; ++i) { + int c = i % C; + + float scale_val = w.ptr()[c]; + float shift_val = b.ptr()[c]; + + const float* srcPtr = X.ptr() + i * planeSize; + float* dstPtr = Y.ptr() + i * planeSize; + + int j = 0; +#if CV_SIMD128 + v_float32x4 v_scale = v_setall_f32(scale_val); + v_float32x4 v_shift = v_setall_f32(shift_val); + for (; j <= (int)planeSize - 4; j += 4) { + v_float32x4 v_src = v_load(srcPtr + j); + v_float32x4 v_dst = v_muladd(v_src, v_scale, v_shift); + v_store(dstPtr + j, v_dst); + } +#endif + for (; j < (int)planeSize; ++j) { + dstPtr[j] = srcPtr[j] * scale_val + shift_val; + } + } + }); + } +private: + bool dynamicInputs; + Mat weights_, bias_; +}; + +Ptr BatchNorm2Layer::create(const LayerParams& params) +{ + return makePtr(params); +} +}} // namespace cv::dnn diff --git a/modules/dnn/src/onnx/onnx_importer2.cpp b/modules/dnn/src/onnx/onnx_importer2.cpp index d740e3f818..10c9472f98 100644 --- a/modules/dnn/src/onnx/onnx_importer2.cpp +++ b/modules/dnn/src/onnx/onnx_importer2.cpp @@ -1321,38 +1321,31 @@ void ONNXImporter2::parseInstanceNormalization(LayerParams& layerParams, const o void ONNXImporter2::parseBatchNormalization(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto) { if (node_proto.input_size() != 5) - CV_Error(Error::StsNotImplemented, - "Expected input, scale, bias, mean and var"); + CV_Error(Error::StsNotImplemented, "Expected input, scale, bias, mean and var"); - layerParams.type = "BatchNorm"; - replaceLayerParam(layerParams, "epsilon", "eps"); - replaceLayerParam(layerParams, "spatial", "use_global_stats"); + layerParams.type = "BatchNorm2"; - CV_Assert(net.isConstArg(node_inputs[3])); - CV_Assert(net.isConstArg(node_inputs[4])); + float eps = 1e-5f; + for (int i = 0; i < node_proto.attribute_size(); i++) { + const opencv_onnx::AttributeProto& attr = node_proto.attribute(i); + if (attr.name() == "epsilon") eps = attr.f(); + } + layerParams.set("epsilon", eps); - Mat meanData = net.argTensor(node_inputs[3]); - Mat stdData = net.argTensor(node_inputs[4]); + bool isStatic = net.isConstArg(node_inputs[1]) && // scale + net.isConstArg(node_inputs[2]) && // bias + net.isConstArg(node_inputs[3]) && // mean + net.isConstArg(node_inputs[4]); // var - layerParams.blobs.push_back(meanData); - layerParams.blobs.push_back(stdData); - - if (!node_proto.input(1).empty()) { - layerParams.set("has_weight", true); - CV_Assert(net.isConstArg(node_inputs[1])); - layerParams.blobs.push_back(net.argTensor(node_inputs[1])); // weightData - } else { - layerParams.set("has_weight", false); + if (isStatic) { + layerParams.blobs.resize(4); + layerParams.blobs[0] = net.argTensor(node_inputs[3]); // mean + layerParams.blobs[1] = net.argTensor(node_inputs[4]); // var + layerParams.blobs[2] = net.argTensor(node_inputs[1]); // scale + layerParams.blobs[3] = net.argTensor(node_inputs[2]); // bias } - if (!node_proto.input(2).empty()) { - layerParams.set("has_bias", true); - CV_Assert(net.isConstArg(node_inputs[1])); - layerParams.blobs.push_back(net.argTensor(node_inputs[2])); // biasData - } else { - layerParams.set("has_bias", false); - } - addLayer(layerParams, node_proto, 1); + addLayer(layerParams, node_proto); } void ONNXImporter2::parseGemm(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto) 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 c0fb5cda1f..497807657b 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 @@ -261,11 +261,11 @@ CASE(test_basic_conv_without_padding) CASE(test_basic_convinteger) // no filter CASE(test_batchnorm_epsilon) - // no filter + SKIP; CASE(test_batchnorm_epsilon_training_mode) // no filter CASE(test_batchnorm_example) - // no filter + SKIP; CASE(test_batchnorm_example_training_mode) // no filter CASE(test_bernoulli) 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 e15af3d8e7..940bb06924 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 @@ -712,3 +712,5 @@ "test_roialign_aligned_false", "test_roialign_aligned_true", "test_roialign_mode_max", +"test_batchnorm_example", +"test_batchnorm_epsilon", 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 94bb1ce17d..2a4a4645c7 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 @@ -124,9 +124,7 @@ "test_basic_convinteger", // Issues::Layer::Can't create layer "onnx_node_output_0!y" of type "ConvInteger" in function 'getLayerInstance' "test_basic_deform_conv_with_padding", "test_basic_deform_conv_without_padding", -"test_batchnorm_epsilon", // Issue:: Unkonwn error::Blob mean not found in const blobs in function 'getBlob' "test_batchnorm_epsilon_training_mode", // ---- same as above --- -"test_batchnorm_example", // ---- same as above --- "test_batchnorm_example_training_mode", // ---- same as above --- "test_bernoulli", // Issues::Layer::Can't create layer "onnx_node_output_0!y" of type "Bernoulli" in function 'getLayerInstance' "test_bernoulli_double", // ---- same as above ---