Python

psdファイルをPythonで読み込みレイヤーの表示・非表示を切り替える

Posted on 2022年3月9日 (Last modified on 2023年1月14日) 2 min read  • 413 words
psdファイルをPythonで読み込みレイヤーの表示・非表示を切り替える

TOP2 | 春日部つむぎ公式HP

最近の動画で使用させていただいている春日部つむぎのpsdファイルを読み込み、動画で使用する口パク動画を作りたいです。 手元にWindowsPCがないためAviUtilが使えず、代替としてPythonで自作してみたいと思います。

※利用規約により画像の配布は禁止されているため、実行結果は規約を読んで素材を取得した上で確認してください

psd-tool

psd-tools — psd-tools 1.9.18 documentation

以下の環境で実行しています。

python = "^3.8"
numpy = "^1.22.3"
psd-tools = "^1.9.18"
matplotlib = "^3.5.1"
jupyter = "^1.0.0"
ipykernel = "^6.9.1"

ファイル読み込み+グループ・レイヤーの確認

import psd_tools
psd = psd_tools.PSDImage.open('./春日部つむぎ立ち絵_公式_v1.1.1/春日部つむぎ立ち絵_公式_v1.1.1.psd')

for group in psd:
    print(group)
    for layer in group:
        print(f'---- {layer}')
Group('!体部分' size=1658x3868)
---- PixelLayer('髪\u3000サイドテール(反転用)' size=340x809 invisible)
---- PixelLayer('髪\u3000ロング' size=764x743)
---- PixelLayer('体' size=1422x3277)
---- PixelLayer('スカート' size=850x579)
---- PixelLayer('シャツ' size=931x1174)
---- PixelLayer('セーター' size=1504x1312)
---- PixelLayer('シュシュ' size=243x284)
---- PixelLayer('ネクタイ' size=275x753)
---- PixelLayer('ネックレス' size=121x123)
---- PixelLayer('缶バッチなど' size=803x746)
---- PixelLayer('腕章' size=158x242)
---- PixelLayer('髪\u3000サイドテール' size=340x857)
---- PixelLayer('髪\u3000もみあげ' size=587x680)
---- PixelLayer('顔' size=383x453)
---- PixelLayer('髪\u3000前髪' size=764x710)
Group('!口' size=76x11)
---- PixelLayer('*3' size=10x25 invisible)
---- PixelLayer('*ω\u3000わ' size=76x58 invisible)
---- PixelLayer('*ω' size=76x11)
---- PixelLayer('*べー' size=61x25 invisible)
...

画像にして確認

import psd_tools
psd = psd_tools.PSDImage.open('./春日部つむぎ立ち絵_公式_v1.1.1/春日部つむぎ立ち絵_公式_v1.1.1.psd')

psd.composite()

指定したレイヤーを非表示にする

visibleをFalseに設定することで非表示に。opacityを0にすることでも非表示にしてもいいかもしれません。Layer/Groupにvisibleの設定が指定でき、ここでは口のレイヤーを非表示にしてみます。ただし、自分の環境ではforce=Trueとしないとレイヤーを非表示にできませんでした。

force – Boolean flag to force vector drawing.

ということですが、なぜ force=False だと出力されないのかは不明です。

import psd_tools
psd = psd_tools.PSDImage.open('./春日部つむぎ立ち絵_公式_v1.1.1/春日部つむぎ立ち絵_公式_v1.1.1.psd')

for group in psd:
    name = group.name
    if name == '!口':
        group.visible = False

psd_without_mouse = psd.composite(force=True)
psd_without_mouse

一部レイヤーのvisible/opacityを変更したpsdを画像として保存する

ここまで来れば、総当たりで全ての表情の組合せをpngにして出力することができます。口だけ変えた全パターンを出力してみます。

visibleを一枚ずつ変化させて画像を出力させると、なぜか出力される画像に欠損が発生してしまいました。そのため、全てのレイヤーでvisible=Trueとした後で透明度を0 (opacity=0) にした上で、一枚ずつ透明度を上げて画像を出力しています。

import os
import psd_tools
import time

output_dir = './output_image'
os.makedirs(output_dir, exist_ok=True)
psd = psd_tools.PSDImage.open('./春日部つむぎ立ち絵_公式_v1.1.1/春日部つむぎ立ち絵_公式_v1.1.1.psd')

for group in psd:
    name = group.name
    if name == '!口':
        # 一旦すべてのレイヤーの透明度を0にする
        for i, layer_i in enumerate(group):
            layer_i.visible = True
            layer_i.opacity = 0

        # レイヤーを一つずつ表示して画像に出力
        for i, layer_i in enumerate(group):
            print(f'> {layer_i.name} の画像を出力中...')
            layer_i.opacity = 255
            img = psd.composite(force=True)
            img.save(os.path.join(output_dir, f'口={layer_i.name}.png'))
            layer_i.opacity = 0
            del img
> *3 の画像を出力中...
> *ω わ の画像を出力中...
...

全ての表情パターンの画像を出力

itertoolsを用いて全ての表情パターンを上と同じ要領で出力します。一枚出力するのに数秒かかるため、全て出力するのにかなり時間がかかるので注意してください。

import itertools
import os
import psd_tools
import time

output_dir = './output_image'
os.makedirs(output_dir, exist_ok=True)
psd = psd_tools.PSDImage.open('./春日部つむぎ立ち絵_公式_v1.1.1/春日部つむぎ立ち絵_公式_v1.1.1.psd')

mouses, eyes, eyebrows = None, None, None
for group in psd:
    name = group.name
    if name in {'!口', '!目', '!眉'}:
        # 一旦group内のすべてのレイヤーを非表示にする
        for i, layer_i in enumerate(group):
            layer_i.visible = True
            layer_i.opacity = 0
            
        if name == '!口':
            mouses = group
        elif name == '!目':
            eyes = group
        elif name == '!眉':
            eyebrows = group

# レイヤーを一つずつ表示して画像に出力
for mouse, eye, eyebrow in itertools.product(mouses, eyes, eyebrows):
    if not eyebrow.name == '普通':
        continue  # 実行時間が長いため眉毛は普通のみ

    print(f'> 口={mouse.name} 目={eye.name} 眉={eyebrow.name} の画像を出力中...')
    mouse.opacity = 255
    eye.opacity = 255
    eyebrow.opacity = 255
    img = psd.composite(force=True)
    img.save(os.path.join(output_dir, f'口={mouse.name}_目={eye.name}_眉={eyebrow.name}.png'))
    mouse.opacity = 0
    eye.opacity = 0
    eyebrow.opacity = 0
> 口=*3 目=*赤目 口=*ん? の画像を出力中...
> 口=*3 目=*赤目 口=*真面目 の画像を出力中...
> 口=*3 目=*赤目 口=*悲しい の画像を出力中...
> 口=*3 目=*赤目 口=*怒る の画像を出力中...
> 口=*3 目=*赤目 口=*困る の画像を出力中...
> 口=*3 目=*赤目 口=*普通 の画像を出力中...
> 口=*3 目=*白目 口=*ん? の画像を出力中...
> 口=*3 目=*白目 口=*真面目 の画像を出力中...
> 口=*3 目=*白目 口=*悲しい の画像を出力中...
> 口=*3 目=*白目 口=*怒る の画像を出力中...
> 口=*3 目=*白目 口=*困る の画像を出力中...
> ...
¯\_(ツ)_/¯

こんちゃす。