この投稿はOpenCV Advent Calendar 2020の14日目の記事です。
はじめに
OpenCVのDNNモジュールで各モデルを試してみようというリポジトリで細々と動作を確認したりしています。
そのなかでClassificationやObject Detection、Pose Estimationなどのタスクのモデルはいろいろ動かしてみたけど、そういえばDepth Estimationのモデルは動かしたことなかったな…って思ったのでやってみました。
DNNモジュールとは?
OpenCVでは機能ごとにモジュール(コア機能ならCore、GUI機能ならHighguiなど)に分かれており、ディープラーニングの推論機能を担当するのがDNNモジュールです。
学習はTensorflowやPyTorch、Darknetなどの各フレームワークやAzure Cognitive ServicesのCustom Visionなどのクラウドサービスで行い、DNNモジュールでは学習されたモデルを利用して推論を行います。
MiDaSとは?
MiDaSとはIntel ISL(Intelligent Systems Lab)が公開している単眼のRGB画像からの深度推定のモデルです。
MiDaSの論文("Towards Robust Monocular Depth Estimation: Mixing Datasets for Zero-shot Cross-dataset Transfer (TPAMI 2020)")はこちらで公開されています。
論文ではザックリこんな内容が書かれているらしい。
単眼画像からの深度推定を汎用的に良い感じにやるには大規模で多様なデータを用意しないといけないよ。
でも、世の中にあるそれぞれのデータセットはスケールが異なったりセンサーが違ったりして特性がバラバラだから単純にごちゃまぜにしては使いにくいよね?
この論文では上手いことそれらの違いを吸収していろんなデータセットを使って学習できるようにするよ!
実装はGitHubでオープンソースで公開されており、ライセンスもMIT Licenseで扱いやすい。
サンプルコード
動作確認環境
- Windows 10
- Visual Studio 2019 (16.8.2)
- OpenCV 4.5.0 *1
- CMake 3.17.5
- CUDA Toolkit 10.1 *2 Option
- cuDNN 7.6.5 *2 Option
ソースコード
ソースコードはGitHubの以下のリポジトリで公開しています。
#include <iostream>
#include <string>
#include <vector>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
// Get Output Layers Name
std::vector<std::string> getOutputsNames( const cv::dnn::Net& net )
{
static std::vector<std::string> names;
if( names.empty() ){
std::vector<int32_t> out_layers = net.getUnconnectedOutLayers();
std::vector<std::string> layers_names = net.getLayerNames();
names.resize( out_layers.size() );
for( size_t i = 0; i < out_layers.size(); ++i ){
names[i] = layers_names[out_layers[i] - 1];
}
}
return names;
}
int main( int argc, char* argv[] )
{
// Open Video Capture
cv::VideoCapture capture = cv::VideoCapture( 0 );
if( !capture.isOpened() ){
return -1;
}
// Read Network
const std::string model = "../model-f6b98070.onnx"; // MiDaS v2.1 Large
//const std::string model = "../model-small.onnx"; // MiDaS v2.1 Small
cv::dnn::Net net = cv::dnn::readNet( model );
if( net.empty() ){
return -1;
}
// Set Preferable Backend and Target
net.setPreferableBackend( cv::dnn::DNN_BACKEND_OPENCV );
net.setPreferableTarget( cv::dnn::DNN_TARGET_CPU );
while( true ){
// Read Frame
cv::Mat input;
capture >> input;
if( input.empty() ){
cv::waitKey( 0 );
break;
}
if( input.channels() == 4 ){
cv::cvtColor( input, input, cv::COLOR_BGRA2BGR );
}
// Create Blob from Input Image
// MiDaS v2.1 Large ( Scale : 1 / 255, Size : 384 x 384, Mean Subtraction : ( 123.675, 116.28, 103.53 ), Channels Order : RGB )
cv::Mat blob = cv::dnn::blobFromImage( input, 1 / 255.f, cv::Size( 384, 384 ), cv::Scalar( 123.675, 116.28, 103.53 ), true, false );
// MiDaS v2.1 Small ( Scale : 1 / 255, Size : 256 x 256, Mean Subtraction : ( 123.675, 116.28, 103.53 ), Channels Order : RGB )
//cv::Mat blob = cv::dnn::blobFromImage( input, 1 / 255.f, cv::Size( 256, 256 ), cv::Scalar( 123.675, 116.28, 103.53 ), true, false );
// Set Input Blob
net.setInput( blob );
// Run Forward Network
cv::Mat output = net.forward( getOutputsNames( net )[0] );
// Convert Size to 384x384 from 1x384x384
const std::vector<int32_t> size = { output.size[1], output.size[2] };
output = cv::Mat( static_cast<int32_t>( size.size() ), &size[0], CV_32F, output.ptr<float>() );
// Resize Output Image to Input Image Size
cv::resize( output, output, input.size() );
// Visualize Output Image
// 1. Normalize ( 0.0 - 1.0 )
// 2. Scaling ( 0 - 255 )
double min, max;
cv::minMaxLoc( output, &min, &max );
const double range = max - min;
output.convertTo( output, CV_32F, 1.0 / range, - ( min / range ) );
output.convertTo( output, CV_8U, 255.0 );
// Show Image
cv::imshow( "input", input );
cv::imshow( "output", output );
const int32_t key = cv::waitKey( 1 );
if( key == 'q' ){
break;
}
}
cv::destroyAllWindows();
return 0;
}
解説
1. ネットワークの読み込みと設定
// Read Network
const std::string model = "../model-f6b98070.onnx"; // MiDaS v2.1 Large
//const std::string model = "../model-small.onnx"; // MiDaS v2.1 Small
cv::dnn::Net net = cv::dnn::readNet( model );
if( net.empty() ){
return -1;
}
MiDaSの学習済みモデルを読み込みます。
OpenCVのDNNモジュールはPyTorchの学習済みモデルを直接読み込むことはできない。
そこでPyTorchのモデルをこちらの手順に従ってONNXに変換したモデルを利用する。
(リリースページにてONNXに変換済みのモデルも配布されています。)
// Set Preferable Backend and Target
net.setPreferableBackend( cv::dnn::DNN_BACKEND_OPENCV );
net.setPreferableTarget( cv::dnn::DNN_TARGET_CPU );
推論を実行するバックエンドとターゲットを設定する。
ここではOpenCVのバックエンド(DNN_BACKEND_OPENCV
)でCPUをターゲット(DNN_TARGET_CPU
)として推論するように設定している。
OpenCVのDNNモジュールではそれぞれのバックエンドで指定できるターゲットが異なるのでこちらの表を参考に設定する。(「+」がそれぞれのバックエンドでサポートしているターゲット)
たとえば、GPUを利用して推論したい場合はバックエンドにDNN_BACKEND_CUDA
、ターゲットにDNN_TARGET_CUDA
を指定する。*2
2. 入力データの作成
// Create Blob from Input Image
// MiDaS v2.1 Large ( Scale : 1 / 255, Size : 384 x 384, Mean Subtraction : ( 123.675, 116.28, 103.53 ), Channels Order : RGB )
cv::Mat blob = cv::dnn::blobFromImage( input, 1 / 255.f, cv::Size( 384, 384 ), cv::Scalar( 123.675, 116.28, 103.53 ), true, false );
// MiDaS v2.1 Small ( Scale : 1 / 255, Size : 256 x 256, Mean Subtraction : ( 123.675, 116.28, 103.53 ), Channels Order : RGB )
//cv::Mat blob = cv::dnn::blobFromImage( input, 1 / 255.f, cv::Size( 256, 256 ), cv::Scalar( 123.675, 116.28, 103.53 ), true, false );
// Set Input Blob
net.setInput( blob );
入力データはcv::dnn::blobFromImage()
で作成する。
画像のサイズやスケールなどのパラメータはモデルによって異なるので確認する。
(間違ってるかもなので教えてほしい。)
3. 推論
// Get Output Layers Name
std::vector<std::string> getOutputsNames( const cv::dnn::Net& net )
{
static std::vector<std::string> names;
if( names.empty() ){
std::vector<int32_t> out_layers = net.getUnconnectedOutLayers();
std::vector<std::string> layers_names = net.getLayerNames();
names.resize( out_layers.size() );
for( size_t i = 0; i < out_layers.size(); ++i ){
names[i] = layers_names[out_layers[i] - 1];
}
}
return names;
}
// Run Forward Network
cv::Mat output = net.forward( getOutputsNames( net )[0] );
cv::dnn::Net::forward()
にネットワークの出力レイヤーの名前を指定して推論する。
ここでは出力レイヤーの名前はネットワークの中から接続がないレイヤーの名前を抽出して利用しているが、出力レイヤーの名前を文字列で直接指定してもよい。
4. 解釈
// Convert Size to 384x384 (256x256) from 1x384x384 (1x256x256)
const std::vector<int32_t> size = { output.size[1], output.size[2] };
output = cv::Mat( static_cast<int32_t>( size.size() ), &size[0], CV_32F, output.ptr<float>() );
// Resize Output Image to Input Image Size
cv::resize( output, output, frame.size() );
MiDaSのLargeモデルでは1x384x384のcv::Mat
で推論結果が出力される。
これを384x384の画像形式に整え、入力画像のサイズにリサイズしてやる。
(Smallモデルでは1x256x256で推論結果が出力される。)
5. 可視化
// Visualize Output Image
// 1. Normalize ( 0.0 - 1.0 )
// 2. Scaling ( 0 - 255 )
double min, max;
cv::minMaxLoc( output, &min, &max );
const double range = max - min;
output.convertTo( output, CV_32F, 1.0 / range, - ( min / range ) );
output.convertTo( output, CV_8U, 255.0 );
あとはお好みの方法で可視化してやる。
ここでは最小値-最大値を0.0-1.0に正規化、0-255のグレースケール画像に変換して可視化している。
(それっぽくテキトーにやっただけなのでもう少し良い方法がある気がする。教えてほしい。)
実行結果
MiDaS v2.1 LargeのモデルはCPUで推論すると1fpsくらい、GPUで推論するとリアルタイムに動作した。*3
MiDaS v2.1 SmallのモデルはCPUでの推論でもリアルタイムに動作した。
全体的に形状がわかる程度には妥当な推論できていると思う。
人物の顔や指などの細かいディティールはちょっと厳しいのかな?
おわりに
MiDaSは思っていたよりも綺麗に深度推定できてちょっと驚きました。
もう少し細かいディティールが推定できるようになるといいな。
OpenCVのDNNモジュールは扱いやすくとりあえず動かしてみるまでの敷居が低くなります。
実際に私のような初心者でも難なく動かせたりして楽しいです。
今後も最新のフレームワークやモデル(レイヤー)に追従してサポートしてくれると嬉しい。
(PyTorchのモデルがそのまま読み込めるようになるといいな。)
明日は@satsukiyaさんのCUDAについての記事です。
1 MiDaS v2.1 Small(model-small.onnx)はOpenCV 4.5.0では読み込めなかった。2020/12/04頃にOpenCV master/HEAD(22d64ae)で確認すると読み込めた。どこかのコミットで修正されたと思われるがそこまでは確認していない。
2 GPUで推論するにはCUDA対応のオプション(OPENCV_DNN_CUDA)をONにしてOpenCVをビルドする必要がある。
3 実行環境はCPU:Intel Core-i9 9900K、GPU:NVIDIA GeForce GTX 1080 Tiです。