OpenCV  4.10.0
开源计算机视觉
正在加载...
正在搜索...
无匹配项
自定义深度学习层支持

上一篇教程: 如何在浏览器中运行深度网络
下一篇教程: 如何运行自定义 OCR 模型

原作者Dmitry Kurtaev
兼容性OpenCV >= 3.4.1

简介

深度学习是一个快速发展的领域。构建神经网络的新方法通常会引入新型的层。这些可能是现有层的修改或对杰出研究成果的实现。

OpenCV 允许导入和运行来自不同深度学习框架的网络。有一些最流行的层。但是,您可能会遇到一个问题,即您的网络无法使用 OpenCV 导入,因为您网络中的某些层可能未在 OpenCV 的深度学习引擎中实现。

第一个解决方案是在 https://github.com/opencv/opencv/issues 上创建一个功能请求,提及模型的来源和新层类型等详细信息。如果 OpenCV 社区共享这种需求,则可以实现新层。

第二种方法是定义一个自定义层,以便 OpenCV 的深度学习引擎知道如何使用它。本教程旨在向您展示深度学习模型导入自定义的过程。

在 C++ 中定义自定义层

深度学习层是网络管道的构建块。它与输入 Blob 相连,并生成结果到输出 Blob。它还有训练好的权重超参数。层的名称、类型、权重和超参数存储在文件中,这些文件是在训练期间由原生框架生成的。如果 OpenCV 遇到未知层类型,它会在尝试读取模型时抛出异常

未指定的错误:无法在函数 getLayerInstance 中创建类型为“MyType”的层“layer_name”

为了正确导入模型,您必须从 cv::dnn::Layer 派生一个类,并包含以下方法

