【Pythonでサムネ制作②】PILで画像の上に透過画像を重ねる

【Pythonでサムネ制作②】PILで画像の上に透過画像を重ねる

こんなことやります。

  • 画像の上に画像を重ねる
  • 画像の位置を調整
  • 画像の大きさをリサイズして重ねる
  • 透過画像と文字を画像の上に重ねる
  • YAMLで管理してWatchdogで自動生成

この記事は、ブログサイトのアイキャッチ画像(サムネ)をPythonで自動生成する企画の第二弾になります。

前回は【Pythonでサムネ制作①】PILで画像の上に文字を重ねて中央表示をやりました。今回は、画像の上に透過画像を重ねるにはどうすればよいかを解説していきますね!

このページのアイキャッチ画像は、今回ご紹介する方法でPythonによって自動生成したものです。文言の修正や色使いなどをYAMLで管理して、YAMLが更新されたらWatchdogでイベントフックしてコンパイルさせています。

ご興味ある方はぜひ、最後までお付き合いください!

画像の上に画像を重ねる

それではさっそく、画像の上に画像を重ねていきます。まずは「ベースとなる画像」とその上に「重ねたい画像」を用意します。

ベースとなる画像
ベースとなる画像

↑この画像に↓こちらのロゴ画像を重ねていきます。ロゴ画像はPNGファイルで作られており、実際の背景は透過であります。

重ねたい画像
重ねたい画像

プログラムはこんな感じです。めちゃカンタンですね。

from PIL import Image
 
base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル

base = Image.open(base_path)
logo = Image.open(logo_path)

# base.paste(logo, (0, 0))
base.paste(logo, (0, 0), logo)
base.save(out_path)

プログラムを実行すると↓こんな感じで画像の上に画像を重ねることができました。

出力結果
出力結果

プログラミングのポイント

プログラム中の paste() は、画像に画像を重ねるためのメソッドです。重ねたい画像を第一引数に指定し、位置をタプルで第二引数にしていします。

また、paste() メソッドの第三引数はマスク画像を指定するためのものです。透過画像自身をマスク画像とすることで、透明部分が表現できているワケですね!

仮に、paste() メソッドの第三引数を省略して出力してみますと、↓下の写真のように透過部分が黒く塗りつぶされてしまいます。

マスクをかけないと透過が有効にならない
マスクをかけないと透過が有効にならない

画像の位置を調整

次に重なる画像の位置を調整してみます。左下にロゴ画像を配置したいので、↓こんな感じでプログラミングしてみました。

from PIL import Image
 
base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル

base = Image.open(base_path)
logo = Image.open(logo_path)

base_w, base_h  = base.size
logo_w, logo_h  = logo.size

# print('base_w:{}, base_h:{}'.format(base_w, base_h))
# print('logo_w:{}, logo_h:{}'.format(logo_w, logo_h))

base.paste(logo, (0, base_h - logo_h), logo)
base.save(out_path)

プログラムを実行します。左下にロゴが表示されましたね。位置設定は【Pythonでサムネ制作①】PILで画像の上に文字を重ねて中央表示でもやりましたのでカンタンですね。

出力結果
出力結果

画像の大きさをリサイズして重ねる

今度は重なる画像の大きさをリサイズして重ねてみましょう。画像を少し大きくして重ねるにはこんな感じのプログラミングになります。

from PIL import Image
 
base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル

base = Image.open(base_path)
logo = Image.open(logo_path)


base_w, base_h  = base.size
logo_w, logo_h  = logo.size

scale = 1.5
logo_resized = logo.resize((int(logo_w * scale), int(logo_h * scale))) # リサイズ
logo_resized_w, logo_resized_h  = logo_resized.size

base.paste(logo_resized, (0, base_h - logo_resized_h), logo_resized)
base.save(out_path)

はい。↓ロゴが少し大きくなって重ねることができました。

出力結果
出力結果

プログラミングのポイント

resize() メソッドにタプルでリサイズしたいwidth、heightの値を渡します。ただしint型でないとエラーになりますのでご注意ください。

ちなみに、画像の一部分をくり抜きたい場合は crop() を使います。

透過画像と文字を画像の上に重ねる

応用編としまして、前回の【Pythonでサムネ制作①】PILで画像の上に文字を重ねて中央表示でやったテキストも組み合わせて、透過画像と文字を画像の上に重ねてみましょう。

位置決めなどはハードコーディングで調整する必要がありますが、できるだけ関数にまとめてみました。

from PIL import Image,ImageFont,ImageDraw


def overlay_text(base_img, text, font_path, font_size, font_color, stroke_color, stroke_width):
    font = ImageFont.truetype(font_path, font_size)
    draw = ImageDraw.Draw(base_img)

    font_w, font_h = font.getsize(text, stroke_width=stroke_width)
    base_img_w, base_img_h  = base_img.size
    margin_left = 40
    margin_bottom = 50
    
    pos = (margin_left, base_img_h - font_h - margin_bottom)

    draw.text(
        pos, text,
        font=font, 
        fill=stroke_color,
        stroke_width=stroke_width * 2,
        stroke_fill=font_color)

    draw.text(
        pos, text,
        font=font, 
        fill=font_color,
        stroke_width=stroke_width,
        stroke_fill=stroke_color)

    return base_img

