見出し画像

Pythonでやってみた(画像処理編5):コラ画像の作成

1.概要

 Twitterで流れてくるコラ画像を自分も簡単に作ってみたいと思ったのでコードを作成してみました。
 大きな流れとしては①Remgbで背景除去、②OpenCVで合成となります。

1-1.サンプル画像

 出典を見つけられなかったのですが、今回は下記画像を使用してみます。背景画像は「pixabay」から適当に探しました。

1-2.参考コード:PIL形式を並列表示

 コラ画像の作成とは関係ないですが、比較に便利のためPIL画像を並べて表示する関数を作成しました。
 参考として背景用画像とコラ用画像の2つを表示します。

[IN]
from PIL import Image

#PIL画像を並列表示する関数
def image_grid(imgs, rows, cols):
    assert len(imgs) == rows*cols #画像の枚数がrows*colsと一致するか確認

    w, h = imgs[0].size #画像のサイズを取得
    grid = Image.new('RGB', size=(cols*w, rows*h)) #新しい画像を作成
    grid_w, grid_h = grid.size #新しい画像のサイズを取得
    
    for i, img in enumerate(imgs):
        grid.paste(img, box=(i%cols*w, i//cols*h)) #画像を貼り付ける
    return grid

path_main = 'image_main/muschle dog.JPG' #コラ画像のパス
path_back = 'image_background/space.jpg' #背景画像のパス

img = Image.open(path_main) #PIL形式で読み込み
img_back = Image.open(path_back)

image_grid([img_back, img], 1, 2)

[OUT]

2.画像の前処理(背景除去):Remgb

 合成したい画像の背景はPythonライブラリの「Remgb」を使用します。使用方法は簡単でライブラリから"remove"を呼び出して背景を除去したい画像をPIL形式で与えるだけです。
 次に①背景用画像、②背景除去したメイン画像を合成していきます。

[IN]
from rembg import remove
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import cv2

#PIL画像を並列表示する関数
def image_grid(imgs, rows, cols):
    assert len(imgs) == rows*cols #画像の枚数がrows*colsと一致するか確認

    w, h = imgs[0].size #画像のサイズを取得
    grid = Image.new('RGB', size=(cols*w, rows*h)) #新しい画像を作成
    grid_w, grid_h = grid.size #新しい画像のサイズを取得
    
    for i, img in enumerate(imgs):
        grid.paste(img, box=(i%cols*w, i//cols*h)) #画像を貼り付ける
    return grid


path_main = 'image_main/muschle dog.JPG' #コラ画像のパス
path_back = 'image_background/space.jpg' #背景画像のパス

img = Image.open(path_main) #PIL形式で読み込み
img_back = Image.open(path_back)
img_rembg = remove(img) #背景除去

image_grid([img, img_rembg], 1, 2)

[OUT]

3.画像の合成:OpenCV

 今回の画像合成のコードにおける設計思想は下記の通りです。

【設計思想:追加機能(関数)】

  • PIL形式をOpenCV(Numpy型)に変換:合成をOpenCVで行うため形式を変換させる関数を追加

  • 背景画像のどこ(左上が座標の起点)に配置するかの設定:コラ画像の位置を移動させたいー>背景画像のサイズに対する比率で調整

  • コラ画像の縮小:コラ画像<背景画像サイズでないとエラーが出るためコラ画像を縮小できるようにしておく

  • 処理時の引数:コラ画像のサイズや位置を微調整したいため、合成時のメソッド(__call__で設定)に画像サイズや位置の調整用引数を設定

 複数の機能を使用して画像を合成するためクラス化しました。

[IN]
class CompositeImager:
    def __init__(self, img, img_back):
        #PIL形式
        self.img_PIL = img  #コラ画像         self.img_back_PIL = img_back #背景画像
        self.img_rembg_PIL = remove(img) #背景除去
        
        #OpenCV形式
        self.img_rembg = self.pil2cv(self.img_rembg_PIL)  #コラ画像         self.img_back = self.pil2cv(img_back) #背景画像
    
    def __call__(self, imgratio=1.0, xcor_r=0.0, ycor_r=0.0):
        #コラ画像のサイズを調整
        img_resize = self.img2resize(self.img_rembg, imgratio)
        
        xcor, ycor = self.set_coordinate(img_resize, self.img_back, xratio=xcor_r, yratio=ycor_r)
        img_output = self.composite_Image(img_resize, self.img_back, xcor, ycor)
        return img_output

    def composite_Image(self, img_resize, img_back, xcor, ycor):
        #注目領域(ROI)(挿入エリア)を作成(左上の角)
        h_resize, w_resize, _ = img_resize.shape #コラ画像のサイズを取得:height,width,channel
        x1, x2, y1, y2 = xcor, xcor+ w_resize, ycor, ycor+h_resize #ROIの座標を設定

        roi = img_back[y1:y2, x1:x2] #注目領域(ROI)の作成


        #マスク、インバースマスクの作成
        img2gray = cv2.cvtColor(img_resize,cv2.COLOR_BGR2GRAY)
        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)
        mask_inv = cv2.bitwise_not(mask)

        #画像の注目領域(ROI)を黒くする
        img_back_bg = cv2.bitwise_and(roi, roi, mask = mask_inv)

        #画像から指定領域のみ(ROI)を抽出する
        img2_fg = cv2.bitwise_and(img_resize ,img_resize ,mask = mask)

        #ROI(置換領域)に追加画像(処理後)を追加
        dst = cv2.add(img_back_bg, img2_fg)
        img_back[y1:y2, x1:x2] = dst #追加したい画像の位置をROIに合わせて設定

        #Numpy配列をPIL形式に変換
        img_output = Image.fromarray(cv2.cvtColor(img_back, cv2.COLOR_BGR2RGB))
        return img_output
    
    def img2resize(self, img, imgratio):
        return cv2.resize(img, dsize=None, fx=imgratio, fy=imgratio) #画像サイズを調整

    #背景画像のサイズを取得して、コラ画像の設置位置を調整
    def set_coordinate(self, img, img_back, xratio=0, yratio=0):
        h_back, w_back, _ = img_back.shape #背景画像のwidth, height, channel
        xcor = int(w_back*xratio) #x座標の位置をxratio(割合)から設定
        ycor = int(h_back*yratio) #y座標の位置をyratio(割合)から設定
        
        #コラ画像が背景内に収まることを確認
        h, w, _ = img.shape #コラ画像のheight,width,channel
        w_limit, h_limit = w_back-xcor, h_back-ycor #コラ画像が収まる範囲
        if w > w_limit or h > h_limit:
            print(f'画像のサイズが大きすぎます。画像 w:{w},h:{h}, 背景 w:{w_back},h:{h_back} , 設置エリア w:{xcor}-{xcor+w}, h:{ycor}-{ycor+h}')
            return False
        else:
            return (xcor, ycor) #画像を設置したいx,y座標を返す
    
    #https://qiita.com/derodero24/items/f22c22b22451609908ee
    def pil2cv(self, image):
        ''' PIL型 -> OpenCV型 '''
        new_image = np.array(image, dtype=np.uint8)
        if new_image.ndim == 2:  # モノクロ
            pass
        elif new_image.shape[2] == 3:  # カラー
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR)
        elif new_image.shape[2] == 4:  # 透過
            # new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA)
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR) #3次元でないとエラーが出るため
        return new_image

#コラ画像作成(PIL形式で入力)
compositeimager = CompositeImager(img=img, img_back=img_back)
compositeimager

[OUT]
<__main__.CompositeImager at 0x144e9632590>

3-1.PIL->OpenCV形式に変換

 PIL->OpenCV形式は下記記事を参考にしました。注意点として「等価画像は4次元になるが画像合成時は3次元でないとエラーが出る」です。

[Qiita記事ver.]
import numpy as np
import cv2

def pil2cv(image):
    ''' PIL型 -> OpenCV型 '''
    new_image = np.array(image, dtype=np.uint8)
    if new_image.ndim == 2:  # モノクロ
        pass
    elif new_image.shape[2] == 3:  # カラー
        new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR)
    elif new_image.shape[2] == 4:  # 透過
        new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA)
    return new_image

 よってクラス内のメソッドは「透過(shape[2]==4)において3次元配列が出力されるようにcv2.COLOR_RGBA2BGRA->cv2.COLOR_RGB2BGRに変更」しています。