class MyLayer : public cv::dnn::Layer
{
public:
MyLayer(const cv::dnn::LayerParams &params);
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;
virtual void forward(cv::InputArrayOfArrays inputs,
virtual void finalize(cv::InputArrayOfArrays inputs,
};
这是用于将只读输入数组传递到 OpenCV 函数的代理类。
定义 mat.hpp:160
该类型与 InputArray 非常相似,不同之处在于它用于输入/输出和输出函数 p...
定义 mat.hpp:296
该类提供了初始化层所需的所有数据。
定义 dnn.hpp:146
该接口类允许构建新的层 - 这些层是网络的构建块。
定义 dnn.hpp:221
virtual void finalize(const std::vector< Mat * > &input, std::vector< Mat > &output)
根据输入、输出和 Blob 计算并设置内部参数。
virtual bool getMemoryShapes(const std::vector< MatShape > &inputs, const int requiredOutputs, std::vector< MatShape > &outputs, std::vector< MatShape > &internals) const
virtual void forward(std::vector< Mat * > &input, std::vector< Mat > &output, std::vector< Mat > &internals)
在给定输入 Blob 的情况下,计算输出 Blob。
std::shared_ptr< _Tp > Ptr
定义 cvstd_wrapper.hpp:23
#define CV_OVERRIDE
定义 cvdef.h:792

并将其在导入之前注册

#include <opencv2/dnn/layer.details.hpp> // CV_DNN_REGISTER_LAYER_CLASS
static inline void loadNet()
{
CV_DNN_REGISTER_LAYER_CLASS(Interp, InterpLayer);
// ...
#define CV_DNN_REGISTER_LAYER_CLASS(type, class)
在运行时注册层类。
定义 layer.details.hpp:27
注意
MyType 是抛出的异常中未实现层的类型。

让我们看看所有方法的作用

  • 构造函数
MyLayer(const cv::dnn::LayerParams &params);

cv::dnn::LayerParams 中检索超参数。如果您的层具有可训练的权重,则这些权重将已存储在层的成员 cv::dnn::Layer::blobs 中。

  • 静态方法 create

该方法应创建一个您的层的实例,并返回一个包含它的 cv::Ptr

  • 输出 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;

根据输入形状返回层的输出形状。您可能需要使用 internals 请求额外的内存。

  • 运行层
virtual void forward(cv::InputArrayOfArrays inputs,

在此实现层的逻辑。为给定的输入计算输出。

注意
OpenCV 管理为层分配的内存。在大多数情况下,同一内存可以在层之间重复使用。因此,您的 forward 实现不应该依赖于 forward 的第二次调用在 outputsinternals 中具有相同的数据。
  • 可选的 finalize 方法
virtual void finalize(cv::InputArrayOfArrays inputs,

方法链的顺序如下:OpenCV 深度学习引擎调用 create 方法一次,然后为每个创建的层调用 getMemoryShapes,然后您可以在 cv::dnn::Layer::finalize 中进行一些准备工作,这些工作取决于已知的输入维度。在网络初始化后,仅对每个网络的输入调用 forward 方法。

注意
不同的输入 Blob 大小(例如高度、宽度或批次大小)会导致 OpenCV 重新分配所有内部内存。这会导致效率差距。尝试使用固定的批次大小和图像尺寸来初始化和部署模型。

示例:来自 Caffe 的自定义层

让我们从 https://github.com/cdmh/deeplab-public 创建一个自定义层 Interp。它只是一个简单的调整大小,它接受大小为 N x C x Hi x Wi 的输入 Blob,并返回大小为 N x C x Ho x Wo 的输出 Blob,其中 N 是批次大小,C 是通道数量,Hi x WiHo x Wo 分别是输入和输出的 高度 x 宽度。该层没有可训练的权重,但它有超参数来指定输出大小。

例如,

layer {
name: "output"
type: "Interp"
bottom: "input"
top: "output"
interp_param {
height: 9
width: 8
}
}

这样,我们的实现看起来像

class InterpLayer : public cv::dnn::Layer
{
public:
InterpLayer(const cv::dnn::LayerParams &params) : Layer(params)
{
outWidth = params.get<int>("width", 0);
outHeight = params.get<int>("height", 0);
}
{
return cv::Ptr<cv::dnn::Layer>(new InterpLayer(params));
}
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
{
CV_UNUSED(requiredOutputs); CV_UNUSED(internals);
std::vector<int> outShape(4);
outShape[0] = inputs[0][0]; // 批次大小
outShape[1] = inputs[0][1]; // 通道数量
outShape[2] = outHeight;
outShape[3] = outWidth;
outputs.assign(1, outShape);
return false;
}
// 此自定义层的实现基于 https://github.com/cdmh/deeplab-public/blob/master/src/caffe/layers/interp_layer.cpp
virtual void forward(cv::InputArrayOfArrays inputs_arr,
{
if (inputs_arr.depth() == CV_16S)
{
// 在 DNN_TARGET_OPENCL_FP16 目标的情况下,以下方法
// 将数据从 FP16 转换为 FP32,并再次调用此 forward。
forward_fallback(inputs_arr, outputs_arr, internals_arr);
return;
}
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 float rheight = (outHeight > 1) ? static_cast<float>(inpHeight - 1) / (outHeight - 1) : 0.f;
const float rwidth = (outWidth > 1) ? static_cast<float>(inpWidth - 1) / (outWidth - 1) : 0.f;
for (int h2 = 0; h2 < outHeight; ++h2)
{
const float h1r = rheight * h2;
const int h1 = static_cast<int>(h1r);
const int h1p = (h1 < inpHeight - 1) ? 1 : 0;
const float h1lambda = h1r - h1;
const float h0lambda = 1.f - h1lambda;
for (int w2 = 0; w2 < outWidth; ++w2)
{
const float w1r = rwidth * w2;
const int w1 = static_cast<int>(w1r);
const int w1p = (w1 < inpWidth - 1) ? 1 : 0;
const float w1lambda = w1r - w1;
const float w0lambda = 1.f - w1lambda;
const float* pos1 = inpData + h1 * inpWidth + w1;
float* pos2 = outData + h2 * outWidth + w2;
for (int c = 0; c < batchSize * numChannels; ++c)
{
pos2[0] =
h0lambda * (w0lambda * pos1[0] + w1lambda * pos1[w1p]) +
h1lambda * (w0lambda * pos1[h1p * inpWidth] + w1lambda * pos1[h1p * inpWidth + w1p]);
pos1 += inpWidth * inpHeight;
pos2 += outWidth * outHeight;
}
}
}
}
private:
int outWidth, outHeight;
};
n维稠密数组类
定义 mat.hpp:812
uchar * data
指向数据的指针
定义 mat.hpp:2140
void forward_fallback(InputArrayOfArrays inputs, OutputArrayOfArrays outputs, OutputArrayOfArrays internals)
在给定输入 Blob 的情况下,计算输出 Blob。
#define CV_16S
定义 interface.h:76

接下来我们需要注册一个新的层类型,并尝试导入模型。

CV_DNN_REGISTER_LAYER_CLASS(Interp, InterpLayer);
cv::dnn::Net caffeNet = cv::dnn::readNet("/path/to/config.prototxt", "/path/to/weights.caffemodel");
此类允许创建和操作全面的神经网络。
定义 dnn.hpp:475
Net readNet(CV_WRAP_FILE_PATH const String &model, CV_WRAP_FILE_PATH const String &config="", const String &framework="")
读取以支持格式之一表示的深度学习网络。

示例:来自 TensorFlow 的自定义层

这是一个关于如何导入包含 tf.image.resize_bilinear 操作的网络的示例。这是一种调整大小,但实现方式不同于 OpenCV 的或上述的 Interp

让我们创建一个单层网络

inp = tf.placeholder(tf.float32, [2, 3, 4, 5], 'input')
resized = tf.image.resize_bilinear(inp, size=[9, 8], name='resize_bilinear')

OpenCV 以以下方式看到 TensorFlow 的图

node {
name: "input"
op: "Placeholder"
attr {
key: "dtype"
value {
type: DT_FLOAT
}
}
}
node {
name: "resize_bilinear/size"
op: "Const"
attr {
key: "dtype"
value {
type: DT_INT32
}
}
attr {
key: "value"
value {
tensor {
dtype: DT_INT32
tensor_shape {
dim {
size: 2
}
}
tensor_content: "\t\000\000\000\010\000\000\000"
}
}
}
}
node {
name: "resize_bilinear"
op: "ResizeBilinear"
input: "input:0"
input: "resize_bilinear/size"
attr {
key: "T"
value {
type: DT_FLOAT
}
}
attr {
key: "align_corners"
value {
b: false
}
}
}
library {
}

从 TensorFlow 导入自定义层旨在将所有层的 attr 放入 cv::dnn::LayerParams,但将输入 Const blob 放入 cv::dnn::Layer::blobs。在我们的例子中,调整大小的输出形状将存储在层的 blobs[0] 中。

class ResizeBilinearLayer CV_FINAL : public cv::dnn::Layer
{
public:
ResizeBilinearLayer(const cv::dnn::LayerParams &params) : Layer(params)
{
CV_Assert(!params.get<bool>("align_corners", false));
CV_Assert(!blobs.empty());
for (size_t i = 0; i < blobs.size(); ++i)
CV_Assert(blobs[i].type() == CV_32SC1);
// 输入 blob 有两种情况:一个包含输出的单个 blob
// 形状和两个带有缩放因子的 blob。
if (blobs.size() == 1)
{
CV_Assert(blobs[0].total() == 2);
outHeight = blobs[0].at<int>(0, 0);
outWidth = blobs[0].at<int>(0, 1);
factorHeight = factorWidth = 0;
}
else
{
CV_Assert(blobs.size() == 2); CV_Assert(blobs[0].total() == 1); CV_Assert(blobs[1].total() == 1);
factorHeight = blobs[0].at<int>(0, 0);
factorWidth = blobs[1].at<int>(0, 0);
outHeight = outWidth = 0;
}
}
{
return cv::Ptr<cv::dnn::Layer>(new ResizeBilinearLayer(params));
}
virtual bool getMemoryShapes(const std::vector<std::vector<int> > &inputs,
const int,
std::vector<std::vector<int> > &outputs,
std::vector<std::vector<int> > &) const CV_OVERRIDE
{
std::vector<int> outShape(4);
outShape[0] = inputs[0][0]; // 批次大小
outShape[1] = inputs[0][1]; // 通道数量
outShape[2] = outHeight != 0 ? outHeight : (inputs[0][2] * factorHeight);
outShape[3] = outWidth != 0 ? outWidth : (inputs[0][3] * factorWidth);
outputs.assign(1, outShape);
return false;
}
{
std::vector<cv::Mat> outputs;
outputs_arr.getMatVector(outputs);
if (!outWidth && !outHeight)
{
outHeight = outputs[0].size[2];
outWidth = outputs[0].size[3];
}
}
// 此实现基于来自的参考实现
// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/lite/kernels/internal/reference/reference_ops.h
virtual void forward(cv::InputArrayOfArrays inputs_arr,
{
if (inputs_arr.depth() == CV_16S)
{
// 在 DNN_TARGET_OPENCL_FP16 目标的情况下,以下方法
// 将数据从 FP16 转换为 FP32,并再次调用此 forward。
forward_fallback(inputs_arr, outputs_arr, internals_arr);
return;
}
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];
float heightScale = static_cast<float>(inpHeight) / outHeight;
float widthScale = static_cast<float>(inpWidth) / outWidth;
for (int b = 0; b < batchSize; ++b)
{
for (int y = 0; y < outHeight; ++y)
{
float input_y = y * heightScale;
int y0 = static_cast<int>(std::floor(input_y));
int y1 = std::min(y0 + 1, inpHeight - 1);
for (int x = 0; x < outWidth; ++x)
{
float input_x = x * widthScale;
int x0 = static_cast<int>(std::floor(input_x));
int x1 = std::min(x0 + 1, inpWidth - 1);
for (int c = 0; c < numChannels; ++c)
{
float interpolation =
inpData[offset(inp.size, c, x0, y0, b)] * (1 - (input_y - y0)) * (1 - (input_x - x0)) +
inpData[offset(inp.size, c, x0, y1, b)] * (input_y - y0) * (1 - (input_x - x0)) +
inpData[offset(inp.size, c, x1, y0, b)] * (1 - (input_y - y0)) * (input_x - x0) +
inpData[offset(inp.size, c, x1, y1, b)] * (input_y - y0) * (input_x - x0);
outData[offset(out.size, c, x, y, b)] = interpolation;
}
}
}
}
}
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));
}
int outWidth, outHeight, factorWidth, factorHeight;
};
String type
用于通过层工厂创建层的类型名称。
定义 dnn.hpp:455
#define CV_32SC1
定义 interface.h:112
#define CV_FINAL
定义 cvdef.h:796
#define CV_Assert(expr)
在运行时检查条件,如果失败则抛出异常。
定义 base.hpp:342
static int total(const MatShape &shape, int start=-1, int end=-1)
定义 shape_utils.hpp:161
GOpaque< Size > size(const GMat &src)
从 Mat 获取尺寸。
定义 mat.hpp:588

接下来,我们注册一个层并尝试导入模型。

CV_DNN_REGISTER_LAYER_CLASS(ResizeBilinear, ResizeBilinearLayer);
cv::dnn::Net tfNet = cv::dnn::readNet("/path/to/graph.pb");

在 Python 中定义自定义层

以下示例展示了如何在 Python 中自定义 OpenCV 的层。

让我们考虑一下 整体嵌套边缘检测 深度学习模型。该模型的训练只有一个与当前版本的 Caffe 框架 不同之处。接收两个输入 blob 并将第一个输入 blob 裁剪以匹配第二个输入 blob 的空间维度的 Crop 层用于从中心进行裁剪。如今 Caffe 的层是从左上角进行裁剪的。因此,使用最新版本的 Caffe 或 OpenCV,您将获得带有填充边界的偏移结果。

接下来,我们将用一个以中心为中心的裁剪层替换 OpenCV 的进行左上角裁剪的 Crop 层。

  • 创建一个包含 getMemoryShapesforward 方法的类
class CropLayer(object)
def __init__(self, params, blobs)
self.xstart = 0
self.xend = 0
self.ystart = 0
self.yend = 0
# 我们的层接收两个输入。我们需要将第一个输入 blob
# 裁剪以匹配第二个输入 blob 的形状(保持批次大小和通道数量)
def getMemoryShapes(self, inputs)
inputShape, targetShape = inputs[0], inputs[1]
batchSize, numChannels = inputShape[0], inputShape[1]
height, width = targetShape[2], targetShape[3]
self.ystart = (inputShape[2] - targetShape[2]) // 2
self.xstart = (inputShape[3] - targetShape[3]) // 2
self.yend = self.ystart + height
self.xend = self.xstart + width
return [[batchSize, numChannels, height, width]]
def forward(self, inputs)
return [inputs[0][:,:,self.ystart:self.yend,self.xstart:self.xend]]
注意
这两个方法都应返回列表。
  • 注册一个新的层。
cv.dnn_registerLayer('Crop', CropLayer)

就是这样!我们已将已实现的 OpenCV 层替换为自定义层。您可以在 源代码 中找到完整的脚本。