tl;dr
カスタムで高速な画像処理を実装したいときは、C++とOpenCV(cv::Mat)を使って実装したいはず。cv::Matの画素はVec型だけど、この型の変換処理をラムダ式を使って簡潔に書ける小ネタ的な関数について備忘録も兼ねて紹介記事を書きます。型変換も同時に行うとき便利。一度、関数を作っておけば記述量も減らせる。ターゲットはC++17。Visual Studio 2017 15.9.3,OpenCV4.0で動作確認。
追記:2019/4/10に続編を書きました。forEachとの連携です。
コード
細かい説明よりも、まずコードから。
#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;
}
[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演算してくれるみたい。
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
//使い方
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;
}
[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など、一要素にも対応できる関数も定義しておくと、いろいろ便利かもしれません。
なお、ソースは無保証でソースを使用したことによって発生した損害に対して、責任を負いません。