アドベントスターターセット

f:id:kohmoli:20211209210920p:plain


本エントリは鉄球連盟アドベントカレンダー1日目の記事です。

目次

1. 美少女に転生する話
2. 計算機を設計する話
3. 劇場版を観劇する話

はじめに

対戦よろしくお願いします。本エントリは『美少女に転生する話』『計算機を設計する話』『劇場版を観劇する話』の全3編です。といっても、文字数は3編合わせても『その欲望……解放しろ!』より少ない*1ので、どなたもお気軽にお読みください。異常者集団の王ことゆかりおんさんは別格として、本エントリは鉄球連盟の皆さんに負けずとも劣らない出来合いと自負しております。それではどうか、本エントリをお楽しみいただければ幸いです。

1. 美少女に転生する話


本話はたいへん健全ですので、どなたも安心してお読みいただけます。


近年話題の「VTuber」をご存知ですか? 実はVTuberとは、アクターの表情や動作を2DCG・3DCGで再現して動画を制作するYouTuberの形態の1つなんです! いったい、どのようにしてイラストを動かしているのでしょうか? 無料でVTuberになる方法は? 調べてみました!

手順1 立ち絵を描く

はい
f:id:kohmoli:20211209214925p:plain



手順2 Pythonを用いて表情識別・音声変換を行う

音声変換では機械学習による手法や声の高さ・ピッチを変更する手法が一般的ですが、実際に試した結果いずれも高い精度を得られませんでした。よって、音声識別サービスを利用して音声をテキスト化し、テキストを音声合成ソフトに読み上げさせる手順をとります。なお、本エントリではPythonの実行環境にGoogle Colaboratoryを用います。

ドライブのマウント
from google.colab import drive
drive.mount('/content/drive')
ライブラリとソフトウェアのインストール

表情検出にはpazライブラリを利用します。が、実装途中でライブラリにバグ(クォータニオンが1次元配列で取得できない)を発見したので、sedコマンドでファイルを直に修正しています。また、動画読み込み・書き込み用のmoviepyにもバグ(書き出した動画が無音になる)があるようなのでこちらもファイルを直に修正しています。これらの処理が最新のバージョンで必要かは都度ご確認ください。年単位で放置されてるから多分修正されないでしょうけど。

ライブラリのインポート
import numpy as np
import moviepy.editor as mp

from paz.backend.camera import Camera
from paz.pipelines import DetectMiniXceptionFER
from paz.pipelines import HeadPoseKeypointNet2D32

import speech_recognition as sr
import subprocess
動画読込
clip = mp.VideoFileClip(videofile, fps_source='fps')
width, height = clip.size

frame_num = int(clip.duration * fps)
顔検出・表情識別
camera = Camera()
camera.distortion = np.zeros((4, 1))
camera.intrinsics = np.array([[width,     0,  width/2],
                              [    0, width, height/2],
                              [    0,     0,        1]])

detectPose = HeadPoseKeypointNet2D32(camera)
detectFace = DetectMiniXceptionFER()

emotions = []
poses = []

for i in range(frame_num):
  frame = clip.get_frame(i/fps)
  estimated_faces = detectFace(frame)['boxes2D']
  estimated_poses = detectPose(frame)['poses6D']
  emotions.append(estimated_faces[0].class_name if estimated_faces else None)
  poses.append(estimated_poses[0].quaternion.tolist() if estimated_poses else None)

顔の向きをクォータニオンで取得し、オイラー角に直します。表情の識別結果は基本6感情(angry、disgust、fear、happy、sad、surprise)とneutralの7種類の文字列のいずれかです。ちなみに、動画読込に利用したmoviepyはRGB形式、pazが内部で利用するOpenCVはBGR形式で画像を扱うので実はRとBが入れ替わっているのですが、どうせ機械学習モデルに入力するときはグレースケールに自動変換されると思うので無視します。

音声識別
recognizer = sr.Recognizer()

tb = 0
audioclip = clip.audio
texts = []
start_times = []

