iMind Developers Blog

iMind開発者ブログ

Pythonでの画像の連結

概要

複数の画像を連結して1枚にして返す用事があったので、NumpyやOpenCVを利用して動作を確認した。だいたいNumPyで愚直に実行。Pillowは利用していない。

バージョン情報

  • Python 3.6.5
  • opencv-python==3.4.2.17

サンプル画像

かわいいワンコが仲睦まじく駆けている下記3画像を用いる。

f:id:imind:20181126220044j:plain

f:id:imind:20181126220054j:plain

f:id:imind:20181126220102j:plain

水平に連結

単純に水平に繋げるだけであれば、OpenCV(かなにか)で読み込んでNumPyでaxis=1を指定してconcatすれば済む。

import cv2, numpy as np
from matplotlib import pylab as plt

# dog1〜3.jpegという名前で保存された3枚の画像をOpenCVで読み込む
images = [cv2.imread('data/concat_images/dog{}.jpeg'.format(i)) for i in range(1, 4)]

# axis=1でconcat
hcon_image = np.concatenate(images, axis=1)
plt.imshow(cv2.cvtColor(hcon_image, cv2.COLOR_BGR2RGB))

f:id:imind:20181126220441p:plain

垂直に連結

続いて垂直版。水平版はaxis=1で連結したが、垂直版はaxis=0で連結する。

# axis=0でconcat
vcon_image = np.concatenate(images, axis=0)
plt.imshow(cv2.cvtColor(vcon_image, cv2.COLOR_BGR2RGB))

f:id:imind:20181126220741p:plain

画像の一部を被せて垂直に連結

画像の一部の座標が重複する状態で被せてconcatする。

例えば100ピクセルほど上に被せる場合、単純に対象イメージの上100ピクセルを切った上でconcatする方法が考えられる。

# 3枚の画像を読み込む
images = [cv2.imread('data/concat_images/dog{}.jpeg'.format(i)) for i in range(1, 4)]
# 2と3の上100pixelを切り取る
images = [images[0], images[1][100:], images[2][100:]]
vcon_image = np.concatenate(images, axis=0)
plt.imshow(cv2.cvtColor(vcon_image, cv2.COLOR_BGR2RGB))

f:id:imind:20181127214220p:plain

2つの画像を混ぜる

OpenCVにAddWeightedという2つの画像をalphaを指定して混ぜる関数が用意されている。

cv.AddWeighted(src1, alpha, src2, beta, gamma, dst)

試しに画像1をalpha=0.7, 画像2をalpha=0.5にして混ぜてみる。

なお今回使う画像はすべてサイズは同じものなのでサイズを揃える等の前処理は必要ない前提。

dest = np.zeros(images[0].shape)
added_images = cv2.addWeighted(images[0], 0.7, images[1], 0.5, gamma=1.0, dst=dest)
plt.imshow(cv2.cvtColor(added_images, cv2.COLOR_BGR2RGB))

f:id:imind:20181127214306p:plain

上記だと背景の位置がズレているので、適当にもう少し背景が近づくようにしてみる。ざっくり90ピクセルくらい近づければ良さそう。

dest = added_images2 = cv2.addWeighted(images[0][:, :-90], 0.3, images[1][:, 90:], 0.8, gamma=1.0)#, dst=dest)
plt.imshow(cv2.cvtColor(added_images2, cv2.COLOR_BGR2RGB))

f:id:imind:20181127215309p:plain

ちょっとブレてるけど背景を固定で取っていればこの手法で動いている物体だけが複数登場するような画像ができあがるはず。

1つの画像の上に別画像を貼り付ける

1つの大きいキャンバスを用意しておいて、そこの上にペタペタと画像を貼り付ける。

Pillowのpaste使うと楽だけど本稿ではNumpy的なやり方で貼り付ける。

# 800*500の黒背景を用意
img = np.zeros((500, 800, 3), dtype=np.uint8)

# 左上に画像1を貼る
x, y = images[0].shape[:2]
img[0:x, 0:y, :] = images[0]

