Opencv中DNN模块添加custom layer

本文档介绍了如何在OpenCV 4.1.0中为DNN模块添加自定义层,以处理图像归一化操作。通过继承`cv::dnn::Layer`,定义DataNormLayer类,实现必要的构造函数、计算输出Blob大小、前向传播等功能。此外,还展示了prototxt文件中自定义层的声明和类定义文件的内容。
部署运行你感兴趣的模型镜像

使用环境:win10 64bits,VS2015,Opencv4.1.0

1. 概述

最近的几年Opencv开始在DNN领域耕耘,推出了网络inference的接口,其速度还是比较快的,特别是在本来就使用Opencv的图像处理代码中可以直接使用DNN模块直接加载model做inference,免除了添加附加依赖支持的问题。在4.1.0的版本中已经实现了将model从caffe、tensorflow、onnx等的导入。下面是这个版本中Opencv支持的网络层(cv::dnn::Layer)。

但是在实际使用过程中还是会出现层不支持的情况,这里使用caffe下的网络定义演示自定义层的定义,这里需要完成的功能是:通过添加一个DataNormLayer来完成不涉及参数加载的数据处理操作(图像归一化操作)

2. custom layer的一些必要元素

dnn模块对应的文档:dnn文档
opencv给出的教程文档:custom layer tutorial

step1: 要添加自定义的层首先需要的就是将自定义的层继承自cv::dnn::Layer,DataNormLayer是自定义层类名

class DataNormLayer : public cv::dnn::Layer
{};

step2: 实现指定格式的类构造函数:

DataNormLayer(const cv::dnn::LayerParams &params);

其中参数的定义为:

class CV_EXPORTS LayerParams : public Dict
{
public:
	//TODO: Add ability to name blob params
	std::vector<Mat> blobs; //存储模型layer的权值

	String name; //layer的名称
	String type; //layer的类型
};

step3: 定义实例创建静态函数:

static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams& params)
{
	return cv::Ptr<cv::dnn::Layer>(new DataNormLayer(params));
}

step4: 根据输入blob的大小计算输出blob的大小

virtual bool getMemoryShapes(const std::vector<std::vector<int> > &inputs,
	const int requiredOutputs,
	std::vector<std::vector<int> > &outputs,
	std::vector<std::vector<int> > &internals) const CV_OVERRIDE;

step5: 在知道输入blob大小之后可以通过finalize来做一些操(optional):

virtual void finalize(cv::InputArrayOfArrays inputs_arr, cv::OutputArrayOfArrays outputs_arr) CV_OVERRIDE;

step6: 网络的前向传播forward,在内部定义数据的处理流程,并返回结果

