OpenCVで射影変換する方法を画像と座標で解説

OpenCVで射影変換する方法を画像と座標で解説

執筆者:

カテゴリ:

Python の OpenCV で、画像の四隅を別の四隅へ移す射影変換をします。この記事を大まかに説明すると、getPerspectiveTransform で変換行列を求め、warpPerspective で画像を変形します。例は 600×600 の格子と人物シルエットです。

OpenCVの射影変換の結果を、格子と人物シルエットの変形を基準に示した図です。四隅の対応で平面が台形や平行四辺形に変わることを説明しています。画像では、正対・せん断・手前が広い台形・右へ遠ざかる台形の四つが並んでいます。
同じ入力を、四隅の行き先だけ変えた例

この記事の要点一覧

見たいこと要点
何をしているか四隅の行き先を決めると、平面が台形に伸び縮みする
コードと結果下のスクリプトを、図と同じフォルダ構成で動かす
フォルダスクリプトと同じ階層に source_pics と save_pics を置く
座標左上が [0, 0]。点の順は左上→左下→右下→右上
残りの行読み込み、サイズ、行列、変形、保存
出力サイズ第三引数より外へ出た画素は切れる
手元の画像点の順と出力サイズだけ、写したい矩形に合わせる
カメラ位置の制約斜めからでも、実物の平面の四隅を合わせれば矩形のデータが取れる

射影変換では、四隅の行き先を決める

アフィン変換(warpAffine)は平行線を平行のまま動かします。射影変換(getPerspectiveTransformwarpPerspective)は、四隅を独立に動かせるので、手前が広く奥が狭い台形にもできます。名刺を真上から見た矩形に直す、床面を俯瞰する、といった用途がこの関数の側です。

入力の四隅と、出力の四隅を同じ順で渡します。行列の中身の数式はこの記事では追いません。四隅がどこへ行くかだけ押さえれば、この関数は使えます。

コードと、処理の前後

先に動く形を置きます。次の節から、座標と残りの行を追います。

test_1.py
import numpy as np
import cv2

image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)

height, width, channels = image.shape[:3]

source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

mat = cv2.getPerspectiveTransform(source_points, target_points)

perspective_image = cv2.warpPerspective(image, mat, (width, height))
cv2.imwrite("save_pics/perspective_image.png", perspective_image)
射影変換の処理前後を、入力ファイルと出力ファイルの対比を基準に示した図です。四隅を寄せると上が狭く下が広い台形になることを説明しています。画像では、左が正対のtest_1.png、右が上が狭いperspective_image.pngです。
処理前の test_1.png と、処理後の perspective_image.png

この例では上辺を内側へ寄せているので、上が狭く下が広い台形になります。黒い余白は、出力キャンバスのうち変換後の四角形の外です。

フォルダはスクリプトと同じ階層に置く

実行に使うフォルダ構成を、スクリプトと入出力フォルダの位置を基準に示した図です。読み込み元と保存先をコードのパスと対応づけることを説明しています。画像では、test_1.pyの横にsource_picsとsave_picsが並んでいます。
test_1.py と同じ階層に、読み込み用と保存用のフォルダを置く

コードのパスは、実行する test_1.py からの相対です。読み込む画像は source_pics/test_1.png、書き出す画像は save_pics/perspective_image.png です。自分のファイルを使うときは、名前を合わせるか、パスの文字列を変えます。

💡 Tip

imread が失敗すると imageNone になり、その直後の image.shape で止まります。動かす前に、フォルダ名のスペルと、実行しているカレントディレクトリを確認してください。

座標は左上を [0, 0] にする

Python
source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

配列の中身は、次の順です。グラフ用紙のように下が原点ではありません。

source_pointstarget_points
左上[0, 0][200, 0]
左下[0, height][0, 600]
右下[width, height][600, 600]
右上[width, 0][400, 0]

この例の画像は 600×600 なので、heightwidth はどちらも 600 です。数値を入れると次のようになります。

