Python, OpenCVで三角形・四角形領域を変形して別画像に貼り付け

Posted: | Tags: 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)

OpenCV warping src with 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 warping dst with 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)

OpenCV warping crop src with 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)

OpenCV warping crop dst with 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)

OpenCV warping crop affine

切り出し画像をマスク処理して合成

アフィン変換は元画像全体に対して変換処理が行われる。必要なのは三角形領域のみなのでマスクで切り抜いて貼り付け先画像と合成する。

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)

OpenCV warping crop affine 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)

OpenCV warping result

まとめて関数化

この処理をまとめて関数化すると以下のようになる。

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)

OpenCV warping triangle

四角形領域に対する処理(射影変換)の例。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)

OpenCV warping rectangle

関連カテゴリー

関連記事