背景画像に別の画像をはめ込む【OpenCV】

OpenCVを用いて、背景画像の上に他の画像をははめ込む方法を説明します。OpenCVでは2つの画像の画素値を加算して合成するような関数は用意されていますが、背景画像に別の画像をはめ込むような関数は用意されていないので、少し工夫する必要があります。ここでは、「ndarrayの数値を直接置き換える方法」「warpAffine関数を用いる方法」の2つの方法を説明します。

開発環境

  • OpenCV 4.2.0
  • Python 3.7.9

背景画像に別の画像を埋め込む

ここでは次のような湖畔の写真に、Lenaの写真をはめ込むことを考えてみましょう。

背景画像(Lake.jpg)
埋め込む画像(Lena.jpg)

この2つの画像を合成すると次のようになります。

なお、ここでは2つの画像(Lake.jpgとLena.jpg)はともに「C:\BioTech-Lab」フォルダに保存してあるものとします。

方法1:ndarrayの画素値を直接置き換える

基本的な使い方

Pythonで画像を扱う際はndarrayとしてピクセルデータが読み込まれるので、背景画像を表すndarrayの該当部分を直接重ね合わせる画像の画素値に置き換えてしまえば画像を重ね合わせることができます。背景画像の左上の「原点から横方向(X方向)にdx、縦方向(Y方向)にdy」の部分に(w, h)のサイズの画像を合成する場合を考えてみます。この時、背景画像をimg_backとして読み込むと、置き換えるピクセルの座標はimg_back[dy:dy+h, dx:dx+w]となるので、この値を直接置き換えてしまいます。

それでは、背景画像(Lake.jpg)の左上の原点から横方向(X方向)・縦方向(Y方向)にそれぞれ100ピクセルの位置に画像をはめ込んでみましょう。

import cv2
 
fore_img = cv2.imread(r'C:\BioTech-Lab\lena.jpg')
back_img = cv2.imread(r'C:\BioTech-Lab\Lake.jpg')

dx = 100    # 横方向の移動距離
dy = 100    # 縦方向の移動距離
h, w = fore_img.shape[:2]
back_img[dy:dy+h, dx:dx+w] = fore_img