Python
source_points = np.array([[0, 0], [0, 600], [600, 600], [600, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)
射影変換の四隅対応を、source_pointsとtarget_pointsの座標を基準に示した図です。左上原点で上辺を内側へ寄せると上が狭くなることを説明しています。画像では、[0,0]と[600,0]が[200,0]と[400,0]へ移っています。
上辺を内側へ寄せると、上が狭い台形になる

上の二つの点が内側へ寄るので、上辺が短くなります。下の二つの点は動いていません。数値を変えれば、同じ四隅対応で別の台形になります。

⚠️ よくある落とし穴

点の順を入力と出力で揃えないと、ねじれた対応になります。左上→左下→右下→右上で両方を書いてください。数学の y 軸(上がプラス)で点を取ると、上下が逆に見えます。

残りの行がしていること

モジュールの読み込み

Python
import numpy as np
import cv2

数値の配列は NumPy、画像の読み書きと変形は OpenCV の cv2 です。

画像の読み込み

Python
image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)

cv2.imread は、実行ファイルと同じ階層の source_pics にある test_1.png をカラーで読み、変数 image に入れます。

画像のサイズ

Python
height, width, channels = image.shape[:3]

shape から縦・横・チャネル数を取り出します。この例ではチャネルは使いませんが、取り出す個数と左辺の変数の個数が違うとエラーになるので、三つとも受けています。

💡 Tip

情報を入れる変数の数と、取り出せる情報の数を揃えます。

変換行列のための座標

Python
source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

それぞれ 4 点の XY です。この例の元画像では [height, width][600, 600] なので、配列の中身は次のとおりです。

Python
source_points =  [[  0.   0.]
 [  0. 600.]
 [600. 600.]
 [600.   0.]]
target_points =  [[200.   0.]
 [  0. 600.]
 [600. 600.]
 [400.   0.]]

図で示した座標が、この二つの変数に入っています。この対応を基に画像を動かします。

変換行列

Python
mat = cv2.getPerspectiveTransform(source_points, target_points)

cv2.getPerspectiveTransform が、入力の四隅と出力の四隅から変換行列を計算します。この例では次の値になります。

Python
mat =  [[ 3.33333333e-01 -3.33333333e-01  2.00000000e+02]
 [ 0.00000000e+00  3.33333333e-01  0.00000000e+00]
 [ 0.00000000e+00 -1.11111111e-03  1.00000000e+00]]

行列の各成分の意味は、ここでは使いません。四隅の行き先が意図どおりなら、この戻り値を次の行へ渡せば足ります。

変形

Python
perspective_image = cv2.warpPerspective(image, mat, (width, height))

cv2.warpPerspective に、元画像、変換行列、出力の幅と高さを渡します。例では辺を縮めているだけなので、出力サイズは元画像と同じ (width, height) にしています。実際のサイズは、変換後の座標を見て決めてください。

保存

Python
cv2.imwrite("save_pics/perspective_image.png", perspective_image)

cv2.imwrite が、実行ファイルと同じ階層の save_picsperspective_image.png を書き出します。フォルダが無いと失敗するので、先に作っておきます。

出力サイズが足りないと、はみ出した分が切れる

出力を 600×600 のまま、次のような点を取ると、広がった上がキャンバスの外へ出ます。

Python
source_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)
target_points = np.array([[0, 0], [0, 600], [600, 600], [600, 0]], dtype=np.float32)
出力サイズが足りないときの切れ方を、変換後の点が枠の外へ出る様子を基準に示した図です。warpPerspectiveの第三引数が画面の切り出しになることを説明しています。画像では、元の[0,0]と[600,0]が600×600の外へ出て、上が切れています。
出力が 600×600 のままだと、はみ出した上が切れる

第三引数は「変換後の世界の大きさ」ではなく、「書き出す画像の大きさ」です。点がその矩形の外へ行くと、その画素は残りません。広げたいときは、出力サイズを先に大きくするか、すべての点が収まるように target を取り直します。

⚠️ よくある落とし穴

target の x や y が出力幅・高さを超えているのに、第三引数を元画像のままにすると、切れます。切れたように見えたら、まず点と出力サイズを見比べてください。

同じ手順で、手元の画像にも使える

四隅の順さえ揃えておけば、格子以外の写真でも同じ二つの関数です。target の数値と出力サイズだけ、写したい矩形に合わせて変えます。

装置のカメラが斜めでも、四隅を合わせればデータは取れる

ここまででやったことは、入力の四隅を出力の四隅へ対応づけ、変換行列を求め、出力キャンバスへ書き出す、というシンプルな流れです。格子の例でも、手元の写真でも、関数は同じです。現場で変わるのは、点の取り方と出力サイズだけです。

カメラ位置が構造で決まっているとき

設備や検査装置では、カメラを対象の真上に置けないことがよくあります。カバー、配管、安全柵、ロボットの可動域が先に決まっていて、レンズの位置はあとから押し込められる側です。斜めから撮った画像をそのまま測ったり読んだりすると、手前が広く奥が狭い台形のまま、長さや位置が歪みます。

ここで四隅の対応を取れば、架台を切り直さなくても、欲しい平面の矩形へ戻せます。カメラ位置が構造的に制約を受けていても、データ収集は成立します。工夫の本体は、点を「画面の四隅」ではなく、実物の平面の四隅に置くことです。ワークの角、治具の基準穴、床の継ぎ目など、あとから同じ場所を指せる点が取れれば、この記事の手順はそのまま使えます。

自分で全部を書き起こさなくてよい

OpenCV は、そのための一つの実装です。NumPy で点を並べ、OpenCV の機能である getPerspectiveTransformwarpPerspective に渡す形は、自分で数式から全部を書く代わりに、維持されている実装を使う、というやり方です。そのやり方は OpenCV に限りません。画像処理、数値計算、通信、制御のまわりには、同じ仕事を支える実装がすでにあり、それを直して版を上げている人たちがいます。射影の数式から自分で全部を書き起こすより、まずその実装を読んで使う方が、装置の制約に早く戻れます。

使うだけでも足ります。使っていて穴や分かりにくさに当たったら、報告する、文書を直す、パッチを出す、という形で返すこともできます。返せる量は人によって違います。ゼロでも、次の版を待って使うこと自体が、その仕事を続けさせる側にいます。道具を自分の装置に合わせて使い、必要なら少し返す。その繰り返しの方が、全部を内製して抱え込むより、エンジニアとしての時間の使い方がよくなります。

斜めのカメラでも、四隅さえ取れれば矩形のデータは作れます。あとは、その矩形の上で測る、読む、残す、という本来の仕事です。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です