OpenCV DNN模块常用操作
在实际利用opencv提供的dnn模块部署onnx格式的模型的时候,一些python端利用numpy可以简单轻易实现的操作,在C++端就得仔细考虑下实现的策略了。因为大多数并没有非常简单方便地使用形式,甚至可能需要自己去实现。这里做一个记录。
需要注意的是,我这里使用的是opencv的4.6.0版本,原则上来说,越新的版本越合适,因为早期版本中很多的算子都不支持,对于你的原始模型限制比较多。动不动汇报个错,尤其是直接在cpp端的报错一直都是非常让人头疼,输出的信息非常难以定位问题。所以用新版本至少可以尽可能规避算子不支持的问题。
加载onnx
权重创建模型
net = cv::dnn::readNetFromONNX(net_onnx);
这里的onnx在cpp端使用的时候,最好现在Python端仔细测试一下导出的onnx文件。
opencv有一点好处就是,python端和cpp端的实现基本一致。
使用cuda
这一操作的前提需要我们在编译opencv的时候必须配置好CUDA的相关内容。这部分内容可以参考一下我的另一篇文章Ubuntu 18.04编译安装支持CUDA的OpenCV4.6.0。
net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
这样利用net
前向传播处理数据的时候就会利用GPU。
模型输入输出
对于输入而言,最好借助于opencv自带的函数进行一下预处理,将我们的图像输入转换为4-dimensional Mat with NCHW dimensions order,这一步如果你想自己手工转换,那可是真的不容易。opencv中的维度变换并不像python端,可以借助于numpy来高效方便地实现。
实际上,这里blobFromImage
还提供了包括用于调整输入数值的减均值、倍数放缩数值操作(要注意,这里仅仅是一个倍数,而不是我们常用的那种对三个通道除以各自的标准差的操作)、R与B通道的交换、图像尺寸的放缩等等设置。
我一般直接将数据的归一化(减均值除以方差)放到模型内部了,这样可以避免过多外部自定义的操作。
cv::Mat blob = cv::dnn::blobFromImage(image_rgb);
如果我们借助于opencv来实现减均值和除以方差又如何处理呢?官方的例子中提供了一个模板:
blobFromImage(frame, blob, scale, Size(inpWidth, inpHeight), mean, swapRB, crop);
// Check std values.
if (std.val[0] != 0.0 && std.val[1] != 0.0 && std.val[2] != 0.0)
{
// Divide blob by std.
divide(blob, std, blob);
}
当然,这是batch为1的情形,实际上我们可以借助于另一个方法blobFromImages
来实现整个batch的输入数据的构建。但是使用batch推理的话,需要模型导出onnx的时候设置好动态轴的配置。
单输入单输出
net.setInput(blob);
output = net.forward();
多输入多输出
参考了这里的例子:c++ dnn multi-inputs and multi-outputs
要注意,这里的使用需要与onnx导出时设置的输入输出名字对应。
输入部分,需要多次使用setInput
。
Mat blobx(3, sizex, CV_32F);
Mat bloby(3, sizey, CV_32F);
net.setInput(blobx, "name_of_first_input");
net.setInput(bloby, "name_of_second_input");
对于输出,则需要根据名字来进行索引输出。这里给出使用forward
的两种重载形式的实现形式:
void cv::dnn::Net::forward(std::vector< std::vector<Mat>> & outputBlobs, const std::vector<String>& outBlobNames)
vector<vector<cv::Mat>> blob_outs;
vector<string> output_names = {"out1", "out2"};
net.forward(blob_outs, output_names);
cv::Mat blob_out1 = blob_outs[0][0];
cv::Mat blob_out2 = blob_outs[1][0];
void cv::dnn::Net::forward(OutputArrayOfArrays outputBlobs, const std::vector<String> & outBlobNames)
vector<cv::Mat> blob_outs(2);
vector<string> output_names = {"out1", "out2"};
net.forward(blob_outs, output_names);
cv::Mat blob_out1 = blob_outs[0];
cv::Mat blob_out2 = blob_outs[1];
这里直接使用了一个固定元素数量的空容器,参考C++初始化向量的不同方式)
明显可见,第二种形式更简单一些。第一种应该是用于一些更加复杂的场景,目前我还没有用到非用不可的情况。
模拟PyTorch和NumPy中的squeeze
在模型输出后,一般是BCHW的形状,但是我们可能后面的处理中,需要去掉一些不需要的独立维度,此时我们如果在python端的话,可能就会使用第三方库中提供的squeeze操作了。但是cpp端我们就需要自己处理一下了。
简单粗暴,直接按地址复制
这里参考一下了文档。
for (int row = 0; row < num_rows; row++) {
const float* row_ptr = preds.ptr<float>(0, 0, row);
float* new_row_ptr = new_preds.ptr<float>(row);
for (int col = 0; col < num_cols; col++) {
new_row_ptr[col] = row_ptr[col];
}
}
复制新建
直接利用cv::Mat
的构造函数来复制数据,一个典型的例子如下,但是这里需要保证通道数与数据格式一致。对于特殊的数据形式可能就不太适合了,例如一个1 x 2 x rows x cols
的形状的数据,但是对于1 x 1 x rows x cols
这样的就可以。
Mat maxVal(rows, cols, CV_32FC1, score.data);
使用cv::Mat::reshape
Mat cv::Mat::reshape(int cn, const std::vector<int> & newshape) const
cv::Mat out = ori_out.reshape(0, {h, w).clone(); // 这里可以指定新的形状,但要确保元素数量一致
这里的reshape
的使用需要注意,各个重载版本中都有cn
这个参数,我们并不需要期调整,所以我们指定为0,表示保持原样。需要注意的是,这里的reshape指定新的形状的时候,或许会默认HWC的布局,这一点有待测试。所以一般使用它的时候要小心。另外一点是,reshape并不会复制数据,所以一般最好clone
下,确保数据之间不会有隐式的干扰。