takminの書きっぱなし備忘録 @はてなブログ

主にコンピュータビジョンなど技術について、たまに自分自身のことや思いついたことなど

2021/04/18 第6回全日本コンピュータビジョン勉強会「Transformer論文読み会」発表資料まとめ

※(2021/04/19)shade-treeさんとlosnuevetorosさんの資料へのリンクが古かったため修正しました。

関東、名古屋、関西のコンピュータビジョン勉強会合同で開催している全日本コンピュータビジョン勉強会の6回目です。

今回は、Visionでも応用が進んできたTransformer縛りの論文読み会を行いました。 注目なテーマなだけに、たくさんの発表者/聴講者の方にご参加いただきました。ありがとうございます。

以下、リンク等をまとめます。

今回、発表資料の中には質疑応答用のSlackのみで公開されているものもありますのでご了承ください。

登録サイト

kantocv.connpass.com

Togetter

togetter.com

Youtube

※勉強会開始は動画開始から30分後

www.youtube.com

発表資料

発表者 論文タイトル 発表資料
Seitaro Shinagawa Learning Transformer in 40 minutes(前編) https://speakerdeck.com/sei88888/quan-ri-ben-cvmian-qiang-hui-fa-biao-zi-liao-learning-transformer-in-40-minutes
ShintaroYamamoto Learning Transformer in 40 minutes(後編) https://drive.google.com/file/d/1dwvc2yNi66iuz9Z63j_2cTic2qmNIOyP/view
shade-tree An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale https://speakerdeck.com/forest1988/an-image-is-worth-16x16-words-transformers-for-image-recognition-at-scale
yasutomo57jp TransPose: Towards Explainable Human Pose Estimation by Transformer
s_aiueo32 Read Like Humans: Autonomous, Bidirectional and Iterative Language Modeling for Scene Text Recognition https://speakerdeck.com/sansandsoc/read-like-humans-autonomous-bidirectional-and-iterative-language-modeling-for-scene-text-recognition
alfredplpl UniT: Multimodal Multitask Learning with a Unified Transformer https://www.slideshare.net/yasunoriozaki12/unit-transformer-is-all-you-need
hrs1985 TransGAN: Two Transformers Can Make One Strong GAN
ground0state Incorporating Convolution Designs into Visual Transformers https://speakerdeck.com/abeta/incorporating-convolution-designs-into-visual-transformers
doiken23 LoFTR: Detector-Free Local Feature Matching with Transformers
andrewshin Perspectives and Prospects on Transformer Architecture for Cross-Modal Tasks with Language and Vision
losnuevetoros Do Transformer Modifications Transfer Across Implementations and Applications? https://speakerdeck.com/yushiku/do-transformer-modifications-transfer-across-implementations-and-applications

PyTorchで学習したモデルをOpenCVで使う (Custom Layer編)

この記事はOpenCV Advent Calendar 2020 18日目の記事です。

はじめに

OpenCVにはDNNモジュールという畳み込みニューラルネットワークの機能が実装されています。この機能は推論専用のため、CaffeやTensorflowなどの深層学習ライブラリ上で学習したモデルを読み込んで使用します。DNNモジュールはPyTorchのモデルを直接はサポートしていませんが、ONNXをサポートしているため、PyTorchからONNX経由でモデルを読ませることができます。  

参考: takmin.hatenablog.com

 

さて、自分たちで開発をしていると、既存のネットワーク層ではなく、自分たちで独自に開発した層を使いたいという要求が出てくると思います。TensorflowやPyTorchなどほとんどの深層学習ライブラリにはこのようなカスタマイズしたネットワーク層を作成する機能がついていますが、OpenCVも同様にそのようなカスタム層を開発する機能がついています。

  OpenCVでカスタム層を開発し、CaffeやTensorflowのモデルを取り込む例は、公式の以下のチュートリアルに解説されているので、ここではPyTorchからONNX経由で出力したモデルをカスタム層を取り込むことができるか試してみたいと思います。

OpenCV: Custom deep learning layers support  

今回は以下のバージョンで動作確認を行いました。

  • OpenCV 4.5.0
  • PyTorch 1.7.0
  • torchvision 0.8.1

尚、今回使用するコードはこちらにアップしてあります。 https://github.com/takmin/OpenCV-ROI-Pooling-Layer

PyTorchでROI Poolingの実装

ここではROI Poolingを例に解説したいと思います。ROI PoolingはFast R-CNN*1で提案された層で、特徴マップ(Feature Map)上のある矩形の領域(Region of Interest)を切り取って、Max Poolingを使用してM x Nサイズの特徴マップへリサイズします。これにより、特徴マップ内の縦横比の異なるような領域でも、固定サイズの特徴マップへ変換することができます。

