LoginSignup
5
4

More than 5 years have passed since last update.

cv::Vec型変数を変換するときに少ない記述量で書く方法 その1

Last updated at Posted at 2019-04-07

tl;dr

カスタムで高速な画像処理を実装したいときは、C++とOpenCV(cv::Mat)を使って実装したいはず。cv::Matの画素はVec型だけど、この型の変換処理をラムダ式を使って簡潔に書ける小ネタ的な関数について備忘録も兼ねて紹介記事を書きます。型変換も同時に行うとき便利。一度、関数を作っておけば記述量も減らせる。ターゲットはC++17。Visual Studio 2017 15.9.3,OpenCV4.0で動作確認。

追記:2019/4/10に続編を書きました。forEachとの連携です。

コード

細かい説明よりも、まずコードから。

サンプルコード1
#include <iostream>
#include <opencv2/core/core.hpp>

using namespace cv;
using namespace std;

template <class VECD, class FUNC, class VEC>
VECD conv(const VEC& s, FUNC func) 
{
    constexpr int ch = VEC::channels < VECD::channels ? VEC::channels : VECD::channels;
    VECD ret;
    for (int i = 0; i < ch; i++) {
        ret[i] = func(s[i]);
    }
    return ret;
}

template <class VEC, class FUNC>
VEC conv(const VEC& s, FUNC func)
{
    constexpr int ch = VEC::channels;
    VEC ret;
    for (int i = 0; i < ch; i++) {
        ret[i] = func(s[i]);
    }
    return ret;
}

template <class VECD, class FUNC, class VEC>
VECD convIdx(const VEC& s, FUNC func) 
{
    constexpr int ch = VEC::channels < VECD::channels ? VEC::channels : VECD::channels;
    VECD ret;
    for (int i = 0; i < ch; i++) {
        ret[i] = func(s[i], i);
    }
    return ret;
}

template <class VEC, class FUNC>
VEC convIdx(const VEC& s, FUNC func)
{
    constexpr int ch = VEC::channels;
    VEC ret;
    for (int i = 0; i < ch; i++) {
        ret[i] = func(s[i], i);
    }
    return ret;
}

//使い方
int main()
{
    Vec4b vb(1, 2, 3, 4);
    auto vb1 = conv<Vec4f>(vb, [](const auto& v) {
        return v * 0.5f;
    });

    cout << vf << endl;

    return 0;
}

結果1
[0.5, 1, 1.5]

解説

C++17までの知識があることを前提に解説します。Idx取得なしバージョンのconv関数と、ありバージョンのconvIdxを用意した。使い方で示したとおり、conv関数の引数にラムダ式で変換コードを書く。cv::Vecで特殊な計算をするときは、要素数分インデックスを変えて同じコード書くか、for文にする必要がある。このときranged based forが使って要素ごとにアクセスできればよいが、対応するためにはcv::Vecに手を加える必要があるので避けたい。そこでconvやconvIdxを用意してranged based forと同様に簡潔に記述できるように便利な関数を作ってみたという話。ちなみに、Visual Studio 2017 15.9.3のアセンブリを見ると、vmulpsというSIMD掛け算命令を使っているので、4要素のVec型はSIMD演算してくれるみたい。

アセンブリ(サンプルコード1)
int main()
{
 mov         qword ptr [rsp+10h],rbx  
 push        rdi  
 sub         rsp,40h  
        return v * 0.5f;
    });

    cout << vf << endl;
 mov         rdi,qword ptr [__imp_std::cout (07FF73B8D2078h)]  
 lea         rdx,[string "[" (07FF73B8D2240h)]  
    Vec4b vb(1, 2, 3, 4);
 mov         dword ptr [rsp+50h],4030201h  
        return v * 0.5f;
    });

    cout << vf << endl;
 mov         rcx,rdi  
    auto vf = conv<Vec4f>(vb, [](const auto& v) {
 vmovd       xmm0,dword ptr [vb]  
 vpmovzxbd   xmm1,xmm0  
 vcvtdq2ps   xmm3,xmm1  
 vmulps      xmm0,xmm3,xmmword ptr [__xmm@3f0000003f0000003f0000003f000000 (07FF73B8D2280h)]  
 vmovups     xmmword ptr [vf],xmm0  
        return v * 0.5f;
    });

    cout << vf << endl;
 call        std::operator<<<std::char_traits<char> > (07FF73B8D10C0h)  
 xor         ebx,ebx  
 nop         word ptr [rax+rax]  
        return v * 0.5f;
    });

    cout << vf << endl;
 vmovss      xmm1,dword ptr vf[rbx*4]  
 mov         rcx,rdi  
 call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF73B8D20A8h)]  
 mov         rcx,rax  
 lea         rdx,[string ", " (07FF73B8D2244h)]  
 call        std::operator<<<std::char_traits<char> > (07FF73B8D10C0h)  
 inc         rbx  
 cmp         rbx,3  
 jl          main+50h (07FF73B8D1050h)  
 vmovss      xmm1,dword ptr [rsp+2Ch]  
 mov         rcx,rdi  
 call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF73B8D20A8h)]  
 mov         rcx,rax  
 lea         rdx,[string "]" (07FF73B8D2248h)]  
 call        std::operator<<<std::char_traits<char> > (07FF73B8D10C0h)  
 lea         rdx,[std::endl<char,std::char_traits<char> > (07FF73B8D1290h)]  
 mov         rcx,rdi  
 call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF73B8D2070h)]  
    return 0;
}
 mov         rbx,qword ptr [rsp+58h]  
 xor         eax,eax  
 add         rsp,40h  
 pop         rdi  
 ret  
サンプルコード2
//使い方
int main()
{
    Vec4b vb(1, 2, 3, 4);
    //1.fでノーマライズしたfloatにガンマ変換かけるときも、このくらいの記述量でかける
    auto vf = conv<Vec4f>(vb, [](const auto& v) {
        return pow(v / 255.f, 2.2f);
    });

    cout << vf << endl;

    //戻り値の型がvbと同じ場合、戻り値の型指定は省略可能。インデックスも渡せる。
    auto vb1 = convIdx(vb, [](const auto& v, int ch) {
        constexpr static int k[3] = { 3,4,5 };
        return v * k[ch];
    });

    cout << vb1 << endl;

    //要素数が違う型にも変換できる。
    auto vb2 = conv<Vec3f>(vb, [](const auto& v) {
        return v * 0.5f;
    });

    cout << vb2 << endl;

    return 0;
}

結果2
[5.07705e-06, 2.3328e-05, 5.69218e-05, 0.000107187]
[3, 8, 15, 0]
[0.5, 1, 1.5]

補足

convという関数名は一般的すぎるので、実際に使うときは、namespaceの中にいれるか名前を変えたほうがよいと思います。あと、引数にintやuchar,floatなど、一要素にも対応できる関数も定義しておくと、いろいろ便利かもしれません。
なお、ソースは無保証でソースを使用したことによって発生した損害に対して、責任を負いません。

5
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4