今回は、YouTubeの解説系動画でおなじみの ゆっくり霊夢 をPythonのtkinterを使って瞬きさせてみた。
作成の動機と経緯
元々は、ラズパイとか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