for t in range(int(clip.duration*10)):
  data = audioclip.subclip(t/10, t/10 + 1).to_soundarray()[:, 0]

  if len(data[np.abs(data) > 0.05]): continue
  if t == tb:            tb = t + 1; continue

  audioclip.subclip(tb/10, t/10).write_audiofile('tmp.wav')
  with sr.AudioFile('tmp.wav') as src: audio = recognizer.record(src)
  txt = recognizer.recognize_google(audio, language='ja-JP')
  
  tb = t + 1

テキスト化すると無音時間の情報が失われるので、あらかじめ音声を無音箇所で分割します。音声識別にはSpeech Recognitionライブラリを利用します。Speech RecognitionはGoogle Speech API等の音声識別APIPythonから呼び出すためのライブラリなので、別途API側でアカウント登録をして引数で認証キーを指定する必要がある……はずなのですが、なぜか引数を指定しなくても動きます。引数を設定しない場合はGoogle Speech APIを利用しているようですが、なぜ動くのか確かなことは分かりませんでした。


いかがでしたか?


一応、ライブラリの公式サンプルには「開発時のみ利用可」と注釈があるため、利用すべきではないでしょう。あと、APIにはwavファイルのパスを渡す必要があるようで、全く好ましくないのですがtmp.wavに書き出してからAPIに渡しています。ここ上手い方法があったら教えてください。

音声合成

音声合成にはOpen JTalkを利用します。別の音声合成ソフトウェアに「ゆっくりしていってね!」で有名なSofTalkがありますが、exeファイルなのでGoogle Colaboratoryで利用するにはwineをインストールする必要があります。

for i, text in enumerate(texts):
  subprocess.Popen(['open_jtalk',
                    '-x', '/var/lib/mecab/dic/open-jtalk/naist-jdic',
                    '-m', '/usr/share/hts-voice/mei/mei_normal.htsvoice',
                    '-ow', f'{i}.wav'],
                   stdin = subprocess.PIPE).communicate(txt.encode())

手順3 Live2Dでアニメーションを作る

やるだけ

手順4 動画を生成する

アニメーションを連番で書き出したら(別に動画でもいいですがLive2Dのフリープランだと動画にロゴが入るらしいので)GoogleDriveにアップロードして動画を書き出します。

captureclip = mp.VideoFileClip(capturefile, fps_source='fps')
scale = np.minimum(1440/captureclip.size[0], 1080/captureclip.size[1])
captureclip = captureclip.resize((captureclip.size[0]*scale, captureclip.size[1]*scale))

background = mp.ImageClip(bg_filename)

clips = []
chara_clips = []
ps = 0
for emotion, pose in zip(emotions, poses):
  em = 'neutral'
  if emotion == 'happy':
    em = 'happy'
  elif emotion == 'sad':
    em = 'sad'
  
  if pose is not None:
    x, y, z, w = pose
    theta = np.arctan2(-2*x*y-2*w*z, 1-2*x*x-2*z*z)
    ps = np.maximum(np.minimum(int(25*theta/0.25), 24), -25) + 25
  
  chara = mp.ImageClip(f'{character_dir}/{em}/{str(ps).zfill(3)}.png')
  clips.append(background.set_duration(1/fps))
  chara_clips.append(chara.set_duration(1/fps))

newclip = mp.concatenate_videoclips(clips, method='compose')
newcharaclip = mp.concatenate_videoclips(chara_clips, method='compose')
newclip = mp.CompositeVideoClip([newclip, newcharaclip.set_position((1413, 316)), captureclip.set_position(((1440-captureclip.size[0])/2, (1080-captureclip.size[1])/2))])

for i, start in enumerate(start_times):
  audioclip = mp.AudioFileClip(f'{i}.wav')
  newclip = newclip.set_audio(mp.CompositeAudioClip([newclip.audio, audioclip.set_start(start)]))

newclip.write_videofile('output.mp4', fps=30, codec='libx264', audio_codec='aac', temp_audiofile='temp-audio.m4a', remove_temp=True)

完成動画


最後になりますが、私はプログラムを書いただけで私自身が今後VTuberとして活動することはありませんので絶対に間違えないでください。本当は高専卒業前にエロゲでも実況してTeamsと紐づいている動画共有サービスに投稿しようと思ったのですが、学生にはアップロード権限が無いようだったので断念しました。


いかがでしたか?




計算機を設計する話