cv2.imshow('img',back_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

10行目で背景画像の外筒範囲に直接合成する画像の画素値を代入しています。

この方法は画素値を置き換えるだけなので非常に大きな画像であっても関係なく高速に処理を行うことができ、またOpenCVに限らずPythonで普遍的に使える方法になります。

合成する画像が背景画像からはみ出る場合

上記の処理は合成する画像が背景画像からはみ出る場合にはエラーになってしまい、このままではバグの温床になりやすい処理でもあります。そこで、合成する画像が背景画像からはみ出る場合でも対応できるように修正したうえで、関数として再利用できるようにしてみましょう。

import cv2

def overlay(fore_img, back_img, shift):

    '''
    fore_img:合成する画像
    back_img:背景画像
    shift:左上を原点としたときの移動量(x, y)
    '''

    shift_x, shift_y = shift

    fore_h, fore_w = fore_img.shape[:2]
    fore_x_min, fore_x_max = 0, fore_w
    fore_y_min, fore_y_max = 0, fore_h

    back_h, back_w = back_img.shape[:2]
    back_x_min, back_x_max = shift_y, shift_y+fore_h
    back_y_min, back_y_max = shift_x, shift_x+fore_w

    if back_x_min < 0:
        fore_x_min = fore_x_min - back_x_min
        back_x_min = 0
        
    if back_x_max > back_w:
        fore_x_max = fore_x_max - (back_x_max - back_w)
        back_x_max = back_w

    if back_y_min < 0:
        fore_y_min = fore_y_min - back_y_min
        back_y_min = 0
        
    if back_y_max > back_h:
        fore_y_max = fore_y_max - (back_y_max - back_h)
        back_y_max = back_h        

    back_img[back_y_min:back_y_max, back_x_min:back_x_max] = fore_img[fore_y_min:fore_y_max, fore_x_min:fore_x_max]

    return back_img

  
fore_img = cv2.imread(r'C:\BioTech-Lab\lena.jpg')
back_img = cv2.imread(r'C:\BioTech-Lab\Lake.jpg')

back_img = overlay(fore_img, back_img, (-100, 1200))

cv2.imshow('img',back_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

3-39行目でoverlay関数を定義し、45行目でその第1引数に合成する画像を、第2引数に背景画像を指定し、第3引数に移動量を(x, y)で指定しています。これを用いると、背景画像からはみ出るような場合でも合成できることが分かります。

方法2:warpAffine関数を用いて画像を埋め込む

warpAffine関数は画像の平行移動や縮小拡大・回転などの変形(アフィン変換)を行うための関数ですが、背景画像を指定することで画像の埋め込みに用いることもできます。なお、アフィン変換については以下の記事をご覧ください。

単純に画像をそのまま埋め込むだけなら方法1で十分ですが、画像を縮小して埋め込んだり、回転して埋め込むような場合はこちらの方法を使えば簡単にできます。

基本的な使い方

ここではまずは基本的な使い方から見ていきましょう。warpAffine関数の第1引数(src引数)には埋め込む画像(Lena.jpg)を指定し、第2引数(M引数)にはその画像をどのように移動・変形するのかを変換行列として指定します。変換行列の指定の仕方は上記のアフィン変換のついての記事をご覧ください。第3引数(dsize引数)には出力する画像のサイズを指定するので、ここは背景画像のサイズになります。さらに、第4引数(dst引数)に背景画像を指定し、第6引数(borderMode引数)にcv2.BORDER_TRANSPARENTを指定して、背景画像を透過するように設定します。

それでは、先ほどと同様にLena.jpgをLake.jpgの左上の原点から横方向(X方向)・縦方向(Y方向)にそれぞれ100ピクセルの位置にはめ込んでみましょう。

import cv2
import numpy as np
 
fore_img = cv2.imread(r'C:\BioTech-Lab\lena.jpg')
back_img = cv2.imread(r'C:\BioTech-Lab\Lake.jpg')
h, w = back_img.shape[:2]

dx = 100
dy = 100
M = np.array([[1, 0, dx], [0, 1, dy]], dtype=float)
img_warped = cv2.warpAffine(fore_img, M, (w, h), back_img, borderMode=cv2.BORDER_TRANSPARENT)

cv2.imshow('img',img_warped)
cv2.waitKey(0)
cv2.destroyAllWindows()

Lena.jpgが指定した位置に合成できていることが分かります。

変形しながら合成する場合

warpAffine関数を用いて合成することのメリットは単純な合成ではなく、縮小拡大や回転などの変形をした上での合成ができる点です。関数の使い方は先ほどと同様で、変換行列だけ変えればOKです。変換行列の指定の仕方はアフィン変換についての記事をご覧ください。ここでは45度回転させた上で、0.5倍に縮小して合成してみましょう。

import cv2
 
fore_img = cv2.imread(r'C:\BioTech-Lab\lena.jpg')
fore_h, fore_w = fore_img.shape[:2]

back_img = cv2.imread(r'C:\BioTech-Lab\Lake.jpg')
back_h, back_w = back_img.shape[:2]

M = cv2.getRotationMatrix2D((int(fore_w/2), int(fore_h/2)), 45, 0.5)
img_warped = cv2.warpAffine(fore_img, M, (back_w, back_h), back_img, borderMode=cv2.BORDER_TRANSPARENT)

cv2.imshow('img',img_warped)
cv2.waitKey(0)
cv2.destroyAllWindows()

これで、回転させたうえで、縮小させて合成することができました。

スポンサーリンク

“背景画像に別の画像をはめ込む【OpenCV】” への1件の返信

  1. 参考にさせていただきました。ありがとうございます。

    2-2. 合成する画像が背景画像からはみ出る場合のoverlay()をそのまま利用すると,
    ValueError: could not broadcast input array from shape (22,25,3) into shape (25,22,3)
    のようなエラーが発生してしまい,

    18-19行目
    back_x_min, back_x_max = shift_y, shift_y+fore_h
    back_y_min, back_y_max = shift_x, shift_x+fore_w

    back_x_min, back_x_max = shift_x, shift_x+fore_w
    back_y_min, back_y_max = shift_y, shift_y+fore_h

    に入れ替えると動作しました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)