[IN]
#画像の読み込み
img = Image.open(path_main) #PIL形式で読み込み
img_rembg = remove(img) #背景除去

#処理後の画像サイズ確認
img_array = compositeimager.pil2cv(img)
img_rembg_array = compositeimager.pil2cv(img_rembg)

new_image = np.array(img_rembg, dtype=np.uint8)
img_rem4dim = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA) #4次元で出力:そのままだとエラー

print(type(img_array), img_array.shape, img_rembg_array.shape, img_rem4dim.shape)

[OUT]
<class 'numpy.ndarray'> (336, 490, 3) (336, 490, 3) (336, 490, 4)

3-2.画像の縮小:cv2.resize()

 大前提として「コラ画像<背景画像サイズ」の必要があり、合成用画像のサイズを調整する機能が必要です。
 画像サイズの変換(縮小)はcv2.resize()を使用します。画像のアスペクト比を維持するため幅・高さ方向の縮小比は同じ値を使用します。

[IN]
img = Image.open(path_main) #PIL形式で読み込み
img_array = compositeimager.pil2cv(img)
ratio = 0.5
img_resize = cv2.resize(img_array, dsize=None, fx=ratio, fy=ratio)
print(img_array.shape, img_resize.shape)

[OUT]
(336, 490, 3) (168, 245, 3)

