2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

これであなたもコメ職人!YouTubeの字幕機能でアスキーアート

Last updated at Posted at 2023-12-06

この記事はデジクリ 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
input.png

それを以下のコードで変換します。

main.py
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が表示されます。

image.png

詳細は省くとして、画像からAAを簡単に生成できました。

AAの情報をytt形式で書きだす

出力時にprintの代わりに、以下のコードに差し替えます。

main.py
 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の字幕機能にファイルとしてアップロードすると正しく変換されていることが確認できました。

image.png

最後に

このように、YouTubeの字幕機能は表現の幅が非常に広く様々なことができます。
うまく生成すれば映像を変換して字幕アニメーションにもできるかもしれません。
皆さんもぜひXMLを活用してみましょう!!

明日の記事は19th シラオキ さんの「お馬さんはとてもいいぞ。」です。
お楽しみに!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?