virtual void forward(cv::InputArrayOfArrays inputs_arr,
	cv::OutputArrayOfArrays outputs_arr,
	cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE;

step7: 将定义好的custom Layer进行注册

#include <opencv2/dnn/layer.details.hpp>  // CV_DNN_REGISTER_LAYER_CLASS

CV_DNN_REGISTER_LAYER_CLASS(DataNorm, DataNormLayer);

这里需要注意的custom layer实例创建的时候是forward函数调用的时候(才会调用create函数),之后依次执行DataNormLayer构造函数getMemoryShapes(...)finalize(...)forward(...)

3. custom layer定义

首先prototxt里面定义自定层:

#layer definition
layer {
	name: "data_norm"
	type: "DataNorm"
	bottom: "data"
	top: "data"
	DataNorm_param {
		scale_ratio: 0.00392157
		mean_value: 127.5
	}
}

自定义层的类声明文件:

class DataNormLayer : public cv::dnn::Layer
{
public:
	DataNormLayer(const cv::dnn::LayerParams &params);
	~DataNormLayer();

	//step 1 called (called when inference)在整个net调用forward的时候调用,网络构建权值提取阶段不调用
	static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams& params)
	{
		return cv::Ptr<cv::dnn::Layer>(new DataNormLayer(params));
	}

	//step 2 called
	virtual bool getMemoryShapes(const std::vector<std::vector<int> > &inputs,
		const int requiredOutputs,
		std::vector<std::vector<int> > &outputs,
		std::vector<std::vector<int> > &internals) const CV_OVERRIDE;

	//step 3 called
	virtual void finalize(cv::InputArrayOfArrays inputs_arr, cv::OutputArrayOfArrays outputs_arr) CV_OVERRIDE;

	//layer forward
	virtual void forward(cv::InputArrayOfArrays inputs_arr,
		cv::OutputArrayOfArrays outputs_arr,
		cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE;

private:
	//根据输入计算数据存储位置偏移
	static inline int offset(const cv::MatSize& size, int c, int x, int y, int b)
	{
		return x + size[3] * (y + size[2] * (c + size[1] * b));
	}
	float scale_ratio;	//归一化系数
	float mean_value;	//均值
};

类的定义文件:

DataNormLayer::DataNormLayer(const cv::dnn::LayerParams &params):Layer(params)
{
	this->scale_ratio = params.get<float>("scale_ratio", .0);
	this->mean_value = params.get<float>("mean_value", .0);
	std::cout << "get param: scale_ratio(" << this->scale_ratio << "), mean_value(" <<
		this->mean_value << ")" << std::endl;
}


DataNormLayer::~DataNormLayer()
{
}

//step 2 call
//根据输入的尺寸计算输出的维度
bool DataNormLayer::getMemoryShapes(const std::vector<std::vector<int> > &inputs,
	const int requiredOutputs,
	std::vector<std::vector<int> > &outputs,
	std::vector<std::vector<int> > &internals) const
{
	std::vector<int> outShape(4);
	outShape[0] = inputs[0][0];  // batch size
	outShape[1] = inputs[0][1];  // number of channels
	outShape[2] = inputs[0][2];	 // number of H
	outShape[3] = inputs[0][3];	 // unmber of W
	outputs.assign(1, outShape);
	return false;
}

//step 3 call
//根据已知的输入数据的维度可以做一些额外的操作
void DataNormLayer::finalize(cv::InputArrayOfArrays inputs_arr, cv::OutputArrayOfArrays outputs_arr)
{
	std::vector<cv::Mat> outputs;
	outputs_arr.getMatVector(outputs);

	std::vector<cv::Mat> inputs;
	inputs_arr.getMatVector(inputs);

	std::cout << "inputs[0] size:[" << inputs[0].size[0] << "," << inputs[0].size[1] << "," <<
		inputs[0].size[2] << "," << inputs[0].size[3] << "]" << std::endl;
	std::cout << "outputs[0] size:[" << outputs[0].size[0] << "," << outputs[0].size[1] << "," <<
		outputs[0].size[2] << "," << outputs[0].size[3] << "]" << std::endl;
}

//layer forward
void DataNormLayer::forward(cv::InputArrayOfArrays inputs_arr,
	cv::OutputArrayOfArrays outputs_arr,
	cv::OutputArrayOfArrays internals_arr)
{
	std::vector<cv::Mat> inputs, outputs;
	inputs_arr.getMatVector(inputs);
	outputs_arr.getMatVector(outputs);

	cv::Mat& inp = inputs[0];
	cv::Mat& out = outputs[0];
	const float* inpData = (float*)inp.data;
	float* outData = (float*)out.data;

	const int batchSize = inp.size[0];
	const int numChannels = inp.size[1];
	const int inpHeight = inp.size[2];
	const int inpWidth = inp.size[3];

	const int outHeight = inpHeight;
	const int outWidth = inpWidth;

	for (int b = 0; b < batchSize; ++b)
	{
		for (int y = 0; y < outHeight; ++y)
		{
			for (int x = 0; x < outWidth; ++x)
			{
				for (int c = 0; c < numChannels; ++c)
				{
					//做减均值与系数缩放
					float interpolation = (inpData[offset(inp.size, c, x, y, b)] - this->mean_value) * this->scale_ratio;
					outData[offset(out.size, c, x, y, b)] = interpolation;
				}
			}
		}
	}

}

您可能感兴趣的与本文相关的镜像

TensorFlow-v2.15

TensorFlow-v2.15

TensorFlow

TensorFlow 是由Google Brain 团队开发的开源机器学习框架,广泛应用于深度学习研究和生产环境。 它提供了一个灵活的平台,用于构建和训练各种机器学习模型

<think>我们正在处理一个ONNX模型解析错误,具体发生在OpenCV 4.12.0的dnn模块中,当尝试解析一个卷积节点(Node [Conv@ai.onnx])时,函数`cv::dnn::util::getKernelSize`无法获取到卷积核的大小,因为模型中没有指定`kernel_size`(或者`kernel_h`和`kernel_w`)。这是一个常见的问题,通常由于模型导出时缺少必要属性或OpenCV版本对ONNX操作的支持不完整导致。 解决方案通常包括以下几个步骤: 1. **检查ONNX模型**:使用Netron等工具可视化模型,确认该卷积节点(node_Conv_1871)的属性,特别是卷积核大小(kernel_shape)是否被正确设置。ONNX的Conv算子应该有`kernel_shape`属性(一个包含两个整数的列表,表示高度和宽度)[^1]。 2. **更新OpenCV版本**:OpenCV 4.12.0可能较旧,尝试升级到最新版本(如4.5.5+),因为新版本可能已经修复了该问题。 3. **重新导出ONNX模型**:如果模型是从其他框架(如PyTorch、TensorFlow等)导出的,确保在导出时正确设置了卷积层的参数。例如,在PyTorch中,使用`torch.onnx.export`时,确保卷积层的`kernel_size`参数被正确传递。 4. **修改模型**:如果无法重新导出模型,可以使用ONNX的Python API(onnx包)来修改模型,为缺失属性的卷积节点添加`kernel_shape`属性。 5. **自定义层**:如果以上方法无效,可以考虑在OpenCV中注册自定义层(Custom Layer),实现该卷积操作。这需要编写C++代码并重新编译OpenCV,或者使用OpenCV的Python接口中的`cv2.dnn_registerLayer`(但注意,OpenCV的Python接口可能不支持所有自定义层功能)。 下面我们将详细说明其中几种方法: ### 方法1:检查并更新OpenCV 首先,确认使用的OpenCV版本。如果可能,升级到最新版本: ```bash pip install --upgrade opencv-python ``` 或者从源码编译最新版本的OpenCV(确保包含dnn模块)。 ### 方法2:检查ONNX模型 使用Netron(https://netron.app/)打开ONNX模型,定位到节点`node_Conv_1871`,查看其属性。正常情况下,Conv节点应包含以下属性: - `kernel_shape`:卷积核的尺寸(如[3,3]) - `strides`:步长(如[1,1]) - `pads`:填充(如[0,0,0,0]) 如果缺少`kernel_shape`,则需要修改模型。 ### 方法3:修改ONNX模型(添加缺失属性) 如果发现模型中该卷积节点确实缺少`kernel_shape`属性,我们可以使用ONNX Python API来修复。 假设我们已经定位到问题节点(`node_Conv_1871`),以下是一个示例代码,为卷积节点添加`kernel_shape`属性(假设我们知道正确的卷积核大小,比如3x3): ```python import onnx # 加载模型 model = onnx.load('model.onnx') # 找到问题节点 node_name = 'node_Conv_1871' # 替换为你的节点名 for node in model.graph.node: if node.name == node_name: # 检查是否已有kernel_shape属性 has_kernel_shape = False for attr in node.attribute: if attr.name == 'kernel_shape': has_kernel_shape = True break if not has_kernel_shape: # 添加kernel_shape属性,假设为3x3 attr = onnx.helper.make_attribute('kernel_shape', [3, 3]) node.attribute.append(attr) # 保存修改后的模型 onnx.save(model, 'fixed_model.onnx') ``` ### 方法4:自定义层(不推荐,较复杂) 如果上述方法无效,可能需要为OpenCV DNN模块注册自定义层。这需要C++编程和重新编译OpenCV。步骤大致如下: 1. 创建一个新的层类,继承自`cv::dnn::Layer`,并实现`getKernelSize`等必要方法。 2. 注册该层到OpenCVdnn模块。 3. 重新编译OpenCV。 由于过程复杂,且需要维护,建议优先尝试前几种方法。 ### 总结 建议首先尝试更新OpenCV到最新版本,如果问题仍然存在,则检查ONNX模型并修复缺失的属性。如果模型无法修改,且更新OpenCV无效,那么可能需要考虑自定义层,但这通常是最后的手段。 希望这些方法能帮助你解决问题。 §§相关问题§§ 1. 如何用Netron查看ONNX模型结构? 2. OpenCV DNN模块支持哪些深度学习框架的模型? 3. 在导出ONNX模型时,如何确保所有属性都正确设置? 4. 如何为OpenCV DNN编写自定义层? </think> 针对您遇到的OpenCV解析ONNX模型时的错误"Node [Conv@ai.onnx] (onnx_node!node_Conv_1871)解析错误,未指定kernel_size",以下提供详细的解决方案: ### 问题原因分析 该错误通常发生在以下情况: 1. ONNX模型中卷积层的`kernel_shape`属性缺失或格式错误 2. OpenCV版本对特定ONNX操作符支持不足 3. 模型导出时未正确设置卷积参数[^2] 4. 使用了非常规卷积核尺寸(如动态尺寸) ### 解决方案 #### 1. 检查并修复ONNX模型 ```python import onnx model = onnx.load("model.onnx") for node in model.graph.node: if node.op_type == "Conv" and node.name == "node_Conv_1871": # 添加缺失的kernel_shape属性 if not any(attr.name == "kernel_shape" for attr in node.attribute): kernel_attr = onnx.helper.make_attribute("kernel_shape", [3, 3]) # 根据实际情况修改尺寸 node.attribute.append(kernel_attr) onnx.save(model, "fixed_model.onnx") ``` #### 2. 更新OpenCV版本 ```bash # 升级到最新版本(推荐4.7.0+) pip install --upgrade opencv-python-headless ``` #### 3. 模型导出时显式指定参数 若使用PyTorch导出: ```python torch.onnx.export( model, input, "model.onnx", opset_version=12, # 使用较新的opset input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, # 确保所有卷积层有kernel_size参数 ) ``` #### 4. 添加自定义层解析(OpenCV C++) ```cpp class CustomConvLayer : public cv::dnn::Layer { public: static cv::Ptr<cv::dnn::Layer> create(...) { return cv::makePtr<CustomConvLayer>(); } virtual bool getMemoryShapes(...) const override { // 自定义获取kernel_size的逻辑 if (params.has("kernel_size")) { // 标准处理 } else if (params.has("kernel_h") && params.has("kernel_w")) { // 从kernel_h/kernel_w获取 } else { // 计算默认值 } } }; // 注册自定义层 CV_DNN_REGISTER_LAYER_CLASS(CustomConv, CustomConvLayer); ``` ### 验证步骤 1. 使用Netron可视化模型,检查`node_Conv_1871`属性 2. 运行简化模型: ```bash python -m onnxsim input_model.onnx output_model.onnx ``` 3. 测试OpenCV加载: ```python net = cv2.dnn.readNetFromONNX("fixed_model.onnx") print(net.getLayerNames()) # 确认所有层加载成功 ``` > **注意**:如果使用昇腾NPU硬件,确保CANN工具链已更新至最新版本,并检查ONNX运行时兼容性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值