f:id:takmin:20201204174557p:plain
ROI Pooling

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の出力内容

こちらの記事を参考に、ONNXの中身を確認してみました。

ONNX形式のモデルを扱う - Qiita

====== 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でROI Poolingの実装

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()

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()

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

2020/12/12 第5回全日本コンピュータビジョン勉強会「ECCV2020読み会」発表資料まとめ

関東、名古屋、関西のコンピュータビジョン勉強会合同で開催している全日本コンピュータビジョン勉強会の5回目です。

今回は、恒例となっているEuropean Conference on Computer Vision (ECCV) 2020の論文読み会を行いました。

以下、リンク等をまとめます。

登録サイト

kantocv.connpass.com

Togetter

togetter.com

YouTube

www.youtube.com

発表資料

発表者 論文タイトル 資料
tomoaki_teshima Flow-edge Guided Video Completion https://www.dropbox.com/s/04qec0axmkjipiv/japancv-fgvc-publish.pdf?dl=0
Kenji DeepSFM: Structure From Motion Via Deep Bundle Adjustment https://speakerdeck.com/tsukamotokenji/deepsfm-structure-from-motion-via-deep-bundle-adjustment
s_aiueo32 Adaptive Text Recognition through Visual Matching https://speakerdeck.com/s_aiueo32/adaptive-text-recognition-through-visual-matching
daigo_hirooka RAFT: Recurrent All Pairs Field Transforms for Optical Flow https://speakerdeck.com/daigo0927/raft-recurrent-all-pairs-field-transforms-for-optical-flow
pacifinapacific Chained-Tracker: Chaining Paired Attentive Regression Results for End-to-End Joint Multiple-Object Detection and Tracking https://www2.slideshare.net/KyominSasaki/eccv2020chained-tracker
neka_nat Domain-invariant Stereo Matching Networks https://www2.slideshare.net/kentatanaka3382/eccv-2020-dsmnet
tomoyukun MotionSqueeze: Neural Motion Feature Learning for Video Understanding https://speakerdeck.com/tomoyukun/eccv2020-lun-wen-du-mihui-motionsqueeze-neural-motion-feature-learning-for-video-understanding
godel Learning to Exploit Multiple Vision Modalities https://speakerdeck.com/godel/lun-wen-du-mihui-learning-to-exploit-multiple-vision-modalities-by-using-grafted-networks
losnuevetoros End-to-End Object Detection with Transformers https://speakerdeck.com/yushiku/end-to-end-object-detection-with-transformers

今回、私は発表がなかったため、運営と聴講に集中できました。

発表者の皆さん、幹事の皆さん、聴講してくれた皆さん、どうもありございました。

2020/10/10 第4回全日本コンピュータビジョン勉強会「人に関する認識・理解論文読み会」発表資料まとめ

今回も前回に引き続き、関東、関西、名古屋のコンピュータビジョン勉強会合同でオンラインにて「全日本コンピュータビジョン勉強会」を開催いたしました。

今回は昨年に引き続きPRMU研究会との共催という形での実施となりました。 お陰様で配信の準備などはすべてお任せでした。

例年PRMUと共催の際はICCVまたはECCV読み会を行っているのですが、今回はPRMU研究会のテーマに合わせ「人に関する認識・理解論文読み会」を行いました。

というわけで資料へのリンクをまとめたいと思います。

申し込みサイト kantocv.connpass.com

PRMU研究会10月開催プログラム

研究会 開催プログラム - 2020-10-PRMU

Tweetまとめ togetter.com

Youtubeは以下より配信致しました。今回は午前中に開催されたPRMUの内容も含まれております。

https://www.youtube.com/watch?v=l_QHRjZ8sew

以下、資料へのリンクです。

発表者 論文タイトル 資料
takmin Person Re-Identificationの論文を読んでみた https://www.slideshare.net/takmin/20201010-personreid
akihiro_fuji Evolving Losses for Unsupervised Video Representation Learning https://www.slideshare.net/AkihiroFujii2/201010-evolving-lossesforunsupervisedvideorepresentationlearning
alfred_plpl MOTS: Multi-Object Tracking and Segmentation https://www.slideshare.net/yasunoriozaki12/cvmots-multiobject-tracking-and-segmentation
Katsutoshi_Shiraki Spatial Temporal Attention Graphによる関節の重要度と関係性を考慮した動作認識
nk116 Inference Stage Optimization for Cross-scenario 3D Human Pose Estimation (NeurIPS 2020) https://speakerdeck.com/nk35jk/inference-stage-optimization-for-cross-scenario-3d-human-pose-estimation
godel マンナビについて(TLIO: Tight Learned Inertial Odometry (IROS2020)) https://speakerdeck.com/godel/man-navi

今回私が発表した内容を別途こちらにもリンクしておきます。

www.slideshare.net