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

:::note info
この記事は**デジクリ Advent Calender 2023** の 7 日目の記事です。
デジクリは芝浦工業大学の創作サークルです。
デジクリについて、詳しくは[こちらのサイト](https://digicre.net/)をご覧ください。
6 日目の記事は (18 th) うだい さんの[「**VSCodeの拡張機能でステータスバーにアイコンボタンを表示する**」](https://qiita.com/yudai1204/items/9ac1e0847829c9daaad0)です。
:::

こんにちは、昨日に引き続いて[うだい](https://twitter.com/yudai1204)です。
さてさて、今回は昨日とは全く異なる、お遊びの記事になります。

## YouTubeの字幕機能でお絵描きをする

皆さんはYouTubeをご覧になりますか？私はヘビーユーザーです。
YouTubeには字幕機能があり、私の[過去の記事](https://qiita.com/yudai1204/items/898c368af8832443a874
)でも解説させていただいたことがありますが**非常に高いカスタマイズ性**があります。

https://qiita.com/yudai1204/items/898c368af8832443a874

今回はこのカスタマイズ性を使って、**アスキーアート**を字幕で作って動画上にお絵かきしてみようと思います！

## YouTubeの字幕でできること

まず前提条件としてYouTubeの字幕機能ではどんなことができるのかをおさらいしておきます。
詳細についてはぜひ[こちらの記事](https://qiita.com/yudai1204/items/898c368af8832443a874
)をご確認ください！

YouTube標準のWebエディタを使った字幕編集では一部しか利用できませんが、**ytt形式**のファイルを使用することで様々なカスタムが行えます。
具体的には、文字色、太字、イタリック、表示座標、フォント、文字サイズなどの指定が行えます。
これを使うことで、色付きのAAを作れるのでは？というのが今回のアイデアになります。

なお、今回作成したプログラムは以下のレポジトリにまとまっています。

https://github.com/yudai1204/youtube-ascii-art-maker

## Pythonで画像からAAを生成する

Pythonを使って画像から白黒のAAを生成します。
今回は[こちらの記事](https://qiita.com/Cartelet/items/542fe3f966b8fa98437a)の実装を全面的に参考にさせていただきました！

https://qiita.com/Cartelet/items/542fe3f966b8fa98437a

まず、`imgs`ディレクトリを作成してその中に`input.png`を用意します

| imgs/input.png |
|-|
| ![input.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449776/8f8850ff-3435-9b28-d11d-36657df31808.png) |


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


```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449776/fe8c7f0c-87f6-3cf9-6dda-cb1077680755.png)

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


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

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

```python: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`要素を使って指定してあげることによって、デバイス差などによる改行の位置ずれを考慮しています。
記法については[こちらの記事](https://qiita.com/yudai1204/items/898c368af8832443a874
)をご確認ください。

## 色情報を付け加える
このままでも十分問題なく表示できますが、せっかくなのでひと手間加えてカラー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$になる、という計算を行います。
この方法はシリアル通信などにおけるデータの圧縮などでよく用いられています。

```python
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`関数を以下のように書き換えます。

```diff_python
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]}
'
            body += '</p>\n'
        f.write(body)
        f.write('</body>\n</timedtext>')
```

こうすることで、対応した色に変化させることができました。

これをYouTubeの字幕機能にファイルとしてアップロードすると正しく変換されていることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449776/494b3f45-6d1e-d2a7-58e8-12a805055857.png)

## 最後に

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

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