3-3.背景への合成座標を取得

 画像の合成はNumpyのスライス機能を使用します。今回のコードでは左上を起点(黄色点)にしており、この起点のx,y座標を背景画像サイズの比率から取得するようにしました。

 本画像サイズ(h, w, c)は「画像:(336, 490, 3), 背景画像:(340, 721, 3)」であり高さ方向は画像を縮小しないと少し移動させただけでエラーになります。
 下記の通り出力は(<x座標(index)>、<y座標(index)>)となります。

[IN]
img_PIL, img_back_PIL = Image.open(path_main), Image.open(path_back) #PIL形式で読み込み
img_rembg = remove(img) #背景除去
img_rembg, img_back = compositeimager.pil2cv(img_rembg), compositeimager.pil2cv(img_back_PIL) #OpenCV形式に変換

coor1 = compositeimager.set_coordinate(img_rembg, img_back, xratio=0.0, yratio=0.0)
coor2 = compositeimager.set_coordinate(img_rembg, img_back, xratio=0.1, yratio=0.01)
coor3 = compositeimager.set_coordinate(img_rembg, img_back, xratio=0.1, yratio=0.1)

print(coor1, coor2, coor3)

[OUT]
画像のサイズが大きすぎます。画像 w:490,h:336, 背景 w:721,h:340 , 設置エリア w:72-562, h:34-370
(0, 0) (72, 3) False

3-4.画像の合成処理

 画像合成処理の流れは下記の通りです。詳細はOpenCVの記事で紹介しているため説明は省きます。

  1. 注目領域(ROI)の作成:合成画像のサイズを取得して背景画像をNumpyのスライスで抽出

  2. グレー処理->2値化->マスク反転処理をする

  3. マスク反転から得られた画像からROIを黒くする:cv2.bitwise_and()を使用

  4. 画像からROIのみを抽出:cv2.bitwise_and()を使用

  5. ROIにコラ画像を追加:cv2.add()を使用

  6. Numpy配列をPILに変換:Image.fromarray()を使用

 参考までに左上から右側移動順で 「1.オリジナル画像、2.背景除去、3.ROI、4.グレースケール、5.マスク、6.インバースマスク、7.ROIを黒くする、8.ROIのみ抽出、9.ROIに追加画像を追加」です。
 「9.=cv2.add(<7.>, <8.>)」であり、9.を背景に追加したものが最終的な生成画像になります。

