【Python】自動でAA（アスキーアート）を生成してくれる単純なコードを書いた

#はじめに
皆さん、**AA（アスキーアート）**はご存知かと思います。まさに職人技ですよね。
一度は自分の好きな画像、好きなキャラクター、はたまた自分？をAAにしてみたいと思ったことはありませんか？
今回はAA職人さんのように、とまでは行きませんが、好きな文字を使って、好きな画像から**簡単にAAを生成**してくれるコードを書いてみたので紹介したいと思います。

一例ですが本記事で紹介するコードで以下のような変換ができます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/ad016da9-896d-fd61-ede9-f50646a01d21.png width=50%><img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/9157dba8-d029-80ce-7c41-d970deab0c04.png  width=50%>
画像掲載元：いらすとや

#方法
AA職人さんの作るAAは輪郭等を`/`などで効果的に表現して少ない文字数でイラストを表現することが多いですが、今回はそんな複雑なことはせずに力技でいきたいと思います。
方法は単純、**グレースケールの256段階の明度に、そのまま密度順に文字を当てはめていきます**。では行きましょう。

#実装
まずは変換元の画像をグレースケールで読み込んで、`Numpy配列`にします。

```python:AA.py
from PIL import Image
import numpy as np

file_path = "./hogehoge.png"
imag = Image.open(file_path).convert('L')
imarray = np.asarray(imag)
```

この時`imarray`に格納されている値は0~255(2^8段階)の整数となっています。
画像側の準備はこれだけです。

次に使用する文字を密度順にソートしていきます。
、、、していくんですが、フォントファイルの扱い方も分かりませんし、わかったとしてもベクター形式の画像の密度を測る方法が思いつきません。
そこで思いついた方法は、一度画像（ラスター）に貼り付けてそれの和なり平均なりを求めて比較する方法です。

```python:AA.py
from PIL import ImageDraw,ImageFont

def make_map(str_list):
    l = []
    font = ImageFont.truetype('msgothic.ttc', 20) #"C:\Windows\Fonts"内の（もしくはパスから書いて）お好きなフォントで #エラーが出なければこのままでOK
    for i in str_list:
        im = Image.new("L",(20,20),"white") #空の白地画像の作成
        draw = ImageDraw.Draw(im)
        draw.text((0,0),i,font=font) #文字入れ
        l.append(np.asarray(im).mean()) #Numpy配列化して平均値を計算、格納
    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)] #要素数を256に調節
    chr_map = np.array(str_list)[l2256] #密度順にソートした文字リスト
    return chr_map
```

この関数では文字のリストを渡すと密度順にソートして`Nampy配列`として返してくれます。

ところで、`Nampy配列`の個人的にめちゃくちゃ気に入っている機能にインデックスを格納した配列をインデックスとして渡すと指定した要素を取り出してくれるというものがあります（`ファンシーインデックス`）。例を示すと、

```python:
import numpy as np

arr=np.array(["あ","い","う","え","お"])
print(arr[[1,1,4,3,2,0]])
```

```python:出力
['い' 'い' 'お' 'え' 'う' 'あ']
```

**う～んNumpy大好き。**
--閑話休題--

今回はこの機能を使うことで一気に**畳み掛け**ます。

```python:AA.py
str_list = list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz +-*/%'"+'"!?#&()~^|@;:.,[]{}<>_0123456789') #使いたい文字を用意して、
chr_map = make_map(str_list) #密度順にソートして、
aa = chr_map[imarray] #ファンシーソート！！！
```

はい、これだけで**`aa`にAAに変換したデータが出来上がって**います。

あとは、見やすいようにTXTファイルに書き込むなり標準出力するなりすればよいですね。

```python:AA.py
aa = aa.tolist() #リストに変換して
out_path="./aa.txt"
with open(out_path,"w") as f:
    for i in range(len(imarray)):f.write(''.join(aa[i])+"\n")
```

もしくは

```python:AA.py
aa = aa.tolist()
for i in range(len(imarray)):print(''.join(aa[i]))
```

以上です。簡単でしたね。


#最後に
出力サイズの変更や全角・半角の選択（参考：[【Python】一行で全角と半角を相互変換する（英字＋数字＋記号）](https://qiita.com/YuukiMiyoshi/items/6ce77bf402a29a99f1bf)）等の細かい機能を追加して関数にまとめたものを挙げておきます

```python:AA.py
from PIL import Image, ImageDraw,ImageFont
import numpy as np

def make_map(str_list):
    l=[]
    font = ImageFont.truetype('msgothic.ttc', 20)
    for i in str_list:
        im = Image.new("L",(20,20),"white")
        draw = ImageDraw.Draw(im)
        draw.text((0,0),i,font=font)
        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 output(chr_map,imarray,isOutText,out_path):
    aa=chr_map[imarray].tolist()
    if isOutText:
        with open(out_path,"w") as f:
            for i in range(len(imarray)):f.write(''.join(aa[i])+"\n")
    else:
        for i in range(len(imarray)):print(''.join(aa[i]))

def make_AA(file_path,str_list="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz +-*/%'"+'"!?#&()~^|@;:.,[]{}<>_0123456789',width=150,isOutText=False,out_path="aa.txt",isFW=False):
    imag=Image.open(file_path).convert('L')
    if isFW:str_list=list(str_list.translate(str.maketrans({chr(0x0021 + i): chr(0xFF01 + i) for i in range(94)})))
    else:str_list=list(str_list)
    imarray=np.asarray(imag.resize((width,width*imag.height//imag.width//(2-int(isFW)))))
    output(make_map(str_list),imarray,isOutText,out_path)
```

`make_AA()`が使用時にこちらから実行する関数です。
引数は、`file_path`：元画像のパス、`str_list`：使用する文字の文字列（リストではなく文字列）、`width`：横の文字数（縦は自動で調節されます）、`isOutText`：TrueだとTXTファイルに出力されます。（デフォルトではFalse）、`out_path`：isOutTextが`True`のときのTXTファイルの出力先とファイル名、`isFW`：全角の文字が含まれる時、もしくは全角で出力したいときに`True`にしてください(FullWidthの略)
`file_path`以外は指定しなくても使用可能です（その場合、横150文字で標準出力されます）。

#まとめ
簡単にですがAAを作るプログラムを紹介していきました、ちなみに前処理として元画像のコントラストをゴリゴリに上げておくと仕上がりがスッキリします。何かの参考になるといいな。
感想やアドバイス等いただけると嬉しいです。