深度神经网络——支持自定义深度学习层 OpenCV v4.8.0

上一个教程如何在浏览器中运行深度网络

下一个教程如何运行自定义 OCR 模型

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

简介

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

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

第一种解决方案是在 https://github.com/opencv/opencv/issues 上创建一个功能请求,提及模型来源和新层类型等详细信息。如果 OpenCV 社区认同这一需求,新层就可以实现。
第二种方法是定义自定义图层,这样 OpenCV 的深度学习引擎就会知道如何使用它。本教程将向您展示深度学习模型导入自定义的过程。

用 C++ 定义自定义层

深度学习层是网络管道的组成部分。它与输入 Blobs 相连接,并向输出 Blobs 生成结果。层中有经过训练的权重超参数。层的名称、类型、权重和超参数存储在本地框架在训练过程中生成的文件中。如果 OpenCV 在尝试读取模型时遇到未知层类型,则会抛出异常:

Unspecified error: Can't create layer "layer_name" of type "MyType" in function getLayerInstance
/*
Unspecified error: 在函数 getLayerInstance 中无法创建 MyType 类型的层 "layer_name"
*/

要正确导入模型,您必须从 cv::dnn::Layer 派生一个具有以下方法的类:

#include <opencv2/dnn/layer.details.hpp>  // CV_DNN_REGISTER_LAYER_CLASS
static inline void loadNet()
{
    CV_DNN_REGISTER_LAYER_CLASS(Interp, InterpLayer);
    // ...

注释
MyType 是抛出异常的未实现层类型。

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

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

cv::dnn::LayerParams 读取超参数。如果你的层有可训练的权重,它们将已存储在层的成员 cv::dnn::Layer::blobs 中。

  • 静态方法 create
static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams& params)

此方法应创建一个图层实例,并返回 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、
                     cv::OutputArrayOfArrays outputs、
                     cv::OutputArrayOfArrays internals) CV_OVERRIDE;

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

注意事项
OpenCV 管理为图层分配的内存。在大多数情况下,层与层之间可以重复使用相同的内存。因此,您的 forward 实现不应依赖于 forward 的第二次调用将在outputsinternals拥有相同的数据。

  • 可选的最终确定方法
    virtual void finalize(cv::InputArrayOfArrays inputs,
                          cv::OutputArrayOfArrays outputs) CV_OVERRIDE;

方法链如下: 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);
    }
    static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams& params)
    {
        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,
                         cv::OutputArrayOfArrays outputs_arr,
                         cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE
    {
        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;
};

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

CV_DNN_REGISTER_LAYER_CLASS(Interp, InterpLayer);
cv::dnn::Net caffeNet = cv::dnn::readNet("/path/to/config.prototxt", "/path/to/weights.caffemodel")

示例:来自 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 blobs 放入 cv::dnn::Layer::blobs。在我们的例子中,resize 的输出形状将存储在层的 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。
        // 形状和两个带有缩放因子的 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;
        }
    }
    static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams& params)
    {
        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;
    }
    virtual void finalize(cv::InputArrayOfArrays, cv::OutputArrayOfArrays outputs_arr) CV_OVERRIDE
    {
        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,
                         cv::OutputArrayOfArrays outputs_arr,
                         cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE
    {
        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;
};

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

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

在 Python 中定义自定义图层

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

让我们来看看整体嵌套边缘检测(Holistically-Nested Edge Detection)深度学习模型。与当前版本的 Caffe 框架相比,该模型在训练时只有一个不同点。裁剪层接收两个输入 Blob,并裁剪第一个 Blob 以匹配第二个 Blob 的空间尺寸,然后从中心开始裁剪。现在,Caffe 的图层是从左上角开始裁剪。因此,使用最新版本的 Caffe 或 OpenCV,你将获得填充边框的偏移结果。

接下来,我们要将 OpenCV 的 "裁剪层 "从左上角裁剪替换为以中心裁剪。

  • 创建一个带有 getMemoryShapesforward 方法的类
class CropLayer(object)def __init__(self, params, blobs):
        self.xstart = 0
        self.xend = 0
        self.ystart = 0
        self.yend = 0
    # 我们的图层接收两个输入。我们需要裁剪第一个输入 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 图层替换为一个自定义图层。您可以在源代码中找到完整的脚本。

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值