本話はたいへん健全ですので、どなたも安心してお読みいただけます。


高専の授業で計算機を作ろうとしたことがあるのですが、結局それは完成しませんでした。なので高専卒業前に計算機およびコンパイラ作成に再挑戦したいと思います。

計算機アーキテクチャ

f:id:kohmoli:20211210003219p:plain
これから作成するのは、いわゆるスタックマシンです。と言いつつ、スタックだけでなくデータレジスタにもデータは格納できます。ご存知の通り、スタックはスタックトップにしかデータを追加できず、1度取り出したデータをもう1度取り出すことはできません。対してデータレジスタは任意の位置にデータを書き込め、何度でも読み込むことができます。一見すると、スタックは不要そうです。実際に1つ上の先輩が授業で作成した計算機はこれと同じアーキテクチャでしたが、私達の授業作品ではスタックを省きました。それによって計算機側の実装コストは軽くなったかもしれません。しかし、コンパイラ側の実装からするとスタックはあった方が楽です。
スタックの利点は、まさしく「一度しか取り出せない」点です。例えば2*3+4*5という式を計算するとき、2*3=6を計算した後に4*5=20を計算するためには、6をどこかに記憶しておく必要があります。けれど6は6+20=26を計算した後は不要になります。このようなデータをいちいちデータレジスタに格納しては削除するのは大変面倒です。しかし、スタックがあればコンパイラの実装は単純になります。

命令セットアーキテクチャ

データ コード 説明
NUMBER 1xxxxxxxxxxxxxxxx 16ビットの符号なし整数
命令 コード1 引数 説明
NOP 00000 0000 なし プログラムカウンタを停止させる
LOAD 00001 1100 あり データレジスタの引数アドレスを参照して値をスタックに入れる
STORE 00010 1010 あり スタックの値を1つ取り出してデータレジスタの引数アドレスに格納する
ADD 00011 1010 なし スタックの値を2つ取り出し和をスタックに入れる
SUB 00100 1010 なし スタックの値を2つ取り出し差をスタックに入れる
MUL 00101 1010 なし スタックの値を2つ取り出し積をスタックに入れる
DIV 00110 1010 なし スタックの値を2つ取り出し商をスタックに入れる
MOD 00111 1010 なし スタックの値を2つ取り出し剰余をスタックに入れる
NOT 01000 1000 なし スタックの値を2つ取り出し論理否定をスタックに入れる
OR 01001 1010 なし スタックの値を2つ取り出し論理和をスタックに入れる
AND 01010 1010 なし スタックの値を2つ取り出し論理積をスタックに入れる
EQ 01011 1010 なし スタックの値を2つ取り出し等しければ1、さもなくば0をスタックに入れる
LT 01100 1010 なし スタックの値を2つ取り出し小さければ1、さもなくば0をスタックに入れる
GT 01101 1010 なし スタックの値を2つ取り出し大きければ1、さもなくば0をスタックに入れる
JMP 01110 0000 あり プログラムカウンタを引数の値に設定する
BEQ 01111 0010 あり スタックの値を1つ取り出しLSBが0であればプログラムカウンタを引数の値に設定する

先代の授業作品でも私達の授業作品でもNOP(何もしない命令)が仕様に含まれていましたが、今回の計算機にNOPは不要です。プログラムを任意時間停止させる処理はループで実現できるからです。

計算機の実装

細部はVerilogソースコードを見た方が分かりやすいでしょう。今回はコードの短さを重視しましたが、授業課題で取り組むならALUやシーケンサといった機能ごとに分割した方が丁寧かつ作業分担しやすいと思います。ちなみに、たったこれだけのコードをビルドするのに1時間弱かかりました。分割云々というよりはデータの桁数が無駄に大きいのが原因な気がします。

