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

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

こんなことやります。

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

この記事は、ブログサイトのアイキャッチ画像(サムネ)をPythonで自動生成する企画の第二弾になります。 前回は 【Pythonでサムネ制作①】PILで画像の上に文字を重ねて中央表示 をやりました。今回は、画像の上に透過画像を重ねるにはどうすればよいかを解説していきますね!

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

画像の上に画像を重ねる

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

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

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

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

プログラムはこんな感じです。めちゃ簡単ですね。

a.py
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() メソッドの第三引数を省略して出力してみますと、↓下の写真のように透過部分が黒く塗りつぶされてしまいます。

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

画像の位置を調整

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

b.py
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で画像の上に文字を重ねて中央表示 でもやりましたので簡単ですね。

出力結果
出力結果

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

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

c.py
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で画像の上に文字を重ねて中央表示 でやったテキストも組み合わせて、透過画像と文字を画像の上に重ねてみましょう。

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

d.py
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 x Watchdog】 の技術を組み合わせてサムネを自動生成させてます。

e.py
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ファイルはこんな感じです。

eyecatch.yml
common_yaml: develop.yml
text: "画像を重ねる"
subtext: "Pythonでサムネ制作への道"
icon_path: file-format.png
stroke_width: 10

プログラムの詳細は解説しませんが、一つ一つの技術は難しいものではありません。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

この記事で紹介した商品

Pandasで画像・動画処理をはじめよう!
Python学習にオススメの本をご紹介!
関連記事