pythonのopencvを利用して射影変換をやっていようと思います。射影変換だけだと面白くないので、射影変換された画像を異なる画像に張り付けるということもやってみます。
こんな感じです。画像はpixabayから用意しました。

目次 非表示
実行環境
OSはWindows10 + WSL2 (ubuntu20.04)を利用しています。
PythonはPipenvを利用して、バージョンは3.8を利用しています。
ディレクトリ構造
paste_image.py内に主な処理を記述しています。
$ tree
.
|-- Pipfile
|-- Pipfile.lock
|-- data
| |-- back.jpg
| `-- overlay.jpg
|-- main.py
`-- paste_image.py
pythonの仮想環境(Pipfile)
dev-packagesにコードフォーマット用のautopep8とflake8を入れています。コードフォーマットがいらない人には、必要ありません。
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
opencv-contrib-python = "*"
numpy = "*"
pillow = "*"
[dev-packages]
autopep8 = "*"
flake8 = "*"
[requires]
python_version = "3.8"
pipenvのインストール方法や簡単な使い方について、下記でまとめているので参考にしてください。
射影変換と画像を重ね合わせる
それではプログラムの説明に入っていきます。主な処理部分のみ説明していきます。
全体のコードは下にまとめてあるので、そちらを参考にしてください。
opencvを利用した射影変換
射影変換の処理部分は下記になります。ほぼドキュメント通りです。
# orig_posとdist_posの型: np.float32([[int, int], [int, int], [int, int], [int, int]])
# np.float32(左上, 右上, 左下, 右下)
# orig_posは変換前の画像の座標
# dist_posは変換後の画像の座標
M = cv2.getPerspectiveTransform(orig_pos, dist_pos)
ret_img = cv2.warpPerspective(
img, # opencv image (BGRA)
M,
img_size, # tuple (int, int)
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_CONSTANT,
borderValue=[255, 255, 255, 0])
特に難しいことはないです。ただ、最終的に画像を重ね合わせるので、変換対象の領域外については透明にしてあります。透明化するために気を付けるべきポイントは2点です。
cv2.warpPerspective()
に渡す画像(img)はアルファチャンネル持ち(BGRA)cv2.warpPerspective()
でboarderMode
とborderValue
を引数に指定
背景画像と射影変換済み画像の重ね合わせ
こちらもアルファチャネルを考慮した変換を行っていますが、特に難しいことはありません。
# background画像にoverlay画像をlocation位置に張り付ける
def overlay_image(self, background, overlay, location):
"""
backgroud: opencv image (BGRA)
overlay: opencv image (BGRA)
lacation: tuple (int, int)
background position to paste overlay
"""
def cv2pil(src):
if src.shape[2] == 3:
src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
elif src.shape[2] == 4:
src = cv2.cvtColor(src, cv2.COLOR_BGRA2RGBA)
src = Image.fromarray(src)
src = src.convert('RGBA')
return src
pil_back = cv2pil(background)
pil_overlay = cv2pil(overlay)
pil_tmp = Image.new('RGBA', pil_back.size, (255, 255, 255, 0))
pil_tmp.paste(pil_overlay, location, pil_overlay)
result_image = Image.alpha_composite(pil_back, pil_tmp)
return cv2.cvtColor(np.asarray(result_image), cv2.COLOR_RGBA2BGRA)
プログラム全体
プログラムの全体です。main.py
とpaste_image.py
を載せておきます。
from paste_image import PasteImage
import numpy as np
import cv2
def read_image(filename, resize_factor=None):
img = cv2.imread(filename)
if img.shape[2] != 4:
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
img[..., 3] = 255
if resize_factor is not None:
height = int(img.shape[0] / resize_factor)
width = int(img.shape[1] / resize_factor)
img = cv2.resize(img, (width, height))
return img
if __name__ == '__main__':
background_file = './data/background.jpg'
overlay_file = './data/overlay.jpg'
back_image = read_image(background_file, resize_factor=3)
overlay_image = read_image(overlay_file)
pi = PasteImage()
img = pi.get_composite_image(
back_image, overlay_image,
np.float32([[450, 130], [550, 114], [450, 200], [550, 193]])
)
cv2.imwrite('./data/output.png', img)
import cv2
import numpy as np
from PIL import Image
class PasteImage:
def __init__(self):
pass
def get_composite_image(self,
background,
overlay,
dist_pos,
orig_pos=None):
if (self.validate(dist_pos) is False):
print('Error: dist_pos is not acceptable. return background')
return background
transformed = self.transform(overlay, dist_pos, orig_pos)
minx = self.get_rectangle_left(dist_pos)
miny = self.get_rectangle_top(dist_pos)
img = self.overlay_image(background, transformed, (minx, miny))
return img
def validate(self, dist_pos):
if (dist_pos[0][0] >= dist_pos[1][0]):
return False
if (dist_pos[0][1] >= dist_pos[2][1]):
return False
if (dist_pos[3][0] <= dist_pos[2][0]):
return False
if (dist_pos[3][1] <= dist_pos[1][1]):
return False
return True
def overlay_image(self, background, overlay, location):
pil_back = self.cv2pil(background)
pil_overlay = self.cv2pil(overlay)
pil_tmp = Image.new('RGBA', pil_back.size, (255, 255, 255, 0))
pil_tmp.paste(pil_overlay, location, pil_overlay)
result_image = Image.alpha_composite(pil_back, pil_tmp)
return cv2.cvtColor(np.asarray(result_image), cv2.COLOR_RGBA2BGRA)
def transform(self, img, dist_pos, orig_pos=None):
orig_pos = orig_pos if orig_pos is not None else \
self.get_corner_position(img)
adjusted_dist_pos = self.adjust_dist_pos(img, dist_pos)
img_size = (
self.get_rectangle_right(adjusted_dist_pos) -
self.get_rectangle_left(adjusted_dist_pos),
self.get_rectangle_bottom(adjusted_dist_pos) -
self.get_rectangle_top(adjusted_dist_pos)
)
M = cv2.getPerspectiveTransform(orig_pos, adjusted_dist_pos)
ret_img = cv2.warpPerspective(
img, M, img_size,
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_CONSTANT,
borderValue=[255, 255, 255, 0]) # transparent
return ret_img
def adjust_dist_pos(self, img, dist_pos):
minx = self.get_rectangle_left(dist_pos)
miny = self.get_rectangle_top(dist_pos)
new_dist_pos = np.float32(
[[xy[0] - minx, xy[1] - miny] for xy in dist_pos])
_, top_right, bottom_left, bottom_right = new_dist_pos
maxx = self.get_rectangle_right(new_dist_pos)
maxy = self.get_rectangle_bottom(new_dist_pos)
height, width = img.shape[:2]
ratio = min(1.0, width / maxx, height / maxy)
if ratio != 1.0:
new_dist_pos = np.float32([int(xy * ratio) for xy in new_dist_pos])
return new_dist_pos
@staticmethod
def get_rectangle_left(pos):
return int(min(pos[0][0], pos[2][0]))
@staticmethod
def get_rectangle_right(pos):
return int(max(pos[1][0], pos[3][0]))
@staticmethod
def get_rectangle_top(pos):
return int(min(pos[0][1], pos[1][1]))
@staticmethod
def get_rectangle_bottom(pos):
return int(max(pos[2][1], pos[3][1]))
@staticmethod
def get_corner_position(img):
height, width = img.shape[:2]
return np.float32([[0, 0], [width, 0], [0, height], [width, height]])
@ staticmethod
def cv2pil(src):
if src.shape[2] == 3:
src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
elif src.shape[2] == 4:
src = cv2.cvtColor(src, cv2.COLOR_BGRA2RGBA)
src = Image.fromarray(src)
src = src.convert('RGBA')
return src
まとめ
この記事では、Pythonのopencvを利用して、射影変換した画像を別の画像に張り付けるプログラムを紹介しました。
ポイントはアルファチャンネルを指定することですが、それ以外では特に難しいことはありません。