OpenCVにおけるwarpAffine等の画像の幾何変換を行う際に必要になる、ソースの画像外の画素値の外挿です。いろいろな種類が用意されています。試してみたうえで、ほんの少しオリジナリティー出してみます。
borderMode
画像変換する際に出てくる引数で、warpAffine、warpPerspective、remapで渡すことができる、画像領域外の処理モードです。
ここによると、いろいろ並んでいますが、実質6つです。
BORDER_CONSTANT | iiiiii|abcdefgh|iiiiiii |
BORDER_REPLICATE | aaaaaa|abcdefgh|hhhhhhh |
BORDER_REFLECT | fedcba|abcdefgh|hgfedcb |
BORDER_WRAP | cdefgh|abcdefgh|abcdefg |
BORDER_REFLECT_101 | gfedcb|abcdefgh|gfedcba |
BORDER_TRANSPARENT | uvwxyz|abcdefgh|ijklmno |
このアルファベットの意味ですが、どうやら、画像が中心の[abcdefgh]の部分で、その外側をそれぞれのアルファベットで書かれた値で外挿するよ。という意味のようです。以下で詳しく見ていきます。
7つ目として
BORDER_ISOLATED do not look outside of ROI
もありますが、これは確実に画像内の値しか参照しない前提で用意されていると思われます。
BORDER_CONSTANT
デフォルトです。固定値で外挿です。iiiiii|abcdefgh|iiiiiiiとあるように、画像[abcdefgh]の外を[i]で埋めるよ。と言っています。borderValue引数でこのiにあたる値を指定できます。デフォルトは0です。RGBの画像の場合には、OpenCV特有のBGR順で(B, G, R)で指定します。
1 | out = cv2.warpAffine(img, mat, (w, h), borderMode = cv2.BORDER_CONSTANT, borderValue = ( 255 , 128 , 128 )) |

BORDER_REPLICATE
replicateとは複製するという意味です。aaaaaa|abcdefgh|hhhhhhhとあるように、画像エッジ部を引きずります。左側は左端の[a]という画素の複製、右側は右端の[h]という画素の複製になっています。
1 | out = cv2.warpAffine(img, mat, (w, h), borderMode = cv2.BORDER_REPLICATE) |

BORDER_REFLECT
反射です。fedcba|abcdefgh|hgfedcbとあるように、端部で画像[abcdefgh]が折り返っています。
1 | out = cv2.warpAffine(img, mat, (w, h), borderMode = cv2.BORDER_REFLECT) |

BORDER_WRAP
繰り返しです。cdefgh|abcdefgh|abcdefgとあるように、外側が画像[abcdefgh]の繰り返しになっています。
1 | out = cv2.warpAffine(img, mat, (w, h), borderMode = cv2.BORDER_WRAP) |

BORDER_REFLECT_101
これも反射ですが、微妙に違います。gfedcb|abcdefgh|gfedcbaとあるように、画像[abcdefgh]の端部[a]と[h]は反射していません。細かいですが、反射面が微妙に違います。(フィルタによる畳み込みなどではこちらがよい場合もありそうです。)ちなみにREFLECTと101の間の_(アンダースコア)は省略可能。
また謎なことに、BORDER_DEFAULTと指定してもこれが選ばれるようです。デフォルトじゃないのに…。
1 | out = cv2.warpAffine(img, mat, (w, h), borderMode = cv2.BORDER_REFLECT_101) |

BORDER_TRANSPARENT
透過です。引数dstで指定した画像が透けます。このdstのサイズが変換先のサイズと一致している必要があります。
1 | out = cv2.warpAffine(img, mat, (w, h), borderMode = cv2.BORDER_TRANSPARENT, dst = dst) |
また、引数のdstは参照で渡されます。なのでこのコードを呼んだ後、outとdstは同じものになります。注意が必要です。
1 2 | cv2.imwrite( "aa.jpg" , dst) cv2.imwrite( "bb.jpg" , out) |
も同じものが保存されます。

