PythonでUnicode点字のアスキーアートを作ろう！

# PythonでUnicode点字のアスキーアートを作ろう！

## 概要

　この記事は、Pythonから画像処理を行い、Unicodeに含まれる8点点字でアスキーアートを作ろうというものです。具体的にこのようなものを作成します。

<img width="350" alt="tennji_d.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524325/1ed9316d-cb56-32a6-2fea-b7898edc6d38.png">

## はじめに

　ある時何気なくUnicodeの表を眺めていたら点字文字があるのを見つけ、この点字のパターンを利用してアスキーアートを作ったら面白いものが作れそうだと思ったのですが、私は極度のめんどくさがりなので手作業ではなく、プログラムで自動的に作成したいと思い作りました。
　本記事は画像データの処理や加工から、点字生成プログラム、点字への置き換えなど内容盛りだくさんですので長くなって申し訳ないですが参考になれば幸いです。

## 実行環境

```text:env.txt
Python 3.9.7
    numpy : 1.21.3
    opencv-python : 4.5.4.58
```

## システムのフロー

　簡単にシステムの流れを紹介します。

1. 数値データとリンクした点字を生成できるように準備する
2. 画像の前処理(リサイズなど)をする。
3. 二値画像にする。
4. 二値画像に対応した点字を配置する。

　このような流れで処理を行います。ポイントとしては点字を8桁のバイナリデータと考え、入力画像も二値化し8桁分ずつとれるようにします。

## プログラム説明

### Unicode点字生成

　それぞれの文字にはコンピュータが扱えるように値が割り振られておりUnicodeでは8点点字は`U+2800`〜`U+28FF`の256文字すなわち、2の8乗個全てのパターンあります。そして、その点字の並び順は点のありなしを二進数表現した順番になっています。具体的な順番は以下のようになっています。


<img width="200" alt="tennji_d.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524325/8692d78a-6be1-7f2a-c84b-123baec832fa.png">

例として、1,2,3にプロットする場合(点字"⠇")は、`2800(16)+00000111(2)`になります。
このようにどこに点を打つかわかればその文字コードを判断できるということになります。今回は以下のプログラムで点字生成プログラムを作成しました。

```python:gen_tenji.py
import cv2
import numpy as np
#バイナリデータから点字にする関数
#縦４横２の二値データの配列が入力されることを想定している
def bin2dot(bins):
    if np.sum(bins) == 0:
        return " "
    ans = np.array([[2**5, 2**2],
                    [2**4, 2**1],
                    [2**3, 2**0],
                    [2**7, 2**6]
                    ])
 
    tem_bin = np.sum(ans*bins)
    a = 0b10100000000000
    return chr(a+tem_bin)

```

　プログラムの基本的な内容としては`ans`に点字に対応した二進数の桁の情報をいれ、入力とのアダマール積(要素ごとの積)の合計に`2800(16)`を足しています。空白でずれることがあったのですべて０だったときは通常の半角空白を出力します。このプログラムを利用して最終的には二値化された画像データを点字に置き換えていきます。

### 前処理

　今回は入力するデータをグレースケールで点字の大きさの関係から縦横ともに4の倍数になるように調整します。それを実現するプログラムは以下のようになります。

```python:preprocessing.py
import cv2
import numpy as np

#cv2.imreadは二バイト文字がダメなので注意！
inputs=input(">>")
img = cv2.imread(inputs)

#最大でも(256x256)位の大きさがちょうどよかったです。
#正方形の画像の場合以下のcv2.resizeを用いて大きさを調整するとよいです。
#img = cv2.resize(img, (256, 256))

#グレースケール化
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hi, we = img.shape
#ボーダーの追加
img = cv2.copyMakeBorder(img, 0, hi % 4, 0, we % 4, cv2.BORDER_REPLICATE)
```

4で割ったときの余りの分、下と右にボーダーを設置することで点字に置き換えた時に余りが出ないようにしています。

### ディザリングでいい感じに二値化

　次に得られた画像から二値画像を作成しますが、できるだけ視覚的にきれいなアスキーアートにしたいという点から、出力がつぶれがちなモード法や判別分析法、P-タイル法といった手法は使わず配列ディザリングを利用し、中間色の表現を行いながら二値化を行います。

　ここで配列ディザリングとは、
>画像をディザリングするアルゴリズムの一つである。このアルゴリズムは通常、色深度の小さいディスプレイに画像を表示するために使用される。
このアルゴリズムでは、表示されるピクセルにしきい値マップを適用することで色数を減らす。その結果、元のピクセルの色がそのディスプレイで使える色数の少ないパレットの色と比べてどのくらい離れているかにもよるが、一部のピクセルの色が変化する。(引用wikipedia)