module StackMachine(input wire CLK1_50, output wire [7:0] HEX0, HEX1, HEX2, HEX3);
   reg [7:0]  pc = 0, sp = 0;
   reg [16:0] instructions[0:255];
	reg [19:0] clk = 0;

   reg signed [15:0] stack[0:255], data[0:255];

   initial $readmemb("./Program", instructions);
   
	always @(posedge CLK1_50) clk = clk + 1;
   always @(posedge clk[19]) begin
      if(instructions[pc][11] || instructions[pc][16]) pc <= pc + 1;
      if(instructions[pc][10] || instructions[pc][16]) sp <= sp + 1;
      if(instructions[pc][9]                         ) sp <= sp - 1;

      case(instructions[pc][16:12])
         5'b00000: ;
         5'b00001: stack[sp] <= data[instructions[pc][7:0]];
         5'b00010: data[instructions[pc][7:0]] <= stack[sp-1];
         5'b00011: stack[sp-2] <= stack[sp-2] + stack[sp-1];
         5'b00100: stack[sp-2] <= stack[sp-2] - stack[sp-1];
         5'b00101: stack[sp-2] <= stack[sp-2] * stack[sp-1];
         5'b00110: stack[sp-2] <= stack[sp-2] / stack[sp-1];
         5'b00111: stack[sp-2] <= stack[sp-2] % stack[sp-1];
         5'b01000: stack[sp-1] <= ~stack[sp-1];
         5'b01001: stack[sp-2] <= stack[sp-2] | stack[sp-1];
         5'b01010: stack[sp-2] <= stack[sp-2] & stack[sp-1];
         5'b01011: stack[sp-2] <= stack[sp-2] == stack[sp-1];
         5'b01100: stack[sp-2] <= stack[sp-2] < stack[sp-1];
         5'b01101: stack[sp-2] <= stack[sp-2] > stack[sp-1];
         5'b01110: pc <= instructions[pc][7:0];
         5'b01111: pc <= stack[sp-1][0] ? pc + 1 : instructions[pc][7:0];
         default : stack[sp] <= instructions[pc][15:0];
      endcase
   end

   BCD_TO_SEG s0(.BCD(data[0][3:0]),   .SEG(HEX0));
   BCD_TO_SEG s1(.BCD(data[0][7:4]),   .SEG(HEX1));
   BCD_TO_SEG s2(.BCD(data[0][11:8]),  .SEG(HEX2));
   BCD_TO_SEG s3(.BCD(data[0][15:12]), .SEG(HEX3));
endmodule


module BCD_TO_SEG(input wire [3:0] BCD, output wire [7:0] SEG);
	reg [7:0] sig;

	assign SEG = sig;

   always @* begin
		case(BCD)
			4'h0: sig = 8'b11000000;
			4'h1: sig = 8'b11111001;
			4'h2: sig = 8'b10100100;
			4'h3: sig = 8'b10110000;
			4'h4: sig = 8'b10011001;
			4'h5: sig = 8'b10010010;
			4'h6: sig = 8'b10000010;
			4'h7: sig = 8'b11011000;
			4'h8: sig = 8'b10000000;
			4'h9: sig = 8'b10010000;
			default: sig = 8'b11111111;
		endcase
	end
endmodule

コンパイラの実装

授業課題時よりは短くなったとはいえ、200行以上あるのでコードは流石に省略します。逆ポーランド記法については詫間キャンパスの教員が公開していた授業資料*2を参考にしました。スタックマシンを実装するなら逆ポーランド記法の知識はほぼ必須っぽいです。もし授業で取り扱っていて、私が忘れただけだったら申し訳ないですが……

プログラミング

サンプルとして、フィボナッチ数列を第10項まで出力するプログラムを書きます。

a = 0
b = 1
i = 0

while i <= 10
    c = a
    a = b
    b = b + c

    print c/10*16 + c%10

    i = i + 1


このようなプログラムをProgramに書いたら、以下のようにコンパイルしてInstruction.vを生成します。

python compiler.py Program

完成

視認性の都合上、クロックをかなり落として実行しています。どうせなら命令に合わせてLEDが光るようにすればもっと見栄えが良くなったかもしれません。


今回のアーキテクチャは命令に無駄なビットがありますね。普通のスタックマシンはオペランドが無いらしいです。関数も実装できていませんし、出来は良くありません。手元にFPGAさえあれば再々挑戦したいところです。それでもまあ一応動きましたし、とりあえず下手な設計書でも残します。何かの役に立てば幸いです。

劇場版を観劇する話


注意:本話は『少女☆歌劇レヴュースタァライト』シリーズのネタバレを含みます。


