OpenCVのremapで変換後の画像の座標に対して元の画像での座標を得る方法について

# OpenCVのremapで変換後の画像の座標に対して元の画像での座標を得る方法について

## 前置き

`OpenCV`の`remap`関数について、たまに使うのですが使うたびに思い出せなくなる処理があるので、自分の頭の整理も兼ねて分かりやすい形にして置いておきます。また、今回はC++の文法で書いてますが、Pythonでも全く同じです。

## 問題設定

設定として、`image`画像を`map_x`と`map_y`を用いて`target`画像に変換するとします。そこから`target`内の点（例えば特徴点）をいくつか取得したとして、その`x`座標の列を`target_x`、`y`座標の列`target_y`とします。今回の目標は、`target_x`および`target_y`に対応する`image`側の座標であるところの`image_x`と`image_y`の取得です。

## 結論と数学的根拠

結論としては以下になります（些末な宣言、型変換、フラグの処理の記述などは省略しています）。

```cpp
cv::remap(map_x, image_x, target_x, target_y);
cv::remap(map_y, image_y, target_x, target_y);
```

まず、`image`画像を`map_x`と`map_y`を用いて`target`画像に変換するという処理は`cv::remap(image, target, map_x, map_y)`と書けます。
これを配列を「座標を入れると値を返す関数」として捉えて数学的に記述すると、

$$\mathrm{target}(i, j) := \mathrm{image}(\mathrm{map\_y}(i, j), \mathrm{map\_x}(i, j))$$

として`target`を定義していると考えられます。なお、正しくは線形補間などの処理が入っているため厳密には異なるのですが、今回は抽象的に捉えるためにその辺りは捨象します。

先程の結論も同様に数学的に記述すると、

$$
\begin{aligned}
\mathrm{image\_x}(i) := \mathrm{map\_x}(\mathrm{target\_y}(i), \mathrm{target\_x}(i)) \\
\mathrm{image\_y}(i) := \mathrm{map\_y}(\mathrm{target\_y}(i), \mathrm{target\_x}(i))
\end{aligned}
$$

になります。

一般に`target`上の座標`(x1, y1)`と`image`上の座標`(x2, y2)`が対応していることは$x_2=\mathrm{map\_x}(y_1, x_1)$かつ$y_2=\mathrm{map\_y}(y_1, x_1)$と記述できるので、この時点で既に`(target_y[i], target_x[i])`が`(image_y[i], image_x[i])`と対応していると分かるのですが、念の為に以下の様な計算もできます。

$$
\begin{aligned}
& \mathrm{image}(\mathrm{image\_y}(i), \mathrm{image\_x}(i)) && \\
=& \mathrm{image}(\mathrm{map\_y}(\mathrm{target\_y}(i), \mathrm{target\_x}(i)), \mathrm{map\_x}(\mathrm{target\_y}(i), \mathrm{target\_x}(i))) && (\because \mathrm{image\_x}と\mathrm{image\_y}の定義) \\
=& \mathrm{target}(\mathrm{target\_y}(i), \mathrm{target\_x}(i)) && (\because \mathrm{target}の定義)
\end{aligned}
$$

これは`target`上の座標`(target_y[i], target_x[i])`に対応する`image`上の座標が`(image_y[i], image_x[i])`であることを表しています。

## 直感的な解説

先ほど配列は「座標を入れると値を返す関数」だと言いましたが、`map_x`と`map_y`については他の考え方として「変換**後**座標を参照すると変換**前**座標値が得られるテーブル」とも考えられます。そのイメージで`remap`関数の処理を言語化すると「**変換テーブル（x座標）と変換テーブル（y座標）をテーブルとして変換前画像の画素値を求める処理**」であると言えます。変換手続きは以下のようなイメージです。

![remap1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/865923/9d67e83e-5633-47e4-a584-a05c3c33a0a6.png)
![remap2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/865923/5daae2ca-06ab-4a02-9285-30d0268d677d.png)
![remap3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/865923/3f614b87-9d04-4f88-b090-fca1cbc97365.png)

これが`remap`関数による画像変換の基本となります。逆に言えば、**この様な処理であれば`remap`関数を用いて実装可能である**ということです。
また、`target_x`と`target_y`は整数値に対して変換**後**座標値を返す関数ですね。
そして求めたいのは`image_x`と`image_y`、つまり整数値に対して変換**前**座標値を返す関数です。

![remap4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/865923/21645699-3a87-4fb8-a499-bed356eb0930.png)

これは以下のような方法で求めることができます。

![remap5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/865923/56ccd346-f27f-4c6f-83fb-61d30cff8f74.png)
![remap6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/865923/8d2774cd-40ba-433a-a367-c94db65a79eb.png)

この処理は、「**変換後x座標列と変換後y座標列をテーブルとして変換テーブルの画素値を求めている**」と言えます。これは先述した`remap`関数の処理ほぼそのままですよね。図で見ても同じ様な処理を行っていることが分かると思います。そのままこの処理を`remap`関数を使って書いたものが以下の結論となる訳です。

```cpp
cv::remap(map_x, image_x, target_x, target_y);
```

## 最後に

実際は型変換だとか、リマップしたら座標が画像の外に行っただとか細かい調整が必要なようです。その辺りは自分はちゃんと確認できていないので、自分の裁量で変なことにならないように注意して実装してください。