上一個教程: 如何在瀏覽器中執行深度網路
下一個教程: 如何執行自定義 OCR 模型
| |
| 原始作者 | Dmitry Kurtaev |
| 相容性 | OpenCV >= 3.4.1 |
簡介
深度學習是一個快速發展的領域。構建神經網路的新方法通常會引入新型別的層。這些層可能是對現有層的修改,也可能是對優秀研究思想的實現。
OpenCV 允許從不同的深度學習框架匯入和執行網路。其中包含了許多最流行的層。然而,你可能會遇到一個問題:你的網路無法使用 OpenCV 匯入,因為網路中的某些層可能未在 OpenCV 的深度學習引擎中實現。
第一個解決方案是在 https://github.com/opencv/opencv/issues 建立一個功能請求,其中提及模型的來源和新層的型別等詳細資訊。如果 OpenCV 社群有此需求,則可以實現新層。
第二種方法是定義一個自定義層,以便 OpenCV 的深度學習引擎知道如何使用它。本教程旨在向您展示深度學習模型匯入定製的過程。
在 C++ 中定義自定義層
深度學習層是網路管道的基本組成部分。它連線到輸入資料塊(blob)併產生結果到輸出資料塊(blob)。其中包含經過訓練的權重和超引數。層的名稱、型別、權重和超引數儲存在訓練期間由原生框架生成的檔案中。如果 OpenCV 遇到未知的層型別,它將在嘗試讀取模型時丟擲異常
Unspecified error: Can't create layer "layer_name" of type "MyType" in function getLayerInstance
要正確匯入模型,您必須從 cv::dnn::Layer 派生一個類,幷包含以下方法
{
public:
const int requiredOutputs,
std::vector<std::vector<int> > &outputs,
std::vector<std::vector<int> > &internals)
const CV_OVERRIDE;
};
並在匯入前註冊它
static inline void loadNet()
{
- 注意
MyType 是丟擲異常中未實現層的型別。
讓我們看看所有這些方法的作用
從 cv::dnn::LayerParams 中檢索超引數。如果您的層具有可訓練的權重,它們將已經儲存在 Layer 的成員 cv::dnn::Layer::blobs 中。
此方法應建立您的層的一個例項,並返回一個包含它的 cv::Ptr。
const int requiredOutputs,
std::vector<std::vector<int> > &outputs,
std::vector<std::vector<int> > &internals)
const CV_OVERRIDE;
根據輸入形狀返回層的輸出形狀。您可以使用 internals 請求額外記憶體。
在此處實現層的邏輯。為給定輸入計算輸出。
- 注意
- OpenCV 管理為層分配的記憶體。在大多數情況下,相同的記憶體可以在層之間重用。因此,您的
forward 實現不應依賴於 forward 的第二次呼叫將在 outputs 和 internals 中擁有相同的資料。
方法鏈如下: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 Wi 和 Ho x Wo 分別是輸入和輸出的 高 x 寬。此層沒有可訓練的權重,但它有超引數來指定輸出大小。
例如,
layer {
name: "output"
type: "Interp"
bottom: "input"
top: "output"
interp_param {
height: 9
width: 8
}
}
這樣我們的實現可能看起來像這樣
{
public:
{
outWidth =
params.get<
int>(
"width", 0);
outHeight =
params.get<
int>(
"height", 0);
}
{
}
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;
}
{
if (inputs_arr.depth() ==
CV_16S)
{
return;
}
std::vector<cv::Mat> inputs, outputs;
inputs_arr.getMatVector(inputs);
outputs_arr.getMatVector(outputs);
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;
};
接下來我們需要註冊新的層型別並嘗試匯入模型。
示例:來自 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。在我們的例子中,調整大小的輸出形狀將儲存在層的 blobs[0] 中。
{
public:
{
for (size_t i = 0; i < blobs.size(); ++i)
if (blobs.size() == 1)
{
outHeight = blobs[0].at<int>(0, 0);
outWidth = blobs[0].at<int>(0, 1);
factorHeight = factorWidth = 0;
}
else
{
factorHeight = blobs[0].at<int>(0, 0);
factorWidth = blobs[1].at<int>(0, 0);
outHeight = outWidth = 0;
}
}
{
}
const int,
std::vector<std::vector<int> > &outputs,
{
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];
}
}
{
if (inputs_arr.depth() ==
CV_16S)
{
return;
}
std::vector<cv::Mat> inputs, outputs;
inputs_arr.getMatVector(inputs);
outputs_arr.getMatVector(outputs);
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)
{
}
int outWidth, outHeight, factorWidth, factorHeight;
};
接下來我們註冊一個層並嘗試匯入模型。
在 Python 中定義自定義層
以下示例展示瞭如何在 Python 中自定義 OpenCV 的層。
讓我們考慮 Holistically-Nested Edge Detection 深度學習模型。該模型與當前版本的 Caffe 框架相比,只有一個不同之處。Crop 層接收兩個輸入資料塊(blob),並裁剪第一個資料塊以匹配第二個資料塊的空間維度,過去是從中心裁剪。現在 Caffe 的層是從左上角裁剪的。因此,使用最新版本的 Caffe 或 OpenCV,您將得到帶有填充邊界的偏移結果。
接下來我們將把 OpenCV 中執行左上角裁剪的 Crop 層替換為一箇中心裁剪的層。
- 建立一個包含
getMemoryShapes 和 forward 方法的類
class CropLayer(object)
def __init__(self, params, blobs)
self.xstart = 0
self.xend = 0
self.ystart = 0
self.yend = 0
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 已實現的層替換為自定義層。您可以在原始碼中找到完整的指令碼。