『劇場版 少女☆歌劇レヴュースタァライト』を観ました。とても素晴らしい作品だと思います。ミニ色紙は大場が当たりました。……他に語ることも無いですし、東京タワーがそこら辺に刺さるような作品に考察も何も無いだろうと思うのですが、ゆかりおんさんが書けというのでひとまず感想だけ述べます。



と思いましたが『少女☆歌劇レヴュースタァライト ロンド・ロンド・ロンド』未視聴なので私はそのステージに立てないようです。欲をいうなら劇場で見たいところですが、もうそんな機会は……

イオンシネマ シアタス心斎橋にて
再生産総集編「少女☆歌劇 レヴュースタァライト ロンド・ロンド・ロンド」と「劇場版 少女☆歌劇レヴュースタァライト」の2作品が上映決定いたしました!

12月10日(金)より上映開始となります!
ぜひぜひご観劇ください!


大阪で今日から始まるらしいです。じゃあ、感想文書かなきゃ……

狩りのレヴュー(revue song ペン:力:刀)

最初「どっちが狩る側?」ってなりました。そっちか〜。大場は裏ボスっぽいですが、負け方はデータキャラかもしれません。星見は裏主人公っぽいですね、努力と勝利はしても友情要素が無いから愛城みたいになれないところとか。星見はんってえらい賢そうやけど英語とか数学とかでも天堂はんに負けてたん? でも生徒会長!って一言で星見にも積み重ねてきたものが確かにあることが分かるシーンはとても良かったです。実は生徒会長になったのは唯一ネタバレされていたのですが、逆に心の準備ができて良かったと思っています。


📕

『少女☆歌劇レヴュースタァライト』には、脳内で脚色され素敵な思い出に変わる子供の頃の記憶のような雰囲気があるように思います。なので時系列や細部の描写について考察することに私は特段の意味を感じません。もちろん作り込まれてはいるのでしょうけど。

恨みのレヴュー(revue song わがままハイウェイ)

デコトラをバックに立ち止まって歌うところ一番好きだし、急に遊郭編始まるところ面白かったです(石動童貞概念!?)。オーディションのときは花柳流の道場が舞台でしたが、劇場版で突然デコトラが登場したのは2人の関係が石動主導に転じたことを示唆しているんでしょうか。

🚚

作中に「落ちていく」という台詞がありますが、私は「大人になる」という意味だと解釈しました。作中に流血の描写がありますが、女性が血を流すことは初経や破瓜といった、急速かつ不可逆な成長および神秘性の喪失を象徴します。青春時代の万能感からの覚醒や、ふわっとした夢をスケールダウンさせて地に足のついた現実的な将来を見ることを指して「落ちていく」と表しているのではないでしょうか。

競演のレヴュー(revue song MEDAL SUZDAL PANIC◎○●)

レヴューソングがホラーになるところが好きです。歌詞だけ見たら滅茶苦茶心配しているだけなんですけどね。本作のテーマが奪うか奪われるかなのに『1等星のプロキオン』ではニコニコ楽しく笑いあって平和に勝敗を決めていたので一番そぐわない気がしましたが、神楽に圧勝していて安心しました。まあスタリラやっていないのでシナリオ知らないんですけど。

🏅

本話を書くにあたって他の方の考察を読みましたが、トマト=禁断の果実とする考察が多かったです。『再生讃美曲(movie ver.)』にも「例えばそれがエデンの果実でも」という歌詞があり、リリックビデオでは監督が字書きを担当されています。エデンの果実は2種類あり、アダムとイブが口にしてしまい不死性を失うことになった善悪の実と、永遠の実があります。『再生讃美曲(movie ver.)』で「それが眩しい」と続く方が永遠の果実、「だから眩しい」と続く方が善悪の果実とも解釈できるかもしれません。永遠の木はオリーブやアカシアのような常緑の木とされます。オリーブの冠はオリンピックの優勝者に送られることで有名ですし、オリンピックにも時事ネタ以上の意味があるように感じられます。

魂のレヴュー(revue song 美しき人、或いは其れは)