BORDER_ISOLATED
画像外を触らないことが前提です。試しに外を触るような変換処理をやってみたところ落ちました。外挿が起こらないような拡大で試したところ通りました。使い方は間違っていなそうです。
2000回の拡大処理でBORDER_REFLECT_101と時間比べしてみましたが、有意差でず。でかい画像で試してみても有意差でず。試し方が悪かったかなぁ…。
自作のborderMode
いろいろなモードがあるのですが、やはり違和感はぬぐえません。photoshopだとクールに「コンテンツに応じた塗りつぶし」というのがあります。これだと違和感なく外側も外挿してくれるのですが、そんなのOpenCVにはありません。
おそらく機械学習とかDeepLearningとかやってるんだと思いますが、そこまで頑張れないので、ほんのちょっぴりだけ外を推定して画像を広げてみます。
やり方としては画像の外周32画素だけを外側に拡大する作戦です。レンズ歪みに似た感じですね。なので画像が上下、左右に64画素だけ大きくなります。そのうえで回転や縮小をしてあげると外側は少し自然になる場合があります。

この画像だと少しいかつい感じに変換されてますけどね…。中心部分の画像には変化はないので、着目する部位が画像の中央部にある場合にはこの拡大で外を推定し、その後画像変換してあげれば、画像外がもう少し自然になります。
当然そんな外挿モードはありません。自分で書いてみます。
アフィン変換行列を場所を変えつつ適用する手法です。画像の外側と内側で異なるアフィン変換をして、結果を得ます。画像の外枠32画素は拡大、画像の内側は何もしない。という行列を作ります。そのうえでその行列群を画像と同じ大きさに広げ、remap関数を使って適用させます。
過去試した手法です。

上記のようなグリッドを入れた画像をソースとすると、

出力はこんな感じ。サンプルの画像が小さい(256×256)なので拡大部が際立っていますが、512×512でかけてみると、

こんな感じ。これくらい大きいと使い物になる気がします。512×512の画像が576×576へ、中心部は変化なく自然に拡大されています。
以下のコードで、アフィン行列の配列をresizeで線形補間して画像+αと同じ大きさに拡大しています。
1 | dst = cv2.resize(mat_array2, dsize = (w + 32 * 4 , h + 32 * 4 )) |
この場合、画像の大きさより128画素大きくなります。このresizeのエッジ部の補間がイマイチなので、少しターゲットの画像より大きいサイズへ拡大して四隅を切り落とします。
1 | dst = dst[ 32 : - 32 , 32 : - 32 , :] |
今回のコードは少し手抜きで、グリッドの大きさは32固定です。あと画像サイズが32の倍数でないとうまく行かないかもしれません。
コード
コードはこんな感じです。それほど複雑ではないと思います。追いかけてもらえればわかるかなと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import numpy as np import cv2 def enlarge(img): h, w = img.shape[: 2 ] # 内側無変化のためのアフィン行列(32画素画像中心に向かってシフト) mat1 = cv2.getRotationMatrix2D(( 0 , 0 ), 0 , 1 ) mat1[ 0 , 2 ] + = 32 mat1[ 1 , 2 ] + = 32 mat1 = cv2.invertAffineTransform(mat1) mat1 = mat1.reshape( 6 ) # 外側拡大のためのアフィン行列(幅高さ共に64画素拡大) mat2 = cv2.getRotationMatrix2D(( 0 , 0 ), 0 , 1 ) mat2[ 0 , 0 ] = (w + 64 ) / w mat2[ 1 , 1 ] = (h + 64 ) / h mat2 = cv2.invertAffineTransform(mat2) mat2 = mat2.reshape( 6 ) # グリッドサイズの計算 ws = w / / 64 hs = h / / 64 # グリッドサイズへ拡張 mat_array2 = np.full((hs + 1 , ws + 1 , 6 ), mat2) mat_array1 = np.full((hs - 1 , ws - 1 , 6 ), mat1) # 真ん中にはめ込む mat_array2[ 1 : - 1 , 1 : - 1 ] = mat_array1 # 画像サイズへ拡大 dst = cv2.resize(mat_array2, dsize = (w + 32 * 4 , h + 32 * 4 ), interpolation = cv2.INTER_LINEAR) dst = dst[ 32 : - 32 , 32 : - 32 , :] # 座標計算 X, Y = np.meshgrid(np.arange(w + 32 * 2 ), np.arange(h + 32 * 2 )) XX = X * dst[:, :, 0 ] + Y * dst[:, :, 1 ] + dst[:, :, 2 ] YY = X * dst[:, :, 3 ] + Y * dst[:, :, 4 ] + dst[:, :, 5 ] return cv2.remap(img, XX.astype( 'float32' ), YY.astype( 'float32' ), cv2.INTER_LINEAR) img = cv2.imread( "lena.jpg" ) out = enlarge(img) cv2.imwrite( "lena_out.jpg" , out) |
コメント