4.完成品

 再度設計思想は記載します。また追加で注意点を記載します。

【設計思想:追加機能(関数)】

  • PIL形式をOpenCV(Numpy型)に変換:合成をOpenCVで行うため形式を変換させる関数を追加

  • 背景画像のどこ(左上が座標の起点)に配置するかの設定:コラ画像の位置を移動させたいー>背景画像のサイズに対する比率で調整

  • コラ画像の縮小:コラ画像<背景画像サイズでないとエラーが出るためコラ画像を縮小できるようにしておく

  • 処理時の引数:コラ画像のサイズや位置を微調整したいため、合成時のメソッド(__call__で設定)に画像サイズや位置の調整用引数を設定

【注意点】

  • コラ画像は背景を含む画像サイズで合成されるため、過剰に背景がある画像の場合は手動で切り抜きが必要。

4-1.全コード

 全コードは下記の通りです。

[IN]
from rembg import remove
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import cv2

#PIL画像を並列表示する関数
def image_grid(imgs, rows, cols):
    assert len(imgs) == rows*cols #画像の枚数がrows*colsと一致するか確認

    w, h = imgs[0].size #画像のサイズを取得
    grid = Image.new('RGB', size=(cols*w, rows*h)) #新しい画像を作成
    grid_w, grid_h = grid.size #新しい画像のサイズを取得
    
    for i, img in enumerate(imgs):
        grid.paste(img, box=(i%cols*w, i//cols*h)) #画像を貼り付ける
    return grid


path_main = 'image_main/muschle dog.JPG' #コラ画像のパス
path_back = 'image_background/space.jpg' #背景画像のパス

img = Image.open(path_main) #PIL形式で読み込み
img_back = Image.open(path_back)
img_rembg = remove(img) #背景除去

class CompositeImager:
    def __init__(self, img, img_back):
        #PIL形式
        self.img_PIL = img  #コラ画像         self.img_back_PIL = img_back #背景画像
        self.img_rembg_PIL = remove(img) #背景除去
        
        #OpenCV形式
        self.img_rembg = self.pil2cv(self.img_rembg_PIL)  #コラ画像         self.img_back = self.pil2cv(img_back) #背景画像
    
    def __call__(self, imgratio=1.0, xcor_r=0.0, ycor_r=0.0):
        #コラ画像のサイズを調整
        img_resize = self.img2resize(self.img_rembg, imgratio)
        
        xcor, ycor = self.set_coordinate(img_resize, self.img_back, xratio=xcor_r, yratio=ycor_r)
        img_output = self.composite_Image(img_resize, self.img_back, xcor, ycor)
        return img_output

    def composite_Image(self, img_resize, img_back, xcor, ycor):
        #注目領域(ROI)(挿入エリア)を作成(左上の角)
        h_resize, w_resize, _ = img_resize.shape #コラ画像のサイズを取得:height,width,channel
        x1, x2, y1, y2 = xcor, xcor+ w_resize, ycor, ycor+h_resize #ROIの座標を設定

        roi = img_back[y1:y2, x1:x2] #注目領域(ROI)の作成


        #マスク、インバースマスクの作成
        img2gray = cv2.cvtColor(img_resize,cv2.COLOR_BGR2GRAY)
        ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)
        mask_inv = cv2.bitwise_not(mask)

        #画像の注目領域(ROI)を黒くする
        img_back_bg = cv2.bitwise_and(roi, roi, mask = mask_inv)

        #画像から指定領域のみ(ROI)を抽出する
        img2_fg = cv2.bitwise_and(img_resize ,img_resize ,mask = mask)

        #ROI(置換領域)に追加画像(処理後)を追加
        dst = cv2.add(img_back_bg, img2_fg)
        img_back[y1:y2, x1:x2] = dst #追加したい画像の位置をROIに合わせて設定

        #Numpy配列をPIL形式に変換
        img_output = Image.fromarray(cv2.cvtColor(img_back, cv2.COLOR_BGR2RGB))
        return img_output
    
    def img2resize(self, img, imgratio):
        return cv2.resize(img, dsize=None, fx=imgratio, fy=imgratio) #画像サイズを調整

    #背景画像のサイズを取得して、コラ画像の設置位置を調整
    def set_coordinate(self, img, img_back, xratio=0, yratio=0):
        h_back, w_back, _ = img_back.shape #背景画像のwidth, height, channel
        xcor = int(w_back*xratio) #x座標の位置をxratio(割合)から設定
        ycor = int(h_back*yratio) #y座標の位置をyratio(割合)から設定
        
        #コラ画像が背景内に収まることを確認
        h, w, _ = img.shape #コラ画像のheight,width,channel
        w_limit, h_limit = w_back-xcor, h_back-ycor #コラ画像が収まる範囲
        if w > w_limit or h > h_limit:
            print(f'画像のサイズが大きすぎます。画像 w:{w},h:{h}, 背景 w:{w_back},h:{h_back} , 設置エリア w:{xcor}-{xcor+w}, h:{ycor}-{ycor+h}')
            return False
        else:
            return (xcor, ycor) #画像を設置したいx,y座標を返す
    
    #https://qiita.com/derodero24/items/f22c22b22451609908ee
    def pil2cv(self, image):
        ''' PIL型 -> OpenCV型 '''
        new_image = np.array(image, dtype=np.uint8)
        if new_image.ndim == 2:  # モノクロ
            pass
        elif new_image.shape[2] == 3:  # カラー
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR)
        elif new_image.shape[2] == 4:  # 透過
            # new_image = cv2.cvtColor(new_image, cv2.COLOR_RGBA2BGRA)
            new_image = cv2.cvtColor(new_image, cv2.COLOR_RGB2BGR) #3次元でないとエラーが出るため
        return new_image

