t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

Python tkinterで ゆっくり霊夢 を瞬きアニメーション

今回は、YouTubeの解説系動画でおなじみの ゆっくり霊夢 をPythonのtkinterを使って瞬きさせてみた。
f:id:t-hom:20210806211627g:plain

作成の動機と経緯

元々は、ラズパイとかArduinoを駆使してデジタル秘書を作りたいというのが発端。
と言ってもそんなに技術はないので、出来上がったのは「〇時〇分です、〇〇をしてください。」と発話するリマインダーボット。
これを作るのは全然大したことではなくて、ラズパイでcronにaplayを仕込んでるだけ。WindowsでいうとTask Schedularで特定の時間に特定のwav再生させてるだけと言えばイメージ湧くだろうか。

このツール、しばらく運用してみて2つの要望が生じた。

  • 折角しゃべるんだから更に愛着が持てるように顔を付けたくなる。
  • wavファイルの更新が面倒なので音声合成にしたい。

長らくこれは願望どまりだったが、先日ラズパイにゆっくりボイス(AquesTalk Pi)をインストールできることを知り、だったら顔もゆっくりで行くか!てな感じで放置されていた改修案件が動き出した。
これが今回の話の発端である。

音声合成から再生までの箇所は検証済なので、今回やりたいのはtkinterでゆっくりキャラのアニメーションだ。

コード

import os
import tkinter
import time
from PIL import Image, ImageTk

script_path = os.path.dirname(os.path.abspath(__file__))
charactor =  "Reimu"

root = tkinter.Tk()
root.title("Yukkuri")
root.geometry("500x380")
root['background']='#800000'

resource_path = script_path + '/' + charactor
body = Image.open(resource_path + '/body/00.png')
skin = Image.open(resource_path + '/skin/00a.png')
mouth = Image.open(resource_path + '/mouth/00.png')
brow = Image.open(resource_path + '/brow/00.png')
eye = [Image.open(resource_path + '/eye/00.png'),
        Image.open(resource_path + '/eye/00a.png'),
        Image.open(resource_path + '/eye/00b.png'),
        Image.open(resource_path + '/eye/00c.png'),
        Image.open(resource_path + '/eye/00d.png'),
        Image.open(resource_path + '/eye/00e.png')]

def createface(n):
    face = Image.alpha_composite(body, eye[n])
    face = Image.alpha_composite(face, skin)
    face = Image.alpha_composite(face, mouth)
    face = Image.alpha_composite(face, brow)
    return face

charactor_image = ImageTk.PhotoImage(image=createface(1))
canvas = tkinter.Canvas(root, width=400, height=320, bd=0, highlightthickness=0, relief='ridge')
canvas['background']=root['background']
imagearea = canvas.create_image(0, 0, image=charactor_image, anchor=tkinter.NW)
canvas.pack()


def animation():
    global charactor_image
    for i in range(6):
        time.sleep(0.05)
        charactor_image = ImageTk.PhotoImage(image=createface(i))
        canvas.itemconfig(imagearea, image=charactor_image)
        canvas.update()
    for i in reversed(range(6)):
        time.sleep(0.05)
        charactor_image = ImageTk.PhotoImage(image=createface(i))
        canvas.itemconfig(imagearea, image=charactor_image)
        canvas.update()
    root.after(3000, animation)


root.after(3000, animation)
root.mainloop()

コーディングでハマったところ

関数内で作成されたImageは関数がおわると崩壊する

最初はanimationが1回終わるとキャラが消失するという事象に悩まされていた。
原因として、関数内で作成されたImageは通常、関数スコープを抜けると消えてしまうようだ。
キャンバス内で保持されるから大丈夫だろうと思っていたんだけど、恐らくキャンバスは画像を受け取って自分で保持しているわけではなく、画像を指定された変数を保持しているんだろう。
だからローカル変数にイメージを格納しているとスコープを抜けたら変数が消えてキャンバスの表示も消える。
animation関数でglobal charactor_imageと書いている部分がその対策として入れたコードである。

after関数に指定する関数名にカッコを付けると即時呼び出しになる

root.after(3000, animation)という記述は、3秒後にanimation関数を呼び出すようスケジュールしなさいという命令だ。
最初、root.after(3000, animation())という風にanimation関数に()を付けていたのだが、なぜか3秒立たずに連続で呼び出されてしまう。しかも途中で再帰上限を超えたというエラーが出た。

この挙動は、pythonでは関数も値として扱うことができるためである。評価せずにそのまま関数値として取り扱う場合は()を付けてはいけない。

今後の展望

記事にするかどうかは別として、次は発話できるようにしたい。
方法としては、subprocessでLinuxのaplayを実行し、ループ中のpollでsubprocessが終了したからどうかををみ取りつつ、終了するまでの間口パクアニメーションを流すということを考えている。

そこまでできればちょっとしたガジェットなのでラズパイに入れて運用しつつ、感情表現を増やしていこうかなと思う。

ゆっくり霊夢って何?

これが分かりやすいかも。
youtu.be

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。