🙌

OpenCV の warpAffine と resize の違い

2024/01/08に公開

はじめに

FPGAなどで画像処理をしていると、例えば 1/2 縮小するのに2画素づつ平均を取って1画素にするという処理を縦横にそれぞれ行ってリサイズするなどと言う事はよくああります。

OpenCV で同じことをしようとして半分にするんだから

\left(\begin{array}{c} x'\\ y' \end{array}\right) =\left(\begin{array}{ccc} 0.5 & 0 & 0\\ 0 & 0.5 & 0 \end{array}\right) \left(\begin{array}{c} x \\ y \\ 1 \end{array}\right)

と、すればいいんだよねと、cv2.warpAffine なんか呼び出してみると結果が合わないことになります。

が、なぜか cv2.resize で 1/2 縮小すると所望の動きのように見えたりもします。

この原因について確認してみます。

warpAffine と resize の動きの違い

簡単の為に1次元にして 8x1 の画像を作って、横1/2縮小して 4x1 の画像を作って中を覗いてみます。

import cv2
import numpy as np

# 1/2倍に縮小してみる
src = np.arange(8).reshape(1, 8).astype(np.float32)
mat = np.array([
    [0.5, 0., 0],
    [0., 1., 0.],
])
a = cv2.warpAffine(src, mat, (4, 1))
r = cv2.resize(src, (4, 1))
print('source :', src)
print('affine :', a)
print('resize :', r)

結果は

source : [[0. 1. 2. 3. 4. 5. 6. 7.]]
affine : [[0. 2. 4. 6.]]
resize : [[0.5 2.5 4.5 6.5]]

となり、違いが分かります。

ちなみにここで拡大も試してみると

import cv2
import numpy as np

# 2倍に拡大してみる
src = np.arange(4).reshape(1, 4).astype(np.float32)
mat = np.array([
    [2.0, 0., 0.0],
    [0., 1., 0.0],
])
a = cv2.warpAffine(src, mat, (8, 1))
r = cv2.resize(src, (8, 1))
print('source :', src)
print('affine :', a)
print('resize :', r)

下記のようになり、値も違う上に何やらボーダーの処理にも違いがありそうです。

source : [[0. 1. 2. 3.]]
affine : [[0.  0.5 1.  1.5 2.  2.5 3.  1.5]]
resize : [[0.   0.25 0.75 1.25 1.75 2.25 2.75 3.  ]]

原点位置の違い?

画像座標の原点をどこに置くかと言うのはなかなか興味深い課題なのですが、しばし左上ピクセルの中央を (0, 0) と置く使い方をします。

この時の注意点は、下記のように単純に同じ画素を2回づつ繰り返して拡大するような場合、対応する原点が移動してしまう事です。

ピクセル中央原点

warpAffine などはどうやらこの座標系のように見受けられます。

一方でピクセルの左上を原点として扱う事も可能で、

ピクセル左上隅が原点

のように座標系を定義することも出来ます。
こうすると単純2回繰り返しのような拡大縮小で画素位置の対応は保たれるのですが、数式的には左上の画素の値は (0.5, 0.5) の位置の値という事になり、画素の値が整数位置に来ないというこれはこれで面倒な事が起こります。

resize の方はどうやらこちらの原点の位置の考え方に近いようで、原点中心に拡大縮小し、(0.5, 0.5) の位置の計算値を最初の画素としているようです。

同じになるようにしてみる

これまでの考えがあっているなら、warpAffine で原点のずれ分を補正してやれば resize と結果が揃うはずです。

resize にはボーダーの指定はありませんがどうやら同じ画素が繰り返されているように見受けられるので、BORDER_REPLICATE を指定して揃えてみます。

縮小後の原点と最初の原点の対応が -0.25 ずれるのでアフィン行列に組み込みます。

import cv2
import numpy as np

# 1/2倍に縮小してみる
src = np.arange(8).reshape(1, 8).astype(np.float32)
mat = np.array([
    [0.5, 0., -0.25],
    [0., 1., 0.],
])
a = cv2.warpAffine(src, mat, (4, 1), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
r = cv2.resize(src, (4, 1), interpolation=cv2.INTER_LINEAR)
print('source :', src)
print('affine :', a)
print('resize :', r)

結果は

source : [[0. 1. 2. 3. 4. 5. 6. 7.]]
affine : [[0.5 2.5 4.5 6.5]]
resize : [[0.5 2.5 4.5 6.5]]

となり同じになりました。

同様に拡大についても 0.5 のずれを組み込むと

import cv2
import numpy as np

# 2倍に拡大してみる
src = np.arange(4).reshape(1, 4).astype(np.float32)
mat = np.array([
    [2.0, 0., 0.5],
    [0., 1., 0.0],
])
a = cv2.warpAffine(src, mat, (8, 1), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
r = cv2.resize(src, (8, 1), interpolation=cv2.INTER_LINEAR)
print('source :', src)
print('affine :', a)
print('resize :', r)

結果が

source : [[0. 1. 2. 3.]]
affine : [[0.   0.25 0.75 1.25 1.75 2.25 2.75 3.  ]]
resize : [[0.   0.25 0.75 1.25 1.75 2.25 2.75 3.  ]]

のようにそろいました。

おわりに

OpenCV だけで作業しているとなかなか気が付きにくいのですが、SIMD命令を使ったり CUDA や FPGA など低レイヤーを自分で書く場合、「OpenCV と結果が合わない」などと言う事はよくあります。
案外座標系があってないなんてこともよくあるので参考になれば幸いです。

GitHubで編集を提案

Discussion