#コラ画像作成(PIL形式で入力)
compositeimager = CompositeImager(img=img, img_back=img_back)
compositeimager(imgratio=1.0, xcor_r=0.0, ycor_r=0.0)

[OUT]

4-2.トライアル

 最後に複数パターンを作ってどのようになるか検証します。

[IN]
params = {
    0: {'imgratio': 1.0, 'xcor_r': 0.0, 'ycor_r': 0.0},
    1: {'imgratio': 0.8, 'xcor_r': 0.0, 'ycor_r': 0.0},
    2: {'imgratio': 0.5, 'xcor_r': 0.5, 'ycor_r': 0.0},
    3: {'imgratio': 0.5, 'xcor_r': 0.0, 'ycor_r': 0.5},
}

imgs = [] #画像を格納するリスト

img = Image.open(path_main) #PIL形式で読み込み
img_back = Image.open(path_back)
img_rembg = remove(img) #背景除去

for idx, (key, value) in enumerate(params.items()):
    print(f'case{key+1}: {value}')
    compositeimager = CompositeImager(img=img, img_back=img_back)
    img_output = compositeimager(**value)
    imgs.append(img_output)
    
image_grid(imgs, 2, 2) #画像を2x2で表示

[OUT]

5.コラ用画像一覧

 今後使うかもしれないコラ用画像です。

 そのまま使うにはちょっと工夫がいるかな・・・・・


参考資料

あとがき

 画像の著作権とか調べるの結構手間なのでうまく効率化したいな


 

この記事が参加している募集

やってみた

この記事が気に入ったらサポートをしてみませんか?