この記事はデジクリ Advent Calender 2023 の 7 日目の記事です。
デジクリは芝浦工業大学の創作サークルです。
デジクリについて、詳しくはこちらのサイトをご覧ください。
6 日目の記事は (18 th) うだい さんの「VSCodeの拡張機能でステータスバーにアイコンボタンを表示する」です。
こんにちは、昨日に引き続いてうだいです。
さてさて、今回は昨日とは全く異なる、お遊びの記事になります。
YouTubeの字幕機能でお絵描きをする
皆さんはYouTubeをご覧になりますか?私はヘビーユーザーです。
YouTubeには字幕機能があり、私の過去の記事でも解説させていただいたことがありますが非常に高いカスタマイズ性があります。
今回はこのカスタマイズ性を使って、アスキーアートを字幕で作って動画上にお絵かきしてみようと思います!
YouTubeの字幕でできること
まず前提条件としてYouTubeの字幕機能ではどんなことができるのかをおさらいしておきます。
詳細についてはぜひこちらの記事をご確認ください!
YouTube標準のWebエディタを使った字幕編集では一部しか利用できませんが、ytt形式のファイルを使用することで様々なカスタムが行えます。
具体的には、文字色、太字、イタリック、表示座標、フォント、文字サイズなどの指定が行えます。
これを使うことで、色付きのAAを作れるのでは?というのが今回のアイデアになります。
なお、今回作成したプログラムは以下のレポジトリにまとまっています。
Pythonで画像からAAを生成する
Pythonを使って画像から白黒のAAを生成します。
今回はこちらの記事の実装を全面的に参考にさせていただきました!
まず、imgs
ディレクトリを作成してその中にinput.png
を用意します
imgs/input.png |
---|
それを以下のコードで変換します。
from PIL import Image, ImageDraw, ImageFont
import numpy as np
def make_map(str_list):
l = []
for i in str_list:
im = Image.new("L", (20, 20), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), i)
l.append(np.asarray(im).mean())
l_as = np.argsort(l)
lenl = len(l)
l2256 = np.r_[np.repeat(l_as[:-(256 % lenl)], 256//lenl),
np.repeat(l_as[-(256 % lenl):], 256//lenl+1)]
chr_map = np.array(str_list)[l2256]
return chr_map
def make_AA(file_path, str_list="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz +-*/%'"+'"!?#&()~^|@;:.,[]{}<>_0123456789', width=60):
img = Image.open(file_path)
str_list = list(str_list)
gray_img_array = np.asarray(img.convert('L').resize(
(width, width*img.height//img.width//2)))
chr_map = make_map(str_list)
aa = chr_map[gray_img_array].tolist()
for i in range(len(gray_img_array)):
print(''.join(aa[i]))
make_AA("imgs/input.png")
こちらを実行すると、ターミナル上に結果のAAが表示されます。
詳細は省くとして、画像からAAを簡単に生成できました。
AAの情報をytt形式で書きだす
出力時にprint
の代わりに、以下のコードに差し替えます。
with open("result/aa.ytt", "w") as f:
f.write(
'<?xml version="1.0" encoding="utf-8" ?>\n' +
'<timedtext format="3">\n' +
'<head>\n')
heads = ""
for i in range(25):
heads += f'<wp id="{i+1}" ap="0" ah="0" av="{i*4}" />\n'
f.write(heads)
f.write('</head>\n<body>\n')
body = ""
for i in range(len(gray_img_array)):
body += f'<p t="0" d="10000" wp="{i%25}">\n'
for j in range(len(gray_img_array[i])):
body += f'{aa[i][j]}'
body += '</p>\n'
f.write(body)
f.write('</body>\n</timedtext>')
こちらのコードは、ytt
形式の規則に従ってXMLファイルを生成してくれます。
工夫した点としては改行についてです。
明示的にy座標をwp
要素を使って指定してあげることによって、デバイス差などによる改行の位置ずれを考慮しています。
記法についてはこちらの記事をご確認ください。
色情報を付け加える
このままでも十分問題なく表示できますが、せっかくなのでひと手間加えてカラーAAにしてみましょう。
ytt
形式では<head />
内に<pen />
というタグを定義して色や座標などのメタ情報を持たせることができます。
色の数だけpen
のIDを発行しなければならないのですが、愚直に元のフルカラーだと$256^3$で16777216個のIDを発行する必要がありYouTubeの字幕プレイヤーに大きく負荷がかかってしまいます。
そのため今回はRGBそれぞれを四階調まで下げることによって$4^3$で64色まで下げることにします。
4階調まで下げるのには以下のような関数を定義して対応します。
この関数は4階調まで下げたのちにその色に対応するpen
タグのIDを返却します。
1つのIDでRGBの三色を表現するため、4進法を使います。
例えば、黄色であればrgb(255, 255, 0)
ですので4階調に直すと[3, 3, 0]
になります。
これを4進数の$\text{330}_{(4)}$であるという風にとらえると、10進数に変換すると $3 \cdot 4^2 + 3 \cdot 4 + 0 = 60$ でIDは$60$になる、という計算を行います。
この方法はシリアル通信などにおけるデータの圧縮などでよく用いられています。
def quantize(pixel):
arr = [196, 128, 64, 0]
quantized = [0, 0, 0]
for i in range(3):
for j in range(4):
if pixel[i] >= arr[j]:
quantized[i] = max(4-j-1, 0)
break
return quantized[0] * 16 + quantized[1] * 4 + quantized[2] + 1
これに対応するようにmake_AA
関数を以下のように書き換えます。
def make_AA(file_path, str_list="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz +-*/%'"+'"!?#&()~^|@;:.,[]{}<>_0123456789', width=60):
img = Image.open(file_path)
str_list = list(str_list)
gray_img_array = np.asarray(img.convert('L').resize(
(width, width*img.height//img.width//2)))
+ color_img_array = np.asarray(img.resize(
+ (width, width*img.height//img.width//2)))
chr_map = make_map(str_list)
aa = chr_map[gray_img_array].tolist()
+ colors = [[quantize(pixel) for pixel in row] for row in color_img_array]
for i in range(len(gray_img_array)):
print(''.join(aa[i]))
with open("result/aa.ytt", "w") as f:
f.write(
'<?xml version="1.0" encoding="utf-8" ?>\n' +
'<timedtext format="3">\n' +
'<head>\n')
heads = ""
+ for i in range(64):
+ r = (i // 16) * 64
+ g = ((i // 4) % 4) * 85
+ b = (i % 4) * 64
+ heads += f'<pen id="{i+1}" fc="#{r:02x}{g:02x}{b:02x}" fs="3" sz="0" fo="255" bo="0" b="1" />\n'
for i in range(25):
heads += f'<wp id="{i+1}" ap="0" ah="0" av="{i*4}" />\n'
f.write(heads)
f.write('</head>\n<body>\n')
body = ""
for i in range(len(gray_img_array)):
body += f'<p t="0" d="10000" wp="{i%25}">\n'
for j in range(len(gray_img_array[i])):
- {aa[i][j]}
+ body += f'<s p="{colors[i][j]}">{aa[i][j]}</s>'
body += '</p>\n'
f.write(body)
f.write('</body>\n</timedtext>')
こうすることで、対応した色に変化させることができました。
これをYouTubeの字幕機能にファイルとしてアップロードすると正しく変換されていることが確認できました。
最後に
このように、YouTubeの字幕機能は表現の幅が非常に広く様々なことができます。
うまく生成すれば映像を変換して字幕アニメーションにもできるかもしれません。
皆さんもぜひXMLを活用してみましょう!!
明日の記事は19th シラオキ さんの「お馬さんはとてもいいぞ。」です。
お楽しみに!