関東、名古屋、関西のコンピュータビジョン勉強会合同で開催している全日本コンピュータビジョン勉強会の7回目です。
今回は、恒例となっているコンピュータビジョンのトップカンファレンス「CVPR2021」の論文読み会の前編です。 後編は7/31に開催予定です。
以下、リンク等をまとめます。
登録サイト
Togetter
Youtube
発表資料
今回は私も発表をしたので、発表資料をこちらにも張っておきます。
www.slideshare.net
関東、名古屋、関西のコンピュータビジョン勉強会合同で開催している全日本コンピュータビジョン勉強会の7回目です。
今回は、恒例となっているコンピュータビジョンのトップカンファレンス「CVPR2021」の論文読み会の前編です。 後編は7/31に開催予定です。
以下、リンク等をまとめます。
今回は私も発表をしたので、発表資料をこちらにも張っておきます。
www.slideshare.net
※(2021/04/19)shade-treeさんとlosnuevetorosさんの資料へのリンクが古かったため修正しました。
関東、名古屋、関西のコンピュータビジョン勉強会合同で開催している全日本コンピュータビジョン勉強会の6回目です。
今回は、Visionでも応用が進んできたTransformer縛りの論文読み会を行いました。 注目なテーマなだけに、たくさんの発表者/聴講者の方にご参加いただきました。ありがとうございます。
以下、リンク等をまとめます。
今回、発表資料の中には質疑応答用のSlackのみで公開されているものもありますのでご了承ください。
※勉強会開始は動画開始から30分後
この記事はOpenCV Advent Calendar 2020 18日目の記事です。
OpenCVにはDNNモジュールという畳み込みニューラルネットワークの機能が実装されています。この機能は推論専用のため、CaffeやTensorflowなどの深層学習ライブラリ上で学習したモデルを読み込んで使用します。DNNモジュールはPyTorchのモデルを直接はサポートしていませんが、ONNXをサポートしているため、PyTorchからONNX経由でモデルを読ませることができます。
さて、自分たちで開発をしていると、既存のネットワーク層ではなく、自分たちで独自に開発した層を使いたいという要求が出てくると思います。TensorflowやPyTorchなどほとんどの深層学習ライブラリにはこのようなカスタマイズしたネットワーク層を作成する機能がついていますが、OpenCVも同様にそのようなカスタム層を開発する機能がついています。
OpenCVでカスタム層を開発し、CaffeやTensorflowのモデルを取り込む例は、公式の以下のチュートリアルに解説されているので、ここではPyTorchからONNX経由で出力したモデルをカスタム層を取り込むことができるか試してみたいと思います。
OpenCV: Custom deep learning layers support
今回は以下のバージョンで動作確認を行いました。
尚、今回使用するコードはこちらにアップしてあります。 https://github.com/takmin/OpenCV-ROI-Pooling-Layer
ここではROI Poolingを例に解説したいと思います。ROI PoolingはFast R-CNN*1で提案された層で、特徴マップ(Feature Map)上のある矩形の領域(Region of Interest)を切り取って、Max Poolingを使用してM x Nサイズの特徴マップへリサイズします。これにより、特徴マップ内の縦横比の異なるような領域でも、固定サイズの特徴マップへ変換することができます。
ROI Pooling層はtorchvisionでサポートされてます。
torchvision.ops — PyTorch 1.7.0 documentation
PyTorchを用いてROI Pooling層のみのシンプルなネットワークを構築し、それをONNX形式で保存するコードは以下の通りです。
import cv2 import torch import torchvision import torchvision.transforms as transforms # This network has only ROI Pooling layer model = torchvision.ops.RoIPool([4,2],1.0) # Load Image img = cv2.imread('2007_000720.jpg') # Convert an image to tensor input = transforms.ToTensor()(img) input = input.unsqueeze(0) # ROI rois = torch.tensor([[0, 216, 112, 304, 267]], dtype=torch.float) # Test Model with image and roi output = model(input, rois) # Print test outputs print("output: ") print(output) # Save Model as ONNX filename = 'roi_pool.onnx' torch.onnx.export(model, (input,rois), filename,input_names=['input','boxes'], output_names=['output'], opset_version=11)
ROI Pooling層はtorchvision.ops.RoIPool()で構築されます。
model = torchvision.ops.RoIPool([4,2],1.0)
ここで、"[4,2]"は出力のサイズで、縦4横2のサイズにPoolingされることを意味します。また1.0はspatial_scaleを表し、入力のROIをここで指定したスケール倍させます。
入力画像は、通常通り(batch, channel, height, width)の4次元のテンソルに変換します。ここでは画像は1枚なので、batchのサイズも1です。
input = transforms.ToTensor()(img) input = input.unsqueeze(0)
またもう一つの入力であるROIは2次元のテンソルで、各行にはバッチIDと選択範囲(ROI)を指定する4つの値(x1, y1, x2, y2)の計5つの値が入ります。
rois = torch.tensor([[0, 112, 216, 267, 304]], dtype=torch.float)
バッチIDは、一枚の特徴マップに複数のROIを指定できるようにするために、そのROIがどの入力画像バッチの中のどこに対応するのかを入力します。 ROIを指定する4つの値は(x1, y1)が特徴マップの左上座標、(x2, y2)が右下座標になります。
出力は以下のようになります。
output: tensor([[[[0.9882, 1.0000], [0.9804, 1.0000], [0.8431, 0.7490], [0.8196, 0.8706]], [[0.9647, 1.0000], [0.9647, 0.9725], [0.8275, 0.7098], [0.8353, 0.8784]], [[1.0000, 1.0000], [0.9608, 0.9804], [0.8510, 0.7725], [0.9098, 0.9490]]]])
最後にモデルをonnxで保存します。
filename = 'roi_pool.onnx' torch.onnx.export(model, (input,rois), filename,input_names=['input','boxes'], output_names=['output'], opset_version=11)
torch.onnx.export()の第2引数の"(input,rois)"は実際の入力のサンプルで、モデル出力のValidationに使用されます。ここはテンソルのフォーマットさえ正しければランダムな値でも構いません。 "opset_version"はOperator Setのバージョンで、ROI Poolingはバージョン11以降でサポートされています。このオプションを指定しない場合、以下のようなエラーが出力されます。(はまりポイント1)
RuntimeError: ONNX export failed on an operator with unrecognized namespace torchvision::roi_pool. If you are trying to export a custom operator, make sure you registered it with the right domain and version.
こちらの記事を参考に、ONNXの中身を確認してみました。
====== Nodes ====== [Node #0] input: "input" input: "boxes" output: "output" name: "MaxRoiPool_0" op_type: "MaxRoiPool" attribute { name: "pooled_shape" ints: 4 ints: 2 type: INTS } attribute { name: "spatial_scale" f: 1.0 type: FLOAT }
ROI Pooling層は"input"と"boxes"という2つの入力と"output"という出力を持ち、"MaxRoiPool"というオペレーション型で定義されていることがわかります。 また、モデル構築時にtorchvision.ops.RoIPool()で指定した出力サイズとspatial_scaleがそれぞれ、"pooled_shape"と"spatial_scale"という属性名で定義されていることがわかります。
OpenCVのDNNモジュールでONNXを読み込むには、cv::dnn::readNet()という関数で、引数にonnxファイル名を指定します。
cv::dnn::Net net = cv::dnn::readNet("roi_pool.onnx");
OpenCV 4.5.0ではRoI Poolingはサポートされてません。 したがって、PyTorchから出力したONNXを上記コードで取り込むと以下のようなエラーが出ます。
OpenCV(4.5.0) /opencv-4.5.0/modules/dnn/src/dnn.cpp:604: error: (-2:Unspecified error) Can't create layer "output" of type "MaxRoiPool" in function 'cv::dnn::dnn4_v20200609::LayerData::getLayerInstance'
そこで、チュートリアルに従い、MaxRoiPoolを自前で実装します。 カスタム層は"cv::dnn::Layer"クラスを継承することで作成します。
class RoIPoolLayer : public cv::dnn::Layer { public: RoIPoolLayer(const cv::dnn::LayerParams& params) : Layer(params) { spatial_scale = params.get<float>("spatial_scale"); cv::dnn::DictValue pooled_shape = params.get("pooled_shape"); pooled_height = pooled_shape.getIntValue(0); pooled_width = pooled_shape.getIntValue(1); } static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams& params) { return cv::Ptr<cv::dnn::Layer>(new RoIPoolLayer(params)); } /* * inputs[0] shape of input image tensor * inputs[1] shape of roi box */ 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[1][0]; // number of box outShape[1] = inputs[0][1]; // number of channels outShape[2] = this->pooled_height; outShape[3] = this->pooled_width; outputs.assign(1, outShape); return false; } virtual void forward(cv::InputArrayOfArrays inputs_arr, cv::OutputArrayOfArrays outputs_arr, cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE { std::vector<cv::Mat> inputs, outputs; inputs_arr.getMatVector(inputs); outputs_arr.getMatVector(outputs); cv::Mat& inp = inputs[0]; cv::Mat& box = inputs[1]; cv::Mat& out = outputs[0]; /****************************** 省略 *******************************/ } private: int pooled_width, pooled_height; float spatial_scale; };
継承したCustom Layerクラスはコンストラクタ、create()、getMemoryShape()、forward()、finalize()をオーバーライドする必要があります(ただし今回はfinalize()は不要)。 ここでは、コンストラクタ、getMemoryShape()、forward()について、もう少し詳しく見ていきます。
コンストラクタでは引数のcv::dnn::LayerParamsからROI Pooling層の属性情報を取得します。 これはONNX内で定義されている"pooled_shape"と"spatial_scale"を指し、cv::dnn::LayerParams::get()という関数で取得します。
RoIPoolLayer(const cv::dnn::LayerParams& params) : Layer(params) { spatial_scale = params.get<float>("spatial_scale"); cv::dnn::DictValue pooled_shape = params.get("pooled_shape"); pooled_height = pooled_shape.getIntValue(0); pooled_width = pooled_shape.getIntValue(1); }
"pooled_shape"は縦と横の2つの値を持つため、一旦cv::dnn:DictValueという型に格納してやり、その後getIntValue()というメンバ関数を使ってそれぞれの値を取得しています。(公式ドキュメント等どこにもやり方が書いていなかった。はまりポイント2)
getMemoryShape()では、出力の形状を定義します。
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[1][0]; // number of box outShape[1] = inputs[0][1]; // number of channels outShape[2] = this->pooled_height; outShape[3] = this->pooled_width; outputs.assign(1, outShape); return false; }
入力の次元は画像(input)が"バッチ数 x チャネル数 x 縦 x 横"、ROI(boxes)が"総box数 x 5"なので、出力の次元は"総box数 x チャネル数 x Pooling縦 x Pooling横"の4次元となります。
forward()にはROI Poolingの処理の実体を記述します。尚、OpenCVでは推論のみを扱っているため、backward関数の実装は必要ありません。
virtual void forward(cv::InputArrayOfArrays inputs_arr, cv::OutputArrayOfArrays outputs_arr, cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE { std::vector<cv::Mat> inputs, outputs; inputs_arr.getMatVector(inputs); outputs_arr.getMatVector(outputs); cv::Mat& inp = inputs[0]; cv::Mat& box = inputs[1]; cv::Mat& out = outputs[0]; /****************************** 省略 *******************************/ }
入力となるcv::InputArrayOfAraysをgetMatVector()を用いてcv::Mat型のvector配列に変換します。この1番目の要素が画像(inputs)2番目の要素がROI(boxes)になります。
ここの処理は元のtorchvisionのものと同じ振る舞いとなるように記述する必要があります。 torchvisionのROI Poolingの実体はC++で記述されており、ここにCPU版のソースがあります。 https://github.com/pytorch/vision/blob/master/torchvision/csrc/cpu/ROIPool_cpu.cpp
今回はこのコードを元にforward()の実装を行いました。具体的な中身については、githubに上げたコードを参照してください。
まず、readNet()でonnxファイルをロードする前に、作成したROI Pooling層の登録を行う必要があります。
// Register RoIPoolLayer as MaxRoiPool CV_DNN_REGISTER_LAYER_CLASS(MaxRoiPool, RoIPoolLayer); // Load ONNX file cv::dnn::Net net = cv::dnn::readNet("roi_pool.onnx");
CV_DNN_REGISTER_LAYER_CLASSでMaxRoiPoolというオペレーションをRoIPoolLayerに紐づけることで、エラーなしにonnxファイルをロードすることができます。
では実際にROI Poolingしてみます。
// Load Image file cv::Mat img = cv::imread("2007_000720.jpg"); // Image to tensor cv::Mat blob = cv::dnn::blobFromImage(img, 1.0 / 255);
画像をimread()で読み込んだ後、blobFromImage()で[0,1]の範囲に正規化しつつテンソルへ変換します。
// ROI cv::Mat rois(1,5,CV_32FC1); rois.at<float>(0, 0) = 0; rois.at<float>(0, 1) = 216; rois.at<float>(0, 2) = 112; rois.at<float>(0, 3) = 304; rois.at<float>(0, 4) = 267;
一方ROIもバッチID(ここでは画像一枚なので0)、矩形の範囲(x1,y1,x2,y2)に適当な値を入れて準備します。
// set inputs net.setInput(blob,"input"); // don't forget name for multiple inputs net.setInput(rois, "boxes");
準備した2つの入力をネットワークにセットします。ここで、複数の入力がある時は必ずsetInput()関数の第2引数に入力名を指定する必要があります。
ここで入力名を指定しないで
net.setInput(blob); net.setInput(rois);
などとやってしまうと、roisの情報でblobの情報が上書きされてしまい、以下のようなエラーが出てしまいます。(はまりポイント3)
dnn.cpp:3095: error: (-215:Assertion failed) inp.total() in function 'cv::dnn::dnn4_v20200908::Net::Impl::allocateLayers'
最後にforwardを実行すればROI Poolingを実行できます。
// Forward cv::Mat output = net.forward(); // output.dims == 4
outputの中身をプリントすると
size: 1 x 3 x 4 x 2 0.988235, 1, 0.980392, 1, 0.843137, 0.74902, 0.819608, 0.870588, 0.964706, 1, 0.964706, 0.972549, 0.827451, 0.709804, 0.835294, 0.878431, 1, 1, 0.960784, 0.980392, 0.85098, 0.772549, 0.909804, 0.94902,
となり、PyTorch上での出力とも一致していることがわかります。
torchvisionに実装されているROI Poolingを参考に、OpenCVのDNNモジュールでカスタム層を作成する方法について解説しました。 今回は簡単のためROI Pooling単体のネットワークですが、実際はROI Poolingを含んだもう少し複雑なモデルをPyTorch上で学習させ、それをONNXを経由して取り込むことになると思います。その場合でも、複雑なネットワークの構築はreadNet()を読み込むときに行われるため、OpenCV側でやることは今回と変わりません。
*1:Girshick, R. (2015). Fast R-CNN. International Conference on Computer Vision
関東、名古屋、関西のコンピュータビジョン勉強会合同で開催している全日本コンピュータビジョン勉強会の5回目です。
今回は、恒例となっているEuropean Conference on Computer Vision (ECCV) 2020の論文読み会を行いました。
以下、リンク等をまとめます。
今回、私は発表がなかったため、運営と聴講に集中できました。
発表者の皆さん、幹事の皆さん、聴講してくれた皆さん、どうもありございました。