効率的な画像処理コードを簡潔に書くためのcv::Mat_の機能拡張　その１～３

# tl;dr
OpenCVのcv::Matやcv::Mat_は便利だけど、画像処理に使うには、もう少し機能がほしい。そこで、cv::Mat_を派生して、**(1)Offset値参照によるアドレス計算の省略機能**,**(2)画面外の画素の参照機能**,**(3)実数座標値による補完画素値の取得機能**を追加してみた。簡潔に記述できオーバーヘッドが少ないことがポイント。実験やプロトタイプ開発むけかも。 
ターゲットはC++17。Visual Studio 2017 15.9.3,OpenCV4.0で動作確認。
最初は、OpenCVや一般的な画像処理のノウハウを備忘録も兼ねて紹介することが目的だったが、割ときちんと書いたので、整備+テストコードも書いてOSSとして正式に公開してもいいのかも。

追記:2019/4/19に**(4)二次元アクセス可能な画素ポインタ**を[続編](https://qiita.com/drafts/e4465ec9d42a8a28ecca/edit)に追加。

# コード
まずは、今回の機能を定義したクラスと、使い方を示したmain関数を含むコードから。長くてすみません。解説を読んでから見返してください。

```c++:サンプルコード
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>


//Offset値格納クラス
struct Offset
{
	Offset(size_t _offset) : offset(_offset) {}
	//誤った利用を抑制するためにあえて戻り値はvoidにする
	void operator++() { ++offset; }
	void operator++(int) { offset++; }
	void operator+=(size_t v) { offset += v; }
	void operator-=(size_t v) { offset -= v; }

	Offset operator+(size_t v) {return Offset(offset + v);}
	Offset operator-(size_t v) {return Offset(offset - v);}

	size_t offset;
};

//拡張したMat_クラス
template<class T>
class Mtx_ : public cv::Mat_<T>
{
public:
	using cv::Mat_<T>::Mat_;
	using cv::Mat_<T>::operator();

	Offset calcOffset(int y, int x) const
	{
		return Offset(this->stepT(0) * y + x);
	}

	T& operator()(const Offset& offset)
	{
		return dataT()[offset.offset];
	}

	const T& operator()(const Offset& offset) const
	{
		return dataT()[offset.offset];
	}

	const T& operator()(int y, int x) const
	{
		return dataT()[y*this->stepT(0) + x];
	}

	T& operator()(int y, int x)
	{
		return dataT()[y*this->stepT(0) + x];
	}

	T operator()(float y, float x)
	{
		float fx0 = std::floor(x);
		float fy0 = std::floor(y);
		float fx1 = fx0 + 1.f;
		float fy1 = fy0 + 1.f;

		int hb = horzBorder();
		int vb = vertBorder();

		int x0 = static_cast<int>(fx0);
		if (x0 < -hb) x0 = -hb;
		if (x0 > this->cols + hb-2) x0 = this->cols + hb - 2;

		int y0 = static_cast<int>(fy0);
		if (y0 < -vb) y0 = -vb;
		if (y0 > this->rows + vb - 2) y0 = this->rows + vb - 2;

		int x1 = x0 + 1;
		int y1 = y0 + 1;

		T* p = (*this)[y0] + x0;
		T v00 = p[0];
		T v10 = p[this->stepT()];
		T v01 = p[1];
		T v11 = p[this->stepT()+1];

		float v0 = v00 * (fx1 - x) + v01 * (x - fx0);
		float v1 = v10 * (fx1 - x) + v11 * (x - fx0);
		return v0 * (fy1 - y) + v1 * (y - fy0);
	}

	T operator()(cv::Point2f pt)
	{
		return (*this)(pt.y, pt.x);
	}


	static Mtx_<T> createWithBorder(int _rows, int _cols, int vborder, int hborder = -1)
	{
      　　　　　 if (hborder < 0) hborder = vborder;
		cv::Mat_<T> mat(_rows + vborder * 2, _cols + hborder * 2);
		Mtx_<T> mtx2 = mat(cv::Rect(hborder, vborder, _cols, _rows));
		return 	mtx2;
	}

	static Mtx_<T> createWithBorder(cv::Size sz, int vborder,　int hborder = -1)
	{
      　　　　　if (hborder < 0) hborder = vborder;
               return createWithBorder(sz.height, sz.width, vborder,
                                          hborder);
	}

	int horzBorder() const {
		cv::Size sz;
		cv::Point pt;
		this->locateROI(sz, pt);
		return pt.x;
	};
	int vertBorder() const {
		cv::Size sz;
		cv::Point pt;
		this->locateROI(sz, pt);
		return pt.y;
	};

	void extrapolate()
	{
		const auto vborder = vertBorder();
		const auto hborder = horzBorder();
		//Left
		for (int y = 0; y < this->rows; y++) {
			auto pix = (*this)(y, 0);
			auto offset = calcOffset(y, -hborder);
			for (int dx = 0; dx < hborder; dx++) {
				(*this)(offset) = pix;
				offset++;
			}
		}
		//Right
		for (int y = 0; y < this->rows; y++) {
			auto pix = (*this)(y, this->cols - 1);
			auto offset = calcOffset(y, this->cols);
			for (int dx = 0; dx < hborder; dx++) {
				(*this)(offset) = pix;
				offset++;
			}
		}
		//Top
		for (int y = 0; y < vborder; y++) {
			auto offset_s = calcOffset(0, -hborder);
			auto offset_d = calcOffset(-y - 1, -hborder);
			for (int dx = 0; dx < this->cols + hborder * 2; dx++) {
				(*this)(offset_d) = (*this)(offset_s);
				offset_d++;
				offset_s++;
			}
		}
		//Bottom
		for (int y = 0; y < vborder; y++) {
			auto offset_s = calcOffset(this->rows - 1, -hborder);
			auto offset_d = calcOffset(this->rows, -hborder);
			for (int dx = 0; dx < this->cols + hborder * 2; dx++) {
				(*this)(offset_d) = (*this)(offset_s);
				offset_d++;
				offset_s++;
			}
		}
	}
private:
	const T* dataT() const { return reinterpret_cast<const T*>(this->data); }
	T* dataT() { return reinterpret_cast<T*>(this->data); }
};

using Mtx1b = Mtx_<uchar>;
using Mtx3b = Mtx_<cv::Vec3b>;
using Mtx4b = Mtx_<cv::Vec4b>;
using Mtx1i = Mtx_<int>;
using Mtx1w = Mtx_<unsigned short>;
using Mtx1f = Mtx_<float>;
using Mtx3f = Mtx_<cv::Vec4f>;
using Mtx4f = Mtx_<cv::Vec4f>;

template<class T>
void print(const Mtx_<T>& m)
{
	int vborder = m.vertBorder();
	int hborder = m.horzBorder();

	for (int y = -vborder; y < m.rows+vborder; y++) {
		for (int x = -hborder; x < m.cols+hborder; x++) {
			std::cout << m(y, x) << " ";
		}
		std::cout << std::endl;
	}
}


template<class MAT>
void fill_value(MAT& m)
{
	int i = 0;
	for (int y = 0; y < m.rows; y++) {
		for (int x = 0; x < m.cols; x++) {
			m(y, x) = i;
			i++;
		}
	}
}


//使い方
int main()
{
	using namespace std;

	//入力用データ生成
	Mtx1w mtx(10, 10);
	fill_value(mtx);

	//(1)Offset値参照によるアドレス再計算の省略機能
	cout << "(1)Offset値参照によるアドレス再計算の省略機能" << endl;
	Mtx1f mtxf(mtx.size());
	assert(mtxf.stepT() == mtx.stepT());
	for (int y = 0; y < mtx.rows; y++) {
		auto os = mtx.calcOffset(y, 0);
		for (int x = 0; x < mtx.cols; x++) {
			mtxf(os) = mtx(os);
			os++;
		}
	}
	cout << "mtx(1,1)=" << mtx(1, 1) << endl;
	cout << "mtxf(1,1)=" << mtxf(1, 1) << endl;
	cout << endl;

	//(2)画面外の画素値の参照
	cout << "(2)画面外の画素値の参照" << endl;
	auto mtx_ext = Mtx1i::createWithBorder(10, 10, 1, 2);
	fill_value(mtx_ext);
	mtx_ext.extrapolate();
	print(mtx_ext);

	cv::Mat_<int> mat = mtx_ext;//としても問題
	cout << "mtx_ext(0,0)=" << mtx_ext(0, 0) << endl;
	cout << "mat(0,0)=" << mat(0, 0) << endl;
	cout << endl;

	//(3)実数座標値による補完画素値の取得
	cout << "(3)実数座標値による補完画素値の取得" << endl;
	cout << "mtxf(0,0)=" << mtxf(0, 0) << endl;
	cout << "mtxf(0,1)=" << mtxf(0, 1) << endl;
	cout << "mtxf(1,0)=" << mtxf(1, 0) << endl;
	cout << "mtxf(1,1)=" << mtxf(1, 1) << endl;
	cout << "mtxf(0.5,0.5)=" << mtxf(0.5f, 0.5f) << endl;
	cout << "mtxf(0.9,0.4)=" << mtxf(0.9f, 0.4f) << endl;
	cout << "mtx(0.9,0.4)=" << mtx(0.9f, 0.4f) << endl;
}
```
# 結果
```console
(1)Offset値参照によるアドレス計算の省略機能
mtx(1,1)=11
mtxf(1,1)=11

(2)画面外の画素値の参照
0 0 0 1 2 3 4 5 6 7 8 9 9 9
0 0 0 1 2 3 4 5 6 7 8 9 9 9
10 10 10 11 12 13 14 15 16 17 18 19 19 19
20 20 20 21 22 23 24 25 26 27 28 29 29 29
30 30 30 31 32 33 34 35 36 37 38 39 39 39
40 40 40 41 42 43 44 45 46 47 48 49 49 49
50 50 50 51 52 53 54 55 56 57 58 59 59 59
60 60 60 61 62 63 64 65 66 67 68 69 69 69
70 70 70 71 72 73 74 75 76 77 78 79 79 79
80 80 80 81 82 83 84 85 86 87 88 89 89 89
90 90 90 91 92 93 94 95 96 97 98 99 99 99
90 90 90 91 92 93 94 95 96 97 98 99 99 99
mtx_ext(0,0)=0
mat(0,0)=0

(3)実数座標値による補完画素値の取得
mtxf(0,0)=0
mtxf(0,1)=1
mtxf(1,0)=10
mtxf(1,1)=11
mtxf(0.5,0.5)=5.5
mtxf(0.9,0.4)=9.4
mtx(0.9,0.4)=9
```

# 解説
実装は難しいことはしていないので使い方を中心に解説します。
## (1)Offset値参照によるアドレス再計算の省略機能
**[対象関数]calcOffset(int y, int x),operetor()(const Offset& offset)**
Offset値を計算するメソッドとOffset値を引数として画素値を返すオペレータを定義することによって、**２つ以上のMtx_型オブジェクトで、同一座標の画素値を参照するときに、y*step+xといったアドレッシングのための再計算を省略**できる。

```c++
			auto os = mtx.calcOffset(y, x);
			mtxf(os) = mtx(os);
			os++;
```
mtxfとmfxは、画素値の型がそれぞれfloatとunsigned shortと違う、つまりサイズが違うけど、このように、
画素値型が異なったMtx_型オブジェクトでも同じ座標は同じOffset値を使える。だだ、Mat_<T>::stepT()で参照されるメモリ確保された行の大きさが異なる場合は、当然使えないので、使う場合には注意が必要。

## (2)画面外の画素値の参照
**[対象関数]createWithBorder(...),operetor()(int y, int x),extrapolation(...)**  
カスタムなN×Nカーネルによるフィルタ処理を書く場合、擬似的に**画面外の画素(座標値が負、もしくは、幅、高さ以上で、コード上はborderと表現)**を参照したい場合がある。画面外の画素を参照できないと座標値をクリップする必要があり、参照ごとにクリップすると処理が重くなる。今回は**ROI機能を使って、borderを設定したMtx_型のオブジェクトを作成し、ペナルティなしで画面外を参照可能**とした。createWithBorder関数が静的メンバ関数になっているので、この関数でMtx_オブジェクトを作成。operator()(int y, int x)のx,yに負の数を設定するなどして、画面外の画素値も参照できる。注意が必要なのは、ROI機能を使っているので、ROIを変えてしまう関数を通すことはできないので注意が必要。

```C++
	auto mtx_ext = Mtx1i::createWithBorder(10, 10, 1, 2);
	mtx_ext(-1,-1) = 10;//書き込みOK
	cout << mtx_ext(-1,-1) << endl;//もちろん取得もOK

```
extrapolation関数で、画面外(border)をcopyモードで埋めることができる。ちなみに、Mtx_::extrapolation関数cv:copyMakeBorder関数との違いは、前者がメンバ関数で自身を拡張するのに対し、後者は新しいborder付きのMatオブジェクトを作る。なお、上記のコードではMatやMat_では実装されているデバッグモード時の座標値範囲チェックを実装していないので注意。逆にこのチェックのためにMatやMat_で、画面外の画素へのアクセスが不可能になっているともいえる。
　
## (3)実数座標値による補完画素値の取得 
**[対象関数]operetor()(float y, float x)** <br> 
**座標値をfloatで受け取り線形補間による画素値を返すことを可能**とした。

```c++
	cout << "mtxf(0.5,0.5)=" << mtxf(0.5f, 0.5f) << endl;
	cout << "mtxf(0.9,0.4)=" << mtxf(0.9f, 0.4f) << endl;
```
現段階では、値の取得だけで、書き込みには未対応。set関数を作ってもいいけど、せっかくだからoperatorで書きたい。効率が落ちるかもしれないが・・・。

# 補足
ソースは無保証でソースを使用したことによって発生した損害に対して、責任を負いません。