def overlay_logo(base_img, path, scale):
    logo = Image.open(path)
    base_w, base_h  = base_img.size
    logo_w, logo_h  = logo.size

    logo_resized = logo.resize((int(logo_w * scale), int(logo_h * scale))) # リサイズ
    logo_resized_w, logo_resized_h  = logo_resized.size
    margin_bottom = 170
    base_img.paste(logo_resized, (0, base_h - logo_resized_h - margin_bottom), logo_resized)
    return base_img


base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル


text = 'Pandasでデータ解析・使い方'
font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W7.ttc'
font_size = 100
font_color = (255,248,196) # 文字の色
stroke_color = (112, 96, 85) # 枠線の色
stroke_width = 10


base = Image.open(base_path)
base = overlay_logo(base, logo_path, 1.0)
base = overlay_text(base, text, font_path, font_size, font_color, stroke_color, stroke_width)
base.save(out_path)

はい。↓サムネの完成でございます。

出力結果
出力結果

これでサムネを半自動化で作れるようになりましたね!

プログラミングのポイント

テキストの枠線を二重に重ねるには、 draw.text()stroke_width の太さを変えて二回呼び出しているのがポイントです。

YAMLで管理してWatchdogで自動生成

さいごに「おまけ」としまして、このサイトを生成している自作ジェネレータのサムネ自動化プログラムをご紹介します。ここまで解説した技術とPythonでファイルが更新されたらイベントフックするWatchdogの使い方の技術を組み合わせてサムネを自動生成させています。

import yaml
import os
import conf
from PIL import Image,ImageFont,ImageDraw
import time
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler


class WatchdogHandler(PatternMatchingEventHandler):
    def __init__(self, callback, patterns):
        super(WatchdogHandler, self).__init__(patterns=patterns)
        self.callback = callback

    def __callback_handler(self, func,*args):
        return func(*args)

    def on_modified(self, event):
        print(event)
        self.__callback_handler(self.callback)


def watch(path, command, extensions):
    event_handler = WatchdogHandler(command, extensions)
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()



def __get_fit_font_size(text, font_path, stroke_width, font_size = 200, limit_width = 1920, margin = 200):
    font = ImageFont.truetype(font_path, font_size)
    w, h = font.getsize(text, stroke_width=stroke_width)
    if w < (limit_width - margin):
        return font_size
    print('{}, {}, {}, {}'.format(text, font_path, stroke_width, font_size - 5))
    return __get_fit_font_size(text, font_path, stroke_width, font_size - 5)


def __overlay_text(base_img, text, font_path, font_size, font_color, stroke_color, stroke_width):
    font = ImageFont.truetype(font_path, font_size)
    draw = ImageDraw.Draw(base_img)

    font_w, font_h = font.getsize(text, stroke_width=stroke_width)
    base_img_w, base_img_h  = base_img.size
    margin_left = 40
    margin_bottom = 50
    
    pos = ((base_img_w - font_w)/2, (base_img_h - font_h)/2)

    draw.text(
        pos, text,
        font=font, 
        fill=font_color,
        stroke_width=stroke_width,
        stroke_fill=stroke_color)

    return base_img

def __overlay_logo(base_img, path, scale):
    logo = Image.open(path)
    base_w, base_h  = base_img.size
    logo_w, logo_h  = logo.size
    logo_resized = logo.resize((int(logo_w * scale), int(logo_h * scale))) # リサイズ
    logo_resized_w, logo_resized_h  = logo_resized.size
    margin_right = 20
    margin_bottom = 50
    base_img.paste(logo_resized, (base_w - logo_resized_w - margin_right, base_h - logo_resized_h - margin_bottom), logo_resized)
    return base_img


def make(target_dir):
    yml_path = target_dir + '/' + 'eyecatch.yml'
    with open(yml_path) as file:
        cf = yaml.safe_load(file)

    base_path = target_dir + '/' + cf['base_path']
    logo_path = conf.LOGO_DIR + '/' + cf['logo_path']
    out_path = target_dir + '/' + cf['out_path']

    text = cf['text']
    font_path = cf['font_path']

    font_color = tuple(cf['font_color'])
    stroke_color = tuple(cf['stroke_color'])
    stroke_width = cf['stroke_width']

    font_size = __get_fit_font_size(text, font_path, stroke_width)
    base = Image.open(base_path)
    base = __overlay_logo(base, logo_path, 0.9)
    base = __overlay_text(base, text, font_path, font_size, font_color, stroke_color, stroke_width)
    base.save(out_path)


class WatchdogHandler(PatternMatchingEventHandler):
    def __init__(self, patterns):
        super(WatchdogHandler, self).__init__(patterns=patterns)

    def on_modified(self, event):
        target_dir = os.path.dirname(event.src_path)
        make(target_dir)





if __name__ == "__main__":
    dir_to_watch = conf.SOURCES_DIR + '/blog'
    extensions = ["*.yml"]

    event_handler = WatchdogHandler(extensions)
    observer = Observer()
    observer.schedule(event_handler, dir_to_watch, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

↓ YAMLファイルはこんな感じです。

common_yaml: common-python-eyecatch.yml
text: "画像を重ねる"
icon_path: file-format.png

プログラムの詳細は解説しませんが、一つ一つの技術は難しいものではありませんので↓過去の記事もあわせてご参考になさってみてください。

記事に関するご質問などがあれば、
Twitter または お問い合わせ までご連絡ください。
Python学習にオススメの本をご紹介!
Pandasでデータサイエンスはじめよう!
関連記事