GpuMatとPtrStep
PtrStep
はcv::cuda::GpuMat
の持っているメモリ領域を参照するための軽量の型で、特にカーネルなどデバイス側の処理において便利です。GpuMat
そのものはデバイスコードで使用出来ないようなので、必然的にこのクラスを使用するかポインタで頑張るかのどちらかを選択する事になるでしょう・・・
PtrStep
は便利なのですがリファレンスが無く何ができるのかよくわかりません。ソースコードを見れば自明なのですが、一々見に行くのもめんどいので記録しておきます。
※ 以下の記述はOpenCV 4.5時点のソースを参考にしています。
cv::cuda::PtrStep
使うときは<opencv2/core/cuda.hpp>
をインクルードすれば使えます。つまり、cv::cuda::GpuMat
を使ってるときは自動で使えるようになってるはずです。実体は<opencv2/core/cuda_types.hpp>
にあります。
PtrStep
はクラステンプレートであり、参照するメモリ領域の要素の型T
を受け取ります。
#include <opencv2/core/cuda.hpp>
using cv::cuda::PtrStep;
void f(int* p, std::size_t step) {
PtrStep<int> ps{p, step};
}
このように構築した場合、p
の指す領域を横幅step
[byte]の2次元でint
型の領域として参照します。
PtrStep<T>
は以下の操作が可能です。
操作 | 説明 |
const オーバーロード |
---|---|---|
T& operator()(int y, int x) |
参照する領域の(x, y) 位置の要素を取得する |
戻り値型がconst T& になる |
T* ptr(int y = 0) |
y 行目の先頭ポインタを取得する |
戻り値型がconst T* になる |
size_t elemSize() |
sizeof(T) を取得する |
あり、変化なし |
operator T*() |
参照する領域ポインタへの暗黙変換 | 戻り値型がconst T* になる |
PtrStep() |
デフォルトコンストラクタ | - |
PtrStep(T* data_, size_t step_) |
data_ の領域を横幅step_ の二次元領域として参照する |
- |
まあ専ら使用するのはoperator()
と2つ目のコンストラクタでしょう。
#include <opencv2/core/cuda.hpp>
using cv::cuda::PtrStep;
__global__ void kernel(PtrStep<float> ps) {
const int x = ...;
const int y = ...;
// 読み出し
int v = ps(y, x);
// 書き込み
ps(y, x) = 10;
// 3行目の先頭ポインタを取得
int* pl = ps.ptr(3);
// デフォルト構築可能
PtrStep<int> psi{};
// これは出来ない(Tが異なる)
psi = ps;
// これはできる
PtrStep<float> ps2 = ps;
}
これらのメンバ関数は全て、ホスト側でもデバイス側でも実行可能です。
また、メンバ変数が公開されており直接アクセス可能です。とはいえこれは知らなかった事にしてアクセスしないことをお勧めします。書き換えると危険なので・・・
変数名 | 説明 |
---|---|
T* data |
参照する領域のポインタ |
size_t step |
参照する二次元領域の横幅 [byte] |
テンプレートパラメータT
についての制約は何もないので、どんな型を指定することもできます。例えばfloat2
とかuint2
とかを指定することもできます。
PtrStepの構造
PtrStep
は単純には次のような構造体です(これは例示のための単純化であり正確ではありません)。
template<typename T>
struct PtrStep {
T* data;
size_t step;
// 略
};
このことから分かるようにsizeof(PtrStep<T>)
はT
によらずに決まり、ほとんどの環境では16バイトになるはずです。
このように、PtrStep
はとても単純なクラスであり、コピーコンストラクタは宣言されていないので、常にTrivially Copyableです。一方、デフォルトコンストラクタがdefault
ではないのでTrivialな型ではなく、実際はメンバがあるクラスを継承しているためStandard Layoutでもありません。
このことは型T
とは無関係に決まります。
template<typename T>
void f() {
// ok
static_assert(std::is_trivially_copyable<PtrStep<T>>::value, "");
// ng
static_assert(std::is_trivial<PtrStep<T>>::value, "");
// ng
static_assert(std::is_standard_layout<PtrStep<T>>::value, "");
}
Trivially Copyableでありサイズがポインタ2つ分程度にしかならないので、PtrStep
は参照渡しではなく値渡しでやり取りする事が推奨されます。
GpuMat -> PtrStep
PtrStep
は主にGpuMat
の領域を参照するのに用いられます。そのため、GpuMat
の側ではPtrStep
への暗黙変換を用意しています。
#include <opencv2/core/cuda.hpp>
using cv::cuda::PtrStep;
__global__ void kernel(PtrStep<unsigned char> ps);
void kernel_caller(const cv::cuda::GpuMat &img) {
// 特に何もせずそのまま渡せる
kernel<<<1, 1>>>(img);
}
(値渡しをする場合は)これがあるので、PtrStep
をどう構築するか(できるか)と言うのは忘れても良いでしょう。GpuMat
によらないメモリ領域を参照する時に思い出せば良いです。
注意点としては、GpuMat
が実際に確保している型と異なる型によるPtrStep
に変換するときでも特に警告やアサートはされない点です。
#include <opencv2/core/cuda.hpp>
using cv::cuda::PtrStep;
// 要素型をfloatとして領域を参照する
__global__ void kernel(PtrStep<float> ps);
void kernel_caller(int width, int height) {
// unsigned charの領域を確保
cv::cuda::GpuMat img{cv::Size(width, height), CV_8UC1};
// 警告も実行時アサートもない
kernel<<<1, 1>>>(img);
}
これはある意味で便利ではありますが、そこそこ危険です。特にこのように型のサイズが異なる場合に、バッファオーバーランの恐れがあります。
なお、変換先と元の要素型がStrict Aliasing Ruleに沿わない型同士の場合未定義動作になります。nvccがそれをどうするのかは知りませんが・・・
入力と出力を明示する
前項の危険性を有効に利用すると、PtrStep
による参照を入力用と出力用に型レベルで区別可能とする事ができます。
#include <opencv2/core/cuda.hpp>
using cv::cuda::PtrStep;
// 要素型をfloatとして領域を参照する
__global__ void kernel(PtrStep<const uchar> in, PtrStep<uchar> out) {
const int x = ...;
const int y = ...;
// コンパイルエラー、書き込み不可
in(y, x) = 0;
// ok
const auto pix = in(y, x);
// ok
out(y, x) = pix;
}
void kernel_caller(const cv::cuda::GpuMat& in_img, cv::cuda::GpuMat& out_img) {
kernel<<<1, 1>>>(in_img, out_img);
}
このように、入力用のPtrStep<T>
をPtrStep<const T>
のように宣言しておく事で、その参照する領域に書き込みをしないことを明示し、書き込まれた場合にコンパイルエラーとなるようにする事ができます。
この場合は何ら未定義動作を踏まないはずです。
とはいえ、メンバ関数がきちんとconst
修飾されているため、PtrStep
自身をconst
にしても同様になります。
// とはいえこっちでも同じになるはず・・・
__global__ void kernel(const PtrStep<uchar> in, PtrStep<uchar> out) {
const int x = ...;
const int y = ...;
// コンパイルエラー、書き込み不可
in(y, x) = 0;
// ok
const auto pix = in(y, x);
// ok
out(y, x) = pix;
}
PtrStep
は巷でview
とかspan
とか呼ばれるもののため、それ自身のconst
性と参照先のconst
性が同期する事に違和感を覚える人もいるかもしれません(そんな人いるんですか?)。そんな場合はテンプレートパラメータのT
をconst
にすると良いでしょうか・・・?
PtrStepファミリ
PtrStep
の定義されているcuda_types.hpp
には、親族とも言うべきクラスがいくつか定義されています。それらは継承関係を持ち、実はPtrStep
もそこに組み込まれています。
// ポインタのラッパ
template<typename T>
struct DevPtr {
T* data;
};
// ポインタとステップ(横幅)のラッパ
template<typename T>
struct PtrStep : public DevPtr<T> {
size_t step;
};
// ポインタとステップと領域サイズ(width × height)のラッパ
template<typename T>
struct PtrStepSz : public PtrStep<T> {
int cols;
int rows;
};
// ポインタと領域サイズのラッパ
template<typename T>
struct PtrSz : public DevPtr<T> {
size_t size;
template <typename U>
explicit PtrStepSz(const PtrStepSz<U>& d);
};
DevPtr
は他の3つの定義を簡略化するための共通部分なので直接使用することはないでしょう。
PtrStepSz
とPtrSz
は両方とも、参照領域のポインタとその領域のサイズを持ちますが、PtrStep
を経由しない分PtrSz
の方がサイズが小さくなります。PtrStepSz
はサイズが大きくなりますが領域の縦横サイズを持っており、PtrStep
でできることは全てできます。
ほとんどの環境では、sizeof(PtrStepSz<T>) == 24
、sizeof(PtrSz<T>) == 16
となるはずです。PtrStepSz
は値渡しと参照渡しの損益分岐点の微妙な所にありますが、多くの場合は値渡しで問題ないでしょう。
PtrStep
で可能な操作のうち次の2つはDevPtr
で定義されているため、この2つはPtrSz
でも使用可能です。
操作 | 説明 |
const オーバーロード |
---|---|---|
size_t elemSize() |
sizeof(T) を取得する |
あり、変化なし |
operator T*() |
参照する領域ポインタへの暗黙変換 | 戻り値型がconst T* になる |
PtrStepSz<T>
はcols, rows
というint
型の二つのメンバをpublic
に持つ事に加えて、T* -> U*
に変換可能なPtrStepSz<U>
からの変換コピーコンストラクタを備えています。ただ、このコンストラクタは特に制約されていないのに加えて、初期化時にCスタイルキャストを使用するために多分なんでも変換できます・・・
GpuMat
はPtrStepSz
に対する暗黙変換も備えているため、カーネルに渡すときなどはそのまま渡す事ができます。
PtrStepSz
はカーネル中で領域アクセスに加えて画像の縦横サイズを参照したい時にPtrStep
の代わりに使用すると良いでしょうか。PtrSz
はポインタとその領域サイズのペアを単に持ち運ぶ時に活用できそうです。
エイリアス
PtrStep
とPtrStepSz
には、よく使うであろう型についてエイリアスが用意されています。
typedef PtrStepSz<unsigned char> PtrStepSzb;
typedef PtrStepSz<unsigned short> PtrStepSzus;
typedef PtrStepSz<float> PtrStepSzf;
typedef PtrStepSz<int> PtrStepSzi;
typedef PtrStep<unsigned char> PtrStepb;
typedef PtrStep<unsigned short> PtrStepus;
typedef PtrStep<float> PtrStepf;
typedef PtrStep<int> PtrStepi;
接尾辞に慣れないと便利かは微妙ですが、引数宣言が長くなる時などに使ってみると良いかもしれません。
Discussion