Python, OpenCVで三角形・四角形領域を変形して別画像に貼り付け
Python, OpenCVを使って、ある画像の任意の三角形または四角形領域を切り出して、別画像の任意の三角形または四角形領域に合わせて変形して貼り付ける処理(ワーピング)を行う。三角形領域に対してはアフィン変換、四角形領域に対しては射影変換を用いる。
アフィン変換でも射影変換でも処理の流れは同じ。
ここではまずアフィン変換を用いた三角形領域に対する以下のような処理を順番に説明する。
- 画像の読み込み、座標の決定
- バウンディングボックスで切り出し
- 切り出し画像に対してアフィン変換
- 切り出し画像をマスク処理して合成
最後に三角形領域に対する処理と四角形領域に対する処理をまとめて関数化したものを示す。
- まとめて関数化
アフィン変換や射影変換、マスク処理については以下の記事を参照。
なお、矩形領域を変形させずにそのまま別画像に貼り付けるのはNumPyのスライスを使うだけで実現できる。
画像の読み込み、座標の決定
以下のように2枚の画像(元画像src
, 貼り付け先画像dst
)を読み込み、適当に3点の座標を決定する。
import cv2
import numpy as np
src = cv2.imread('data/src/lena.jpg')
dst = cv2.imread('data/src/rocket.jpg')
src_pts = [[100, 80], [150, 200], [300, 20]]
dst_pts = [[280, 120], [320, 300], [400, 150]]
座標にマークを描画したものは以下の通り。
src_mark = src.copy()
for pt in src_pts:
cv2.drawMarker(src_mark, tuple(pt), (0, 255, 0), thickness=4)
cv2.imwrite('data/dst/opencv_warp_src_mark.jpg', src_mark)
dst_mark = dst.copy()
for pt in dst_pts:
cv2.drawMarker(dst_mark, tuple(pt), (0, 255, 0), thickness=4)
cv2.imwrite('data/dst/opencv_warp_dst_mark.jpg', dst_mark)
OpenCVの図形描画については以下の記事を参照。
バウンディングボックスで切り出し
3点の座標を含む最小の矩形領域(バウンディングボックス)で切り出す。
バウンディングボックスはcv2.boundingRect()
で取得できる。
データ型がnp.float32
のNumPy配列ndarray
を引数に指定する。返り値は(左上のx座標, 左上のy座標, 幅, 高さ)
のタプル。
src_pts_arr = np.array(src_pts, dtype=np.float32)
dst_pts_arr = np.array(dst_pts, dtype=np.float32)
src_rect = cv2.boundingRect(src_pts_arr)
dst_rect = cv2.boundingRect(dst_pts_arr)
print(src_rect)
# (100, 20, 201, 181)
print(dst_rect)
# (280, 120, 121, 181)
これをもとにスライスで画像を切り出す。
さらに次の処理のために3点の座標をバウンディングボックスの左上を原点(0, 0)
とした座標に変換する。
src_crop = src[src_rect[1]:src_rect[1] + src_rect[3], src_rect[0]:src_rect[0] + src_rect[2]]
dst_crop = dst[dst_rect[1]:dst_rect[1] + dst_rect[3], dst_rect[0]:dst_rect[0] + dst_rect[2]]
src_pts_crop = src_pts_arr - src_rect[:2]
dst_pts_crop = dst_pts_arr - dst_rect[:2]
print(src_pts_crop)
# [[ 0. 60.]
# [ 50. 180.]
# [200. 0.]]
print(dst_pts_crop)
# [[ 0. 0.]
# [ 40. 180.]
# [120. 30.]]
切り出し画像に座標にマークを描画したものは以下の通り。
src_crop_mark = src_crop.copy()
for pt in src_pts_crop.astype(np.int):
cv2.drawMarker(src_crop_mark, tuple(pt), (0, 255, 0), thickness=4)
cv2.imwrite('data/dst/opencv_warp_src_crop_mark.jpg', src_crop_mark)
dst_crop_mark = dst_crop.copy()
for pt in dst_pts_crop.astype(np.int):
cv2.drawMarker(dst_crop_mark, tuple(pt), (0, 255, 0), thickness=4)
cv2.imwrite('data/dst/opencv_warp_dst_crop_mark.jpg', dst_crop_mark)
切り出し画像に対してアフィン変換
切り出し画像に対してアフィン変換を行う。cv2.getAffineTransform()
で変換行列を生成し、cv2.warpAffine()
で変換する。詳細は以下の記事を参照。
mat = cv2.getAffineTransform(src_pts_crop.astype(np.float32), dst_pts_crop.astype(np.float32))
affine_img = cv2.warpAffine(src_crop, mat, tuple(dst_rect[2:]))
cv2.imwrite('data/dst/opencv_warp_affine_crop.jpg', affine_img)
切り出し画像をマスク処理して合成
アフィン変換は元画像全体に対して変換処理が行われる。必要なのは三角形領域のみなのでマスクで切り抜いて貼り付け先画像と合成する。
cv2.fillConvexPoly()
で三角形領域が1.0
、そのほかの領域が0.0
となるマスクを作成し合成する。マスク処理についての詳細は以下の記事を参照。
mask = np.zeros_like(dst_crop, dtype=np.float32)
cv2.fillConvexPoly(mask, dst_pts_crop.astype(np.int), (1.0, 1.0, 1.0), cv2.LINE_AA)
dst_crop_merge = affine_img * mask + dst_crop * (1 - mask)
cv2.imwrite('data/dst/opencv_warp_affine_crop_merge.jpg', dst_crop_merge)
これをdst
に貼り付けて完成。スライスを使う。
dst[dst_rect[1]:dst_rect[1] + dst_rect[3], dst_rect[0]:dst_rect[0] + dst_rect[2]] = dst_crop_merge
cv2.imwrite('data/dst/opencv_warp_dst_result.jpg', dst)
まとめて関数化
この処理をまとめて関数化すると以下のようになる。
import cv2
import numpy as np
def warp(src, dst, src_pts, dst_pts, transform_func, warp_func, **kwargs):
src_pts_arr = np.array(src_pts, dtype=np.float32)
dst_pts_arr = np.array(dst_pts, dtype=np.float32)
src_rect = cv2.boundingRect(src_pts_arr)
dst_rect = cv2.boundingRect(dst_pts_arr)
src_crop = src[src_rect[1]:src_rect[1] + src_rect[3], src_rect[0]:src_rect[0] + src_rect[2]]
dst_crop = dst[dst_rect[1]:dst_rect[1] + dst_rect[3], dst_rect[0]:dst_rect[0] + dst_rect[2]]
src_pts_crop = src_pts_arr - src_rect[:2]
dst_pts_crop = dst_pts_arr - dst_rect[:2]
mat = transform_func(src_pts_crop.astype(np.float32), dst_pts_crop.astype(np.float32))
warp_img = warp_func(src_crop, mat, tuple(dst_rect[2:]), **kwargs)
mask = np.zeros_like(dst_crop, dtype=np.float32)
cv2.fillConvexPoly(mask, dst_pts_crop.astype(np.int), (1.0, 1.0, 1.0), cv2.LINE_AA)
dst_crop_merge = warp_img * mask + dst_crop * (1 - mask)
dst[dst_rect[1]:dst_rect[1] + dst_rect[3], dst_rect[0]:dst_rect[0] + dst_rect[2]] = dst_crop_merge
def warp_triangle(src, dst, src_pts, dst_pts, **kwargs):
warp(src, dst, src_pts, dst_pts,
cv2.getAffineTransform, cv2.warpAffine, **kwargs)
def warp_rectangle(src, dst, src_pts, dst_pts, **kwargs):
warp(src, dst, src_pts, dst_pts,
cv2.getPerspectiveTransform, cv2.warpPerspective, **kwargs)
三角形領域に対する処理と四角形領域に対する処理との違いはアフィン変換を使うか射影変換を使うかだけ。
cv2.warpAffine()
およびcv2.warpPerspective()
の引数は可変長引数**kwargs
で指定できるようにしている。
以下のように使う。
三角形領域に対する処理(アフィン変換)の例。
src = cv2.imread('data/src/lena.jpg')
dst = cv2.imread('data/src/rocket.jpg')
src_pts = [[100, 80], [150, 200], [300, 20]]
dst_pts = [[280, 120], [320, 300], [400, 150]]
warp_triangle(src, dst, src_pts, dst_pts)
cv2.imwrite('data/dst/opencv_warp_triangle.jpg', dst)
四角形領域に対する処理(射影変換)の例。cv2.warpPerspective()
の補間処理を変更する引数flags
を指定している。必要であればborderMode
なども同じように指定できる。
src = cv2.imread('data/src/lena.jpg')
dst = cv2.imread('data/src/rocket.jpg')
src_pts = [[100, 80], [150, 200], [350, 160], [300, 20]]
dst_pts = [[280, 120], [200, 280], [500, 300], [400, 150]]
warp_rectangle(src, dst, src_pts, dst_pts, flags=cv2.INTER_CUBIC)
cv2.imwrite('data/dst/opencv_warp_rectangle.jpg', dst)