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

この記事の要点一覧
| 見たいこと | 要点 |
|---|---|
| 何をしているか | 四隅の行き先を決めると、平面が台形に伸び縮みする |
| コードと結果 | 下のスクリプトを、図と同じフォルダ構成で動かす |
| フォルダ | スクリプトと同じ階層に source_pics と save_pics を置く |
| 座標 | 左上が [0, 0]。点の順は左上→左下→右下→右上 |
| 残りの行 | 読み込み、サイズ、行列、変形、保存 |
| 出力サイズ | 第三引数より外へ出た画素は切れる |
| 手元の画像 | 点の順と出力サイズだけ、写したい矩形に合わせる |
| カメラ位置の制約 | 斜めからでも、実物の平面の四隅を合わせれば矩形のデータが取れる |
射影変換では、四隅の行き先を決める
アフィン変換(warpAffine)は平行線を平行のまま動かします。射影変換(getPerspectiveTransform と warpPerspective)は、四隅を独立に動かせるので、手前が広く奥が狭い台形にもできます。名刺を真上から見た矩形に直す、床面を俯瞰する、といった用途がこの関数の側です。
入力の四隅と、出力の四隅を同じ順で渡します。行列の中身の数式はこの記事では追いません。四隅がどこへ行くかだけ押さえれば、この関数は使えます。
コードと、処理の前後
先に動く形を置きます。次の節から、座標と残りの行を追います。
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.py からの相対です。読み込む画像は source_pics/test_1.png、書き出す画像は save_pics/perspective_image.png です。自分のファイルを使うときは、名前を合わせるか、パスの文字列を変えます。
imread が失敗すると image は None になり、その直後の image.shape で止まります。動かす前に、フォルダ名のスペルと、実行しているカレントディレクトリを確認してください。
座標は左上を [0, 0] にする
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_points | target_points |
|---|---|---|
| 左上 | [0, 0] | [200, 0] |
| 左下 | [0, height] | [0, 600] |
| 右下 | [width, height] | [600, 600] |
| 右上 | [width, 0] | [400, 0] |
この例の画像は 600×600 なので、height と width はどちらも 600 です。数値を入れると次のようになります。
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]へ移っています。](https://toolcluster.app/wp-content/uploads/2026/08/opencv-perspective-coords.png)
上の二つの点が内側へ寄るので、上辺が短くなります。下の二つの点は動いていません。数値を変えれば、同じ四隅対応で別の台形になります。
点の順を入力と出力で揃えないと、ねじれた対応になります。左上→左下→右下→右上で両方を書いてください。数学の y 軸(上がプラス)で点を取ると、上下が逆に見えます。
残りの行がしていること
モジュールの読み込み
import numpy as np
import cv2
数値の配列は NumPy、画像の読み書きと変形は OpenCV の cv2 です。
画像の読み込み
image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)
cv2.imread は、実行ファイルと同じ階層の source_pics にある test_1.png をカラーで読み、変数 image に入れます。
画像のサイズ
height, width, channels = image.shape[:3]
shape から縦・横・チャネル数を取り出します。この例ではチャネルは使いませんが、取り出す個数と左辺の変数の個数が違うとエラーになるので、三つとも受けています。
情報を入れる変数の数と、取り出せる情報の数を揃えます。
変換行列のための座標
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] なので、配列の中身は次のとおりです。
source_points = [[ 0. 0.]
[ 0. 600.]
[600. 600.]
[600. 0.]]
target_points = [[200. 0.]
[ 0. 600.]
[600. 600.]
[400. 0.]]
図で示した座標が、この二つの変数に入っています。この対応を基に画像を動かします。
変換行列
mat = cv2.getPerspectiveTransform(source_points, target_points)
cv2.getPerspectiveTransform が、入力の四隅と出力の四隅から変換行列を計算します。この例では次の値になります。
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]]
行列の各成分の意味は、ここでは使いません。四隅の行き先が意図どおりなら、この戻り値を次の行へ渡せば足ります。
変形
perspective_image = cv2.warpPerspective(image, mat, (width, height))
cv2.warpPerspective に、元画像、変換行列、出力の幅と高さを渡します。例では辺を縮めているだけなので、出力サイズは元画像と同じ (width, height) にしています。実際のサイズは、変換後の座標を見て決めてください。
保存
cv2.imwrite("save_pics/perspective_image.png", perspective_image)
cv2.imwrite が、実行ファイルと同じ階層の save_pics に perspective_image.png を書き出します。フォルダが無いと失敗するので、先に作っておきます。
出力サイズが足りないと、はみ出した分が切れる
出力を 600×600 のまま、次のような点を取ると、広がった上がキャンバスの外へ出ます。
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の外へ出て、上が切れています。](https://toolcluster.app/wp-content/uploads/2026/08/opencv-perspective-output-crop.png)
第三引数は「変換後の世界の大きさ」ではなく、「書き出す画像の大きさ」です。点がその矩形の外へ行くと、その画素は残りません。広げたいときは、出力サイズを先に大きくするか、すべての点が収まるように target を取り直します。
target の x や y が出力幅・高さを超えているのに、第三引数を元画像のままにすると、切れます。切れたように見えたら、まず点と出力サイズを見比べてください。
同じ手順で、手元の画像にも使える
四隅の順さえ揃えておけば、格子以外の写真でも同じ二つの関数です。target の数値と出力サイズだけ、写したい矩形に合わせて変えます。
装置のカメラが斜めでも、四隅を合わせればデータは取れる
ここまででやったことは、入力の四隅を出力の四隅へ対応づけ、変換行列を求め、出力キャンバスへ書き出す、というシンプルな流れです。格子の例でも、手元の写真でも、関数は同じです。現場で変わるのは、点の取り方と出力サイズだけです。
カメラ位置が構造で決まっているとき
設備や検査装置では、カメラを対象の真上に置けないことがよくあります。カバー、配管、安全柵、ロボットの可動域が先に決まっていて、レンズの位置はあとから押し込められる側です。斜めから撮った画像をそのまま測ったり読んだりすると、手前が広く奥が狭い台形のまま、長さや位置が歪みます。
ここで四隅の対応を取れば、架台を切り直さなくても、欲しい平面の矩形へ戻せます。カメラ位置が構造的に制約を受けていても、データ収集は成立します。工夫の本体は、点を「画面の四隅」ではなく、実物の平面の四隅に置くことです。ワークの角、治具の基準穴、床の継ぎ目など、あとから同じ場所を指せる点が取れれば、この記事の手順はそのまま使えます。
自分で全部を書き起こさなくてよい
OpenCV は、そのための一つの実装です。NumPy で点を並べ、OpenCV の機能である getPerspectiveTransform と warpPerspective に渡す形は、自分で数式から全部を書く代わりに、維持されている実装を使う、というやり方です。そのやり方は OpenCV に限りません。画像処理、数値計算、通信、制御のまわりには、同じ仕事を支える実装がすでにあり、それを直して版を上げている人たちがいます。射影の数式から自分で全部を書き起こすより、まずその実装を読んで使う方が、装置の制約に早く戻れます。
使うだけでも足ります。使っていて穴や分かりにくさに当たったら、報告する、文書を直す、パッチを出す、という形で返すこともできます。返せる量は人によって違います。ゼロでも、次の版を待って使うこと自体が、その仕事を続けさせる側にいます。道具を自分の装置に合わせて使い、必要なら少し返す。その繰り返しの方が、全部を内製して抱え込むより、エンジニアとしての時間の使い方がよくなります。
斜めのカメラでも、四隅さえ取れれば矩形のデータは作れます。あとは、その矩形の上で測る、読む、残す、という本来の仕事です。

コメントを残す