のことで二値化画像にて中間色を表現するのに事前に用意した、しきい値マップ(行列情報)を用いて計算していく手法になります。

```python:ordered_dithering.py
import cv2
import numpy as np

def ordered_dithering(imgs):
    img=imgs.copy()
    base = [[0, 8, 2, 10],
            [12, 4, 14, 6],
            [3, 11, 1, 9],
            [15, 7, 13, 5]]

    for i, img_rows in enumerate(img):
        for ii, img_pixel in enumerate(img_rows):
            #二値化は点字生成のことも考えて0,1にする
            img[i][ii] = np.where(img_pixel > base[i % 4][ii % 4]*16, 1, 0)
    return img

```

　閾値が配列情報になっているためスライドして計算しています。画像の縦横のループに合わせてディザリング配列も同時に動くようになっています。ここも剰余を使うことでディザリング配列分のループをカットしています。
ここまでの処理で、中間色が再現された二値画像データが作成できます。

![1f48e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524325/8b92f66d-bad2-4cd6-6968-666163f1c932.png)　　→　　![tests.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524325/87f4d944-d2b5-a9dd-184a-f9cf82f296e8.png)
(この画像は見やすいように疎が白、密が黒になるように0,255で二値化したものです。
実際は黒背景のコンソールで表示したいので色は反転しておきます。)

　次にその得られたデータからUnicode点字生成プログラムもとにどんどん置き換えていきます。
具体的にループで文字の大きさごとにスライドしながら置き換えていきますが、ここで0,1の二値化にしているためそのまま点字生成プログラムにて適応できます！！

```python:chara_plot.py
import cv2
import numpy as np

f = open("dot.txt", "w", encoding="utf-8")
for i in range(0, hi-4, 4):
    for ii in range(0, we-2, 2):
        print(bin2dot(img[i:i+4, ii:ii+2]), end="")
        f.write(bin2dot(img[i:i+4, ii:ii+2]))
    else:
        f.write("\n")
        print()
f.close()
```

　初めに作成した点字生成プログラム(プログラム上で`bin2dot`)を用いて順次適応し出力された点字に対して`print`による表示とファイルに書き込みを行っています。

## 生成結果

<img width="350" alt="tennji_d.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524325/596ab510-6863-063f-31d2-a4be7f7d0da6.png">

　満足する結果が得られました！:hugging:　離れてみるとなお良しです！

## 作成したプログラム

```python:main.py
import cv2
import numpy as np

#配列ディザリング関数
def ordered_dithering(imgs):
    img=imgs.copy()
    base = [[0, 8, 2, 10],
            [12, 4, 14, 6],
            [3, 11, 1, 9],
            [15, 7, 13, 5]]

    for i, img_rows in enumerate(img):
        for ii, img_pixel in enumerate(img_rows):
            #二値化は点字生成のことも考えて0,1にする
            img[i][ii] = np.where(img_pixel > base[i % 4][ii % 4]*16, 1, 0)
    return img

#バイナリデータから点字にする関数
def bin2dot(bins):
    if np.sum(bins) == 0:
        return " "
    ans = np.array([[2**5, 2**2],
                    [2**4, 2**1],
                    [2**3, 2**0],
                    [2**7, 2**6]
                    ])
 
    tem_bin = np.sum(ans*bins)
    a = 0b10100000000000
    return chr(a+tem_bin)

#画像読み込み＆前処理

inputs=input(">>")
img = cv2.imread(inputs)
#最大でも(256x256)位の大きさがちょうどよかったです。
#正方形の画像の場合以下のcv2.resizeを用いて大きさを調整するとよいです。
#img = cv2.resize(img, (256, 256))
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hi, we = img.shape
img = cv2.copyMakeBorder(img, 0, hi % 4, 0, we % 4, cv2.BORDER_REPLICATE)

hi, we = img.shape

img = ordered_dithering(img)

#順番に点字に置き換え、結果を出力
f = open("dot.txt", "w", encoding="utf-8")
for i in range(0, hi-4, 4):
    for ii in range(0, we-2, 2):
        print(bin2dot(img[i:i+4, ii:ii+2]), end="")
        f.write(bin2dot(img[i:i+4, ii:ii+2]))
    else:
        f.write("\n")
        print()
f.close()
```

## 参考文献

https://qiita.com/zakuroishikuro/items/15d1a69178895edf9a21
https://ja.wikipedia.org/wiki/%E9%85%8D%E5%88%97%E3%83%87%E3%82%A3%E3%82%B6%E3%83%AA%E3%83%B3%E3%82%B0