クライマックスで曲調が『誇りと驕り』みたいになったところが一番テンション上がりました。やっぱり天堂のベストな相手役は西條って感じがして、とても良かったです。歌詞に分かりやすくお互いの名前が入っているのもお互いに分かり合っている感じがして、とても良かったです。天堂が今まで全部演技でしたとか言いだしたシーンは舞台版で激昂する天堂*3*4とか4コマでただの大食いキャラになった天堂*5とかを思い返して首を傾げました。それとも大食いキャラも食材的な伏線だったんですかね。そんなわけないか。

🍅

能動的に禁断の果実を口にする行為は、親からの独立やモラトリアムからの卒業を示唆していると考えられます。「自立」はよく肯定的に捉えられますが、一神教において「神の庇護下から独立する」ことを能動的に選択し、それを肯定することは私の中に無かった発想でした。神の存在・非存在を議論せずに無神論の思想を説明できるすごい考え方だと思います。作中では知恵の実を食べて獲得したもの(知性)についての描写はありませんでしたね。あまり知性と野性が結びつかない気もします。

皆殺しのレヴュー(revue song wi(l)d-screen baroque)

自分も本気出してなかったし未来に怯えていたくせに九九組に怒る大場、だいぶ悪くて良かったです。はしゃいじゃう花柳を初手で仕留めるところと、お話に割り込んじゃう西條をガン無視するところが一番愉快でした。あと、突っ立っている星見の横をすっと通り過ぎるところですね。TVアニメでは星見戦から始まったレヴューが劇場版では大場戦から始まるのがエモでした。レヴューソングも格好良かったです。でも話が資本主義社会前提で進みますけど、自然界の弱肉強食ルールをそのまま人間社会に適用するのは早計ではないかと少し思いました。

🦒

『少女☆歌劇レヴュースタァライト』に登場する数々のファンタジー設定の根底には青春時代の何にでもなれるような万能感や子供の頃にしかない熱気の共有があるように思います。それはつまり、少女から大人になり舞台”少女”でなくなってしまえば、ファンタジー設定から卒業しなければいけないということです。もしかしたら、トップスタァという夢さえポケモンマスターくらいふわっとした、いつか卒業すべき幻想なのかもしれません。少なくとも、具体的な進路を考える上ではもっとパーソナライズないしスケールダウンが必要でしょう。しかし、青春時代の幻想だとしてもキリンのオーディションは魅力的ですし、現実を見ることは怖くもあります。まだ伝えられていないこと、やり残したこともあるでしょう。各々がそれを解決し、精神的に区切りをつけることが本作のメインストーリーでした。

再生産のレヴュー(revue song スーパースタァスペクタクル)

死の概念自体は「舞台に生かされている」ことの裏返しですが、劇場版からは「死」に「将来への恐怖」という意味が加わりより強くフィーチャーされました。劇場版『少女☆歌劇レヴュースタァライト』という物語自体が九九組の演じる劇中劇であるという考察を立てている人もいるらしいですが、「演じている」ことが事実だとしても比喩以上のものではないと個人的には思います。私だってある日突然「もっとわがままになろう」と思ってこんな性格になったわけですし、普通じゃないですか? なので愛城が本当は朝一人で起きられるようなことはないと思います。

🗼

『私たちはもう舞台の上』の「戻れるよあのページ」という歌詞が一番衝撃的でした。作中世界ではファンタジー設定で戻れるかもしれませんけど、現実世界では過ぎ去った時間には二度と戻れませんし、大場関連はそういう話だと思っていました。そしてこれまでの登場人物の思想自体はファンタジー設定によらず、結論は常に現実に則したものでした。だから「戻れる」という言葉が出てきたことは意外でしたが『星のダイアローグ』の「あの頃には戻れない」って歌詞と対比させているんでしょうか。


余談ですが、映画を見終わった後のエレベーターは当然同じ映画を見ていた人ばかりで、そこで誰かのスマートフォンからアニソンが流れ始めたので場の空気がヒリっとしました。あやうくレヴューが始まるかと思いましたし、余韻は吹き飛びました。許しません。

おわりに

本エントリは以上ですが、明日以降のエントリもきっと本エントリ以上に愉快ですので、ぜひご覧いただき、そして共にお楽しみいただければ幸いです。対戦ありがとうございました。