# 右下に画像2を貼る
img[500-x:500, 800-y:800, :] = images[0]

plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

f:id:imind:20181207200730p:plain

楕円形に切り抜いて貼り付け

写真を楕円形に切り抜いた上で、上の例のように1枚の絵の中にペタペタと貼り付ける。

楕円形はcv2.ellipseで生成できる。

# 楕円形のマスクを生成
# thickness=-1にすると生成する楕円の中身がfillされる
y, x = images[0].shape[:2]
mask = np.zeros(images[0].shape, np.uint8)
cv2.ellipse(mask, center=(int(x / 2), int(y / 2)), axes=(int(x / 2), int(y / 2)), angle=0, startAngle=0, endAngle=365, color=(255, 255, 255), thickness=-1)

# 対象画像にbitwise_andでmaskをかける
img = images[0]
dest = cv2.bitwise_and(img, mask)

plt.imshow(cv2.cvtColor(dest, cv2.COLOR_BGR2RGB))

f:id:imind:20181207204525p:plain

こうしてできた画像1〜3をリサイズしつつ貼り付けてみる。

# 画像1〜3を楕円で切り抜いてx, y共に半分にリサイズする
masked_images = []
for i in range(3):
    img = images[i]
    y, x = img.shape[:2]
    mask = np.zeros(img.shape, np.uint8)
    half_xy = (int(x / 2), int(y / 2))
    cv2.ellipse(mask, center=half_xy, axes=half_xy, angle=0, startAngle=0, endAngle=360, color=(255, 255, 255), thickness=-1)
    dest = cv2.bitwise_and(img, mask)
    dest = cv2.resize(dest, (half_xy[0], half_xy[1]))
    masked_images.append(dest)

# 500*300の黒背景を用意
img = np.zeros((300, 500, 3), dtype=np.uint8)

# 左上に画像1を
x, y = masked_images[0].shape[:2]
img[0:x, 0:y, :] = masked_images[0]

# 中央に画像2を
img[int(300/2 - x/2):int(300/2 + x/2), int(500/2 - y/2):int(500/2 + y/2), :] = masked_images[1]

# 右下に画像2を貼る
img[300-x:300, 500-y:500, :] = masked_images[2]

plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

あ、透過になってるわけではないのでmaskの左上の黒が隣の画像に被ってしまっている。

f:id:imind:20181207210642p:plain

元画像のmaskした箇所を一度消して、その後に貼り付ける画像を加算する形にすれば良さそう。

# 貼り付け先の背景画像
BG_X = 500
BG_Y = 420
bg = np.zeros((BG_Y, BG_X, 3), dtype=np.uint8)

for i in range(3):
    img = images[i]
    # 画像のリサイズ
    y, x = img.shape[:2]
    half_xy = (int(x / 2), int(y / 2))
    half_img = cv2.resize(img, (half_xy[0], half_xy[1]))
    # サイズの取り直し
    y, x = half_img.shape[:2]
    half_xy = (int(x / 2), int(y / 2))
    # 楕円のmask生成
    mask = np.zeros(half_img.shape, np.uint8)
    cv2.ellipse(mask, center=half_xy, axes=half_xy, angle=0, startAngle=0, endAngle=360, color=(1, 1, 1), thickness=-1)
    # 貼り付ける位置のx, yを計算
    paste_x = int((BG_X - half_xy[0]) / 3 * i)
    paste_y = int((BG_Y - half_xy[1]) / 3 * i)
    # 楕円と被る箇所の背景画像を0に
    bg[paste_y:paste_y+y, paste_x:paste_x+x, :] *= np.where(mask == 1, 0, 1).astype(np.uint8)
    # 0になった箇所に貼り付け画像を加算する
    bg[paste_y:paste_y+y, paste_x:paste_x+x, :] += np.where(mask == 1, half_img, 0)

plt.figure(figsize=(7, 7))
plt.imshow(cv2.cvtColor(bg, cv2.COLOR_BGR2RGB))

f:id:imind:20181207213248p:plain

改定履歴

Author: Masato Watanabe, Date: 2019-01-15, 記事投稿